mirror of
https://github.com/nostrlabs-io/zap-stream-flutter.git
synced 2025-06-16 11:58:50 +00:00
@ -3,6 +3,7 @@ import 'package:zap_stream_flutter/theme.dart';
|
||||
|
||||
class BasicButton extends StatelessWidget {
|
||||
final Widget? child;
|
||||
final Color? color;
|
||||
final BoxDecoration? decoration;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final EdgeInsetsGeometry? margin;
|
||||
@ -12,6 +13,7 @@ class BasicButton extends StatelessWidget {
|
||||
const BasicButton(
|
||||
this.child, {
|
||||
super.key,
|
||||
this.color,
|
||||
this.decoration,
|
||||
this.padding,
|
||||
this.margin,
|
||||
@ -21,6 +23,7 @@ class BasicButton extends StatelessWidget {
|
||||
|
||||
static Widget text(
|
||||
String text, {
|
||||
Color? color,
|
||||
BoxDecoration? decoration,
|
||||
EdgeInsetsGeometry? padding,
|
||||
EdgeInsetsGeometry? margin,
|
||||
@ -46,6 +49,7 @@ class BasicButton extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
disabled: disabled,
|
||||
color: color,
|
||||
decoration: decoration,
|
||||
padding: padding ?? EdgeInsets.symmetric(vertical: 4, horizontal: 12),
|
||||
margin: margin,
|
||||
@ -55,12 +59,17 @@ class BasicButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(
|
||||
!(color != null && decoration != null),
|
||||
"Cant set both 'color' and 'decoration'",
|
||||
);
|
||||
final defaultBr = BorderRadius.all(Radius.circular(100));
|
||||
final inner = Container(
|
||||
padding: padding,
|
||||
margin: margin,
|
||||
decoration:
|
||||
decoration ?? BoxDecoration(color: LAYER_2, borderRadius: defaultBr),
|
||||
decoration ??
|
||||
BoxDecoration(color: color ?? LAYER_2, borderRadius: defaultBr),
|
||||
child: Center(child: child),
|
||||
);
|
||||
return GestureDetector(
|
||||
|
@ -18,8 +18,17 @@ import 'package:zap_stream_flutter/widgets/profile.dart';
|
||||
|
||||
class ChatWidget extends StatelessWidget {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
@ -31,7 +40,8 @@ class ChatWidget extends StatelessWidget {
|
||||
|
||||
var filters = [
|
||||
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: [1314], authors: moderators),
|
||||
Filter(kinds: [8], authors: [stream.info.host]),
|
||||
@ -108,10 +118,13 @@ class ChatWidget extends StatelessWidget {
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (zaps.isNotEmpty) _TopZappersWidget(events: zaps),
|
||||
if (stream.info.goal != null) GoalWidget.id(stream.info.goal!),
|
||||
if (zaps.isNotEmpty && (showTopZappers ?? true))
|
||||
_TopZappersWidget(events: zaps),
|
||||
if (stream.info.goal != null && (showGoals ?? true))
|
||||
GoalWidget.id(stream.info.goal!),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.only(top: 80),
|
||||
reverse: true,
|
||||
itemCount: filteredChat.length,
|
||||
itemBuilder: (ctx, idx) {
|
||||
|
@ -24,6 +24,7 @@ class __WriteMessageWidget extends State<WriteMessageWidget> {
|
||||
OverlayEntry? _entry;
|
||||
late FocusNode _focusNode;
|
||||
List<List<String>> _tags = List.empty(growable: true);
|
||||
final GlobalKey _positioned = GlobalKey();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -69,7 +70,8 @@ class __WriteMessageWidget extends State<WriteMessageWidget> {
|
||||
_entry = null;
|
||||
}
|
||||
|
||||
final pos = context.findRenderObject() as RenderBox?;
|
||||
final pos = _positioned.currentContext!.findRenderObject() as RenderBox?;
|
||||
final posGlobal = pos?.localToGlobal(Offset.zero);
|
||||
_entry = OverlayEntry(
|
||||
builder: (context) {
|
||||
return ValueListenableBuilder(
|
||||
@ -85,12 +87,13 @@ class __WriteMessageWidget extends State<WriteMessageWidget> {
|
||||
if (search.isEmpty) {
|
||||
return SizedBox();
|
||||
}
|
||||
final mq = MediaQuery.of(context);
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
bottom: (pos?.paintBounds.bottom ?? 0),
|
||||
width: MediaQuery.of(context).size.width,
|
||||
left: posGlobal?.dx,
|
||||
bottom: mq.size.height - (posGlobal?.dy ?? 0) - 30,
|
||||
width: pos?.size.width,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
@ -162,15 +165,17 @@ class __WriteMessageWidget extends State<WriteMessageWidget> {
|
||||
_entry = null;
|
||||
}
|
||||
|
||||
final pos = context.findRenderObject() as RenderBox?;
|
||||
final pos = _positioned.currentContext!.findRenderObject() as RenderBox?;
|
||||
final posGlobal = pos?.localToGlobal(Offset.zero);
|
||||
_entry = OverlayEntry(
|
||||
builder: (context) {
|
||||
final mq = MediaQuery.of(context);
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
bottom: (pos?.paintBounds.bottom ?? 0),
|
||||
width: MediaQuery.of(context).size.width,
|
||||
left: posGlobal?.dx,
|
||||
bottom: mq.size.height - (posGlobal?.dy ?? 0) - 30,
|
||||
width: pos?.size.width,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
@ -239,9 +244,13 @@ class __WriteMessageWidget extends State<WriteMessageWidget> {
|
||||
final isLogin = ndk.accounts.isLoggedIn;
|
||||
|
||||
return Container(
|
||||
key: _positioned,
|
||||
margin: EdgeInsets.fromLTRB(4, 8, 4, 0),
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(color: LAYER_2, borderRadius: DEFAULT_BR),
|
||||
decoration: BoxDecoration(
|
||||
color: LAYER_2.withAlpha(200),
|
||||
borderRadius: DEFAULT_BR,
|
||||
),
|
||||
child:
|
||||
canSign
|
||||
? Row(
|
||||
|
@ -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/theme.dart';
|
||||
import 'package:zap_stream_flutter/widgets/avatar.dart';
|
||||
import 'package:zap_stream_flutter/widgets/button.dart';
|
||||
|
||||
class HeaderWidget extends StatefulWidget {
|
||||
const HeaderWidget({super.key});
|
||||
@ -39,12 +40,36 @@ class LoginButtonWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (ndk.accounts.isLoggedIn) {
|
||||
return GestureDetector(
|
||||
onTap:
|
||||
() => context.go(
|
||||
"/p/${Nip19.encodePubKey(ndk.accounts.getPublicKey()!)}",
|
||||
return Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
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 {
|
||||
return GestureDetector(
|
||||
@ -59,10 +84,7 @@ class LoginButtonWidget extends StatelessWidget {
|
||||
),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(t.button.login),
|
||||
Icon(Icons.login, size: 16),
|
||||
],
|
||||
children: [Text(t.button.login), Icon(Icons.login, size: 16)],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'package:duration/duration.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:zap_stream_flutter/theme.dart';
|
||||
import 'package:zap_stream_flutter/utils.dart';
|
||||
import 'package:zap_stream_flutter/widgets/pill.dart';
|
||||
|
||||
class LiveTimerWidget extends StatefulWidget {
|
||||
@ -37,12 +37,13 @@ class _LiveTimerWidget extends State<LiveTimerWidget> {
|
||||
return PillWidget(
|
||||
color: LAYER_2,
|
||||
child: Text(
|
||||
formatSecondsToHHMMSS(
|
||||
((DateTime.now().millisecondsSinceEpoch -
|
||||
widget.started.millisecondsSinceEpoch) /
|
||||
1000)
|
||||
.toInt(),
|
||||
),
|
||||
Duration(
|
||||
seconds:
|
||||
((DateTime.now().millisecondsSinceEpoch -
|
||||
widget.started.millisecondsSinceEpoch) /
|
||||
1000)
|
||||
.toInt(),
|
||||
).pretty(abbreviated: true),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
174
lib/widgets/stream_config.dart
Normal file
174
lib/widgets/stream_config.dart
Normal 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();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user