12 Commits

102 changed files with 1762 additions and 310 deletions

View File

@ -12,7 +12,7 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: stable channel: stable
flutter-version: 3.29.3 flutter-version: 3.32.0
- run: flutter pub get - run: flutter pub get
- run: flutter build appbundle - run: flutter build appbundle
env: env:
@ -53,6 +53,6 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: stable channel: stable
flutter-version: 3.29.3 flutter-version: 3.32.0
- run: flutter pub get - run: flutter pub get
- run: flutter build ios --no-codesign - run: flutter build ios --no-codesign

View File

@ -16,7 +16,7 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: stable channel: stable
flutter-version: 3.29.3 flutter-version: 3.32.0
- run: flutter pub get - run: flutter pub get
- name: Build apk - name: Build apk
env: env:

View File

@ -1,5 +1,4 @@
import com.android.build.api.dsl.ApkSigningConfig import com.android.build.api.dsl.ApkSigningConfig
import com.android.build.api.dsl.SigningConfig
import org.jetbrains.kotlin.gradle.targets.js.toHex import org.jetbrains.kotlin.gradle.targets.js.toHex
import java.io.FileInputStream import java.io.FileInputStream
import java.util.Base64 import java.util.Base64
@ -8,11 +7,9 @@ import java.util.Properties
plugins { plugins {
id("com.android.application") id("com.android.application")
// START: FlutterFire Configuration
id("com.google.gms.google-services")
// END: FlutterFire Configuration
id("kotlin-android") id("kotlin-android")
id("dev.flutter.flutter-gradle-plugin") id("dev.flutter.flutter-gradle-plugin")
id("com.google.gms.google-services")
} }
fun getKeystoreFile(base64String: String?, hash: String, fileName: String): File { fun getKeystoreFile(base64String: String?, hash: String, fileName: String): File {

View File

@ -3,6 +3,5 @@
the Flutter tool needs it to communicate with the running application the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
</manifest> </manifest>

View File

@ -1,53 +1,76 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<application <application
android:label="zap.stream"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher"
android:label="zap.stream">
<activity <activity
android:name=".MainActivity" android:name="com.ryanheise.audioservice.AudioServiceActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true" android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:taskAffinity="" android:taskAffinity=""
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data <meta-data
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" android:resource="@style/NormalTheme" />
/>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter android:autoVerify="true"> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" android:host="zap.stream" />
<data android:host="zap.stream" />
<data android:scheme="http" />
<data android:scheme="https" /> <data android:scheme="https" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="zswc" /> <data android:scheme="zswc" />
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below. <service
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> android:name="com.ryanheise.audioservice.AudioService"
android:exported="true"
android:foregroundServiceType="mediaPlayback"
android:permission="android.permission.FOREGROUND_SERVICE"
tools:ignore="Instantiatable">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<receiver
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
android:exported="true"
tools:ignore="Instantiatable">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
</application> </application>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT. https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
@ -55,10 +78,11 @@
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. --> In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries> <queries>
<intent> <intent>
<action android:name="android.intent.action.PROCESS_TEXT"/> <action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain"/> <data android:mimeType="text/plain" />
</intent> </intent>
</queries> </queries>
<meta-data <meta-data
android:name="firebase_messaging_auto_init_enabled" android:name="firebase_messaging_auto_init_enabled"
android:value="false" /> android:value="false" />

View File

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="33"
android:viewportHeight="23"
android:tint="#FFFFFF">
<group android:scaleX="0.777027"
android:scaleY="0.5415643"
android:translateX="3.679054"
android:translateY="5.272011">
<path
android:pathData="M32.788,1.721C32.356,0.677 31.344,0 30.216,0H10.68C10.674,0 10.667,0 10.661,0C10.655,0 10.648,0 10.642,0C6.542,0 3.21,3.332 3.21,7.425C3.21,9.578 4.138,11.582 5.704,12.975L0.812,17.867C0.013,18.666 -0.225,19.858 0.207,20.896C0.638,21.94 1.65,22.617 2.778,22.617H22.314C22.32,22.617 22.327,22.617 22.34,22.617C22.346,22.617 22.353,22.617 22.366,22.617C26.458,22.617 29.791,19.285 29.791,15.192C29.791,13.039 28.862,11.035 27.296,9.642L32.188,4.75C32.981,3.951 33.22,2.759 32.788,1.721ZM2.714,19.858C2.694,19.813 2.707,19.8 2.733,19.775L8.109,14.399L22.391,19.452C22.404,19.459 22.424,19.465 22.437,19.465C22.456,19.472 22.475,19.478 22.494,19.491C22.527,19.51 22.552,19.542 22.572,19.581C22.578,19.588 22.578,19.594 22.585,19.601C22.591,19.613 22.591,19.626 22.591,19.639C22.591,19.652 22.598,19.665 22.598,19.678C22.598,19.794 22.469,19.897 22.327,19.897H2.785C2.753,19.91 2.733,19.91 2.714,19.858ZM25.208,18.956C25.208,18.95 25.201,18.937 25.201,18.93C25.176,18.827 25.143,18.73 25.105,18.634C25.098,18.621 25.098,18.608 25.092,18.595C25.053,18.492 25.002,18.395 24.95,18.299C24.944,18.286 24.931,18.266 24.924,18.253C24.815,18.06 24.686,17.88 24.538,17.712C24.525,17.699 24.512,17.686 24.499,17.673C24.422,17.596 24.344,17.519 24.26,17.448C24.248,17.441 24.235,17.428 24.228,17.422C24.151,17.358 24.067,17.299 23.977,17.242C23.964,17.235 23.951,17.222 23.938,17.216C23.848,17.158 23.751,17.106 23.655,17.055C23.635,17.042 23.61,17.035 23.59,17.022C23.493,16.977 23.39,16.932 23.281,16.9L16.674,14.56L9.037,11.86L9.011,11.853C9.004,11.853 9.004,11.853 8.998,11.847C8.811,11.776 8.624,11.692 8.437,11.595C6.884,10.777 5.924,9.178 5.924,7.425C5.924,5.891 6.658,4.525 7.799,3.661C7.799,3.667 7.806,3.68 7.806,3.687C7.831,3.783 7.864,3.88 7.902,3.977C7.915,4.009 7.928,4.041 7.941,4.074C7.973,4.144 8.005,4.209 8.038,4.28C8.051,4.312 8.07,4.344 8.083,4.37C8.128,4.454 8.179,4.531 8.237,4.608C8.263,4.641 8.283,4.673 8.308,4.705C8.353,4.763 8.399,4.821 8.45,4.873C8.469,4.899 8.489,4.924 8.515,4.944C8.579,5.015 8.656,5.079 8.727,5.143C8.753,5.169 8.779,5.189 8.811,5.208C8.882,5.266 8.953,5.317 9.03,5.369C9.043,5.382 9.056,5.388 9.075,5.401C9.166,5.459 9.262,5.511 9.359,5.556C9.385,5.569 9.411,5.582 9.436,5.595C9.539,5.64 9.643,5.685 9.746,5.717L24.009,10.764C24.016,10.764 24.022,10.77 24.022,10.77C24.209,10.841 24.389,10.919 24.563,11.015C26.117,11.834 27.077,13.432 27.077,15.185C27.084,16.726 26.342,18.092 25.208,18.956ZM30.267,2.836L24.892,8.211C24.879,8.205 24.86,8.199 24.847,8.192L10.622,3.165C10.603,3.158 10.584,3.152 10.571,3.145H10.564C10.545,3.139 10.532,3.132 10.513,3.12C10.506,3.113 10.5,3.107 10.493,3.1C10.48,3.094 10.474,3.081 10.467,3.074C10.461,3.068 10.455,3.055 10.455,3.049C10.448,3.036 10.442,3.023 10.435,3.01C10.435,3.004 10.429,2.991 10.429,2.978C10.429,2.965 10.422,2.946 10.422,2.926C10.422,2.913 10.429,2.907 10.429,2.894C10.448,2.797 10.564,2.714 10.687,2.714H30.229C30.261,2.714 30.28,2.714 30.3,2.759C30.306,2.797 30.293,2.817 30.267,2.836Z"
android:fillColor="#ffffff"/>
</group>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 771 B

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 784 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -72,6 +72,7 @@
<array> <array>
<string>fetch</string> <string>fetch</string>
<string>remote-notification</string> <string>remote-notification</string>
<string>audio</string>
</array> </array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
@ -90,5 +91,9 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>NSCameraUsageDescription</key>
<string>Live streaming</string>
<key>NSMicrophoneUsageDescription</key>
<string>Live streaming</string>
</dict> </dict>
</plist> </plist>

260
lib/api.dart Normal file
View File

@ -0,0 +1,260 @@
import 'dart:convert';
import 'dart:developer' as developer;
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http;
import 'package:ndk/ndk.dart';
import 'package:zap_stream_flutter/const.dart';
class IngestEndpoint {
final String name;
final String url;
final String key;
final IngestCost cost;
final List<String> capabilities;
const IngestEndpoint({
required this.name,
required this.url,
required this.key,
required this.cost,
required this.capabilities,
});
static IngestEndpoint fromJson(Map<String, dynamic> json) {
return IngestEndpoint(
name: json["name"],
url: json["url"],
key: json["key"],
cost: IngestCost.fromJson(json["cost"]),
capabilities: List<String>.from(json["capabilities"]),
);
}
@override
int get hashCode => name.hashCode;
@override
bool operator ==(Object other) {
if (other is IngestEndpoint) {
return other.name == name;
}
return false;
}
}
class IngestCost {
final String unit;
final double rate;
const IngestCost({required this.unit, required this.rate});
static IngestCost fromJson(Map<String, dynamic> json) {
return IngestCost(unit: json["unit"], rate: json["rate"]);
}
}
class TosAccepted {
final bool accepted;
final String? link;
const TosAccepted({required this.accepted, required this.link});
static TosAccepted fromJson(Map<String, dynamic> json) {
return TosAccepted(accepted: json["accepted"], link: json["link"]);
}
}
class AccountInfo {
final double balance;
final List<IngestEndpoint> endpoints;
final TosAccepted tos;
final EventInfo? details;
const AccountInfo({
required this.balance,
required this.endpoints,
required this.tos,
this.details,
});
static AccountInfo fromJson(Map<String, dynamic> json) {
final balance = json["balance"] as int;
final endpoints = json["endpoints"] as Iterable<dynamic>;
return AccountInfo(
balance: balance.toDouble(),
endpoints: endpoints.map((e) => IngestEndpoint.fromJson(e)).toList(),
tos: TosAccepted.fromJson(json["tos"]),
details:
json.containsKey("details")
? EventInfo.fromJson(json["details"])
: null,
);
}
}
class EventInfo {
final String? id;
final String? title;
final String? summary;
final String? image;
final String? contentWarning;
final String? goal;
final List<String>? tags;
EventInfo({
required this.id,
required this.title,
required this.summary,
required this.image,
required this.contentWarning,
required this.goal,
required this.tags,
});
static EventInfo fromJson(Map<String, dynamic> json) {
return EventInfo(
id: json["id"],
title: json["title"],
summary: json["summary"],
image: json["image"],
contentWarning: json["content_warning"],
goal: json["goal"],
tags: json.containsKey("tags") ? List<String>.from(json["tags"]) : null,
);
}
}
class ZapStreamApi {
final String base;
final EventSigner signer;
ZapStreamApi(this.base, this.signer);
static ZapStreamApi instance() {
return ZapStreamApi(apiUrl, ndk.accounts.getLoggedAccount()!.signer);
}
Future<AccountInfo> getAccountInfo() async {
final url = "$base/account";
final rsp = await _sendGetRequest(url);
return AccountInfo.fromJson(JsonCodec().decode(rsp.body));
}
Future<void> updateDefaultStreamInfo({
String? title,
String? summary,
String? image,
String? contentWarning,
String? goal,
List<String>? tags,
}) async {
final url = "$base/event";
await _sendPatchRequest(
url,
body: {
"title": title,
"summary": summary,
"image": image,
"content_warning": contentWarning,
"goal": goal,
"tags": tags,
},
);
}
Future<void> acceptTos() async {
await _sendPatchRequest("$base/account", body: {"accept_tos": true});
}
Future<http.Response> _sendPatchRequest(String url, {Object? body}) async {
final jsonBody = body != null ? JsonCodec().encode(body) : null;
final auth = await _makeAuth("PATCH", url, body: jsonBody);
final rsp = await http
.patch(
Uri.parse(url),
body: jsonBody,
headers: {
"authorization": "Nostr $auth",
"accept": "application/json",
"content-type": "application/json",
},
)
.timeout(Duration(seconds: 10));
developer.log(rsp.body);
return rsp;
}
Future<http.Response> _sendPutRequest(String url, {Object? body}) async {
final jsonBody = body != null ? JsonCodec().encode(body) : null;
final auth = await _makeAuth("PUT", url, body: jsonBody);
final rsp = await http
.put(
Uri.parse(url),
body: jsonBody,
headers: {
"authorization": "Nostr $auth",
"accept": "application/json",
"content-type": "application/json",
},
)
.timeout(Duration(seconds: 10));
developer.log(rsp.body);
return rsp;
}
Future<http.Response> _sendGetRequest(String url, {Object? body}) async {
final jsonBody = body != null ? JsonCodec().encode(body) : null;
final auth = await _makeAuth("GET", url, body: jsonBody);
final rsp = await http
.get(
Uri.parse(url),
headers: {
"authorization": "Nostr $auth",
"accept": "application/json",
"content-type": "application/json",
},
)
.timeout(Duration(seconds: 10));
developer.log(rsp.body);
return rsp;
}
Future<http.Response> _sendDeleteRequest(String url, {Object? body}) async {
final jsonBody = body != null ? JsonCodec().encode(body) : null;
final auth = await _makeAuth("DELETE", url, body: jsonBody);
final rsp = await http
.delete(
Uri.parse(url),
headers: {
"authorization": "Nostr $auth",
"accept": "application/json",
"content-type": "application/json",
},
)
.timeout(Duration(seconds: 10));
developer.log(rsp.body);
return rsp;
}
Future<String> _makeAuth(String method, String url, {String? body}) async {
final pubkey = signer.getPublicKey();
var tags = [
["u", url],
["method", method],
];
if (body != null) {
final hash = hex.encode(sha256.convert(utf8.encode(body)).bytes);
tags.add(["payload", hash]);
}
final authEvent = Nip01Event(
pubKey: pubkey,
kind: 27235,
tags: tags,
content: "",
);
await signer.sign(authEvent);
return authEvent.toBase64();
}
}

View File

@ -6,6 +6,7 @@ import 'package:zap_stream_flutter/i18n/strings.g.dart';
import 'package:zap_stream_flutter/pages/category.dart'; import 'package:zap_stream_flutter/pages/category.dart';
import 'package:zap_stream_flutter/pages/hashtag.dart'; import 'package:zap_stream_flutter/pages/hashtag.dart';
import 'package:zap_stream_flutter/pages/home.dart'; import 'package:zap_stream_flutter/pages/home.dart';
import 'package:zap_stream_flutter/pages/live.dart';
import 'package:zap_stream_flutter/pages/login.dart'; import 'package:zap_stream_flutter/pages/login.dart';
import 'package:zap_stream_flutter/pages/login_input.dart'; import 'package:zap_stream_flutter/pages/login_input.dart';
import 'package:zap_stream_flutter/pages/new_account.dart'; import 'package:zap_stream_flutter/pages/new_account.dart';
@ -25,7 +26,11 @@ void runZapStream() {
supportedLocales: AppLocaleUtils.supportedLocales, supportedLocales: AppLocaleUtils.supportedLocales,
localizationsDelegates: GlobalMaterialLocalizations.delegates, localizationsDelegates: GlobalMaterialLocalizations.delegates,
theme: ThemeData.localize( theme: ThemeData.localize(
ThemeData(colorScheme: ColorScheme.dark(), highlightColor: PRIMARY_1), ThemeData(
colorScheme: ColorScheme.dark(),
highlightColor: PRIMARY_1,
useMaterial3: true,
),
TextTheme(), TextTheme(),
), ),
routerConfig: GoRouter( routerConfig: GoRouter(
@ -131,6 +136,10 @@ void runZapStream() {
), ),
], ],
), ),
GoRoute(
path: "/live",
builder: (context, state) => LivePage(),
),
GoRoute( GoRoute(
path: "/:id", path: "/:id",
redirect: (context, state) { redirect: (context, state) {

View File

@ -1,6 +1,7 @@
import 'package:amberflutter/amberflutter.dart'; import 'package:amberflutter/amberflutter.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:ndk/ndk.dart'; import 'package:ndk/ndk.dart';
import 'package:ndk_amber/ndk_amber.dart'; import 'package:ndk_amber/ndk_amber.dart';
@ -17,7 +18,7 @@ class NoVerify extends EventVerifier {
final ndkCache = DbObjectBox(); final ndkCache = DbObjectBox();
final eventVerifier = kDebugMode ? NoVerify() : RustEventVerifier(); final eventVerifier = kDebugMode ? NoVerify() : RustEventVerifier();
var ndk = Ndk( final ndk = Ndk(
NdkConfig( NdkConfig(
eventVerifier: eventVerifier, eventVerifier: eventVerifier,
cache: ndkCache, cache: ndkCache,
@ -36,6 +37,7 @@ const defaultRelays = [
]; ];
const searchRelays = ["wss://relay.nostr.band", "wss://search.nos.today"]; const searchRelays = ["wss://relay.nostr.band", "wss://search.nos.today"];
const nwcRelays = ["wss://relay.getalby.com/v1"]; const nwcRelays = ["wss://relay.getalby.com/v1"];
final apiUrl = dotenv.env["API_URL"] ?? "https://api.zap.stream/api/nostr";
final loginData = LoginData(); final loginData = LoginData();
final RouteObserver<ModalRoute<void>> routeObserver = final RouteObserver<ModalRoute<void>> routeObserver =

View File

@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang` /// To regenerate, run: `dart run slang`
/// ///
/// Locales: 22 /// Locales: 22
/// Strings: 1628 (74 per locale) /// Strings: 1668 (75 per locale)
/// ///
/// Built on 2025-05-28 at 12:41 UTC /// Built on 2025-05-30 at 11:38 UTC
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: type=lint, unused_import // ignore_for_file: type=lint, unused_import

View File

@ -54,7 +54,7 @@ class TranslationsAr extends Translations {
/// عدد مشاهدي البث /// عدد مشاهدي البث
@override String viewers({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ar'))(n, @override String viewers({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ar'))(n,
one: '1 مشاهد', one: '1 مشاهد',
other: '${NumberFormat.decimalPattern('ar').format(n)} المشاهدين', other: '{n:decimalPattern} المشاهدين',
); );
@override late final _TranslationsStreamAr stream = _TranslationsStreamAr._(_root); @override late final _TranslationsStreamAr stream = _TranslationsStreamAr._(_root);
@ -80,6 +80,7 @@ class _TranslationsStreamAr extends TranslationsStreamEn {
// Translations // Translations
@override late final _TranslationsStreamStatusAr status = _TranslationsStreamStatusAr._(_root); @override late final _TranslationsStreamStatusAr status = _TranslationsStreamStatusAr._(_root);
@override String started({required Object timestamp}) => 'بدأ ${timestamp}'; @override String started({required Object timestamp}) => 'بدأ ${timestamp}';
@override String notification({required Object name}) => '${name} بدأ البث المباشر!';
@override late final _TranslationsStreamChatAr chat = _TranslationsStreamChatAr._(_root); @override late final _TranslationsStreamChatAr chat = _TranslationsStreamChatAr._(_root);
} }
@ -381,12 +382,13 @@ extension on TranslationsAr {
case 'anon': return 'هوية مخفية'; case 'anon': return 'هوية مخفية';
case 'viewers': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ar'))(n, case 'viewers': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ar'))(n,
one: '1 مشاهد', one: '1 مشاهد',
other: '${NumberFormat.decimalPattern('ar').format(n)} المشاهدين', other: '{n:decimalPattern} المشاهدين',
); );
case 'stream.status.live': return 'بث مباشر'; case 'stream.status.live': return 'بث مباشر';
case 'stream.status.ended': return 'انتهى'; case 'stream.status.ended': return 'انتهى';
case 'stream.status.planned': return 'مخطط'; case 'stream.status.planned': return 'مخطط';
case 'stream.started': return ({required Object timestamp}) => 'بدأ ${timestamp}'; case 'stream.started': return ({required Object timestamp}) => 'بدأ ${timestamp}';
case 'stream.notification': return ({required Object name}) => '${name} بدأ البث المباشر!';
case 'stream.chat.disabled': return 'تم تعطيل الدردشة'; case 'stream.chat.disabled': return 'تم تعطيل الدردشة';
case 'stream.chat.disabled_timeout': return ({required Object time}) => 'تنتهي المهلة: ${time}'; case 'stream.chat.disabled_timeout': return ({required Object time}) => 'تنتهي المهلة: ${time}';
case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [ case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [

View File

@ -80,6 +80,7 @@ class _TranslationsStreamCs extends TranslationsStreamEn {
// Translations // Translations
@override late final _TranslationsStreamStatusCs status = _TranslationsStreamStatusCs._(_root); @override late final _TranslationsStreamStatusCs status = _TranslationsStreamStatusCs._(_root);
@override String started({required Object timestamp}) => 'Založeno ${timestamp}'; @override String started({required Object timestamp}) => 'Založeno ${timestamp}';
@override String notification({required Object name}) => '${name} byl spuštěn!';
@override late final _TranslationsStreamChatCs chat = _TranslationsStreamChatCs._(_root); @override late final _TranslationsStreamChatCs chat = _TranslationsStreamChatCs._(_root);
} }
@ -387,6 +388,7 @@ extension on TranslationsCs {
case 'stream.status.ended': return 'KONEC'; case 'stream.status.ended': return 'KONEC';
case 'stream.status.planned': return 'PLÁNOVANÉ'; case 'stream.status.planned': return 'PLÁNOVANÉ';
case 'stream.started': return ({required Object timestamp}) => 'Založeno ${timestamp}'; case 'stream.started': return ({required Object timestamp}) => 'Založeno ${timestamp}';
case 'stream.notification': return ({required Object name}) => '${name} byl spuštěn!';
case 'stream.chat.disabled': return 'CHAT ZRUŠEN'; case 'stream.chat.disabled': return 'CHAT ZRUŠEN';
case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Časový limit vyprší: ${time}'; case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Časový limit vyprší: ${time}';
case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [ case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [

View File

@ -80,6 +80,7 @@ class _TranslationsStreamDa extends TranslationsStreamEn {
// Translations // Translations
@override late final _TranslationsStreamStatusDa status = _TranslationsStreamStatusDa._(_root); @override late final _TranslationsStreamStatusDa status = _TranslationsStreamStatusDa._(_root);
@override String started({required Object timestamp}) => 'Startet ${timestamp}'; @override String started({required Object timestamp}) => 'Startet ${timestamp}';
@override String notification({required Object name}) => '${name} gik live!';
@override late final _TranslationsStreamChatDa chat = _TranslationsStreamChatDa._(_root); @override late final _TranslationsStreamChatDa chat = _TranslationsStreamChatDa._(_root);
} }
@ -387,6 +388,7 @@ extension on TranslationsDa {
case 'stream.status.ended': return 'AFSLUTTET'; case 'stream.status.ended': return 'AFSLUTTET';
case 'stream.status.planned': return 'PLANLAGT'; case 'stream.status.planned': return 'PLANLAGT';
case 'stream.started': return ({required Object timestamp}) => 'Startet ${timestamp}'; case 'stream.started': return ({required Object timestamp}) => 'Startet ${timestamp}';
case 'stream.notification': return ({required Object name}) => '${name} gik live!';
case 'stream.chat.disabled': return 'CHAT DEAKTIVERET'; case 'stream.chat.disabled': return 'CHAT DEAKTIVERET';
case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Timeout udløber: ${time}'; case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Timeout udløber: ${time}';
case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [ case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [

View File

@ -42,7 +42,7 @@ class TranslationsDe extends Translations {
/// Text, der den Benutzer auffordert, auf den Avatar-Platzhalter zu klicken, um den Upload zu starten /// Text, der den Benutzer auffordert, auf den Avatar-Platzhalter zu klicken, um den Upload zu starten
@override String get upload_avatar => 'Avatar hochladen'; @override String get upload_avatar => 'Avatar hochladen';
/// Überschrift über gelistete Top-Streamer von zaps /// Überschrift über gelistete Top-Streamer nach Zaps
@override String get most_zapped_streamers => 'Meistgezappte Streamer'; @override String get most_zapped_streamers => 'Meistgezappte Streamer';
/// Kein Benutzer bei der Suche gefunden /// Kein Benutzer bei der Suche gefunden
@ -51,7 +51,7 @@ class TranslationsDe extends Translations {
/// Ein anonymer Benutzer /// Ein anonymer Benutzer
@override String get anon => 'Anon'; @override String get anon => 'Anon';
/// Anzahl der Betrachter des Streams /// Anzahl der Zuschauer des Streams
@override String viewers({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'))(n, @override String viewers({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'))(n,
one: '1 Zuschauer', one: '1 Zuschauer',
other: '${NumberFormat.decimalPattern('de').format(n)} Zuschauer', other: '${NumberFormat.decimalPattern('de').format(n)} Zuschauer',
@ -80,6 +80,7 @@ class _TranslationsStreamDe extends TranslationsStreamEn {
// Translations // Translations
@override late final _TranslationsStreamStatusDe status = _TranslationsStreamStatusDe._(_root); @override late final _TranslationsStreamStatusDe status = _TranslationsStreamStatusDe._(_root);
@override String started({required Object timestamp}) => 'Gestartet ${timestamp}'; @override String started({required Object timestamp}) => 'Gestartet ${timestamp}';
@override String notification({required Object name}) => '${name} ging live!';
@override late final _TranslationsStreamChatDe chat = _TranslationsStreamChatDe._(_root); @override late final _TranslationsStreamChatDe chat = _TranslationsStreamChatDe._(_root);
} }
@ -212,7 +213,7 @@ class _TranslationsStreamStatusDe extends TranslationsStreamStatusEn {
// Translations // Translations
@override String get live => 'LIVE'; @override String get live => 'LIVE';
@override String get ended => 'ENDED'; @override String get ended => 'BEENDET';
@override String get planned => 'GEPLANT'; @override String get planned => 'GEPLANT';
} }
@ -224,21 +225,21 @@ class _TranslationsStreamChatDe extends TranslationsStreamChatEn {
// Translations // Translations
@override String get disabled => 'CHAT DEAKTIVIERT'; @override String get disabled => 'CHAT DEAKTIVIERT';
@override String disabled_timeout({required Object time}) => 'Die Zeitüberschreitung läuft ab: ${time}'; @override String disabled_timeout({required Object time}) => 'Timeout läuft ab: ${time}';
/// Chat-Nachricht mit Zeitüberschreitungsereignissen /// Chat-Nachricht mit Timeout-Ereignissen
@override TextSpan timeout({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [ @override TextSpan timeout({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [
mod, mod,
const TextSpan(text: ' Zeitüberschreitung '), const TextSpan(text: ' gibt '),
user, user,
const TextSpan(text: ' für '), const TextSpan(text: ' einen Timeout für '),
time, time,
]); ]);
/// Stream beendet Fußzeile am Ende des Chats /// Stream beendet Fußzeile am Ende des Chats
@override String get ended => 'STREAM BEENDET'; @override String get ended => 'STREAM BEENDET';
/// Chatnachricht mit Stream Zaps /// Chat-Nachricht mit Stream-Zaps
@override TextSpan zap({required InlineSpan user, required InlineSpan amount}) => TextSpan(children: [ @override TextSpan zap({required InlineSpan user, required InlineSpan amount}) => TextSpan(children: [
user, user,
const TextSpan(text: ' hat '), const TextSpan(text: ' hat '),
@ -384,16 +385,17 @@ extension on TranslationsDe {
other: '${NumberFormat.decimalPattern('de').format(n)} Zuschauer', other: '${NumberFormat.decimalPattern('de').format(n)} Zuschauer',
); );
case 'stream.status.live': return 'LIVE'; case 'stream.status.live': return 'LIVE';
case 'stream.status.ended': return 'ENDED'; case 'stream.status.ended': return 'BEENDET';
case 'stream.status.planned': return 'GEPLANT'; case 'stream.status.planned': return 'GEPLANT';
case 'stream.started': return ({required Object timestamp}) => 'Gestartet ${timestamp}'; case 'stream.started': return ({required Object timestamp}) => 'Gestartet ${timestamp}';
case 'stream.notification': return ({required Object name}) => '${name} ging live!';
case 'stream.chat.disabled': return 'CHAT DEAKTIVIERT'; case 'stream.chat.disabled': return 'CHAT DEAKTIVIERT';
case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Die Zeitüberschreitung läuft ab: ${time}'; case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Timeout läuft ab: ${time}';
case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [ case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [
mod, mod,
const TextSpan(text: ' Zeitüberschreitung '), const TextSpan(text: ' gibt '),
user, user,
const TextSpan(text: ' für '), const TextSpan(text: ' einen Timeout für '),
time, time,
]); ]);
case 'stream.chat.ended': return 'STREAM BEENDET'; case 'stream.chat.ended': return 'STREAM BEENDET';

View File

@ -80,6 +80,7 @@ class _TranslationsStreamEl extends TranslationsStreamEn {
// Translations // Translations
@override late final _TranslationsStreamStatusEl status = _TranslationsStreamStatusEl._(_root); @override late final _TranslationsStreamStatusEl status = _TranslationsStreamStatusEl._(_root);
@override String started({required Object timestamp}) => 'Ξεκίνησε ${timestamp}'; @override String started({required Object timestamp}) => 'Ξεκίνησε ${timestamp}';
@override String notification({required Object name}) => '${name} βγήκε ζωντανά!';
@override late final _TranslationsStreamChatEl chat = _TranslationsStreamChatEl._(_root); @override late final _TranslationsStreamChatEl chat = _TranslationsStreamChatEl._(_root);
} }
@ -387,6 +388,7 @@ extension on TranslationsEl {
case 'stream.status.ended': return 'ENDED'; case 'stream.status.ended': return 'ENDED';
case 'stream.status.planned': return 'ΣΧΕΔΙΑΣΜΟΣ'; case 'stream.status.planned': return 'ΣΧΕΔΙΑΣΜΟΣ';
case 'stream.started': return ({required Object timestamp}) => 'Ξεκίνησε ${timestamp}'; case 'stream.started': return ({required Object timestamp}) => 'Ξεκίνησε ${timestamp}';
case 'stream.notification': return ({required Object name}) => '${name} βγήκε ζωντανά!';
case 'stream.chat.disabled': return 'ΑΠΕΝΕΡΓΟΠΟΙΗΜΈΝΗ ΣΥΝΟΜΙΛΊΑ'; case 'stream.chat.disabled': return 'ΑΠΕΝΕΡΓΟΠΟΙΗΜΈΝΗ ΣΥΝΟΜΙΛΊΑ';
case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Το χρονικό όριο λήγει: ${time}'; case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Το χρονικό όριο λήγει: ${time}';
case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [ case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [

View File

@ -52,6 +52,8 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
/// An anonymous user /// An anonymous user
String get anon => 'Anon'; String get anon => 'Anon';
String full_amount_sats({required num n}) => '${NumberFormat.decimalPattern('en').format(n)} sats';
/// Number of viewers of the stream /// Number of viewers of the stream
String viewers({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, String viewers({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n,
one: '1 viewer', one: '1 viewer',
@ -70,6 +72,7 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
late final TranslationsProfileEn profile = TranslationsProfileEn.internal(_root); late final TranslationsProfileEn profile = TranslationsProfileEn.internal(_root);
late final TranslationsSettingsEn settings = TranslationsSettingsEn.internal(_root); late final TranslationsSettingsEn settings = TranslationsSettingsEn.internal(_root);
late final TranslationsLoginEn login = TranslationsLoginEn.internal(_root); late final TranslationsLoginEn login = TranslationsLoginEn.internal(_root);
late final TranslationsLiveEn live = TranslationsLiveEn.internal(_root);
} }
// Path: stream // Path: stream
@ -81,6 +84,7 @@ class TranslationsStreamEn {
// Translations // Translations
late final TranslationsStreamStatusEn status = TranslationsStreamStatusEn.internal(_root); late final TranslationsStreamStatusEn status = TranslationsStreamStatusEn.internal(_root);
String started({required Object timestamp}) => 'Started ${timestamp}'; String started({required Object timestamp}) => 'Started ${timestamp}';
String notification({required Object name}) => '${name} went live!';
late final TranslationsStreamChatEn chat = TranslationsStreamChatEn.internal(_root); late final TranslationsStreamChatEn chat = TranslationsStreamChatEn.internal(_root);
} }
@ -205,6 +209,30 @@ class TranslationsLoginEn {
late final TranslationsLoginErrorEn error = TranslationsLoginErrorEn.internal(_root); late final TranslationsLoginErrorEn error = TranslationsLoginErrorEn.internal(_root);
} }
// Path: live
class TranslationsLiveEn {
TranslationsLiveEn.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
String get start => 'GO LIVE';
String get configure_stream => 'Configure Stream';
String get endpoint => 'Endpoint';
String get accept_tos => 'Accept TOS';
String balance_left({required num n, required Object time}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n,
zero: '',
other: '~${time}',
);
String get title => 'Title';
String get summary => 'Summary';
String get image => 'Cover Image';
String get tags => 'Tags';
String get nsfw => 'NSFW Content';
String get nsfw_description => 'Check here if this stream contains nudity or pornographic content.';
late final TranslationsLiveErrorEn error = TranslationsLiveErrorEn.internal(_root);
}
// Path: stream.status // Path: stream.status
class TranslationsStreamStatusEn { class TranslationsStreamStatusEn {
TranslationsStreamStatusEn.internal(this._root); TranslationsStreamStatusEn.internal(this._root);
@ -289,6 +317,8 @@ class TranslationsSettingsWalletEn {
String get disconnect_wallet => 'Disconnect Wallet'; String get disconnect_wallet => 'Disconnect Wallet';
String get connect_1tap => '1-Tap Connection'; String get connect_1tap => '1-Tap Connection';
String get paste => 'Paste URL'; String get paste => 'Paste URL';
String get balance => 'Balance';
String get name => 'Wallet';
late final TranslationsSettingsWalletErrorEn error = TranslationsSettingsWalletErrorEn.internal(_root); late final TranslationsSettingsWalletErrorEn error = TranslationsSettingsWalletErrorEn.internal(_root);
} }
@ -302,6 +332,18 @@ class TranslationsLoginErrorEn {
String get invalid_key => 'Invalid key'; String get invalid_key => 'Invalid key';
} }
// Path: live.error
class TranslationsLiveErrorEn {
TranslationsLiveErrorEn.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
String get failed => 'Stream failed';
String get connection_error => 'Connection Error';
String get start_failed => 'Stream start failed, please check your balance';
}
// Path: stream.chat.write // Path: stream.chat.write
class TranslationsStreamChatWriteEn { class TranslationsStreamChatWriteEn {
TranslationsStreamChatWriteEn.internal(this._root); TranslationsStreamChatWriteEn.internal(this._root);
@ -380,6 +422,7 @@ extension on Translations {
case 'most_zapped_streamers': return 'Most Zapped Streamers'; case 'most_zapped_streamers': return 'Most Zapped Streamers';
case 'no_user_found': return 'No user found'; case 'no_user_found': return 'No user found';
case 'anon': return 'Anon'; case 'anon': return 'Anon';
case 'full_amount_sats': return ({required num n}) => '${NumberFormat.decimalPattern('en').format(n)} sats';
case 'viewers': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, case 'viewers': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n,
one: '1 viewer', one: '1 viewer',
other: '${NumberFormat.decimalPattern('en').format(n)} viewers', other: '${NumberFormat.decimalPattern('en').format(n)} viewers',
@ -388,6 +431,7 @@ extension on Translations {
case 'stream.status.ended': return 'ENDED'; case 'stream.status.ended': return 'ENDED';
case 'stream.status.planned': return 'PLANNED'; case 'stream.status.planned': return 'PLANNED';
case 'stream.started': return ({required Object timestamp}) => 'Started ${timestamp}'; case 'stream.started': return ({required Object timestamp}) => 'Started ${timestamp}';
case 'stream.notification': return ({required Object name}) => '${name} went live!';
case 'stream.chat.disabled': return 'CHAT DISABLED'; case 'stream.chat.disabled': return 'CHAT DISABLED';
case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Timeout expires: ${time}'; 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}) => TextSpan(children: [ case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [
@ -456,6 +500,8 @@ extension on Translations {
case 'settings.wallet.disconnect_wallet': return 'Disconnect Wallet'; case 'settings.wallet.disconnect_wallet': return 'Disconnect Wallet';
case 'settings.wallet.connect_1tap': return '1-Tap Connection'; case 'settings.wallet.connect_1tap': return '1-Tap Connection';
case 'settings.wallet.paste': return 'Paste URL'; case 'settings.wallet.paste': return 'Paste URL';
case 'settings.wallet.balance': return 'Balance';
case 'settings.wallet.name': return 'Wallet';
case 'settings.wallet.error.logged_out': return 'Cant connect wallet when logged out'; case 'settings.wallet.error.logged_out': return 'Cant connect wallet when logged out';
case 'settings.wallet.error.nwc_auth_event_not_found': return 'No wallet auth event found'; case 'settings.wallet.error.nwc_auth_event_not_found': return 'No wallet auth event found';
case 'login.username': return 'Username'; case 'login.username': return 'Username';
@ -463,6 +509,23 @@ extension on Translations {
case 'login.key': return 'Login with Key'; case 'login.key': return 'Login with Key';
case 'login.create': return 'Create Account'; case 'login.create': return 'Create Account';
case 'login.error.invalid_key': return 'Invalid key'; case 'login.error.invalid_key': return 'Invalid key';
case 'live.start': return 'GO LIVE';
case 'live.configure_stream': return 'Configure Stream';
case 'live.endpoint': return 'Endpoint';
case 'live.accept_tos': return 'Accept TOS';
case 'live.balance_left': return ({required num n, required Object time}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n,
zero: '',
other: '~${time}',
);
case 'live.title': return 'Title';
case 'live.summary': return 'Summary';
case 'live.image': return 'Cover Image';
case 'live.tags': return 'Tags';
case 'live.nsfw': return 'NSFW Content';
case 'live.nsfw_description': return 'Check here if this stream contains nudity or pornographic content.';
case 'live.error.failed': return 'Stream failed';
case 'live.error.connection_error': return 'Connection Error';
case 'live.error.start_failed': return 'Stream start failed, please check your balance';
default: return null; default: return null;
} }
} }

View File

@ -80,6 +80,7 @@ class _TranslationsStreamEs extends TranslationsStreamEn {
// Translations // Translations
@override late final _TranslationsStreamStatusEs status = _TranslationsStreamStatusEs._(_root); @override late final _TranslationsStreamStatusEs status = _TranslationsStreamStatusEs._(_root);
@override String started({required Object timestamp}) => 'Comenzó ${timestamp}'; @override String started({required Object timestamp}) => 'Comenzó ${timestamp}';
@override String notification({required Object name}) => '${name} ¡se ha puesto en marcha!';
@override late final _TranslationsStreamChatEs chat = _TranslationsStreamChatEs._(_root); @override late final _TranslationsStreamChatEs chat = _TranslationsStreamChatEs._(_root);
} }
@ -387,6 +388,7 @@ extension on TranslationsEs {
case 'stream.status.ended': return 'FIN'; case 'stream.status.ended': return 'FIN';
case 'stream.status.planned': return 'PLANIFICADO'; case 'stream.status.planned': return 'PLANIFICADO';
case 'stream.started': return ({required Object timestamp}) => 'Comenzó ${timestamp}'; case 'stream.started': return ({required Object timestamp}) => 'Comenzó ${timestamp}';
case 'stream.notification': return ({required Object name}) => '${name} ¡se ha puesto en marcha!';
case 'stream.chat.disabled': return 'CHAT DESHABILITADO'; case 'stream.chat.disabled': return 'CHAT DESHABILITADO';
case 'stream.chat.disabled_timeout': return ({required Object time}) => 'El tiempo de espera expira: ${time}'; case 'stream.chat.disabled_timeout': return ({required Object time}) => 'El tiempo de espera expira: ${time}';
case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [ case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [

View File

@ -80,6 +80,7 @@ class _TranslationsStreamFi extends TranslationsStreamEn {
// Translations // Translations
@override late final _TranslationsStreamStatusFi status = _TranslationsStreamStatusFi._(_root); @override late final _TranslationsStreamStatusFi status = _TranslationsStreamStatusFi._(_root);
@override String started({required Object timestamp}) => 'Aloitettu ${timestamp}'; @override String started({required Object timestamp}) => 'Aloitettu ${timestamp}';
@override String notification({required Object name}) => '${name} meni suoraksi!';
@override late final _TranslationsStreamChatFi chat = _TranslationsStreamChatFi._(_root); @override late final _TranslationsStreamChatFi chat = _TranslationsStreamChatFi._(_root);
} }
@ -387,6 +388,7 @@ extension on TranslationsFi {
case 'stream.status.ended': return 'ENDED'; case 'stream.status.ended': return 'ENDED';
case 'stream.status.planned': return 'SUUNNITELTU'; case 'stream.status.planned': return 'SUUNNITELTU';
case 'stream.started': return ({required Object timestamp}) => 'Aloitettu ${timestamp}'; case 'stream.started': return ({required Object timestamp}) => 'Aloitettu ${timestamp}';
case 'stream.notification': return ({required Object name}) => '${name} meni suoraksi!';
case 'stream.chat.disabled': return 'CHAT POISTETTU KÄYTÖSTÄ'; case 'stream.chat.disabled': return 'CHAT POISTETTU KÄYTÖSTÄ';
case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Aikakatkaisu päättyy: ${time}'; case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Aikakatkaisu päättyy: ${time}';
case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [ case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [

View File

@ -54,7 +54,7 @@ class TranslationsFr extends Translations {
/// Nombre de spectateurs du flux /// Nombre de spectateurs du flux
@override String viewers({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('fr'))(n, @override String viewers({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('fr'))(n,
one: '1 téléspectateur', one: '1 téléspectateur',
other: '${NumberFormat.decimalPattern('fr').format(n)} téléspectateurs', other: '{n:decimalPattern} téléspectateurs',
); );
@override late final _TranslationsStreamFr stream = _TranslationsStreamFr._(_root); @override late final _TranslationsStreamFr stream = _TranslationsStreamFr._(_root);
@ -80,6 +80,7 @@ class _TranslationsStreamFr extends TranslationsStreamEn {
// Translations // Translations
@override late final _TranslationsStreamStatusFr status = _TranslationsStreamStatusFr._(_root); @override late final _TranslationsStreamStatusFr status = _TranslationsStreamStatusFr._(_root);
@override String started({required Object timestamp}) => 'Commencé à ${timestamp}'; @override String started({required Object timestamp}) => 'Commencé à ${timestamp}';
@override String notification({required Object name}) => '${name} est en ligne !';
@override late final _TranslationsStreamChatFr chat = _TranslationsStreamChatFr._(_root); @override late final _TranslationsStreamChatFr chat = _TranslationsStreamChatFr._(_root);
} }
@ -381,12 +382,13 @@ extension on TranslationsFr {
case 'anon': return 'Anonyme'; case 'anon': return 'Anonyme';
case 'viewers': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('fr'))(n, case 'viewers': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('fr'))(n,
one: '1 téléspectateur', one: '1 téléspectateur',
other: '${NumberFormat.decimalPattern('fr').format(n)} téléspectateurs', other: '{n:decimalPattern} téléspectateurs',
); );
case 'stream.status.live': return 'VIVRE'; case 'stream.status.live': return 'VIVRE';
case 'stream.status.ended': return 'FINI'; case 'stream.status.ended': return 'FINI';
case 'stream.status.planned': return 'PRÉVU'; case 'stream.status.planned': return 'PRÉVU';
case 'stream.started': return ({required Object timestamp}) => 'Commencé à ${timestamp}'; case 'stream.started': return ({required Object timestamp}) => 'Commencé à ${timestamp}';
case 'stream.notification': return ({required Object name}) => '${name} est en ligne !';
case 'stream.chat.disabled': return 'CHAT DISABLED'; case 'stream.chat.disabled': return 'CHAT DISABLED';
case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Le délai expire : ${time}'; case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Le délai expire : ${time}';
case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [ case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [

View File

@ -80,6 +80,7 @@ class _TranslationsStreamHu extends TranslationsStreamEn {
// Translations // Translations
@override late final _TranslationsStreamStatusHu status = _TranslationsStreamStatusHu._(_root); @override late final _TranslationsStreamStatusHu status = _TranslationsStreamStatusHu._(_root);
@override String started({required Object timestamp}) => 'Elindult ${timestamp}'; @override String started({required Object timestamp}) => 'Elindult ${timestamp}';
@override String notification({required Object name}) => '${name} elindult!';
@override late final _TranslationsStreamChatHu chat = _TranslationsStreamChatHu._(_root); @override late final _TranslationsStreamChatHu chat = _TranslationsStreamChatHu._(_root);
} }
@ -132,7 +133,7 @@ class _TranslationsEmbedHu extends TranslationsEmbedEn {
// Translations // Translations
@override String article_by({required Object name}) => 'Cikk ${name}'; @override String article_by({required Object name}) => 'Cikk ${name}';
@override String note_by({required Object name}) => '${name} bejegyzése'; @override String note_by({required Object name}) => '${name} bejegyzése';
@override String live_stream_by({required Object name}) => 'Élő közvetítés a ${name}oldalon'; @override String live_stream_by({required Object name}) => 'Élő közvetítés a ${name} oldalon';
} }
// Path: stream_list // Path: stream_list
@ -347,7 +348,7 @@ class _TranslationsStreamChatRaidHu extends TranslationsStreamChatRaidEn {
@override String from({required Object name}) => 'RAID FROM ${name}'; @override String from({required Object name}) => 'RAID FROM ${name}';
/// Visszaszámláló időzítő az automatikus lovagláshoz /// Visszaszámláló időzítő az automatikus lovagláshoz
@override String countdown({required Object time}) => 'Raiding a ${time}oldalon'; @override String countdown({required Object time}) => 'Raiding a ${time} oldalon';
} }
// Path: settings.profile.error // Path: settings.profile.error
@ -388,6 +389,7 @@ extension on TranslationsHu {
case 'stream.status.ended': return 'ENDED'; case 'stream.status.ended': return 'ENDED';
case 'stream.status.planned': return 'TERVEZETT'; case 'stream.status.planned': return 'TERVEZETT';
case 'stream.started': return ({required Object timestamp}) => 'Elindult ${timestamp}'; case 'stream.started': return ({required Object timestamp}) => 'Elindult ${timestamp}';
case 'stream.notification': return ({required Object name}) => '${name} elindult!';
case 'stream.chat.disabled': return 'CHAT KIKAPCSOLVA'; case 'stream.chat.disabled': return 'CHAT KIKAPCSOLVA';
case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Az időkorlát lejár: ${time}'; case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Az időkorlát lejár: ${time}';
case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [ case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [
@ -411,7 +413,7 @@ extension on TranslationsHu {
case 'stream.chat.badge.awarded_to': return 'Elnyerte:'; case 'stream.chat.badge.awarded_to': return 'Elnyerte:';
case 'stream.chat.raid.to': return ({required Object name}) => 'RAIDING ${name}'; 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.from': return ({required Object name}) => 'RAID FROM ${name}';
case 'stream.chat.raid.countdown': return ({required Object time}) => 'Raiding a ${time}oldalon'; case 'stream.chat.raid.countdown': return ({required Object time}) => 'Raiding a ${time} oldalon';
case 'goal.title': return ({required Object amount}) => 'Cél: ${amount}'; case 'goal.title': return ({required Object amount}) => 'Cél: ${amount}';
case 'goal.remaining': return ({required Object amount}) => 'Maradék: ${amount}'; case 'goal.remaining': return ({required Object amount}) => 'Maradék: ${amount}';
case 'goal.complete': return 'TELJES'; case 'goal.complete': return 'TELJES';
@ -428,7 +430,7 @@ extension on TranslationsHu {
case 'button.settings': return 'Beállítások'; case 'button.settings': return 'Beállítások';
case 'embed.article_by': return ({required Object name}) => 'Cikk ${name}'; case 'embed.article_by': return ({required Object name}) => 'Cikk ${name}';
case 'embed.note_by': return ({required Object name}) => '${name} bejegyzése'; case 'embed.note_by': return ({required Object name}) => '${name} bejegyzése';
case 'embed.live_stream_by': return ({required Object name}) => 'Élő közvetítés a ${name}oldalon'; case 'embed.live_stream_by': return ({required Object name}) => 'Élő közvetítés a ${name} oldalon';
case 'stream_list.following': return 'Követettek bejegyzései'; case 'stream_list.following': return 'Követettek bejegyzései';
case 'stream_list.live': return 'Élő'; case 'stream_list.live': return 'Élő';
case 'stream_list.planned': return 'Tervezett'; case 'stream_list.planned': return 'Tervezett';

View File

@ -80,6 +80,7 @@ class _TranslationsStreamIt extends TranslationsStreamEn {
// Translations // Translations
@override late final _TranslationsStreamStatusIt status = _TranslationsStreamStatusIt._(_root); @override late final _TranslationsStreamStatusIt status = _TranslationsStreamStatusIt._(_root);
@override String started({required Object timestamp}) => 'Avviato ${timestamp}'; @override String started({required Object timestamp}) => 'Avviato ${timestamp}';
@override String notification({required Object name}) => '${name} è andato in onda!';
@override late final _TranslationsStreamChatIt chat = _TranslationsStreamChatIt._(_root); @override late final _TranslationsStreamChatIt chat = _TranslationsStreamChatIt._(_root);
} }
@ -387,6 +388,7 @@ extension on TranslationsIt {
case 'stream.status.ended': return 'FINE'; case 'stream.status.ended': return 'FINE';
case 'stream.status.planned': return 'PREVISTO'; case 'stream.status.planned': return 'PREVISTO';
case 'stream.started': return ({required Object timestamp}) => 'Avviato ${timestamp}'; case 'stream.started': return ({required Object timestamp}) => 'Avviato ${timestamp}';
case 'stream.notification': return ({required Object name}) => '${name} è andato in onda!';
case 'stream.chat.disabled': return 'CHAT DISABILITATA'; case 'stream.chat.disabled': return 'CHAT DISABILITATA';
case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Il timeout scade: ${time}'; case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Il timeout scade: ${time}';
case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [ case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [

View File

@ -80,6 +80,7 @@ class _TranslationsStreamJa extends TranslationsStreamEn {
// Translations // Translations
@override late final _TranslationsStreamStatusJa status = _TranslationsStreamStatusJa._(_root); @override late final _TranslationsStreamStatusJa status = _TranslationsStreamStatusJa._(_root);
@override String started({required Object timestamp}) => '${timestamp} を開始'; @override String started({required Object timestamp}) => '${timestamp} を開始';
@override String notification({required Object name}) => '${name} がライブを開始した!';
@override late final _TranslationsStreamChatJa chat = _TranslationsStreamChatJa._(_root); @override late final _TranslationsStreamChatJa chat = _TranslationsStreamChatJa._(_root);
} }
@ -387,6 +388,7 @@ extension on TranslationsJa {
case 'stream.status.ended': return '終了'; case 'stream.status.ended': return '終了';
case 'stream.status.planned': return '予定'; case 'stream.status.planned': return '予定';
case 'stream.started': return ({required Object timestamp}) => '${timestamp} を開始'; case 'stream.started': return ({required Object timestamp}) => '${timestamp} を開始';
case 'stream.notification': return ({required Object name}) => '${name} がライブを開始した!';
case 'stream.chat.disabled': return 'チャット無効'; case 'stream.chat.disabled': return 'チャット無効';
case 'stream.chat.disabled_timeout': return ({required Object time}) => 'タイムアウト: ${time}'; case 'stream.chat.disabled_timeout': return ({required Object time}) => 'タイムアウト: ${time}';
case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [ case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [

View File

@ -54,7 +54,7 @@ class TranslationsKo extends Translations {
/// 스트림 시청자 수 /// 스트림 시청자 수
@override String viewers({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ko'))(n, @override String viewers({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ko'))(n,
one: '시청자 1명', one: '시청자 1명',
other: '${NumberFormat.decimalPattern('ko').format(n)} 시청자', other: '{n:decimalPattern} 시청자',
); );
@override late final _TranslationsStreamKo stream = _TranslationsStreamKo._(_root); @override late final _TranslationsStreamKo stream = _TranslationsStreamKo._(_root);
@ -80,6 +80,7 @@ class _TranslationsStreamKo extends TranslationsStreamEn {
// Translations // Translations
@override late final _TranslationsStreamStatusKo status = _TranslationsStreamStatusKo._(_root); @override late final _TranslationsStreamStatusKo status = _TranslationsStreamStatusKo._(_root);
@override String started({required Object timestamp}) => '시작 ${timestamp}'; @override String started({required Object timestamp}) => '시작 ${timestamp}';
@override String notification({required Object name}) => '${name} 라이브가 시작되었습니다!';
@override late final _TranslationsStreamChatKo chat = _TranslationsStreamChatKo._(_root); @override late final _TranslationsStreamChatKo chat = _TranslationsStreamChatKo._(_root);
} }
@ -381,12 +382,13 @@ extension on TranslationsKo {
case 'anon': return 'Anon'; case 'anon': return 'Anon';
case 'viewers': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ko'))(n, case 'viewers': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ko'))(n,
one: '시청자 1명', one: '시청자 1명',
other: '${NumberFormat.decimalPattern('ko').format(n)} 시청자', other: '{n:decimalPattern} 시청자',
); );
case 'stream.status.live': return '라이브'; case 'stream.status.live': return '라이브';
case 'stream.status.ended': return '종료'; case 'stream.status.ended': return '종료';
case 'stream.status.planned': return '계획된'; case 'stream.status.planned': return '계획된';
case 'stream.started': return ({required Object timestamp}) => '시작 ${timestamp}'; case 'stream.started': return ({required Object timestamp}) => '시작 ${timestamp}';
case 'stream.notification': return ({required Object name}) => '${name} 라이브가 시작되었습니다!';
case 'stream.chat.disabled': return '채팅 사용 안 함'; case 'stream.chat.disabled': return '채팅 사용 안 함';
case 'stream.chat.disabled_timeout': return ({required Object time}) => '시간 초과가 만료되었습니다: ${time}'; case 'stream.chat.disabled_timeout': return ({required Object time}) => '시간 초과가 만료되었습니다: ${time}';
case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [ case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [

View File

@ -80,6 +80,7 @@ class _TranslationsStreamNl extends TranslationsStreamEn {
// Translations // Translations
@override late final _TranslationsStreamStatusNl status = _TranslationsStreamStatusNl._(_root); @override late final _TranslationsStreamStatusNl status = _TranslationsStreamStatusNl._(_root);
@override String started({required Object timestamp}) => 'Begonnen met ${timestamp}'; @override String started({required Object timestamp}) => 'Begonnen met ${timestamp}';
@override String notification({required Object name}) => '${name} ging live!';
@override late final _TranslationsStreamChatNl chat = _TranslationsStreamChatNl._(_root); @override late final _TranslationsStreamChatNl chat = _TranslationsStreamChatNl._(_root);
} }
@ -387,6 +388,7 @@ extension on TranslationsNl {
case 'stream.status.ended': return 'GESLOTEN'; case 'stream.status.ended': return 'GESLOTEN';
case 'stream.status.planned': return 'GEPLAND'; case 'stream.status.planned': return 'GEPLAND';
case 'stream.started': return ({required Object timestamp}) => 'Begonnen met ${timestamp}'; case 'stream.started': return ({required Object timestamp}) => 'Begonnen met ${timestamp}';
case 'stream.notification': return ({required Object name}) => '${name} ging live!';
case 'stream.chat.disabled': return 'CHAT UITGESCHAKELD'; case 'stream.chat.disabled': return 'CHAT UITGESCHAKELD';
case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Time-out loopt af: ${time}'; case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Time-out loopt af: ${time}';
case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [ case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [

View File

@ -80,6 +80,7 @@ class _TranslationsStreamPl extends TranslationsStreamEn {
// Translations // Translations
@override late final _TranslationsStreamStatusPl status = _TranslationsStreamStatusPl._(_root); @override late final _TranslationsStreamStatusPl status = _TranslationsStreamStatusPl._(_root);
@override String started({required Object timestamp}) => 'Start ${timestamp}'; @override String started({required Object timestamp}) => 'Start ${timestamp}';
@override String notification({required Object name}) => '${name} został uruchomiony!';
@override late final _TranslationsStreamChatPl chat = _TranslationsStreamChatPl._(_root); @override late final _TranslationsStreamChatPl chat = _TranslationsStreamChatPl._(_root);
} }
@ -387,6 +388,7 @@ extension on TranslationsPl {
case 'stream.status.ended': return 'ZAKOŃCZONY'; case 'stream.status.ended': return 'ZAKOŃCZONY';
case 'stream.status.planned': return 'PLANOWANE'; case 'stream.status.planned': return 'PLANOWANE';
case 'stream.started': return ({required Object timestamp}) => 'Start ${timestamp}'; case 'stream.started': return ({required Object timestamp}) => 'Start ${timestamp}';
case 'stream.notification': return ({required Object name}) => '${name} został uruchomiony!';
case 'stream.chat.disabled': return 'CZAT WYŁĄCZONY'; case 'stream.chat.disabled': return 'CZAT WYŁĄCZONY';
case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Upłynął limit czasu: ${time}'; case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Upłynął limit czasu: ${time}';
case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [ case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [

View File

@ -80,6 +80,7 @@ class _TranslationsStreamPt extends TranslationsStreamEn {
// Translations // Translations
@override late final _TranslationsStreamStatusPt status = _TranslationsStreamStatusPt._(_root); @override late final _TranslationsStreamStatusPt status = _TranslationsStreamStatusPt._(_root);
@override String started({required Object timestamp}) => 'Iniciado em ${timestamp}'; @override String started({required Object timestamp}) => 'Iniciado em ${timestamp}';
@override String notification({required Object name}) => '${name} foi ao ar!';
@override late final _TranslationsStreamChatPt chat = _TranslationsStreamChatPt._(_root); @override late final _TranslationsStreamChatPt chat = _TranslationsStreamChatPt._(_root);
} }
@ -387,6 +388,7 @@ extension on TranslationsPt {
case 'stream.status.ended': return 'FINALIZADO'; case 'stream.status.ended': return 'FINALIZADO';
case 'stream.status.planned': return 'PLANEJADO'; case 'stream.status.planned': return 'PLANEJADO';
case 'stream.started': return ({required Object timestamp}) => 'Iniciado em ${timestamp}'; case 'stream.started': return ({required Object timestamp}) => 'Iniciado em ${timestamp}';
case 'stream.notification': return ({required Object name}) => '${name} foi ao ar!';
case 'stream.chat.disabled': return 'BATE-PAPO DESATIVADO'; case 'stream.chat.disabled': return 'BATE-PAPO DESATIVADO';
case 'stream.chat.disabled_timeout': return ({required Object time}) => 'O tempo limite expira: ${time}'; case 'stream.chat.disabled_timeout': return ({required Object time}) => 'O tempo limite expira: ${time}';
case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [ case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [

View File

@ -80,6 +80,7 @@ class _TranslationsStreamRo extends TranslationsStreamEn {
// Translations // Translations
@override late final _TranslationsStreamStatusRo status = _TranslationsStreamStatusRo._(_root); @override late final _TranslationsStreamStatusRo status = _TranslationsStreamStatusRo._(_root);
@override String started({required Object timestamp}) => 'A început ${timestamp}'; @override String started({required Object timestamp}) => 'A început ${timestamp}';
@override String notification({required Object name}) => '${name} a intrat în direct!';
@override late final _TranslationsStreamChatRo chat = _TranslationsStreamChatRo._(_root); @override late final _TranslationsStreamChatRo chat = _TranslationsStreamChatRo._(_root);
} }
@ -387,6 +388,7 @@ extension on TranslationsRo {
case 'stream.status.ended': return 'TERMINAT'; case 'stream.status.ended': return 'TERMINAT';
case 'stream.status.planned': return 'PLANIFICATE'; case 'stream.status.planned': return 'PLANIFICATE';
case 'stream.started': return ({required Object timestamp}) => 'A început ${timestamp}'; case 'stream.started': return ({required Object timestamp}) => 'A început ${timestamp}';
case 'stream.notification': return ({required Object name}) => '${name} a intrat în direct!';
case 'stream.chat.disabled': return 'CHAT DEZACTIVAT'; case 'stream.chat.disabled': return 'CHAT DEZACTIVAT';
case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Timpul expiră: ${time}'; case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Timpul expiră: ${time}';
case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [ case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [

View File

@ -80,6 +80,7 @@ class _TranslationsStreamRu extends TranslationsStreamEn {
// Translations // Translations
@override late final _TranslationsStreamStatusRu status = _TranslationsStreamStatusRu._(_root); @override late final _TranslationsStreamStatusRu status = _TranslationsStreamStatusRu._(_root);
@override String started({required Object timestamp}) => 'Начало ${timestamp}'; @override String started({required Object timestamp}) => 'Начало ${timestamp}';
@override String notification({required Object name}) => '${name} запустился!';
@override late final _TranslationsStreamChatRu chat = _TranslationsStreamChatRu._(_root); @override late final _TranslationsStreamChatRu chat = _TranslationsStreamChatRu._(_root);
} }
@ -387,6 +388,7 @@ extension on TranslationsRu {
case 'stream.status.ended': return 'КОНЕЦ'; case 'stream.status.ended': return 'КОНЕЦ';
case 'stream.status.planned': return 'ПЛАНИРУЕМЫЙ'; case 'stream.status.planned': return 'ПЛАНИРУЕМЫЙ';
case 'stream.started': return ({required Object timestamp}) => 'Начало ${timestamp}'; case 'stream.started': return ({required Object timestamp}) => 'Начало ${timestamp}';
case 'stream.notification': return ({required Object name}) => '${name} запустился!';
case 'stream.chat.disabled': return 'ЧАТ ОТКЛЮЧЕН'; case 'stream.chat.disabled': return 'ЧАТ ОТКЛЮЧЕН';
case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Таймаут истекает: ${time}'; case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Таймаут истекает: ${time}';
case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [ case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [

View File

@ -39,19 +39,19 @@ class TranslationsSv extends Translations {
// Translations // Translations
/// Text som uppmanar användaren att trycka på avatarplatshållaren för att påbörja uppladdningen /// Text som uppmanar användaren att trycka på avatar platshållaren för att påbörja uppladdningen
@override String get upload_avatar => 'Ladda upp avatar'; @override String get upload_avatar => 'Ladda upp avatar';
/// Rubrik över listade toppstreamers av zaps /// Rubrik över listade topp streamers av zaps
@override String get most_zapped_streamers => 'De flesta zappade streamers'; @override String get most_zapped_streamers => 'De flest zappade streamers';
/// Ingen användare hittades vid sökning /// Ingen användare hittades vid sökning
@override String get no_user_found => 'Ingen användare hittades'; @override String get no_user_found => 'Ingen användare hittades';
/// En anonym användare /// En anonym användare
@override String get anon => 'Anon'; @override String get anon => 'Anno';
/// Antal tittare på streamingen /// Antal tittare på strömmingen
@override String viewers({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('sv'))(n, @override String viewers({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('sv'))(n,
one: '1 tittare', one: '1 tittare',
other: '${NumberFormat.decimalPattern('sv').format(n)} tittare', other: '${NumberFormat.decimalPattern('sv').format(n)} tittare',
@ -79,7 +79,8 @@ class _TranslationsStreamSv extends TranslationsStreamEn {
// Translations // Translations
@override late final _TranslationsStreamStatusSv status = _TranslationsStreamStatusSv._(_root); @override late final _TranslationsStreamStatusSv status = _TranslationsStreamStatusSv._(_root);
@override String started({required Object timestamp}) => 'Startade ${timestamp}'; @override String started({required Object timestamp}) => 'Startad ${timestamp}';
@override String notification({required Object name}) => '${name} gick live!';
@override late final _TranslationsStreamChatSv chat = _TranslationsStreamChatSv._(_root); @override late final _TranslationsStreamChatSv chat = _TranslationsStreamChatSv._(_root);
} }
@ -112,7 +113,7 @@ class _TranslationsButtonSv extends TranslationsButtonEn {
/// Knapptext för följ-knappen /// Knapptext för följ-knappen
@override String get follow => 'Följ'; @override String get follow => 'Följ';
/// Knapptext för avföljningsknappen /// Knapptext för sluta följa knappen
@override String get unfollow => 'Sluta följa'; @override String get unfollow => 'Sluta följa';
@override String get mute => 'Tysta'; @override String get mute => 'Tysta';
@ -235,7 +236,7 @@ class _TranslationsStreamChatSv extends TranslationsStreamChatEn {
time, time,
]); ]);
/// Stream avslutade sidfoten längst ner på chatten /// Streama slutade sidfot längst ned i chatten
@override String get ended => 'STREAM AVSLUTAD'; @override String get ended => 'STREAM AVSLUTAD';
/// Chattmeddelande som visar strömavbrott /// Chattmeddelande som visar strömavbrott
@ -272,8 +273,8 @@ class _TranslationsSettingsProfileSv extends TranslationsSettingsProfileEn {
// Translations // Translations
@override String get display_name => 'Visa namn'; @override String get display_name => 'Visa namn';
@override String get about => 'Om'; @override String get about => 'Om';
@override String get nip05 => 'Nostr Adress'; @override String get nip05 => 'Nostr adress';
@override String get lud16 => 'Adress för blixtnedslag'; @override String get lud16 => 'Lightning-adress';
@override late final _TranslationsSettingsProfileErrorSv error = _TranslationsSettingsProfileErrorSv._(_root); @override late final _TranslationsSettingsProfileErrorSv error = _TranslationsSettingsProfileErrorSv._(_root);
} }
@ -284,9 +285,9 @@ class _TranslationsSettingsWalletSv extends TranslationsSettingsWalletEn {
final TranslationsSv _root; // ignore: unused_field final TranslationsSv _root; // ignore: unused_field
// Translations // Translations
@override String get connect_wallet => 'Connect plånbok (NWC nostr+walletconnect://)'; @override String get connect_wallet => 'Anslut plånbok (NWC nostr+walletconnect://)';
@override String get disconnect_wallet => 'Koppla bort plånboken'; @override String get disconnect_wallet => 'Koppla bort plånboken';
@override String get connect_1tap => '1-Tap-anslutning'; @override String get connect_1tap => '1-tryck anslutning';
@override String get paste => 'Klistra in URL'; @override String get paste => 'Klistra in URL';
@override late final _TranslationsSettingsWalletErrorSv error = _TranslationsSettingsWalletErrorSv._(_root); @override late final _TranslationsSettingsWalletErrorSv error = _TranslationsSettingsWalletErrorSv._(_root);
} }
@ -312,8 +313,8 @@ class _TranslationsStreamChatWriteSv extends TranslationsStreamChatWriteEn {
/// Etikett på inmatningsrutan för chattmeddelanden /// Etikett på inmatningsrutan för chattmeddelanden
@override String get label => 'Skriv meddelande'; @override String get label => 'Skriv meddelande';
/// Chattinmatningsmeddelande som visas när användaren endast är inloggad med pubkey /// Chattinmatningsmeddelande som visas när användaren endast är inloggad med publik nyckel
@override String get no_signer => 'Det går inte att skriva meddelanden med npub-inloggning'; @override String get no_signer => 'Det går inte att skriva meddelanden med n-pub inloggning';
/// Chattinmatningsmeddelande som visas när användaren är utloggad /// Chattinmatningsmeddelande som visas när användaren är utloggad
@override String get login => 'Logga in för att skicka meddelanden'; @override String get login => 'Logga in för att skicka meddelanden';
@ -327,7 +328,7 @@ class _TranslationsStreamChatBadgeSv extends TranslationsStreamChatBadgeEn {
// Translations // Translations
/// Rubrik över lista över användare som tilldelats en badge /// Rubrik över listan över användare som tilldelas ett märke
@override String get awarded_to => 'Tilldelas till:'; @override String get awarded_to => 'Tilldelas till:';
} }
@ -339,14 +340,14 @@ class _TranslationsStreamChatRaidSv extends TranslationsStreamChatRaidEn {
// Translations // Translations
/// Chatta raidmeddelande till en annan ström /// Chatt raid meddelande till en annan ström
@override String to({required Object name}) => 'RAIDING ${name}'; @override String to({required Object name}) => 'RAIDING ${name}';
/// Chat raid-meddelande från en annan ström /// Chatt raid meddelande från en annan ström
@override String from({required Object name}) => 'RAID FRÅN ${name}'; @override String from({required Object name}) => 'RAID FRÅN ${name}';
/// Nedräkningstimer för auto-raiding /// Nedräkningstimer för auto- radiering
@override String countdown({required Object time}) => 'Raiding ${time}'; @override String countdown({required Object time}) => 'Radiering i ${time}';
} }
// Path: settings.profile.error // Path: settings.profile.error
@ -366,7 +367,7 @@ class _TranslationsSettingsWalletErrorSv extends TranslationsSettingsWalletError
final TranslationsSv _root; // ignore: unused_field final TranslationsSv _root; // ignore: unused_field
// Translations // Translations
@override String get logged_out => 'Kan inte ansluta plånbok när du är inloggad'; @override String get logged_out => 'Kan inte ansluta plånbok när du är utloggad';
@override String get nwc_auth_event_not_found => 'Inget autentiseringshändelse för plånbok hittades'; @override String get nwc_auth_event_not_found => 'Inget autentiseringshändelse för plånbok hittades';
} }
@ -376,9 +377,9 @@ extension on TranslationsSv {
dynamic _flatMapFunction(String path) { dynamic _flatMapFunction(String path) {
switch (path) { switch (path) {
case 'upload_avatar': return 'Ladda upp avatar'; case 'upload_avatar': return 'Ladda upp avatar';
case 'most_zapped_streamers': return 'De flesta zappade streamers'; case 'most_zapped_streamers': return 'De flest zappade streamers';
case 'no_user_found': return 'Ingen användare hittades'; case 'no_user_found': return 'Ingen användare hittades';
case 'anon': return 'Anon'; case 'anon': return 'Anno';
case 'viewers': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('sv'))(n, case 'viewers': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('sv'))(n,
one: '1 tittare', one: '1 tittare',
other: '${NumberFormat.decimalPattern('sv').format(n)} tittare', other: '${NumberFormat.decimalPattern('sv').format(n)} tittare',
@ -386,7 +387,8 @@ extension on TranslationsSv {
case 'stream.status.live': return 'LIVE'; case 'stream.status.live': return 'LIVE';
case 'stream.status.ended': return 'AVSLUTAD'; case 'stream.status.ended': return 'AVSLUTAD';
case 'stream.status.planned': return 'PLANERADE'; case 'stream.status.planned': return 'PLANERADE';
case 'stream.started': return ({required Object timestamp}) => 'Startade ${timestamp}'; case 'stream.started': return ({required Object timestamp}) => 'Startad ${timestamp}';
case 'stream.notification': return ({required Object name}) => '${name} gick live!';
case 'stream.chat.disabled': return 'CHAT AVSTÄNGD'; case 'stream.chat.disabled': return 'CHAT AVSTÄNGD';
case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Tidsgränsen går ut: ${time}'; case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Tidsgränsen går ut: ${time}';
case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [ case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [
@ -404,12 +406,12 @@ extension on TranslationsSv {
const TextSpan(text: ' sats'), const TextSpan(text: ' sats'),
]); ]);
case 'stream.chat.write.label': return 'Skriv meddelande'; case 'stream.chat.write.label': return 'Skriv meddelande';
case 'stream.chat.write.no_signer': return 'Det går inte att skriva meddelanden med npub-inloggning'; case 'stream.chat.write.no_signer': return 'Det går inte att skriva meddelanden med n-pub inloggning';
case 'stream.chat.write.login': return 'Logga in för att skicka meddelanden'; case 'stream.chat.write.login': return 'Logga in för att skicka meddelanden';
case 'stream.chat.badge.awarded_to': return 'Tilldelas till:'; case 'stream.chat.badge.awarded_to': return 'Tilldelas till:';
case 'stream.chat.raid.to': return ({required Object name}) => 'RAIDING ${name}'; case 'stream.chat.raid.to': return ({required Object name}) => 'RAIDING ${name}';
case 'stream.chat.raid.from': return ({required Object name}) => 'RAID FRÅN ${name}'; case 'stream.chat.raid.from': return ({required Object name}) => 'RAID FRÅN ${name}';
case 'stream.chat.raid.countdown': return ({required Object time}) => 'Raiding ${time}'; case 'stream.chat.raid.countdown': return ({required Object time}) => 'Radiering i ${time}';
case 'goal.title': return ({required Object amount}) => 'Mål: ${amount}'; case 'goal.title': return ({required Object amount}) => 'Mål: ${amount}';
case 'goal.remaining': return ({required Object amount}) => 'Kvarvarande: ${amount}'; case 'goal.remaining': return ({required Object amount}) => 'Kvarvarande: ${amount}';
case 'goal.complete': return 'KOMPLETT'; case 'goal.complete': return 'KOMPLETT';
@ -448,14 +450,14 @@ extension on TranslationsSv {
case 'settings.button_wallet': return 'Inställningar för plånbok'; case 'settings.button_wallet': return 'Inställningar för plånbok';
case 'settings.profile.display_name': return 'Visa namn'; case 'settings.profile.display_name': return 'Visa namn';
case 'settings.profile.about': return 'Om'; case 'settings.profile.about': return 'Om';
case 'settings.profile.nip05': return 'Nostr Adress'; case 'settings.profile.nip05': return 'Nostr adress';
case 'settings.profile.lud16': return 'Adress för blixtnedslag'; case 'settings.profile.lud16': return 'Lightning-adress';
case 'settings.profile.error.logged_out': return 'Kan inte redigera profil när jag är utloggad'; case 'settings.profile.error.logged_out': return 'Kan inte redigera profil när jag är utloggad';
case 'settings.wallet.connect_wallet': return 'Connect plånbok (NWC nostr+walletconnect://)'; case 'settings.wallet.connect_wallet': return 'Anslut plånbok (NWC nostr+walletconnect://)';
case 'settings.wallet.disconnect_wallet': return 'Koppla bort plånboken'; case 'settings.wallet.disconnect_wallet': return 'Koppla bort plånboken';
case 'settings.wallet.connect_1tap': return '1-Tap-anslutning'; case 'settings.wallet.connect_1tap': return '1-tryck anslutning';
case 'settings.wallet.paste': return 'Klistra in URL'; case 'settings.wallet.paste': return 'Klistra in URL';
case 'settings.wallet.error.logged_out': return 'Kan inte ansluta plånbok när du är inloggad'; case 'settings.wallet.error.logged_out': return 'Kan inte ansluta plånbok när du är utloggad';
case 'settings.wallet.error.nwc_auth_event_not_found': return 'Inget autentiseringshändelse för plånbok hittades'; case 'settings.wallet.error.nwc_auth_event_not_found': return 'Inget autentiseringshändelse för plånbok hittades';
case 'login.username': return 'Användarnamn'; case 'login.username': return 'Användarnamn';
case 'login.amber': return 'Logga in med Amber'; case 'login.amber': return 'Logga in med Amber';

View File

@ -80,6 +80,7 @@ class _TranslationsStreamTr extends TranslationsStreamEn {
// Translations // Translations
@override late final _TranslationsStreamStatusTr status = _TranslationsStreamStatusTr._(_root); @override late final _TranslationsStreamStatusTr status = _TranslationsStreamStatusTr._(_root);
@override String started({required Object timestamp}) => 'Başlatıldı ${timestamp}'; @override String started({required Object timestamp}) => 'Başlatıldı ${timestamp}';
@override String notification({required Object name}) => '${name} yayına girdi!';
@override late final _TranslationsStreamChatTr chat = _TranslationsStreamChatTr._(_root); @override late final _TranslationsStreamChatTr chat = _TranslationsStreamChatTr._(_root);
} }
@ -231,8 +232,9 @@ class _TranslationsStreamChatTr extends TranslationsStreamChatEn {
mod, mod,
const TextSpan(text: ' zaman aşımına uğradı '), const TextSpan(text: ' zaman aşımına uğradı '),
user, user,
const TextSpan(text: ' için '), const TextSpan(text: ' '),
time, time,
const TextSpan(text: 'için'),
]); ]);
/// Sohbetin alt kısmında akış sona erdi altbilgisi /// Sohbetin alt kısmında akış sona erdi altbilgisi
@ -343,7 +345,7 @@ class _TranslationsStreamChatRaidTr extends TranslationsStreamChatRaidEn {
@override String to({required Object name}) => 'RAIDING ${name}'; @override String to({required Object name}) => 'RAIDING ${name}';
/// Başka bir akıştan sohbet baskını mesajı /// Başka bir akıştan sohbet baskını mesajı
@override String from({required Object name}) => '${name}ADRESINDEN RAID'; @override String from({required Object name}) => '${name} ADRESINDEN RAID';
/// Otomatik sürüş için geri sayım sayacı /// Otomatik sürüş için geri sayım sayacı
@override String countdown({required Object time}) => '${time}adresinde baskın'; @override String countdown({required Object time}) => '${time}adresinde baskın';
@ -387,14 +389,16 @@ extension on TranslationsTr {
case 'stream.status.ended': return 'SONLANDI'; case 'stream.status.ended': return 'SONLANDI';
case 'stream.status.planned': return 'PLANLANMIŞ'; case 'stream.status.planned': return 'PLANLANMIŞ';
case 'stream.started': return ({required Object timestamp}) => 'Başlatıldı ${timestamp}'; case 'stream.started': return ({required Object timestamp}) => 'Başlatıldı ${timestamp}';
case 'stream.notification': return ({required Object name}) => '${name} yayına girdi!';
case 'stream.chat.disabled': return 'SOHBET DEVRE DIŞI'; case 'stream.chat.disabled': return 'SOHBET DEVRE DIŞI';
case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Zaman aşımı sona eriyor: ${time}'; case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Zaman aşımı sona eriyor: ${time}';
case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [ case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [
mod, mod,
const TextSpan(text: ' zaman aşımına uğradı '), const TextSpan(text: ' zaman aşımına uğradı '),
user, user,
const TextSpan(text: ' için '), const TextSpan(text: ' '),
time, time,
const TextSpan(text: 'için'),
]); ]);
case 'stream.chat.ended': return 'YAYIN SONLANDI'; case 'stream.chat.ended': return 'YAYIN SONLANDI';
case 'stream.chat.zap': return ({required InlineSpan user, required InlineSpan amount}) => TextSpan(children: [ case 'stream.chat.zap': return ({required InlineSpan user, required InlineSpan amount}) => TextSpan(children: [
@ -408,7 +412,7 @@ extension on TranslationsTr {
case 'stream.chat.write.login': return 'Mesaj göndermek için lütfen giriş yapın'; case 'stream.chat.write.login': return 'Mesaj göndermek için lütfen giriş yapın';
case 'stream.chat.badge.awarded_to': return 'Ödüllendirildi:'; case 'stream.chat.badge.awarded_to': return 'Ödüllendirildi:';
case 'stream.chat.raid.to': return ({required Object name}) => 'RAIDING ${name}'; case 'stream.chat.raid.to': return ({required Object name}) => 'RAIDING ${name}';
case 'stream.chat.raid.from': return ({required Object name}) => '${name}ADRESINDEN RAID'; case 'stream.chat.raid.from': return ({required Object name}) => '${name} ADRESINDEN RAID';
case 'stream.chat.raid.countdown': return ({required Object time}) => '${time}adresinde baskın'; case 'stream.chat.raid.countdown': return ({required Object time}) => '${time}adresinde baskın';
case 'goal.title': return ({required Object amount}) => 'Hedef: ${amount}'; case 'goal.title': return ({required Object amount}) => 'Hedef: ${amount}';
case 'goal.remaining': return ({required Object amount}) => 'Kalan: ${amount}'; case 'goal.remaining': return ({required Object amount}) => 'Kalan: ${amount}';

View File

@ -80,6 +80,7 @@ class _TranslationsStreamUk extends TranslationsStreamEn {
// Translations // Translations
@override late final _TranslationsStreamStatusUk status = _TranslationsStreamStatusUk._(_root); @override late final _TranslationsStreamStatusUk status = _TranslationsStreamStatusUk._(_root);
@override String started({required Object timestamp}) => 'Запустив ${timestamp}'; @override String started({required Object timestamp}) => 'Запустив ${timestamp}';
@override String notification({required Object name}) => '${name} запрацював!';
@override late final _TranslationsStreamChatUk chat = _TranslationsStreamChatUk._(_root); @override late final _TranslationsStreamChatUk chat = _TranslationsStreamChatUk._(_root);
} }
@ -387,6 +388,7 @@ extension on TranslationsUk {
case 'stream.status.ended': return 'ЗАКІНЧЕНО'; case 'stream.status.ended': return 'ЗАКІНЧЕНО';
case 'stream.status.planned': return 'ЗАПЛАНОВАНО'; case 'stream.status.planned': return 'ЗАПЛАНОВАНО';
case 'stream.started': return ({required Object timestamp}) => 'Запустив ${timestamp}'; case 'stream.started': return ({required Object timestamp}) => 'Запустив ${timestamp}';
case 'stream.notification': return ({required Object name}) => '${name} запрацював!';
case 'stream.chat.disabled': return 'ЧАТ ВІДКЛЮЧЕНО'; case 'stream.chat.disabled': return 'ЧАТ ВІДКЛЮЧЕНО';
case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Тайм-аут закінчився: ${time}'; case 'stream.chat.disabled_timeout': return ({required Object time}) => 'Тайм-аут закінчився: ${time}';
case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [ case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [

View File

@ -80,6 +80,7 @@ class _TranslationsStreamZh extends TranslationsStreamEn {
// Translations // Translations
@override late final _TranslationsStreamStatusZh status = _TranslationsStreamStatusZh._(_root); @override late final _TranslationsStreamStatusZh status = _TranslationsStreamStatusZh._(_root);
@override String started({required Object timestamp}) => '開始 ${timestamp}'; @override String started({required Object timestamp}) => '開始 ${timestamp}';
@override String notification({required Object name}) => '${name} 已啟用!';
@override late final _TranslationsStreamChatZh chat = _TranslationsStreamChatZh._(_root); @override late final _TranslationsStreamChatZh chat = _TranslationsStreamChatZh._(_root);
} }
@ -387,6 +388,7 @@ extension on TranslationsZh {
case 'stream.status.ended': return '結束'; case 'stream.status.ended': return '結束';
case 'stream.status.planned': return '計劃'; case 'stream.status.planned': return '計劃';
case 'stream.started': return ({required Object timestamp}) => '開始 ${timestamp}'; case 'stream.started': return ({required Object timestamp}) => '開始 ${timestamp}';
case 'stream.notification': return ({required Object name}) => '${name} 已啟用!';
case 'stream.chat.disabled': return '關閉聊天'; case 'stream.chat.disabled': return '關閉聊天';
case 'stream.chat.disabled_timeout': return ({required Object time}) => '超時過期: ${time}'; case 'stream.chat.disabled_timeout': return ({required Object time}) => '超時過期: ${time}';
case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [ case 'stream.chat.timeout': return ({required InlineSpan mod, required InlineSpan user, required InlineSpan time}) => TextSpan(children: [

View File

@ -10,7 +10,7 @@ no_user_found: لم يتم العثور على مستخدم
anon: هوية مخفية anon: هوية مخفية
viewers: viewers:
one: 1 مشاهد one: 1 مشاهد
other: "${n:decimalPattern} المشاهدين" other: "{n:decimalPattern} المشاهدين"
"@viewers": "@viewers":
description: عدد مشاهدي البث description: عدد مشاهدي البث
"@anon": "@anon":
@ -21,10 +21,11 @@ stream:
ended: انتهى ended: انتهى
planned: مخطط planned: مخطط
started: بدأ $timestamp started: بدأ $timestamp
notification: ${name} بدأ البث المباشر!
chat: chat:
disabled: تم تعطيل الدردشة disabled: تم تعطيل الدردشة
disabled_timeout: "تنتهي المهلة: $time" disabled_timeout: "تنتهي المهلة: $time"
timeout(rich): $mod انتهى الوقت $user لـ $time timeout(rich): $mod انتهى الوقت $user لـ ${time}
"@timeout": "@timeout":
description: رسالة دردشة تظهر أحداث المهلة description: رسالة دردشة تظهر أحداث المهلة
ended: انتهى البث ended: انتهى البث

View File

@ -22,10 +22,11 @@ stream:
ended: KONEC ended: KONEC
planned: PLÁNOVANÉ planned: PLÁNOVANÉ
started: Založeno $timestamp started: Založeno $timestamp
notification: ${name} byl spuštěn!
chat: chat:
disabled: CHAT ZRUŠEN disabled: CHAT ZRUŠEN
disabled_timeout: "Časový limit vyprší: $time" disabled_timeout: "Časový limit vyprší: $time"
timeout(rich): $mod vypršel čas $user pro $time timeout(rich): $mod vypršel čas $user pro ${time}
"@timeout": "@timeout":
description: Zpráva chatu zobrazující události časového limitu description: Zpráva chatu zobrazující události časového limitu
ended: STREAM UKONČEN ended: STREAM UKONČEN

View File

@ -22,10 +22,11 @@ stream:
ended: AFSLUTTET ended: AFSLUTTET
planned: PLANLAGT planned: PLANLAGT
started: Startet $timestamp started: Startet $timestamp
notification: ${name} gik live!
chat: chat:
disabled: CHAT DEAKTIVERET disabled: CHAT DEAKTIVERET
disabled_timeout: "Timeout udløber: $time" disabled_timeout: "Timeout udløber: $time"
timeout(rich): $mod udløbet $user for $time timeout(rich): $mod udløbet $user for ${time}
"@timeout": "@timeout":
description: Chatbesked, der viser timeout-hændelser description: Chatbesked, der viser timeout-hændelser
ended: STREAM AFSLUTTET ended: STREAM AFSLUTTET

View File

@ -4,7 +4,7 @@ upload_avatar: Avatar hochladen
klicken, um den Upload zu starten klicken, um den Upload zu starten
most_zapped_streamers: Meistgezappte Streamer most_zapped_streamers: Meistgezappte Streamer
"@most_zapped_streamers": "@most_zapped_streamers":
description: Überschrift über gelistete Top-Streamer von zaps description: Überschrift über gelistete Top-Streamer nach Zaps
no_user_found: Kein Benutzer gefunden no_user_found: Kein Benutzer gefunden
"@no_user_found": "@no_user_found":
description: Kein Benutzer bei der Suche gefunden description: Kein Benutzer bei der Suche gefunden
@ -13,27 +13,28 @@ viewers:
one: 1 Zuschauer one: 1 Zuschauer
other: ${n:decimalPattern} Zuschauer other: ${n:decimalPattern} Zuschauer
"@viewers": "@viewers":
description: Anzahl der Betrachter des Streams description: Anzahl der Zuschauer des Streams
"@anon": "@anon":
description: Ein anonymer Benutzer description: Ein anonymer Benutzer
stream: stream:
status: status:
live: LIVE live: LIVE
ended: ENDED ended: BEENDET
planned: GEPLANT planned: GEPLANT
started: Gestartet $timestamp started: Gestartet $timestamp
notification: ${name} ging live!
chat: chat:
disabled: CHAT DEAKTIVIERT disabled: CHAT DEAKTIVIERT
disabled_timeout: "Die Zeitüberschreitung läuft ab: $time" disabled_timeout: "Timeout läuft ab: $time"
timeout(rich): $mod Zeitüberschreitung $user für $time timeout(rich): $mod gibt $user einen Timeout für ${time}
"@timeout": "@timeout":
description: Chat-Nachricht mit Zeitüberschreitungsereignissen description: Chat-Nachricht mit Timeout-Ereignissen
ended: STREAM BEENDET ended: STREAM BEENDET
"@ended": "@ended":
description: Stream beendet Fußzeile am Ende des Chats description: Stream beendet Fußzeile am Ende des Chats
zap(rich): $user hat $amount sats gezappt zap(rich): $user hat $amount sats gezappt
"@zap": "@zap":
description: Chatnachricht mit Stream Zaps description: Chat-Nachricht mit Stream-Zaps
write: write:
label: Nachricht schreiben label: Nachricht schreiben
"@label": "@label":

View File

@ -22,10 +22,11 @@ stream:
ended: ENDED ended: ENDED
planned: ΣΧΕΔΙΑΣΜΟΣ planned: ΣΧΕΔΙΑΣΜΟΣ
started: Ξεκίνησε $timestamp started: Ξεκίνησε $timestamp
notification: ${name} βγήκε ζωντανά!
chat: chat:
disabled: ΑΠΕΝΕΡΓΟΠΟΙΗΜΈΝΗ ΣΥΝΟΜΙΛΊΑ disabled: ΑΠΕΝΕΡΓΟΠΟΙΗΜΈΝΗ ΣΥΝΟΜΙΛΊΑ
disabled_timeout: "Το χρονικό όριο λήγει: $time" disabled_timeout: "Το χρονικό όριο λήγει: $time"
timeout(rich): $mod χρονομετρημένη λήξη $user για $time timeout(rich): $mod χρονομετρημένη λήξη $user για ${time}
"@timeout": "@timeout":
description: Μήνυμα συνομιλίας που εμφανίζει συμβάντα timeout description: Μήνυμα συνομιλίας που εμφανίζει συμβάντα timeout
ended: STREAM ΤΕΛΕΙΩΣΕ ended: STREAM ΤΕΛΕΙΩΣΕ

View File

@ -8,6 +8,7 @@ no_user_found: No user found
"@no_user_found": "@no_user_found":
description: No user found when searching description: No user found when searching
anon: Anon anon: Anon
full_amount_sats: ${n:decimalPattern} sats
viewers: viewers:
one: 1 viewer one: 1 viewer
other: ${n:decimalPattern} viewers other: ${n:decimalPattern} viewers
@ -21,6 +22,7 @@ stream:
ended: ENDED ended: ENDED
planned: PLANNED planned: PLANNED
started: Started $timestamp started: Started $timestamp
notification: ${name} went live!
chat: chat:
disabled: CHAT DISABLED disabled: CHAT DISABLED
disabled_timeout: "Timeout expires: $time" disabled_timeout: "Timeout expires: $time"
@ -121,6 +123,8 @@ settings:
disconnect_wallet: Disconnect Wallet disconnect_wallet: Disconnect Wallet
connect_1tap: 1-Tap Connection connect_1tap: 1-Tap Connection
paste: Paste URL paste: Paste URL
balance: Balance
name: Wallet
error: error:
logged_out: Cant connect wallet when logged out logged_out: Cant connect wallet when logged out
nwc_auth_event_not_found: No wallet auth event found nwc_auth_event_not_found: No wallet auth event found
@ -131,3 +135,21 @@ login:
create: Create Account create: Create Account
error: error:
invalid_key: Invalid key invalid_key: Invalid key
live:
start: "GO LIVE"
configure_stream: Configure Stream
endpoint: Endpoint
accept_tos: Accept TOS
balance_left:
zero: "∞"
other: "~${time}"
title: Title
summary: Summary
image: Cover Image
tags: Tags
nsfw: NSFW Content
nsfw_description: Check here if this stream contains nudity or pornographic content.
error:
failed: Stream failed
connection_error: Connection Error
start_failed: Stream start failed, please check your balance

View File

@ -22,10 +22,11 @@ stream:
ended: FIN ended: FIN
planned: PLANIFICADO planned: PLANIFICADO
started: Comenzó $timestamp started: Comenzó $timestamp
notification: ${name} ¡se ha puesto en marcha!
chat: chat:
disabled: CHAT DESHABILITADO disabled: CHAT DESHABILITADO
disabled_timeout: "El tiempo de espera expira: $time" disabled_timeout: "El tiempo de espera expira: $time"
timeout(rich): $mod timed out $user para $time timeout(rich): $mod timed out $user para ${time}
"@timeout": "@timeout":
description: Mensaje de chat que muestra los eventos de tiempo de espera description: Mensaje de chat que muestra los eventos de tiempo de espera
ended: STREAM FINED ended: STREAM FINED

View File

@ -22,10 +22,11 @@ stream:
ended: ENDED ended: ENDED
planned: SUUNNITELTU planned: SUUNNITELTU
started: Aloitettu $timestamp started: Aloitettu $timestamp
notification: ${name} meni suoraksi!
chat: chat:
disabled: CHAT POISTETTU KÄYTÖSTÄ disabled: CHAT POISTETTU KÄYTÖSTÄ
disabled_timeout: "Aikakatkaisu päättyy: $time" disabled_timeout: "Aikakatkaisu päättyy: $time"
timeout(rich): $mod ajastettu $user for $time timeout(rich): $mod ajastettu $user for ${time}
"@timeout": "@timeout":
description: Chat-viesti, joka näyttää aikakatkaisutapahtumat description: Chat-viesti, joka näyttää aikakatkaisutapahtumat
ended: STREAM PÄÄTTYNYT ended: STREAM PÄÄTTYNYT

View File

@ -11,7 +11,7 @@ no_user_found: Aucun utilisateur trouvé
anon: Anonyme anon: Anonyme
viewers: viewers:
one: 1 téléspectateur one: 1 téléspectateur
other: "${n:decimalPattern} téléspectateurs" other: "{n:decimalPattern} téléspectateurs"
"@viewers": "@viewers":
description: Nombre de spectateurs du flux description: Nombre de spectateurs du flux
"@anon": "@anon":
@ -22,10 +22,11 @@ stream:
ended: FINI ended: FINI
planned: PRÉVU planned: PRÉVU
started: Commencé à $timestamp started: Commencé à $timestamp
notification: ${name} est en ligne !
chat: chat:
disabled: CHAT DISABLED disabled: CHAT DISABLED
disabled_timeout: "Le délai expire : $time" disabled_timeout: "Le délai expire : $time"
timeout(rich): $mod $user a expiré dans le temps pour $time timeout(rich): $mod $user a expiré dans le temps pour ${time}
"@timeout": "@timeout":
description: Message de chat indiquant les événements de dépassement de délai description: Message de chat indiquant les événements de dépassement de délai
ended: STREAM ENDED ended: STREAM ENDED

View File

@ -22,6 +22,7 @@ stream:
ended: ENDED ended: ENDED
planned: TERVEZETT planned: TERVEZETT
started: Elindult $timestamp started: Elindult $timestamp
notification: ${name} elindult!
chat: chat:
disabled: CHAT KIKAPCSOLVA disabled: CHAT KIKAPCSOLVA
disabled_timeout: "Az időkorlát lejár: $time" disabled_timeout: "Az időkorlát lejár: $time"
@ -56,7 +57,7 @@ stream:
from: RAID FROM $name from: RAID FROM $name
"@from": "@from":
description: Chat raid üzenet egy másik folyamból description: Chat raid üzenet egy másik folyamból
countdown: Raiding a ${time}oldalon countdown: Raiding a ${time} oldalon
"@countdown": "@countdown":
description: Visszaszámláló időzítő az automatikus lovagláshoz description: Visszaszámláló időzítő az automatikus lovagláshoz
goal: goal:
@ -84,7 +85,7 @@ button:
embed: embed:
article_by: Cikk ${name} article_by: Cikk ${name}
note_by: $name bejegyzése note_by: $name bejegyzése
live_stream_by: Élő közvetítés a ${name}oldalon live_stream_by: Élő közvetítés a ${name} oldalon
stream_list: stream_list:
following: Követettek bejegyzései following: Követettek bejegyzései
live: Élő live: Élő

View File

@ -22,10 +22,11 @@ stream:
ended: FINE ended: FINE
planned: PREVISTO planned: PREVISTO
started: Avviato $timestamp started: Avviato $timestamp
notification: ${name} è andato in onda!
chat: chat:
disabled: CHAT DISABILITATA disabled: CHAT DISABILITATA
disabled_timeout: "Il timeout scade: $time" disabled_timeout: "Il timeout scade: $time"
timeout(rich): $mod time out $user per $time timeout(rich): $mod time out $user per ${time}
"@timeout": "@timeout":
description: Messaggio di chat che mostra gli eventi di timeout description: Messaggio di chat che mostra gli eventi di timeout
ended: STREAM ENDED ended: STREAM ENDED

View File

@ -21,10 +21,11 @@ stream:
ended: 終了 ended: 終了
planned: 予定 planned: 予定
started: $timestamp を開始 started: $timestamp を開始
notification: ${name} がライブを開始した!
chat: chat:
disabled: チャット無効 disabled: チャット無効
disabled_timeout: タイムアウト: $time disabled_timeout: タイムアウト: $time
timeout(rich): $mod タイムアウト $user for $time timeout(rich): $mod タイムアウト $user for ${time}
"@timeout": "@timeout":
description: タイムアウトイベントを表示するチャットメッセージ description: タイムアウトイベントを表示するチャットメッセージ
ended: 配信終了 ended: 配信終了

View File

@ -10,7 +10,7 @@ no_user_found: 사용자를 찾을 수 없습니다.
anon: Anon anon: Anon
viewers: viewers:
one: 시청자 1명 one: 시청자 1명
other: "${n:decimalPattern} 시청자" other: "{n:decimalPattern} 시청자"
"@viewers": "@viewers":
description: 스트림 시청자 수 description: 스트림 시청자 수
"@anon": "@anon":
@ -21,10 +21,11 @@ stream:
ended: 종료 ended: 종료
planned: 계획된 planned: 계획된
started: 시작 $timestamp started: 시작 $timestamp
notification: ${name} 라이브가 시작되었습니다!
chat: chat:
disabled: 채팅 사용 안 함 disabled: 채팅 사용 안 함
disabled_timeout: "시간 초과가 만료되었습니다: $time" disabled_timeout: "시간 초과가 만료되었습니다: $time"
timeout(rich): $mod 시간 초과됨 $user $time timeout(rich): $mod 시간 초과됨 $user ${time}
"@timeout": "@timeout":
description: 시간 초과 이벤트를 표시하는 채팅 메시지 description: 시간 초과 이벤트를 표시하는 채팅 메시지
ended: 스트림 종료 ended: 스트림 종료

View File

@ -22,10 +22,11 @@ stream:
ended: GESLOTEN ended: GESLOTEN
planned: GEPLAND planned: GEPLAND
started: Begonnen met $timestamp started: Begonnen met $timestamp
notification: ${name} ging live!
chat: chat:
disabled: CHAT UITGESCHAKELD disabled: CHAT UITGESCHAKELD
disabled_timeout: "Time-out loopt af: $time" disabled_timeout: "Time-out loopt af: $time"
timeout(rich): $mod timed out $user voor $time timeout(rich): $mod timed out $user voor ${time}
"@timeout": "@timeout":
description: Chatbericht met time-outgebeurtenissen description: Chatbericht met time-outgebeurtenissen
ended: STREAM BEËINDIGD ended: STREAM BEËINDIGD

View File

@ -22,10 +22,11 @@ stream:
ended: ZAKOŃCZONY ended: ZAKOŃCZONY
planned: PLANOWANE planned: PLANOWANE
started: Start $timestamp started: Start $timestamp
notification: ${name} został uruchomiony!
chat: chat:
disabled: CZAT WYŁĄCZONY disabled: CZAT WYŁĄCZONY
disabled_timeout: "Upłynął limit czasu: $time" disabled_timeout: "Upłynął limit czasu: $time"
timeout(rich): $mod upłynął limit czasu $user dla $time timeout(rich): $mod upłynął limit czasu $user dla ${time}
"@timeout": "@timeout":
description: Komunikat czatu pokazujący zdarzenia przekroczenia limitu czasu description: Komunikat czatu pokazujący zdarzenia przekroczenia limitu czasu
ended: TRANSMISJA ZAKOŃCZONA ended: TRANSMISJA ZAKOŃCZONA

View File

@ -22,10 +22,11 @@ stream:
ended: FINALIZADO ended: FINALIZADO
planned: PLANEJADO planned: PLANEJADO
started: Iniciado em $timestamp started: Iniciado em $timestamp
notification: ${name} foi ao ar!
chat: chat:
disabled: BATE-PAPO DESATIVADO disabled: BATE-PAPO DESATIVADO
disabled_timeout: "O tempo limite expira: $time" disabled_timeout: "O tempo limite expira: $time"
timeout(rich): $mod Tempo esgotado $user para $time timeout(rich): $mod Tempo esgotado $user para ${time}
"@timeout": "@timeout":
description: Mensagem de bate-papo mostrando eventos de tempo limite description: Mensagem de bate-papo mostrando eventos de tempo limite
ended: TRANSMISSÃO ENCERRADA ended: TRANSMISSÃO ENCERRADA

View File

@ -22,10 +22,11 @@ stream:
ended: TERMINAT ended: TERMINAT
planned: PLANIFICATE planned: PLANIFICATE
started: A început $timestamp started: A început $timestamp
notification: ${name} a intrat în direct!
chat: chat:
disabled: CHAT DEZACTIVAT disabled: CHAT DEZACTIVAT
disabled_timeout: "Timpul expiră: $time" disabled_timeout: "Timpul expiră: $time"
timeout(rich): $mod Timed out $user pentru $time timeout(rich): $mod Timed out $user pentru ${time}
"@timeout": "@timeout":
description: Mesaj de chat care afișează evenimentele de timeout description: Mesaj de chat care afișează evenimentele de timeout
ended: STREAM ÎNCHEIAT ended: STREAM ÎNCHEIAT

View File

@ -22,10 +22,11 @@ stream:
ended: КОНЕЦ ended: КОНЕЦ
planned: ПЛАНИРУЕМЫЙ planned: ПЛАНИРУЕМЫЙ
started: Начало $timestamp started: Начало $timestamp
notification: ${name} запустился!
chat: chat:
disabled: ЧАТ ОТКЛЮЧЕН disabled: ЧАТ ОТКЛЮЧЕН
disabled_timeout: "Таймаут истекает: $time" disabled_timeout: "Таймаут истекает: $time"
timeout(rich): $mod тайм-аут $user для $time timeout(rich): $mod тайм-аут $user для ${time}
"@timeout": "@timeout":
description: Сообщение в чате, показывающее события по тайм-ауту description: Сообщение в чате, показывающее события по тайм-ауту
ended: ТРАНСЛЯЦИЯ ОКОНЧЕНА ended: ТРАНСЛЯЦИЯ ОКОНЧЕНА

View File

@ -1,19 +1,19 @@
upload_avatar: Ladda upp avatar upload_avatar: Ladda upp avatar
"@upload_avatar": "@upload_avatar":
description: Text som uppmanar användaren att trycka på avatarplatshållaren för description: Text som uppmanar användaren att trycka på avatar platshållaren för
att påbörja uppladdningen att påbörja uppladdningen
most_zapped_streamers: De flesta zappade streamers most_zapped_streamers: De flest zappade streamers
"@most_zapped_streamers": "@most_zapped_streamers":
description: Rubrik över listade toppstreamers av zaps description: Rubrik över listade topp streamers av zaps
no_user_found: Ingen användare hittades no_user_found: Ingen användare hittades
"@no_user_found": "@no_user_found":
description: Ingen användare hittades vid sökning description: Ingen användare hittades vid sökning
anon: Anon anon: Anno
viewers: viewers:
one: 1 tittare one: 1 tittare
other: ${n:decimalPattern} tittare other: ${n:decimalPattern} tittare
"@viewers": "@viewers":
description: Antal tittare på streamingen description: Antal tittare på strömmingen
"@anon": "@anon":
description: En anonym användare description: En anonym användare
stream: stream:
@ -21,16 +21,17 @@ stream:
live: LIVE live: LIVE
ended: AVSLUTAD ended: AVSLUTAD
planned: PLANERADE planned: PLANERADE
started: Startade $timestamp started: Startad $timestamp
notification: ${name} gick live!
chat: chat:
disabled: CHAT AVSTÄNGD disabled: CHAT AVSTÄNGD
disabled_timeout: "Tidsgränsen går ut: $time" disabled_timeout: "Tidsgränsen går ut: $time"
timeout(rich): $mod tidsbegränsad $user för $time timeout(rich): $mod tidsbegränsad $user för ${time}
"@timeout": "@timeout":
description: Chattmeddelande som visar timeout-händelser description: Chattmeddelande som visar timeout-händelser
ended: STREAM AVSLUTAD ended: STREAM AVSLUTAD
"@ended": "@ended":
description: Stream avslutade sidfoten längst ner på chatten description: Streama slutade sidfot längst ned i chatten
zap(rich): $user zapped $amount sats zap(rich): $user zapped $amount sats
"@zap": "@zap":
description: Chattmeddelande som visar strömavbrott description: Chattmeddelande som visar strömavbrott
@ -38,27 +39,27 @@ stream:
label: Skriv meddelande label: Skriv meddelande
"@label": "@label":
description: Etikett på inmatningsrutan för chattmeddelanden description: Etikett på inmatningsrutan för chattmeddelanden
no_signer: Det går inte att skriva meddelanden med npub-inloggning no_signer: Det går inte att skriva meddelanden med n-pub inloggning
"@no_signer": "@no_signer":
description: Chattinmatningsmeddelande som visas när användaren endast är description: Chattinmatningsmeddelande som visas när användaren endast är
inloggad med pubkey inloggad med publik nyckel
login: Logga in för att skicka meddelanden login: Logga in för att skicka meddelanden
"@login": "@login":
description: Chattinmatningsmeddelande som visas när användaren är utloggad description: Chattinmatningsmeddelande som visas när användaren är utloggad
badge: badge:
awarded_to: "Tilldelas till:" awarded_to: "Tilldelas till:"
"@awarded_to": "@awarded_to":
description: Rubrik över lista över användare som tilldelats en badge description: Rubrik över listan över användare som tilldelas ett märke
raid: raid:
to: RAIDING $name to: RAIDING ${name}
"@to": "@to":
description: Chatta raidmeddelande till en annan ström description: Chatt raid meddelande till en annan ström
from: RAID FRÅN $name from: RAID FRÅN ${name}
"@from": "@from":
description: Chat raid-meddelande från en annan ström description: Chatt raid meddelande från en annan ström
countdown: Raiding $time countdown: Radiering i ${time}
"@countdown": "@countdown":
description: Nedräkningstimer för auto-raiding description: Nedräkningstimer för auto- radiering
goal: goal:
title: "Mål: $amount" title: "Mål: $amount"
remaining: "Kvarvarande: $amount" remaining: "Kvarvarande: $amount"
@ -74,7 +75,7 @@ button:
description: Knapptext för följ-knappen description: Knapptext för följ-knappen
unfollow: Sluta följa unfollow: Sluta följa
"@unfollow": "@unfollow":
description: Knapptext för avföljningsknappen description: Knapptext för sluta följa knappen
mute: Tysta mute: Tysta
unmute: Avtysta unmute: Avtysta
share: Dela share: Dela
@ -82,9 +83,9 @@ button:
connect: Anslut connect: Anslut
settings: Inställningar settings: Inställningar
embed: embed:
article_by: Artikel av $name article_by: Artikel av ${name}
note_by: Anteckning av $name note_by: Anteckning av $name
live_stream_by: Direktsändning via $name live_stream_by: Direktsändning via ${name}
stream_list: stream_list:
following: Följer following: Följer
live: Live live: Live
@ -114,17 +115,17 @@ settings:
profile: profile:
display_name: Visa namn display_name: Visa namn
about: Om about: Om
nip05: Nostr Adress nip05: Nostr adress
lud16: Adress för blixtnedslag lud16: Lightning-adress
error: error:
logged_out: Kan inte redigera profil när jag är utloggad logged_out: Kan inte redigera profil när jag är utloggad
wallet: wallet:
connect_wallet: Connect plånbok (NWC nostr+walletconnect://) connect_wallet: Anslut plånbok (NWC nostr+walletconnect://)
disconnect_wallet: Koppla bort plånboken disconnect_wallet: Koppla bort plånboken
connect_1tap: 1-Tap-anslutning connect_1tap: 1-tryck anslutning
paste: Klistra in URL paste: Klistra in URL
error: error:
logged_out: Kan inte ansluta plånbok när du är inloggad logged_out: Kan inte ansluta plånbok när du är utloggad
nwc_auth_event_not_found: Inget autentiseringshändelse för plånbok hittades nwc_auth_event_not_found: Inget autentiseringshändelse för plånbok hittades
login: login:
username: Användarnamn username: Användarnamn

View File

@ -22,10 +22,11 @@ stream:
ended: SONLANDI ended: SONLANDI
planned: PLANLANMIŞ planned: PLANLANMIŞ
started: Başlatıldı $timestamp started: Başlatıldı $timestamp
notification: ${name} yayına girdi!
chat: chat:
disabled: SOHBET DEVRE DIŞI disabled: SOHBET DEVRE DIŞI
disabled_timeout: "Zaman aşımı sona eriyor: $time" disabled_timeout: "Zaman aşımı sona eriyor: $time"
timeout(rich): $mod zaman aşımına uğradı $user için $time timeout(rich): $mod zaman aşımına uğradı $user ${time}için
"@timeout": "@timeout":
description: Zaman aşımı olaylarını gösteren sohbet mesajı description: Zaman aşımı olaylarını gösteren sohbet mesajı
ended: YAYIN SONLANDI ended: YAYIN SONLANDI
@ -53,7 +54,7 @@ stream:
to: RAIDING ${name} to: RAIDING ${name}
"@to": "@to":
description: Başka bir akışa sohbet baskını mesajı description: Başka bir akışa sohbet baskını mesajı
from: ${name}ADRESINDEN RAID from: ${name} ADRESINDEN RAID
"@from": "@from":
description: Başka bir akıştan sohbet baskını mesajı description: Başka bir akıştan sohbet baskını mesajı
countdown: ${time}adresinde baskın countdown: ${time}adresinde baskın

View File

@ -22,10 +22,11 @@ stream:
ended: ЗАКІНЧЕНО ended: ЗАКІНЧЕНО
planned: ЗАПЛАНОВАНО planned: ЗАПЛАНОВАНО
started: Запустив $timestamp started: Запустив $timestamp
notification: ${name} запрацював!
chat: chat:
disabled: ЧАТ ВІДКЛЮЧЕНО disabled: ЧАТ ВІДКЛЮЧЕНО
disabled_timeout: "Тайм-аут закінчився: $time" disabled_timeout: "Тайм-аут закінчився: $time"
timeout(rich): $mod таймінг $user для $time timeout(rich): $mod таймінг $user для ${time}
"@timeout": "@timeout":
description: Повідомлення в чаті про події тайм-ауту description: Повідомлення в чаті про події тайм-ауту
ended: СТРІМ ЗАКІНЧИВСЯ ended: СТРІМ ЗАКІНЧИВСЯ

View File

@ -21,10 +21,11 @@ stream:
ended: 結束 ended: 結束
planned: 計劃 planned: 計劃
started: 開始 $timestamp started: 開始 $timestamp
notification: ${name} 已啟用!
chat: chat:
disabled: 關閉聊天 disabled: 關閉聊天
disabled_timeout: 超時過期: $time disabled_timeout: 超時過期: $time
timeout(rich): $mod 超時 $user for $time timeout(rich): $mod 超時 $user for ${time}
"@timeout": "@timeout":
description: 顯示逾時事件的聊天訊息 description: 顯示逾時事件的聊天訊息
ended: 串流結束 ended: 串流結束

View File

@ -38,8 +38,16 @@ class WalletConfig {
} }
} }
class WalletInfo {
final String name;
final int balance;
const WalletInfo({required this.name, required this.balance});
}
abstract class SimpleWallet { abstract class SimpleWallet {
Future<String> payInvoice(String pr); Future<String> payInvoice(String pr);
Future<WalletInfo> getInfo();
} }
class NWCWrapper extends SimpleWallet { class NWCWrapper extends SimpleWallet {
@ -60,6 +68,13 @@ class NWCWrapper extends SimpleWallet {
return rsp.preimage!; return rsp.preimage!;
} }
} }
@override
Future<WalletInfo> getInfo() async {
final info = await ndk.nwc.getInfo(_conn);
final balance = await ndk.nwc.getBalance(_conn);
return WalletInfo(name: info.alias, balance: balance.balanceSats);
}
} }
class LoginAccount { class LoginAccount {
@ -68,6 +83,7 @@ class LoginAccount {
final String? privateKey; final String? privateKey;
final List<String>? signerRelays; final List<String>? signerRelays;
final WalletConfig? wallet; final WalletConfig? wallet;
final String? streamEndpoint;
SimpleWallet? _cachedWallet; SimpleWallet? _cachedWallet;
@ -77,6 +93,7 @@ class LoginAccount {
this.privateKey, this.privateKey,
this.signerRelays, this.signerRelays,
this.wallet, this.wallet,
this.streamEndpoint,
}); });
static LoginAccount nip19(String key) { static LoginAccount nip19(String key) {
@ -124,6 +141,7 @@ class LoginAccount {
"pubKey": acc?.pubkey, "pubKey": acc?.pubkey,
"privateKey": acc?.privateKey, "privateKey": acc?.privateKey,
"wallet": acc?.wallet?.toJson(), "wallet": acc?.wallet?.toJson(),
"streamEndpoint": acc?.streamEndpoint,
}; };
static LoginAccount? fromJson(Map<String, dynamic> json) { static LoginAccount? fromJson(Map<String, dynamic> json) {
@ -147,6 +165,7 @@ class LoginAccount {
json.containsKey("wallet") && json["wallet"] != null json.containsKey("wallet") && json["wallet"] != null
? WalletConfig.fromJson(json["wallet"]) ? WalletConfig.fromJson(json["wallet"])
: null, : null,
streamEndpoint: json["streamEndpoint"],
); );
} }
return null; return null;
@ -200,4 +219,21 @@ class LoginData extends ValueNotifier<LoginAccount?> {
} }
} }
} }
void configure({
List<String>? signerRelays,
WalletConfig? wallet,
String? streamEndpoint,
}) {
if (value != null) {
value = LoginAccount(
type: value!.type,
pubkey: value!.pubkey,
privateKey: value!.privateKey,
signerRelays: signerRelays ?? value!.signerRelays,
wallet: wallet,
streamEndpoint: streamEndpoint ?? value!.streamEndpoint,
);
}
}
} }

View File

@ -1,5 +1,6 @@
import 'dart:developer' as developer; import 'dart:developer' as developer;
import 'package:audio_service/audio_service.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
@ -7,6 +8,9 @@ import 'package:zap_stream_flutter/app.dart';
import 'package:zap_stream_flutter/const.dart'; import 'package:zap_stream_flutter/const.dart';
import 'package:zap_stream_flutter/i18n/strings.g.dart'; import 'package:zap_stream_flutter/i18n/strings.g.dart';
import 'package:zap_stream_flutter/notifications.dart'; import 'package:zap_stream_flutter/notifications.dart';
import 'package:zap_stream_flutter/player.dart';
late final MainPlayer mainPlayer;
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -20,5 +24,14 @@ Future<void> main() async {
developer.log("Failed to setup notifications: $e"); developer.log("Failed to setup notifications: $e");
}); });
mainPlayer = await AudioService.init(
builder: () => MainPlayer(),
config: AudioServiceConfig(
androidNotificationChannelId: "io.nostrlabs.zap_stream_flutter.player",
androidNotificationChannelName: "Player Status",
androidNotificationOngoing: true
),
);
runZapStream(); runZapStream();
} }

View File

@ -10,11 +10,14 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:ndk/ndk.dart'; import 'package:ndk/ndk.dart';
import 'package:ndk_objectbox/ndk_objectbox.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:zap_stream_flutter/const.dart'; import 'package:zap_stream_flutter/const.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:zap_stream_flutter/firebase_options.dart'; import 'package:zap_stream_flutter/firebase_options.dart';
import 'package:zap_stream_flutter/i18n/strings.g.dart';
import 'package:zap_stream_flutter/utils.dart'; import 'package:zap_stream_flutter/utils.dart';
import 'package:zap_stream_flutter/widgets/profile.dart';
class Notepush { class Notepush {
final String base; final String base;
@ -183,6 +186,82 @@ class NotificationsStore extends ValueNotifier<NotificationsState?> {
} }
} }
Future<void> _initLocalNotifications() async {
await localNotifications.initialize(
InitializationSettings(
android: AndroidInitializationSettings("@drawable/ic_stat_name"),
iOS: DarwinInitializationSettings(),
),
);
}
@pragma('vm:entry-point')
Future<void> _onBackgroundNotification(RemoteMessage msg) async {
await LocaleSettings.useDeviceLocale();
final cache = DbObjectBox(attach: true);
await _initLocalNotifications();
await _handleNotification(msg, cache);
}
Future<void> _onNotification(RemoteMessage msg) async {
await _handleNotification(msg, ndkCache);
}
Future<void> _handleNotification(RemoteMessage msg, DbObjectBox cache) async {
final notification = msg.notification;
if (notification != null && notification.android != null) {
final String? json = msg.data["nostr_event"];
final event =
json != null ? Nip01Event.fromJson(JsonCodec().decode(json)) : null;
await _showNotification(notification, ndkCache, event);
}
}
Future<void> _showNotification(
RemoteNotification notification,
DbObjectBox cache,
Nip01Event? event,
) async {
final stream = event != null ? StreamEvent(event) : null;
final hostProfile =
stream != null ? await cache.loadMetadata(stream.info.host) : null;
final newTitle =
hostProfile != null
? t.stream.notification(
name: ProfileNameWidget.nameFromProfile(hostProfile),
)
: null;
localNotifications.show(
notification.hashCode,
newTitle ?? notification.title,
stream?.info.title ?? notification.body,
NotificationDetails(
android: AndroidNotificationDetails(
notification.android!.channelId ?? "fcm",
"Push Notifications",
category: AndroidNotificationCategory.social
),
),
);
}
Future<void> _onOpenMessage(RemoteMessage msg) async {
try {
final notification = msg.notification;
final String? json = msg.data["nostr_event"];
if (notification != null && json != null) {
// Just launch the URL because we support deep links
final event = Nip01Event.fromJson(JsonCodec().decode(json));
final stream = StreamEvent(event);
launchUrl(Uri.parse("https://zap.stream/${stream.link}"));
}
} catch (e) {
developer.log("Failed to process push notification\n ${e.toString()}");
}
}
// global notifications store // global notifications store
final notifications = NotificationsStore(null); final notifications = NotificationsStore(null);
@ -191,47 +270,20 @@ Future<void> setupNotifications() async {
final signer = ndk.accounts.getLoggedAccount()?.signer; final signer = ndk.accounts.getLoggedAccount()?.signer;
if (signer != null) { if (signer != null) {
final pusher = Notepush(dotenv.env["NOTEPUSH_URL"]!, signer: signer); FirebaseMessaging.onMessage.listen(_onNotification);
final fbase = FirebaseMessaging.instance; //FirebaseMessaging.onBackgroundMessage(_onBackgroundNotification);
FirebaseMessaging.onMessage.listen((msg) { FirebaseMessaging.onMessageOpenedApp.listen(_onOpenMessage);
developer.log(msg.notification?.body ?? "");
final notification = msg.notification;
if (notification != null && notification.android != null) {
FlutterLocalNotificationsPlugin().show(
notification.hashCode,
notification.title,
notification.body,
NotificationDetails(
android: AndroidNotificationDetails(
notification.android!.channelId ?? "fcm",
"fcm",
),
),
);
}
});
FirebaseMessaging.onMessageOpenedApp.listen((msg) {
try {
final notification = msg.notification;
final String? json = msg.data["nostr_event"];
if (notification != null && json != null) {
// Just launch the URL because we support deep links
final event = Nip01Event.fromJson(JsonCodec().decode(json));
final stream = StreamEvent(event);
launchUrl(Uri.parse("https://zap.stream/${stream.link}"));
}
} catch (e) {
developer.log("Failed to process push notification\n ${e.toString()}");
}
});
final settings = await fbase.requestPermission(provisional: true); final settings = await FirebaseMessaging.instance.requestPermission(
await fbase.setAutoInitEnabled(true); provisional: true,
await fbase.setForegroundNotificationPresentationOptions(
alert: true,
badge: true,
sound: true,
); );
await FirebaseMessaging.instance.setAutoInitEnabled(true);
await FirebaseMessaging.instance
.setForegroundNotificationPresentationOptions(
alert: true,
badge: true,
sound: true,
);
if (Platform.isIOS) { if (Platform.isIOS) {
final apnsToken = await FirebaseMessaging.instance.getAPNSToken(); final apnsToken = await FirebaseMessaging.instance.getAPNSToken();
@ -239,13 +291,10 @@ Future<void> setupNotifications() async {
throw "APNS token not availble"; throw "APNS token not availble";
} }
} }
await localNotifications.initialize( await _initLocalNotifications();
InitializationSettings(
android: AndroidInitializationSettings("@mipmap/ic_launcher"), final pusher = Notepush(dotenv.env["NOTEPUSH_URL"]!, signer: signer);
iOS: DarwinInitializationSettings(), FirebaseMessaging.instance.onTokenRefresh.listen((token) async {
),
);
fbase.onTokenRefresh.listen((token) async {
developer.log("NEW TOKEN: $token"); developer.log("NEW TOKEN: $token");
await pusher.register(token); await pusher.register(token);
await pusher.setNotificationSettings(token, [30_311]); await pusher.setNotificationSettings(token, [30_311]);

429
lib/pages/live.dart Normal file
View File

@ -0,0 +1,429 @@
import 'dart:async';
import 'dart:developer' as developer;
import 'package:apivideo_live_stream/apivideo_live_stream.dart';
import 'package:collection/collection.dart';
import 'package:duration/duration.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import 'package:ndk/ndk.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:zap_stream_flutter/api.dart';
import 'package:zap_stream_flutter/const.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';
import 'package:zap_stream_flutter/widgets/button.dart';
import 'package:zap_stream_flutter/widgets/chat.dart';
import 'package:zap_stream_flutter/widgets/pill.dart';
import 'package:zap_stream_flutter/widgets/stream_config.dart';
Future<bool?> showExitStreamDialog(BuildContext context) {
return showDialog<bool>(
context: context,
barrierDismissible: false,
useRootNavigator: false,
builder: (context) {
return Dialog(
child: Container(
padding: EdgeInsets.all(10),
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 16,
children: [
Text("Exit live stream?", style: TextStyle(fontSize: 24)),
Row(
spacing: 16,
children: [
Flexible(
child: BasicButton.text(
"Yes, stop stream",
onTap: (context) => context.pop(true),
),
),
Flexible(
child: BasicButton.text(
"No",
onTap: (context) => context.pop(false),
),
),
],
),
],
),
),
);
},
);
}
class LivePage extends StatefulWidget {
const LivePage({super.key});
@override
State<StatefulWidget> createState() => _LivePage();
}
class _LivePage extends State<LivePage>
implements ApiVideoLiveStreamEventsListener {
late final ApiVideoLiveStreamController _controller;
late final ZapStreamApi _api;
AccountInfo? _account;
late final Timer _accountTimer;
bool _streaming = false;
Future<void> _reloadAccount() async {
final info = await _api.getAccountInfo();
setState(() {
_account = info;
});
}
@override
void initState() {
_controller = ApiVideoLiveStreamController(
initialAudioConfig: AudioConfig(),
initialVideoConfig: VideoConfig.withDefaultBitrate(),
);
_controller.initialize();
_api = ZapStreamApi.instance();
_reloadAccount();
_accountTimer = Timer.periodic(Duration(seconds: 30), (_) async {
await _reloadAccount();
});
_controller.addEventsListener(this);
WakelockPlus.enable();
super.initState();
}
@override
void dispose() {
_accountTimer.cancel();
_controller.stopStreaming();
_controller.dispose();
WakelockPlus.disable();
super.dispose();
}
void _showError(BuildContext context, String msg, {Exception? error}) {
if (error != null) {
developer.log(error.toString());
}
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: WARNING,
content: Text(msg, style: TextStyle(fontWeight: FontWeight.bold)),
),
);
}
String _calcTimeRemaining(IngestEndpoint endpoint, double balance) {
if (endpoint.cost.rate == 0) {
return "";
}
final units = balance / endpoint.cost.rate;
if (endpoint.cost.unit == "min") {
return Duration(
seconds: (units * 60).clamp(0, double.infinity).floor(),
).pretty(abbreviated: true);
}
return "0s";
}
@override
Widget build(BuildContext context) {
final mq = MediaQuery.of(context);
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (_streaming) {
final go = await showExitStreamDialog(context);
if (context.mounted) {
if (go == true) {
context.go("/");
}
}
} else {
context.go("/");
}
},
child: ValueListenableBuilder(
valueListenable: loginData,
builder: (context, state, _) {
final endpoint = _account?.endpoints.firstWhereOrNull(
(e) => e.name == state?.streamEndpoint,
);
final balance = _account?.balance ?? 0;
return RxFilter<Nip01Event>(
Key("live-stream"),
filters: [
Filter(
kinds: [30_311],
limit: 100,
pTags: [loginData.value!.pubkey],
),
Filter(
kinds: [30_311],
limit: 100,
authors: [loginData.value!.pubkey],
),
],
builder: (context, streamState) {
final ev = streamState
?.sortedBy((e) => e.createdAt)
.firstWhereOrNull((e) => e.getFirstTag("status") == "live");
final stream = ev != null ? StreamEvent(ev) : null;
return Stack(
children: [
ApiVideoCameraPreview(controller: _controller),
Positioned(
top: 10,
left: 10,
width: mq.size.width - 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
PillWidget(
color: LAYER_2,
child: Row(
spacing: 4,
children: [
Text(t.full_amount_sats(n: balance)),
if (endpoint != null)
Text(
t.live.balance_left(
n: endpoint.cost.rate,
time: _calcTimeRemaining(endpoint, balance),
),
style: TextStyle(color: LAYER_5),
),
],
),
),
if ((stream?.info.participants ?? 0) > 0)
PillWidget(
color: LAYER_2,
child: Text(
t.viewers(n: stream?.info.participants ?? 0),
style: TextStyle(
color: Colors.white,
fontSize: 14,
),
),
),
],
),
),
if (_account != null)
Positioned(
width: mq.size.width,
bottom: 15,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton.filled(
iconSize: 40,
style: ButtonStyle(
iconColor: WidgetStateColor.resolveWith(
(_) => FONT_COLOR,
),
backgroundColor: WidgetStateColor.resolveWith(
(_) => LAYER_3,
),
),
onPressed: () {
_controller.switchCamera();
},
icon: Icon(Icons.cameraswitch_rounded),
),
Spacer(),
if (_account != null && !_account!.tos.accepted)
Column(
spacing: 16,
children: [
BasicButton.text(
"Read TOS",
onTap: (context) {
if (_account?.tos.link != null) {
launchUrl(Uri.parse(_account!.tos.link!));
}
},
),
BasicButton.text(
t.live.accept_tos,
color: WARNING,
onTap: (context) {
_api
.acceptTos()
.then((_) {
_reloadAccount();
})
.catchError((e) {
_showError(
context,
e.toString(),
error: e,
);
});
},
),
],
)
else if (state?.streamEndpoint == null ||
endpoint == null)
BasicButton.text(
t.live.configure_stream,
color: WARNING,
),
if (endpoint != null)
IconButton.filled(
iconSize: 40,
style: ButtonStyle(
iconColor: WidgetStateColor.resolveWith(
(_) => WARNING,
),
backgroundColor: WidgetStateColor.resolveWith(
(_) => LAYER_3,
),
),
onPressed: () async {
if (_streaming) {
_controller.stopStreaming().catchError((e) {
_showError(context, e.toString(), error: e);
});
} else {
_controller
.startStreaming(
streamKey: endpoint.key,
url: endpoint.url,
)
.catchError((e) {
_showError(
context,
t.live.error.start_failed,
error: e,
);
});
}
},
icon: Icon(
_streaming ? Icons.stop : Icons.circle,
),
),
Spacer(),
IconButton.filled(
iconSize: 40,
style: ButtonStyle(
iconColor: WidgetStateColor.resolveWith(
(_) => FONT_COLOR,
),
backgroundColor: WidgetStateColor.resolveWith(
(_) => LAYER_3,
),
),
onPressed: () {
showModalBottomSheet(
context: context,
constraints: BoxConstraints.expand(),
builder: (context) {
return StreamConfigWidget(
api: _api,
account: _account!,
hideEndpointConfig: _streaming,
);
},
).then((_) {
_reloadAccount();
});
},
icon: Icon(Icons.settings),
),
],
),
),
if (_account != null && stream != null)
Positioned(
bottom: 80,
child: Container(
width: mq.size.width,
padding: EdgeInsets.symmetric(horizontal: 10),
constraints: BoxConstraints(
maxHeight: mq.size.height * 0.3,
minHeight: 200,
),
child: ShaderMask(
shaderCallback: (Rect bounds) {
return LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.white.withAlpha(255),
Colors.white.withAlpha(200),
Colors.white.withAlpha(0),
],
stops: [0.0, 0.7, 1.0],
).createShader(bounds);
},
blendMode: BlendMode.dstIn,
child: ChatWidget(
stream: stream,
showGoals: false,
showTopZappers: false,
),
),
),
),
],
);
},
);
},
),
);
}
@override
get onConnectionFailed => (s) {
developer.log(s, name: "onConnectionFailed");
_showError(context, t.live.error.connection_error);
};
@override
get onConnectionSuccess => () {
developer.log("Connected", name: "onConnectionSuccess");
setState(() {
_streaming = true;
});
};
@override
get onDisconnection => () {
developer.log("Disconnected", name: "onDisconnection");
setState(() {
_streaming = false;
});
};
@override
get onError => (e) {
developer.log(e.toString(), name: "onError");
if (e is PlatformException) {
if (e.details is String &&
(e.details as String).contains("Connection error")) {
_showError(context, t.live.error.connection_error, error: e);
}
}
};
@override
get onVideoSizeChanged => (s) {
developer.log(s.toString(), name: "onVideoSizeChanged");
};
}

View File

@ -38,6 +38,7 @@ class _NewAccountPage extends State<NewAccountPage> {
pubKey: _privateKey.publicKey, pubKey: _privateKey.publicKey,
name: _name.text, name: _name.text,
picture: _avatar, picture: _avatar,
lud16: "${_privateKey.publicKey}@zap.stream",
), ),
); );
} }

View File

@ -12,6 +12,7 @@ import 'package:zap_stream_flutter/widgets/button.dart';
import 'package:zap_stream_flutter/widgets/button_follow.dart'; import 'package:zap_stream_flutter/widgets/button_follow.dart';
import 'package:zap_stream_flutter/widgets/header.dart'; import 'package:zap_stream_flutter/widgets/header.dart';
import 'package:zap_stream_flutter/widgets/nostr_text.dart'; import 'package:zap_stream_flutter/widgets/nostr_text.dart';
import 'package:zap_stream_flutter/widgets/notifications_button.dart';
import 'package:zap_stream_flutter/widgets/profile.dart'; import 'package:zap_stream_flutter/widgets/profile.dart';
import 'package:zap_stream_flutter/widgets/stream_grid.dart'; import 'package:zap_stream_flutter/widgets/stream_grid.dart';
@ -91,7 +92,14 @@ class ProfilePage extends StatelessWidget {
), ),
], ],
), ),
if (!isMe) FollowButton(pubkey: hexPubkey), if (!isMe)
Row(
spacing: 8,
children: [
FollowButton(pubkey: hexPubkey),
NotificationsButtonWidget(pubkey: hexPubkey),
],
),
Text( Text(
t.profile.past_streams, t.profile.past_streams,
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600), style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600),

View File

@ -87,7 +87,7 @@ class _Inner extends State<SettingsWalletPage> with ProtocolListener {
queryParameters: { queryParameters: {
"relay": nwcRelays, "relay": nwcRelays,
"name": "zap.stream", "name": "zap.stream",
"request_methods": "pay_invoice", "request_methods": "pay_invoice get_info get_balance",
"icon": "https://zap.stream/logo.png", "icon": "https://zap.stream/logo.png",
"return_to": nwaHandlerUrl, "return_to": nwaHandlerUrl,
}, },
@ -100,13 +100,7 @@ class _Inner extends State<SettingsWalletPage> with ProtocolListener {
} }
_setWallet(WalletConfig? cfg) { _setWallet(WalletConfig? cfg) {
loginData.value = LoginAccount( loginData.configure(wallet: cfg);
type: loginData.value!.type,
pubkey: loginData.value!.pubkey,
privateKey: loginData.value!.privateKey,
signerRelays: loginData.value!.signerRelays,
wallet: cfg,
);
} }
@override @override
@ -174,13 +168,43 @@ class _Inner extends State<SettingsWalletPage> with ProtocolListener {
], ],
); );
} else { } else {
return BasicButton.text( return FutureBuilder(
t.settings.wallet.disconnect_wallet, future: () async {
onTap: (context) { final wallet = await state!.getWallet();
_setWallet(null); return await wallet?.getInfo();
if (context.mounted) { }(),
context.pop(); builder: (context, state) {
} return Column(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Wallet: ${state.data?.name ?? ""}",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 24),
),
Text.rich(
TextSpan(
style: TextStyle(fontWeight: FontWeight.w500),
children: [
TextSpan(text: t.settings.wallet.balance),
TextSpan(text: ": "),
TextSpan(
text: t.full_amount_sats(n: state.data?.balance ?? 0),
),
],
),
),
BasicButton.text(
t.settings.wallet.disconnect_wallet,
onTap: (context) {
_setWallet(null);
if (context.mounted) {
context.pop();
}
},
),
],
);
}, },
); );
} }

View File

@ -17,7 +17,7 @@ import 'package:zap_stream_flutter/widgets/notifications_button.dart';
import 'package:zap_stream_flutter/widgets/pill.dart'; import 'package:zap_stream_flutter/widgets/pill.dart';
import 'package:zap_stream_flutter/widgets/profile.dart'; import 'package:zap_stream_flutter/widgets/profile.dart';
import 'package:zap_stream_flutter/widgets/stream_info.dart'; import 'package:zap_stream_flutter/widgets/stream_info.dart';
import 'package:zap_stream_flutter/widgets/video_player.dart'; import 'package:zap_stream_flutter/widgets/video_player_main.dart';
import 'package:zap_stream_flutter/widgets/zap.dart'; import 'package:zap_stream_flutter/widgets/zap.dart';
class StreamPage extends StatefulWidget { class StreamPage extends StatefulWidget {
@ -141,11 +141,11 @@ class _StreamPage extends State<StreamPage> with RouteAware {
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
child: child:
(stream.info.stream != null && !_offScreen) (stream.info.stream != null && !_offScreen)
? VideoPlayerWidget( ? MainVideoPlayerWidget(
url: stream.info.stream!, url: stream.info.stream!,
placeholder: stream.info.image, placeholder: stream.info.image,
aspectRatio: 16 / 9,
isLive: true, isLive: true,
title: stream.info.title,
) )
: (stream.info.image?.isNotEmpty ?? false) : (stream.info.image?.isNotEmpty ?? false)
? ProxyImg(url: stream.info.image) ? ProxyImg(url: stream.info.image)
@ -159,7 +159,7 @@ class _StreamPage extends State<StreamPage> with RouteAware {
ProfileWidget.pubkey( ProfileWidget.pubkey(
stream.info.host, stream.info.host,
children: [ children: [
NotificationsButtonWidget(stream: widget.stream), NotificationsButtonWidget(pubkey: widget.stream.info.host),
BasicButton( BasicButton(
Row( Row(
children: [Icon(Icons.bolt, size: 14), Text(t.zap.button_zap)], children: [Icon(Icons.bolt, size: 14), Text(t.zap.button_zap)],

101
lib/player.dart Normal file
View File

@ -0,0 +1,101 @@
import 'package:audio_service/audio_service.dart';
import 'package:chewie/chewie.dart';
import 'package:video_player/video_player.dart';
import 'package:zap_stream_flutter/const.dart';
import 'package:zap_stream_flutter/imgproxy.dart';
class MainPlayer extends BaseAudioHandler {
VideoPlayerController? _controller;
ChewieController? _chewieController;
ChewieController? get chewie {
return _chewieController;
}
@override
Future<void> play() async {
await _chewieController?.play();
}
@override
Future<void> pause() async {
await _chewieController?.pause();
}
@override
Future<void> stop() async {
await _chewieController?.pause();
}
void loadUrl(
String url, {
String? title,
bool? autoPlay,
double? aspectRatio,
bool? isLive,
String? placeholder,
String? artist,
}) {
if (_chewieController != null) {
_chewieController!.dispose();
_controller!.dispose();
}
_controller = VideoPlayerController.networkUrl(
Uri.parse(url),
httpHeaders: Map.from({"user-agent": userAgent}),
videoPlayerOptions: VideoPlayerOptions(allowBackgroundPlayback: true),
);
_chewieController = ChewieController(
videoPlayerController: _controller!,
autoPlay: autoPlay ?? true,
aspectRatio: aspectRatio,
isLive: isLive ?? false,
autoInitialize: true,
allowedScreenSleep: false,
placeholder:
(placeholder?.isNotEmpty ?? false)
? ProxyImg(url: placeholder!)
: null,
);
// insert media item
mediaItem.add(
MediaItem(
id: url.hashCode.toString(),
title: title ?? url,
artist: artist,
isLive: _chewieController!.isLive,
artUri:
(placeholder?.isNotEmpty ?? false) ? Uri.parse(placeholder!) : null,
),
);
_chewieController!.videoPlayerController.addListener(updatePlayerState);
}
void updatePlayerState() {
final isPlaying =
_chewieController?.videoPlayerController.value.isPlaying ?? false;
playbackState.add(
playbackState.value.copyWith(
controls: [
if (playbackState.value.playing)
MediaControl.pause
else
MediaControl.play,
MediaControl.stop,
],
playing: isPlaying,
processingState: switch (_chewieController
?.videoPlayerController
.value
.isInitialized) {
true => AudioProcessingState.ready,
false => AudioProcessingState.idle,
_ => AudioProcessingState.completed,
},
),
);
}
}

View File

@ -375,14 +375,6 @@ Map<String, TopZaps> topZapReceiver(Iterable<ZapReceipt> zaps) {
); );
} }
String formatSecondsToHHMMSS(int seconds) {
int hours = seconds ~/ 3600;
int minutes = (seconds % 3600) ~/ 60;
int remainingSeconds = seconds % 60;
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}';
}
String bech32ToHex(String bech32) { String bech32ToHex(String bech32) {
final decoder = Bech32Decoder(); final decoder = Bech32Decoder();
final data = decoder.convert(bech32, 10_000); final data = decoder.convert(bech32, 10_000);

View File

@ -3,6 +3,7 @@ import 'package:zap_stream_flutter/theme.dart';
class BasicButton extends StatelessWidget { class BasicButton extends StatelessWidget {
final Widget? child; final Widget? child;
final Color? color;
final BoxDecoration? decoration; final BoxDecoration? decoration;
final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? padding;
final EdgeInsetsGeometry? margin; final EdgeInsetsGeometry? margin;
@ -12,6 +13,7 @@ class BasicButton extends StatelessWidget {
const BasicButton( const BasicButton(
this.child, { this.child, {
super.key, super.key,
this.color,
this.decoration, this.decoration,
this.padding, this.padding,
this.margin, this.margin,
@ -21,6 +23,7 @@ class BasicButton extends StatelessWidget {
static Widget text( static Widget text(
String text, { String text, {
Color? color,
BoxDecoration? decoration, BoxDecoration? decoration,
EdgeInsetsGeometry? padding, EdgeInsetsGeometry? padding,
EdgeInsetsGeometry? margin, EdgeInsetsGeometry? margin,
@ -46,6 +49,7 @@ class BasicButton extends StatelessWidget {
), ),
), ),
disabled: disabled, disabled: disabled,
color: color,
decoration: decoration, decoration: decoration,
padding: padding ?? EdgeInsets.symmetric(vertical: 4, horizontal: 12), padding: padding ?? EdgeInsets.symmetric(vertical: 4, horizontal: 12),
margin: margin, margin: margin,
@ -55,12 +59,17 @@ class BasicButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(
!(color != null && decoration != null),
"Cant set both 'color' and 'decoration'",
);
final defaultBr = BorderRadius.all(Radius.circular(100)); final defaultBr = BorderRadius.all(Radius.circular(100));
final inner = Container( final inner = Container(
padding: padding, padding: padding,
margin: margin, margin: margin,
decoration: decoration:
decoration ?? BoxDecoration(color: LAYER_2, borderRadius: defaultBr), decoration ??
BoxDecoration(color: color ?? LAYER_2, borderRadius: defaultBr),
child: Center(child: child), child: Center(child: child),
); );
return GestureDetector( return GestureDetector(

View File

@ -18,8 +18,17 @@ import 'package:zap_stream_flutter/widgets/profile.dart';
class ChatWidget extends StatelessWidget { class ChatWidget extends StatelessWidget {
final StreamEvent stream; final StreamEvent stream;
final bool? showGoals;
final bool? showTopZappers;
final bool? showRaids;
const ChatWidget({super.key, required this.stream}); const ChatWidget({
super.key,
required this.stream,
this.showGoals,
this.showTopZappers,
this.showRaids,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -31,7 +40,8 @@ class ChatWidget extends StatelessWidget {
var filters = [ var filters = [
Filter(kinds: [1311, 9735], limit: 200, aTags: [stream.aTag]), Filter(kinds: [1311, 9735], limit: 200, aTags: [stream.aTag]),
Filter(kinds: [1312, 1313], limit: 200, aTags: [stream.aTag]), if (showRaids ?? true)
Filter(kinds: [1312, 1313], limit: 200, aTags: [stream.aTag]),
Filter(kinds: [Nip51List.kMute], authors: moderators), Filter(kinds: [Nip51List.kMute], authors: moderators),
Filter(kinds: [1314], authors: moderators), Filter(kinds: [1314], authors: moderators),
Filter(kinds: [8], authors: [stream.info.host]), Filter(kinds: [8], authors: [stream.info.host]),
@ -108,10 +118,13 @@ class ChatWidget extends StatelessWidget {
spacing: 8, spacing: 8,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (zaps.isNotEmpty) _TopZappersWidget(events: zaps), if (zaps.isNotEmpty && (showTopZappers ?? true))
if (stream.info.goal != null) GoalWidget.id(stream.info.goal!), _TopZappersWidget(events: zaps),
if (stream.info.goal != null && (showGoals ?? true))
GoalWidget.id(stream.info.goal!),
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
padding: EdgeInsets.only(top: 80),
reverse: true, reverse: true,
itemCount: filteredChat.length, itemCount: filteredChat.length,
itemBuilder: (ctx, idx) { itemBuilder: (ctx, idx) {

View File

@ -24,6 +24,7 @@ class __WriteMessageWidget extends State<WriteMessageWidget> {
OverlayEntry? _entry; OverlayEntry? _entry;
late FocusNode _focusNode; late FocusNode _focusNode;
List<List<String>> _tags = List.empty(growable: true); List<List<String>> _tags = List.empty(growable: true);
final GlobalKey _positioned = GlobalKey();
@override @override
void initState() { void initState() {
@ -69,7 +70,8 @@ class __WriteMessageWidget extends State<WriteMessageWidget> {
_entry = null; _entry = null;
} }
final pos = context.findRenderObject() as RenderBox?; final pos = _positioned.currentContext!.findRenderObject() as RenderBox?;
final posGlobal = pos?.localToGlobal(Offset.zero);
_entry = OverlayEntry( _entry = OverlayEntry(
builder: (context) { builder: (context) {
return ValueListenableBuilder( return ValueListenableBuilder(
@ -85,12 +87,13 @@ class __WriteMessageWidget extends State<WriteMessageWidget> {
if (search.isEmpty) { if (search.isEmpty) {
return SizedBox(); return SizedBox();
} }
final mq = MediaQuery.of(context);
return Stack( return Stack(
children: [ children: [
Positioned( Positioned(
left: 0, left: posGlobal?.dx,
bottom: (pos?.paintBounds.bottom ?? 0), bottom: mq.size.height - (posGlobal?.dy ?? 0) - 30,
width: MediaQuery.of(context).size.width, width: pos?.size.width,
child: Container( child: Container(
padding: EdgeInsets.symmetric(horizontal: 4, vertical: 8), padding: EdgeInsets.symmetric(horizontal: 4, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -162,15 +165,17 @@ class __WriteMessageWidget extends State<WriteMessageWidget> {
_entry = null; _entry = null;
} }
final pos = context.findRenderObject() as RenderBox?; final pos = _positioned.currentContext!.findRenderObject() as RenderBox?;
final posGlobal = pos?.localToGlobal(Offset.zero);
_entry = OverlayEntry( _entry = OverlayEntry(
builder: (context) { builder: (context) {
final mq = MediaQuery.of(context);
return Stack( return Stack(
children: [ children: [
Positioned( Positioned(
left: 0, left: posGlobal?.dx,
bottom: (pos?.paintBounds.bottom ?? 0), bottom: mq.size.height - (posGlobal?.dy ?? 0) - 30,
width: MediaQuery.of(context).size.width, width: pos?.size.width,
child: Container( child: Container(
padding: EdgeInsets.symmetric(horizontal: 4, vertical: 8), padding: EdgeInsets.symmetric(horizontal: 4, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -239,9 +244,13 @@ class __WriteMessageWidget extends State<WriteMessageWidget> {
final isLogin = ndk.accounts.isLoggedIn; final isLogin = ndk.accounts.isLoggedIn;
return Container( return Container(
key: _positioned,
margin: EdgeInsets.fromLTRB(4, 8, 4, 0), margin: EdgeInsets.fromLTRB(4, 8, 4, 0),
padding: EdgeInsets.symmetric(horizontal: 8), padding: EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(color: LAYER_2, borderRadius: DEFAULT_BR), decoration: BoxDecoration(
color: LAYER_2.withAlpha(200),
borderRadius: DEFAULT_BR,
),
child: child:
canSign canSign
? Row( ? Row(

View File

@ -6,6 +6,7 @@ import 'package:zap_stream_flutter/i18n/strings.g.dart';
import 'package:zap_stream_flutter/const.dart'; import 'package:zap_stream_flutter/const.dart';
import 'package:zap_stream_flutter/theme.dart'; import 'package:zap_stream_flutter/theme.dart';
import 'package:zap_stream_flutter/widgets/avatar.dart'; import 'package:zap_stream_flutter/widgets/avatar.dart';
import 'package:zap_stream_flutter/widgets/button.dart';
class HeaderWidget extends StatefulWidget { class HeaderWidget extends StatefulWidget {
const HeaderWidget({super.key}); const HeaderWidget({super.key});
@ -39,12 +40,36 @@ class LoginButtonWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (ndk.accounts.isLoggedIn) { if (ndk.accounts.isLoggedIn) {
return GestureDetector( return Row(
onTap: spacing: 8,
() => context.go( children: [
"/p/${Nip19.encodePubKey(ndk.accounts.getPublicKey()!)}", BasicButton(
padding: EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
border: Border.all(color: WARNING),
borderRadius: DEFAULT_BR,
), ),
child: AvatarWidget.pubkey(ndk.accounts.getPublicKey()!), Row(
spacing: 4,
children: [
Icon(Icons.videocam),
Text(
t.live.start,
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
onTap: (context) => context.push("/live"),
),
GestureDetector(
onTap:
() => context.push(
"/p/${Nip19.encodePubKey(ndk.accounts.getPublicKey()!)}",
),
child: AvatarWidget.pubkey(ndk.accounts.getPublicKey()!),
),
],
); );
} else { } else {
return GestureDetector( return GestureDetector(
@ -59,10 +84,7 @@ class LoginButtonWidget extends StatelessWidget {
), ),
child: Row( child: Row(
spacing: 8, spacing: 8,
children: [ children: [Text(t.button.login), Icon(Icons.login, size: 16)],
Text(t.button.login),
Icon(Icons.login, size: 16),
],
), ),
), ),
); );

View File

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:duration/duration.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:zap_stream_flutter/theme.dart'; import 'package:zap_stream_flutter/theme.dart';
import 'package:zap_stream_flutter/utils.dart';
import 'package:zap_stream_flutter/widgets/pill.dart'; import 'package:zap_stream_flutter/widgets/pill.dart';
class LiveTimerWidget extends StatefulWidget { class LiveTimerWidget extends StatefulWidget {
@ -37,12 +37,13 @@ class _LiveTimerWidget extends State<LiveTimerWidget> {
return PillWidget( return PillWidget(
color: LAYER_2, color: LAYER_2,
child: Text( child: Text(
formatSecondsToHHMMSS( Duration(
((DateTime.now().millisecondsSinceEpoch - seconds:
widget.started.millisecondsSinceEpoch) / ((DateTime.now().millisecondsSinceEpoch -
1000) widget.started.millisecondsSinceEpoch) /
.toInt(), 1000)
), .toInt(),
).pretty(abbreviated: true),
), ),
); );
} }

View File

@ -1,12 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:zap_stream_flutter/notifications.dart'; import 'package:zap_stream_flutter/notifications.dart';
import 'package:zap_stream_flutter/theme.dart'; import 'package:zap_stream_flutter/theme.dart';
import 'package:zap_stream_flutter/utils.dart';
class NotificationsButtonWidget extends StatefulWidget { class NotificationsButtonWidget extends StatefulWidget {
final StreamEvent stream; final String pubkey;
const NotificationsButtonWidget({super.key, required this.stream}); const NotificationsButtonWidget({super.key, required this.pubkey});
@override @override
State<StatefulWidget> createState() => _NotificationsButtonWidget(); State<StatefulWidget> createState() => _NotificationsButtonWidget();
@ -18,9 +17,7 @@ class _NotificationsButtonWidget extends State<NotificationsButtonWidget> {
return ValueListenableBuilder( return ValueListenableBuilder(
valueListenable: notifications, valueListenable: notifications,
builder: (context, state, _) { builder: (context, state, _) {
final isNotified = (state?.notifyKeys ?? []).contains( final isNotified = (state?.notifyKeys ?? []).contains(widget.pubkey);
widget.stream.info.host,
);
return IconButton( return IconButton(
iconSize: 20, iconSize: 20,
onPressed: () async { onPressed: () async {
@ -28,9 +25,9 @@ class _NotificationsButtonWidget extends State<NotificationsButtonWidget> {
if (n == null) return; if (n == null) return;
if (isNotified) { if (isNotified) {
await n.removeWatchPubkey(widget.stream.info.host); await n.removeWatchPubkey(widget.pubkey);
} else { } else {
await n.watchPubkey(widget.stream.info.host, [30311]); await n.watchPubkey(widget.pubkey, [30311]);
} }
await notifications.reload(); await notifications.reload();
}, },

View File

@ -0,0 +1,174 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:zap_stream_flutter/api.dart';
import 'package:zap_stream_flutter/const.dart';
import 'package:zap_stream_flutter/i18n/strings.g.dart';
import 'package:zap_stream_flutter/theme.dart';
import 'package:zap_stream_flutter/widgets/button.dart';
import 'package:zap_stream_flutter/widgets/pill.dart';
class StreamConfigWidget extends StatefulWidget {
final ZapStreamApi api;
final AccountInfo account;
final bool? hideEndpointConfig;
const StreamConfigWidget({
super.key,
required this.api,
required this.account,
this.hideEndpointConfig,
});
@override
State<StatefulWidget> createState() => _StreamConfigWidget();
}
class _StreamConfigWidget extends State<StreamConfigWidget> {
late bool _nsfw;
late final TextEditingController _title;
late final TextEditingController _summary;
late final TextEditingController _tags;
@override
void initState() {
_title = TextEditingController(text: widget.account.details?.title);
_summary = TextEditingController(text: widget.account.details?.summary);
_tags = TextEditingController(
text: widget.account.details?.tags?.join(",") ?? "irl",
);
_nsfw = widget.account.details?.contentWarning?.isNotEmpty ?? false;
super.initState();
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: loginData,
builder: (context, state, _) {
final endpoint = widget.account.endpoints.firstWhereOrNull(
(e) => e.name == state?.streamEndpoint,
);
return Padding(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
Text(t.live.configure_stream, style: TextStyle(fontSize: 24)),
if (!(widget.hideEndpointConfig ?? false))
Row(
spacing: 8,
children: [
Icon(Icons.power),
Expanded(
child: DropdownButton<IngestEndpoint>(
value: endpoint,
hint: Text(t.live.endpoint),
items:
widget.account.endpoints
.map(
(e) => DropdownMenuItem(
value: e,
child: Text(e.name),
),
)
.toList(),
onChanged: (x) {
if (x != null) {
loginData.configure(streamEndpoint: x.name);
}
},
),
),
if (endpoint != null)
Text(
"${t.full_amount_sats(n: endpoint.cost.rate)}/${endpoint.cost.unit}",
),
],
),
if (endpoint != null && !(widget.hideEndpointConfig ?? false))
Row(
spacing: 8,
children:
endpoint.capabilities
.map(
(e) => PillWidget(color: LAYER_3, child: Text(e)),
)
.toList(),
),
TextField(
controller: _title,
decoration: InputDecoration(labelText: t.live.title),
),
TextField(
controller: _summary,
decoration: InputDecoration(labelText: t.live.summary),
minLines: 3,
maxLines: 5,
),
GestureDetector(
onTap: () {
setState(() {
_nsfw = !_nsfw;
});
},
child: Container(
decoration: BoxDecoration(
border: Border.all(color: WARNING),
borderRadius: DEFAULT_BR,
),
padding: EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Checkbox(
value: _nsfw,
onChanged: (v) {
setState(() {
_nsfw = !_nsfw;
});
},
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
t.live.nsfw,
style: TextStyle(
color: WARNING,
fontWeight: FontWeight.bold,
),
),
Text(t.live.nsfw_description),
],
),
),
],
),
),
),
BasicButton.text(
t.button.save,
onTap: (context) async {
await widget.api.updateDefaultStreamInfo(
title: _title.text,
summary: _summary.text,
contentWarning: _nsfw ? "nsfw" : null,
tags: _tags.text.split(","),
);
if (context.mounted) {
context.pop();
}
},
),
],
),
);
},
);
}
}

View File

@ -0,0 +1,53 @@
import 'package:chewie/chewie.dart';
import 'package:flutter/widgets.dart';
import 'package:zap_stream_flutter/main.dart';
class MainVideoPlayerWidget extends StatefulWidget {
final String url;
final String? title;
final String? placeholder;
final double? aspectRatio;
final bool? autoPlay;
final bool? isLive;
const MainVideoPlayerWidget({
super.key,
required this.url,
this.title,
this.placeholder,
this.aspectRatio,
this.autoPlay,
this.isLive,
});
@override
State<StatefulWidget> createState() => _MainVideoPlayerWidget();
}
class _MainVideoPlayerWidget extends State<MainVideoPlayerWidget> {
@override
void initState() {
mainPlayer.loadUrl(
widget.url,
title: widget.title,
placeholder: widget.placeholder,
aspectRatio: widget.aspectRatio,
autoPlay: widget.autoPlay,
isLive: widget.isLive,
artist: "zap.stream"
);
super.initState();
}
@override
void dispose() {
mainPlayer.stop();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Chewie(controller: mainPlayer.chewie!);
}
}

View File

@ -5,6 +5,8 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import audio_service
import audio_session
import emoji_picker_flutter import emoji_picker_flutter
import file_selector_macos import file_selector_macos
import firebase_core import firebase_core
@ -23,6 +25,8 @@ import video_player_avfoundation
import wakelock_plus import wakelock_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin"))
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin")) EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))

Some files were not shown because too many files have changed in this diff Show More