mirror of
https://github.com/nostrlabs-io/zap-stream-flutter.git
synced 2025-06-16 20:08:50 +00:00
feat: zaps / profiles
This commit is contained in:
@ -4,4 +4,5 @@
|
|||||||
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>
|
||||||
|
@ -31,6 +31,10 @@
|
|||||||
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.
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:developer' as developer;
|
||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
@ -19,14 +20,18 @@ class Account {
|
|||||||
Nip19.isKey("nsec", key) ? Bip340.getPublicKey(keyData) : keyData;
|
Nip19.isKey("nsec", key) ? Bip340.getPublicKey(keyData) : keyData;
|
||||||
final privateKey = Nip19.isKey("npub", key) ? null : keyData;
|
final privateKey = Nip19.isKey("npub", key) ? null : keyData;
|
||||||
return Account._(
|
return Account._(
|
||||||
type: AccountType.privateKey, pubkey: pubkey, privateKey: privateKey);
|
type: AccountType.privateKey,
|
||||||
|
pubkey: pubkey,
|
||||||
|
privateKey: privateKey,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Account privateKeyHex(String key) {
|
static Account privateKeyHex(String key) {
|
||||||
return Account._(
|
return Account._(
|
||||||
type: AccountType.privateKey,
|
type: AccountType.privateKey,
|
||||||
privateKey: key,
|
privateKey: key,
|
||||||
pubkey: Bip340.getPublicKey(key));
|
pubkey: Bip340.getPublicKey(key),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Account externalPublicKeyHex(String key) {
|
static Account externalPublicKeyHex(String key) {
|
||||||
@ -36,16 +41,18 @@ class Account {
|
|||||||
static Map<String, dynamic> toJson(Account? acc) => {
|
static Map<String, dynamic> toJson(Account? acc) => {
|
||||||
"type": acc?.type.name,
|
"type": acc?.type.name,
|
||||||
"pubKey": acc?.pubkey,
|
"pubKey": acc?.pubkey,
|
||||||
"privateKey": acc?.privateKey
|
"privateKey": acc?.privateKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
static Account? fromJson(Map<String, dynamic> json) {
|
static Account? fromJson(Map<String, dynamic> json) {
|
||||||
if (json.length > 2 && json.containsKey("pubKey")) {
|
if (json.length > 2 && json.containsKey("pubKey")) {
|
||||||
return Account._(
|
return Account._(
|
||||||
type: AccountType.values
|
type: AccountType.values.firstWhere(
|
||||||
.firstWhere((v) => v.toString().endsWith(json["type"] as String)),
|
(v) => v.toString().endsWith(json["type"] as String),
|
||||||
|
),
|
||||||
pubkey: json["pubKey"],
|
pubkey: json["pubKey"],
|
||||||
privateKey: json["privateKey"]);
|
privateKey: json["privateKey"],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -62,10 +69,19 @@ class LoginData extends ValueNotifier<Account?> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> logout() async {
|
||||||
|
super.value = null;
|
||||||
|
await _storage.delete(key: _storageKey);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> load() async {
|
Future<void> load() async {
|
||||||
final acc = await _storage.read(key: _storageKey);
|
final acc = await _storage.read(key: _storageKey);
|
||||||
if (acc != null) {
|
if (acc?.isNotEmpty ?? false) {
|
||||||
super.value = Account.fromJson(json.decode(acc));
|
try {
|
||||||
|
super.value = Account.fromJson(json.decode(acc!));
|
||||||
|
} catch (e) {
|
||||||
|
developer.log(e.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,6 +7,7 @@ import 'package:ndk_amber/ndk_amber.dart';
|
|||||||
import 'package:ndk_objectbox/ndk_objectbox.dart';
|
import 'package:ndk_objectbox/ndk_objectbox.dart';
|
||||||
import 'package:ndk_rust_verifier/ndk_rust_verifier.dart';
|
import 'package:ndk_rust_verifier/ndk_rust_verifier.dart';
|
||||||
import 'package:zap_stream_flutter/pages/login.dart';
|
import 'package:zap_stream_flutter/pages/login.dart';
|
||||||
|
import 'package:zap_stream_flutter/pages/profile.dart';
|
||||||
import 'package:zap_stream_flutter/pages/stream.dart';
|
import 'package:zap_stream_flutter/pages/stream.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/utils.dart';
|
||||||
@ -42,26 +43,29 @@ Future<void> main() async {
|
|||||||
// reload / cache login data
|
// reload / cache login data
|
||||||
loginData.addListener(() {
|
loginData.addListener(() {
|
||||||
if (loginData.value != null) {
|
if (loginData.value != null) {
|
||||||
if (!ndk.accounts.hasAccount(loginData.value!.pubkey)) {
|
final pubkey = loginData.value!.pubkey;
|
||||||
|
if (!ndk.accounts.hasAccount(pubkey)) {
|
||||||
switch (loginData.value!.type) {
|
switch (loginData.value!.type) {
|
||||||
case AccountType.privateKey:
|
case AccountType.privateKey:
|
||||||
ndk.accounts.loginPrivateKey(
|
ndk.accounts.loginPrivateKey(
|
||||||
pubkey: loginData.value!.pubkey,
|
pubkey: pubkey,
|
||||||
privkey: loginData.value!.privateKey!,
|
privkey: loginData.value!.privateKey!,
|
||||||
);
|
);
|
||||||
case AccountType.externalSigner:
|
case AccountType.externalSigner:
|
||||||
ndk.accounts.loginExternalSigner(
|
ndk.accounts.loginExternalSigner(
|
||||||
signer: AmberEventSigner(
|
signer: AmberEventSigner(
|
||||||
publicKey: loginData.value!.pubkey,
|
publicKey: pubkey,
|
||||||
amberFlutterDS: AmberFlutterDS(Amberflutter()),
|
amberFlutterDS: AmberFlutterDS(Amberflutter()),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
case AccountType.publicKey:
|
case AccountType.publicKey:
|
||||||
ndk.accounts.loginPublicKey(pubkey: loginData.value!.pubkey);
|
ndk.accounts.loginPublicKey(pubkey: pubkey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ndk.metadata.loadMetadata(loginData.value!.pubkey);
|
ndk.metadata.loadMetadata(pubkey);
|
||||||
ndk.follows.getContactList(loginData.value!.pubkey);
|
ndk.follows.getContactList(pubkey);
|
||||||
|
} else {
|
||||||
|
ndk.accounts.logout();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -94,6 +98,12 @@ Future<void> main() async {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: "/p/:id",
|
||||||
|
builder: (ctx, state) {
|
||||||
|
return ProfilePage(pubkey: state.pathParameters["id"]!);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -16,7 +16,9 @@ class HomePage extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
HeaderWidget(),
|
HeaderWidget(),
|
||||||
RxFilter<Nip01Event>(
|
RxFilter<Nip01Event>(
|
||||||
filter: Filter(kinds: [30_311], limit: 10),
|
filters: [
|
||||||
|
Filter(kinds: [30_311], limit: 50),
|
||||||
|
],
|
||||||
builder: (ctx, state) {
|
builder: (ctx, state) {
|
||||||
if (state == null) {
|
if (state == null) {
|
||||||
return SizedBox.shrink();
|
return SizedBox.shrink();
|
||||||
|
@ -4,10 +4,11 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:ndk/shared/nips/nip19/nip19.dart';
|
import 'package:ndk/shared/nips/nip19/nip19.dart';
|
||||||
import 'package:zap_stream_flutter/login.dart';
|
import 'package:zap_stream_flutter/login.dart';
|
||||||
import 'package:zap_stream_flutter/main.dart';
|
import 'package:zap_stream_flutter/main.dart';
|
||||||
import 'package:zap_stream_flutter/theme.dart';
|
|
||||||
import 'package:zap_stream_flutter/widgets/button.dart';
|
import 'package:zap_stream_flutter/widgets/button.dart';
|
||||||
|
|
||||||
class LoginPage extends StatefulWidget {
|
class LoginPage extends StatefulWidget {
|
||||||
|
const LoginPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _LoginPage();
|
State<StatefulWidget> createState() => _LoginPage();
|
||||||
}
|
}
|
||||||
@ -42,7 +43,7 @@ class _LoginPage extends State<LoginPage> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
BasicButton.text("Login with Key"),
|
/*BasicButton.text("Login with Key"),
|
||||||
Container(
|
Container(
|
||||||
margin: EdgeInsets.symmetric(vertical: 20),
|
margin: EdgeInsets.symmetric(vertical: 20),
|
||||||
height: 1,
|
height: 1,
|
||||||
@ -50,7 +51,7 @@ class _LoginPage extends State<LoginPage> {
|
|||||||
border: Border(bottom: BorderSide(color: LAYER_1)),
|
border: Border(bottom: BorderSide(color: LAYER_1)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text("Create Account"),
|
Text("Create Account"),*/
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
104
lib/pages/profile.dart
Normal file
104
lib/pages/profile.dart
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:ndk/ndk.dart';
|
||||||
|
import 'package:ndk/shared/nips/nip19/nip19.dart';
|
||||||
|
import 'package:zap_stream_flutter/imgproxy.dart';
|
||||||
|
import 'package:zap_stream_flutter/main.dart';
|
||||||
|
import 'package:zap_stream_flutter/rx_filter.dart';
|
||||||
|
import 'package:zap_stream_flutter/theme.dart';
|
||||||
|
import 'package:zap_stream_flutter/widgets/avatar.dart';
|
||||||
|
import 'package:zap_stream_flutter/widgets/button.dart';
|
||||||
|
import 'package:zap_stream_flutter/widgets/header.dart';
|
||||||
|
import 'package:zap_stream_flutter/widgets/profile.dart';
|
||||||
|
import 'package:zap_stream_flutter/widgets/stream_grid.dart';
|
||||||
|
|
||||||
|
class ProfilePage extends StatelessWidget {
|
||||||
|
final String pubkey;
|
||||||
|
|
||||||
|
const ProfilePage({super.key, required this.pubkey});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final hexPubkey = Nip19.decode(pubkey);
|
||||||
|
return ProfileLoaderWidget(hexPubkey, (ctx, state) {
|
||||||
|
final profile = state.data ?? Metadata(pubKey: hexPubkey);
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 16,
|
||||||
|
children: [
|
||||||
|
HeaderWidget(),
|
||||||
|
if (profile.banner != null)
|
||||||
|
SizedBox(
|
||||||
|
height: 140,
|
||||||
|
width: double.maxFinite,
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: proxyImg(context, profile.banner!),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
AvatarWidget(profile: profile, size: 80),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ProfileNameWidget(
|
||||||
|
profile: profile,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
profile.about ?? "",
|
||||||
|
style: TextStyle(color: LAYER_5),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
if (ndk.accounts.getPublicKey() == hexPubkey)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
BasicButton.text(
|
||||||
|
"Logout",
|
||||||
|
onTap: () {
|
||||||
|
loginData.logout();
|
||||||
|
context.go("/");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Past Streams",
|
||||||
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
|
||||||
|
RxFilter<Nip01Event>(
|
||||||
|
key: Key("profile-streams:$hexPubkey"),
|
||||||
|
relays: defaultRelays,
|
||||||
|
filters: [
|
||||||
|
Filter(kinds: [30_311], limit: 200, pTags: [hexPubkey]),
|
||||||
|
Filter(kinds: [30_311], limit: 200, authors: [hexPubkey]),
|
||||||
|
],
|
||||||
|
builder: (ctx, state) {
|
||||||
|
return StreamGrid(
|
||||||
|
events: state ?? [],
|
||||||
|
showLive: true,
|
||||||
|
showEnded: true,
|
||||||
|
showPlanned: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,15 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:chewie/chewie.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
import 'package:zap_stream_flutter/main.dart';
|
import 'package:zap_stream_flutter/main.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/utils.dart';
|
||||||
|
import 'package:zap_stream_flutter/widgets/button.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/chat.dart';
|
import 'package:zap_stream_flutter/widgets/chat.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/zap.dart';
|
||||||
|
|
||||||
class StreamPage extends StatefulWidget {
|
class StreamPage extends StatefulWidget {
|
||||||
final StreamEvent stream;
|
final StreamEvent stream;
|
||||||
@ -19,6 +22,7 @@ class StreamPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _StreamPage extends State<StreamPage> {
|
class _StreamPage extends State<StreamPage> {
|
||||||
VideoPlayerController? _controller;
|
VideoPlayerController? _controller;
|
||||||
|
ChewieController? _chewieController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -34,10 +38,15 @@ class _StreamPage extends State<StreamPage> {
|
|||||||
_controller = VideoPlayerController.networkUrl(
|
_controller = VideoPlayerController.networkUrl(
|
||||||
Uri.parse(url),
|
Uri.parse(url),
|
||||||
httpHeaders: Map.from({"user-agent": userAgent}),
|
httpHeaders: Map.from({"user-agent": userAgent}),
|
||||||
|
videoPlayerOptions: VideoPlayerOptions(allowBackgroundPlayback: true),
|
||||||
);
|
);
|
||||||
() async {
|
() async {
|
||||||
await _controller!.initialize();
|
await _controller!.initialize();
|
||||||
await _controller!.play();
|
|
||||||
|
_chewieController = ChewieController(
|
||||||
|
videoPlayerController: _controller!,
|
||||||
|
autoPlay: true,
|
||||||
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
// nothing
|
// nothing
|
||||||
});
|
});
|
||||||
@ -64,8 +73,8 @@ class _StreamPage extends State<StreamPage> {
|
|||||||
AspectRatio(
|
AspectRatio(
|
||||||
aspectRatio: 16 / 9,
|
aspectRatio: 16 / 9,
|
||||||
child:
|
child:
|
||||||
_controller != null
|
_chewieController != null
|
||||||
? VideoPlayer(_controller!)
|
? Chewie(controller: _chewieController!)
|
||||||
: Container(color: LAYER_1),
|
: Container(color: LAYER_1),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
@ -76,15 +85,45 @@ class _StreamPage extends State<StreamPage> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
ProfileWidget.pubkey(widget.stream.info.host),
|
ProfileWidget.pubkey(widget.stream.info.host),
|
||||||
|
Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
BasicButton(
|
||||||
|
Row(children: [Icon(Icons.bolt, size: 14), Text("Zap")]),
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: PRIMARY_1,
|
||||||
|
borderRadius: DEFAULT_BR,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
constraints: BoxConstraints.expand(),
|
||||||
|
builder: (ctx) {
|
||||||
|
return ZapWidget(
|
||||||
|
pubkey: widget.stream.info.host,
|
||||||
|
target: widget.stream.event,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (widget.stream.info.participants != null)
|
||||||
PillWidget(
|
PillWidget(
|
||||||
color: LAYER_1,
|
color: LAYER_1,
|
||||||
child: Text(
|
child: Text(
|
||||||
"${widget.stream.info.participants} viewers",
|
"${widget.stream.info.participants} viewers",
|
||||||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 10),
|
||||||
Expanded(child: ChatWidget(stream: widget.stream)),
|
Expanded(child: ChatWidget(stream: widget.stream)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -8,7 +8,7 @@ import 'package:zap_stream_flutter/main.dart';
|
|||||||
|
|
||||||
/// Reactive filter which builds the widget with a snapshot of the data
|
/// Reactive filter which builds the widget with a snapshot of the data
|
||||||
class RxFilter<T> extends StatefulWidget {
|
class RxFilter<T> extends StatefulWidget {
|
||||||
final Filter filter;
|
final List<Filter> filters;
|
||||||
final bool leaveOpen;
|
final bool leaveOpen;
|
||||||
final Widget Function(BuildContext, List<T>?) builder;
|
final Widget Function(BuildContext, List<T>?) builder;
|
||||||
final T Function(Nip01Event)? mapper;
|
final T Function(Nip01Event)? mapper;
|
||||||
@ -16,7 +16,7 @@ class RxFilter<T> extends StatefulWidget {
|
|||||||
|
|
||||||
const RxFilter({
|
const RxFilter({
|
||||||
super.key,
|
super.key,
|
||||||
required this.filter,
|
required this.filters,
|
||||||
required this.builder,
|
required this.builder,
|
||||||
this.mapper,
|
this.mapper,
|
||||||
this.leaveOpen = true,
|
this.leaveOpen = true,
|
||||||
@ -34,16 +34,16 @@ class _RxFilter<T> extends State<RxFilter<T>> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
developer.log("RX:SEDNING ${widget.filter}");
|
developer.log("RX:SEDNING ${widget.filters}");
|
||||||
_response = ndk.requests.subscription(
|
_response = ndk.requests.subscription(
|
||||||
filters: [widget.filter],
|
filters: widget.filters,
|
||||||
cacheRead: true,
|
cacheRead: true,
|
||||||
cacheWrite: true,
|
cacheWrite: true,
|
||||||
explicitRelays: widget.relays,
|
explicitRelays: widget.relays,
|
||||||
);
|
);
|
||||||
if (!widget.leaveOpen) {
|
if (!widget.leaveOpen) {
|
||||||
_response.future.then((_) {
|
_response.future.then((_) {
|
||||||
developer.log("RX:CLOSING ${widget.filter}");
|
developer.log("RX:CLOSING ${widget.filters}");
|
||||||
ndk.requests.closeSubscription(_response.requestId);
|
ndk.requests.closeSubscription(_response.requestId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -56,7 +56,7 @@ class _RxFilter<T> extends State<RxFilter<T>> {
|
|||||||
.listen((events) {
|
.listen((events) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_events ??= HashMap();
|
_events ??= HashMap();
|
||||||
developer.log("RX:GOT ${events.length} events for ${widget.filter}");
|
developer.log("RX:GOT ${events.length} events for ${widget.filters}");
|
||||||
events.forEach(_replaceInto);
|
events.forEach(_replaceInto);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -85,7 +85,7 @@ class _RxFilter<T> extends State<RxFilter<T>> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|
||||||
developer.log("RX:CLOSING ${widget.filter}");
|
developer.log("RX:CLOSING ${widget.filters}");
|
||||||
ndk.requests.closeSubscription(_response.requestId);
|
ndk.requests.closeSubscription(_response.requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,7 +98,7 @@ class _RxFilter<T> extends State<RxFilter<T>> {
|
|||||||
|
|
||||||
/// An async filter loader into [RxFilter]
|
/// An async filter loader into [RxFilter]
|
||||||
class RxFutureFilter<T> extends StatelessWidget {
|
class RxFutureFilter<T> extends StatelessWidget {
|
||||||
final Future<Filter> Function() filterBuilder;
|
final Future<List<Filter>> Function() filterBuilder;
|
||||||
final bool leaveOpen;
|
final bool leaveOpen;
|
||||||
final Widget Function(BuildContext, List<T>?) builder;
|
final Widget Function(BuildContext, List<T>?) builder;
|
||||||
final Widget? loadingWidget;
|
final Widget? loadingWidget;
|
||||||
@ -115,12 +115,12 @@ class RxFutureFilter<T> extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FutureBuilder<Filter>(
|
return FutureBuilder<List<Filter>>(
|
||||||
future: filterBuilder(),
|
future: filterBuilder(),
|
||||||
builder: (ctx, data) {
|
builder: (ctx, data) {
|
||||||
if (data.hasData) {
|
if (data.hasData) {
|
||||||
return RxFilter<T>(
|
return RxFilter<T>(
|
||||||
filter: data.data!, mapper: mapper, builder: builder);
|
filters: data.data!, mapper: mapper, builder: builder);
|
||||||
} else {
|
} else {
|
||||||
return loadingWidget ?? SizedBox.shrink();
|
return loadingWidget ?? SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
@ -10,8 +10,8 @@ Color LAYER_1 = Color.fromARGB(255, 23, 23, 23);
|
|||||||
Color LAYER_2 = Color.fromARGB(255, 34, 34, 34);
|
Color LAYER_2 = Color.fromARGB(255, 34, 34, 34);
|
||||||
Color LAYER_3 = Color.fromARGB(255, 50, 50, 50);
|
Color LAYER_3 = Color.fromARGB(255, 50, 50, 50);
|
||||||
Color LAYER_4 = Color.fromARGB(255, 121, 121, 121);
|
Color LAYER_4 = Color.fromARGB(255, 121, 121, 121);
|
||||||
|
Color LAYER_5 = Color.fromARGB(255, 173, 173, 173);
|
||||||
|
|
||||||
Color PRIMARY_1 = Color.fromARGB(255, 248, 56, 217);
|
Color PRIMARY_1 = Color.fromARGB(255, 248, 56, 217);
|
||||||
Color SECONDARY_1 = Color.fromARGB(255, 52, 210, 254);
|
Color SECONDARY_1 = Color.fromARGB(255, 52, 210, 254);
|
||||||
Color NEUTRAL_500 = Color.fromARGB(255, 155, 155, 155);
|
Color ZAP_1 = Color.fromARGB(255, 255, 141, 43);
|
||||||
Color NEUTRAL_800 = Color.fromARGB(255, 32, 32, 32);
|
|
||||||
|
@ -114,7 +114,11 @@ StreamInfo extractStreamInfo(Nip01Event ev) {
|
|||||||
matchTag(t, 'recording', (v) => ret.recording = v);
|
matchTag(t, 'recording', (v) => ret.recording = v);
|
||||||
matchTag(t, 'url', (v) => ret.recording = v);
|
matchTag(t, 'url', (v) => ret.recording = v);
|
||||||
matchTag(t, 'content-warning', (v) => ret.contentWarning = v);
|
matchTag(t, 'content-warning', (v) => ret.contentWarning = v);
|
||||||
matchTag(t, 'current_participants', (v) => ret.participants = int.tryParse(v));
|
matchTag(
|
||||||
|
t,
|
||||||
|
'current_participants',
|
||||||
|
(v) => ret.participants = int.tryParse(v),
|
||||||
|
);
|
||||||
matchTag(t, 'goal', (v) => ret.goal = v);
|
matchTag(t, 'goal', (v) => ret.goal = v);
|
||||||
matchTag(t, 'starts', (v) => ret.starts = int.tryParse(v));
|
matchTag(t, 'starts', (v) => ret.starts = int.tryParse(v));
|
||||||
matchTag(t, 'ends', (v) => ret.ends = int.tryParse(v));
|
matchTag(t, 'ends', (v) => ret.ends = int.tryParse(v));
|
||||||
@ -135,6 +139,10 @@ StreamInfo extractStreamInfo(Nip01Event ev) {
|
|||||||
} else {
|
} else {
|
||||||
ret.stream = ret.streams.firstWhereOrNull((a) => a.contains('.m3u8'));
|
ret.stream = ret.streams.firstWhereOrNull((a) => a.contains('.m3u8'));
|
||||||
}
|
}
|
||||||
|
if (ret.status == StreamStatus.ended &&
|
||||||
|
(ret.recording?.isNotEmpty ?? false)) {
|
||||||
|
ret.stream = ret.recording;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
@ -213,3 +221,20 @@ class Category {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Category> AllCategories = []; // Implement as needed
|
List<Category> AllCategories = []; // Implement as needed
|
||||||
|
|
||||||
|
String formatSats(int n) {
|
||||||
|
if (n >= 1000000) {
|
||||||
|
return "${(n / 1000000).toStringAsFixed(1)}M";
|
||||||
|
} else if (n >= 1000) {
|
||||||
|
return "${(n / 1000).toStringAsFixed(1)}k";
|
||||||
|
} else {
|
||||||
|
return "$n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String zapSum(List<Nip01Event> zaps) {
|
||||||
|
final total = zaps
|
||||||
|
.map((e) => ZapReceipt.fromEvent(e))
|
||||||
|
.fold(0, (acc, v) => acc + (v.amountSats ?? 0));
|
||||||
|
return formatSats(total);
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:zap_stream_flutter/theme.dart';
|
import 'package:zap_stream_flutter/theme.dart';
|
||||||
|
|
||||||
class BasicButton extends StatelessWidget {
|
class BasicButton extends StatelessWidget {
|
||||||
@ -35,7 +35,7 @@ class BasicButton extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
decoration: decoration,
|
decoration: decoration,
|
||||||
padding: padding ?? EdgeInsets.symmetric(vertical: 10),
|
padding: padding ?? EdgeInsets.symmetric(vertical: 4, horizontal: 12),
|
||||||
margin: margin,
|
margin: margin,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
);
|
);
|
||||||
@ -43,6 +43,7 @@ class BasicButton extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final defaultBr = BorderRadius.all(Radius.circular(100));
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
@ -50,10 +51,7 @@ class BasicButton extends StatelessWidget {
|
|||||||
margin: margin,
|
margin: margin,
|
||||||
decoration:
|
decoration:
|
||||||
decoration ??
|
decoration ??
|
||||||
BoxDecoration(
|
BoxDecoration(color: LAYER_2, borderRadius: defaultBr),
|
||||||
color: LAYER_2,
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(100)),
|
|
||||||
),
|
|
||||||
child: Center(child: child),
|
child: Center(child: child),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -24,7 +24,9 @@ class ChatWidget extends StatelessWidget {
|
|||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
reverse: true,
|
reverse: true,
|
||||||
child: RxFilter<Nip01Event>(
|
child: RxFilter<Nip01Event>(
|
||||||
filter: Filter(kinds: [1311], limit: 200, aTags: [stream.aTag]),
|
filters: [
|
||||||
|
Filter(kinds: [1311, 9735], limit: 200, aTags: [stream.aTag]),
|
||||||
|
],
|
||||||
builder: (ctx, state) {
|
builder: (ctx, state) {
|
||||||
return Column(
|
return Column(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
@ -32,14 +34,105 @@ class ChatWidget extends StatelessWidget {
|
|||||||
children:
|
children:
|
||||||
(state ?? [])
|
(state ?? [])
|
||||||
.sortedBy((c) => c.createdAt)
|
.sortedBy((c) => c.createdAt)
|
||||||
.map((c) => ChatMessageWidget(stream: stream, msg: c))
|
.map(
|
||||||
|
(c) => switch (c.kind) {
|
||||||
|
1311 => ChatMessageWidget(stream: stream, msg: c),
|
||||||
|
9735 => ChatZapWidget(stream: stream, zap: c),
|
||||||
|
_ => SizedBox.shrink(),
|
||||||
|
},
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (stream.info.status == StreamStatus.live)
|
||||||
WriteMessageWidget(stream: stream),
|
WriteMessageWidget(stream: stream),
|
||||||
|
if (stream.info.status == StreamStatus.ended)
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(8),
|
||||||
|
margin: EdgeInsets.symmetric(vertical: 8),
|
||||||
|
width: double.maxFinite,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: PRIMARY_1,
|
||||||
|
borderRadius: DEFAULT_BR,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
"STREAM ENDED",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatZapWidget extends StatelessWidget {
|
||||||
|
final StreamEvent stream;
|
||||||
|
final Nip01Event zap;
|
||||||
|
|
||||||
|
const ChatZapWidget({super.key, required this.stream, required this.zap});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final parsed = ZapReceipt.fromEvent(zap);
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: ZAP_1),
|
||||||
|
borderRadius: DEFAULT_BR,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_zapperRowZap(parsed),
|
||||||
|
if (parsed.comment?.isNotEmpty ?? false) Text(parsed.comment!),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _zapperRowZap(ZapReceipt parsed) {
|
||||||
|
if (parsed.sender != null) {
|
||||||
|
return ProfileLoaderWidget(parsed.sender!, (ctx, state) {
|
||||||
|
final name = ProfileNameWidget.nameFromProfile(
|
||||||
|
state.data ?? Metadata(pubKey: parsed.sender!),
|
||||||
|
);
|
||||||
|
return _zapperRow(name, parsed.amountSats ?? 0, state.data);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return _zapperRow("Anon", parsed.amountSats ?? 0, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _zapperRow(String name, int amount, Metadata? profile) {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
style: TextStyle(color: ZAP_1),
|
||||||
|
children: [
|
||||||
|
WidgetSpan(
|
||||||
|
child: Icon(Icons.bolt, color: ZAP_1),
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
),
|
||||||
|
if (profile != null)
|
||||||
|
WidgetSpan(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(right: 8),
|
||||||
|
child: AvatarWidget(profile: profile, size: 20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(text: name),
|
||||||
|
TextSpan(text: " zapped ", style: TextStyle(color: FONT_COLOR)),
|
||||||
|
TextSpan(text: formatSats(amount)),
|
||||||
|
TextSpan(text: " sats", style: TextStyle(color: FONT_COLOR)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -58,12 +151,19 @@ class ChatMessageWidget extends StatelessWidget {
|
|||||||
return RichText(
|
return RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
children: [
|
children: [
|
||||||
WidgetSpan(child: AvatarWidget(profile: profile, size: 20)),
|
WidgetSpan(
|
||||||
|
child: AvatarWidget(profile: profile, size: 24),
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
),
|
||||||
TextSpan(text: " "),
|
TextSpan(text: " "),
|
||||||
TextSpan(
|
WidgetSpan(
|
||||||
text: ProfileNameWidget.nameFromProfile(profile),
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
child: ProfileNameWidget(
|
||||||
|
profile: profile,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: msg.pubKey == stream.info.host ? PRIMARY_1 : SECONDARY_1,
|
color:
|
||||||
|
msg.pubKey == stream.info.host ? PRIMARY_1 : SECONDARY_1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextSpan(text: " "),
|
TextSpan(text: " "),
|
||||||
@ -120,6 +220,7 @@ class _WriteMessageWidget extends State<WriteMessageWidget> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
|
onSubmitted: (_) => _sendMessage(),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "Write message",
|
labelText: "Write message",
|
||||||
contentPadding: EdgeInsets.symmetric(vertical: 4),
|
contentPadding: EdgeInsets.symmetric(vertical: 4),
|
||||||
@ -128,7 +229,7 @@ class _WriteMessageWidget extends State<WriteMessageWidget> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(onPressed: () {}, icon: Icon(Icons.mood)),
|
//IconButton(onPressed: () {}, icon: Icon(Icons.mood)),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_sendMessage();
|
_sendMessage();
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:ndk/shared/nips/nip19/nip19.dart';
|
||||||
import 'package:zap_stream_flutter/main.dart';
|
import 'package:zap_stream_flutter/main.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';
|
||||||
@ -20,7 +21,10 @@ class _HeaderWidget extends State<HeaderWidget> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
SvgPicture.asset("assets/svg/logo.svg", height: 23),
|
GestureDetector(
|
||||||
|
onTap: () => context.go("/"),
|
||||||
|
child: SvgPicture.asset("assets/svg/logo.svg", height: 23),
|
||||||
|
),
|
||||||
LoginButtonWidget(),
|
LoginButtonWidget(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -29,10 +33,18 @@ class _HeaderWidget extends State<HeaderWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class LoginButtonWidget extends StatelessWidget {
|
class LoginButtonWidget extends StatelessWidget {
|
||||||
|
const LoginButtonWidget({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (ndk.accounts.isLoggedIn) {
|
if (ndk.accounts.isLoggedIn) {
|
||||||
return AvatarWidget.pubkey(ndk.accounts.getPublicKey()!);
|
return GestureDetector(
|
||||||
|
onTap:
|
||||||
|
() => context.go(
|
||||||
|
"/p/${Nip19.encodePubKey(ndk.accounts.getPublicKey()!)}",
|
||||||
|
),
|
||||||
|
child: AvatarWidget.pubkey(ndk.accounts.getPublicKey()!),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:ndk/ndk.dart';
|
import 'package:ndk/ndk.dart';
|
||||||
import 'package:ndk/shared/nips/nip19/nip19.dart';
|
import 'package:ndk/shared/nips/nip19/nip19.dart';
|
||||||
import 'package:zap_stream_flutter/main.dart';
|
import 'package:zap_stream_flutter/main.dart';
|
||||||
@ -48,7 +49,14 @@ class ProfileNameWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Text(ProfileNameWidget.nameFromProfile(profile), style: style);
|
return GestureDetector(
|
||||||
|
onTap:
|
||||||
|
() => context.push(
|
||||||
|
"/p/${Nip19.encodePubKey(profile.pubKey)}",
|
||||||
|
extra: profile,
|
||||||
|
),
|
||||||
|
child: Text(ProfileNameWidget.nameFromProfile(profile), style: style),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,9 +72,12 @@ class ProfileWidget extends StatelessWidget {
|
|||||||
this.size,
|
this.size,
|
||||||
});
|
});
|
||||||
|
|
||||||
static Widget pubkey(String pubkey) {
|
static Widget pubkey(String pubkey, {double? size}) {
|
||||||
return ProfileLoaderWidget(pubkey, (ctx, state) {
|
return ProfileLoaderWidget(pubkey, (ctx, state) {
|
||||||
return ProfileWidget(profile: state.data ?? Metadata(pubKey: pubkey));
|
return ProfileWidget(
|
||||||
|
profile: state.data ?? Metadata(pubKey: pubkey),
|
||||||
|
size: size,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:ndk/ndk.dart';
|
import 'package:ndk/ndk.dart';
|
||||||
import 'package:zap_stream_flutter/theme.dart';
|
import 'package:zap_stream_flutter/theme.dart';
|
||||||
@ -6,13 +7,35 @@ import 'package:zap_stream_flutter/widgets/stream_tile.dart';
|
|||||||
|
|
||||||
class StreamGrid extends StatelessWidget {
|
class StreamGrid extends StatelessWidget {
|
||||||
final List<Nip01Event> events;
|
final List<Nip01Event> events;
|
||||||
const StreamGrid({super.key, required this.events});
|
final bool showEnded;
|
||||||
|
final bool showLive;
|
||||||
|
final bool showPlanned;
|
||||||
|
|
||||||
|
const StreamGrid({
|
||||||
|
super.key,
|
||||||
|
required this.events,
|
||||||
|
this.showLive = true,
|
||||||
|
this.showEnded = false,
|
||||||
|
this.showPlanned = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final streams = events.map((e) => StreamEvent(e));
|
final streams = events
|
||||||
|
.map((e) => StreamEvent(e))
|
||||||
|
.where((e) => e.info.stream?.isNotEmpty ?? false)
|
||||||
|
.sortedBy((a) => a.info.starts ?? a.event.createdAt);
|
||||||
final live = streams.where((s) => s.info.status == StreamStatus.live);
|
final live = streams.where((s) => s.info.status == StreamStatus.live);
|
||||||
return Column(children: [_streamGroup("Live", live)]);
|
final ended = streams.where((s) => s.info.status == StreamStatus.ended);
|
||||||
|
final planned = streams.where((s) => s.info.status == StreamStatus.planned);
|
||||||
|
return Column(
|
||||||
|
spacing: 16,
|
||||||
|
children: [
|
||||||
|
if (showLive && live.isNotEmpty) _streamGroup("Live", live),
|
||||||
|
if (showPlanned && planned.isNotEmpty) _streamGroup("Planned", planned),
|
||||||
|
if (showEnded && ended.isNotEmpty) _streamGroup("Ended", ended),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _streamGroup(String title, Iterable<StreamEvent> events) {
|
Widget _streamGroup(String title, Iterable<StreamEvent> events) {
|
||||||
@ -30,7 +53,7 @@ class StreamGrid extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
height: 1,
|
height: 1,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(bottom: BorderSide(color: LAYER_1)),
|
border: Border(bottom: BorderSide(color: LAYER_2)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:ndk/shared/nips/nip19/nip19.dart';
|
import 'package:ndk/shared/nips/nip19/nip19.dart';
|
||||||
import 'package:zap_stream_flutter/imgproxy.dart';
|
import 'package:zap_stream_flutter/imgproxy.dart';
|
||||||
@ -34,15 +35,31 @@ class StreamTileWidget extends StatelessWidget {
|
|||||||
aspectRatio: 16 / 9,
|
aspectRatio: 16 / 9,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
CachedNetworkImage(
|
Center(
|
||||||
|
child: CachedNetworkImage(
|
||||||
imageUrl: proxyImg(context, stream.info.image ?? ""),
|
imageUrl: proxyImg(context, stream.info.image ?? ""),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder:
|
||||||
|
(ctx, url) => SvgPicture.asset(
|
||||||
|
"assets/svg/logo.svg",
|
||||||
|
height: 100,
|
||||||
|
),
|
||||||
|
errorWidget:
|
||||||
|
(context, url, error) => SvgPicture.asset(
|
||||||
|
"assets/svg/logo.svg",
|
||||||
|
height: 100,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (stream.info.status != null)
|
if (stream.info.status != null)
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 8,
|
right: 8,
|
||||||
top: 8,
|
top: 8,
|
||||||
child: PillWidget(
|
child: PillWidget(
|
||||||
color: Theme.of(context).highlightColor,
|
color: switch (stream.info.status) {
|
||||||
|
StreamStatus.live => Theme.of(context).highlightColor,
|
||||||
|
_ => LAYER_3,
|
||||||
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
stream.info.status!.name.toUpperCase(),
|
stream.info.status!.name.toUpperCase(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
192
lib/widgets/zap.dart
Normal file
192
lib/widgets/zap.dart
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import 'package:clipboard/clipboard.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:ndk/domain_layer/usecases/lnurl/lnurl.dart';
|
||||||
|
import 'package:ndk/ndk.dart';
|
||||||
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
import 'package:zap_stream_flutter/main.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/profile.dart';
|
||||||
|
|
||||||
|
class ZapWidget extends StatefulWidget {
|
||||||
|
final String pubkey;
|
||||||
|
final Nip01Event? target;
|
||||||
|
|
||||||
|
const ZapWidget({super.key, required this.pubkey, this.target});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _ZapWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ZapWidget extends State<ZapWidget> {
|
||||||
|
final TextEditingController _comment = TextEditingController();
|
||||||
|
String? _error;
|
||||||
|
String? _pr;
|
||||||
|
int? _amount;
|
||||||
|
final _zapAmounts = [
|
||||||
|
50,
|
||||||
|
100,
|
||||||
|
200,
|
||||||
|
500,
|
||||||
|
1000,
|
||||||
|
5000,
|
||||||
|
10000,
|
||||||
|
50000,
|
||||||
|
100000,
|
||||||
|
1000000,
|
||||||
|
];
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(10),
|
||||||
|
child: Column(
|
||||||
|
spacing: 10,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
spacing: 5,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Zap",
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
ProfileNameWidget.pubkey(
|
||||||
|
widget.pubkey,
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_pr == null) ..._inputs(),
|
||||||
|
if (_pr != null) ..._invoice(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _inputs() {
|
||||||
|
return [
|
||||||
|
GridView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: _zapAmounts.length,
|
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 5,
|
||||||
|
mainAxisSpacing: 5,
|
||||||
|
crossAxisSpacing: 5,
|
||||||
|
childAspectRatio: 1.5,
|
||||||
|
),
|
||||||
|
itemBuilder: (ctx, idx) => _zapAmount(_zapAmounts[idx]),
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
controller: _comment,
|
||||||
|
decoration: InputDecoration(labelText: "Comment"),
|
||||||
|
),
|
||||||
|
BasicButton.text(
|
||||||
|
"Zap",
|
||||||
|
onTap: () {
|
||||||
|
try {
|
||||||
|
_loadZap();
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_error = e.toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (_error != null) Text(_error!),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _invoice() {
|
||||||
|
return [
|
||||||
|
QrImageView(
|
||||||
|
data: _pr!,
|
||||||
|
size: 256,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
dataModuleStyle: QrDataModuleStyle(
|
||||||
|
dataModuleShape: QrDataModuleShape.square,
|
||||||
|
color: FONT_COLOR,
|
||||||
|
),
|
||||||
|
eyeStyle: QrEyeStyle(eyeShape: QrEyeShape.square, color: FONT_COLOR),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
await FlutterClipboard.copy(_pr!);
|
||||||
|
},
|
||||||
|
child: Text(_pr!, overflow: TextOverflow.ellipsis),
|
||||||
|
),
|
||||||
|
BasicButton.text(
|
||||||
|
"Open in Wallet",
|
||||||
|
onTap: () async {
|
||||||
|
try {
|
||||||
|
await launchUrlString("lightning:${_pr!}");
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_error = e is String ? e : e.toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (_error != null) Text(_error!),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadZap() async {
|
||||||
|
final profile = await ndk.metadata.loadMetadata(widget.pubkey);
|
||||||
|
if (profile?.lud16 == null) {
|
||||||
|
throw "No lightning address found";
|
||||||
|
}
|
||||||
|
final signer = ndk.accounts.getLoggedAccount()?.signer;
|
||||||
|
|
||||||
|
final zapRequest =
|
||||||
|
signer != null
|
||||||
|
? await ndk.zaps.createZapRequest(
|
||||||
|
amountSats: _amount!,
|
||||||
|
signer: signer,
|
||||||
|
pubKey: widget.pubkey,
|
||||||
|
eventId: widget.target?.id,
|
||||||
|
addressableId:
|
||||||
|
widget.target != null
|
||||||
|
? "${widget.target!.kind}:${widget.target!.pubKey}:${widget.target!.getDtag()!}"
|
||||||
|
: null,
|
||||||
|
relays: defaultRelays,
|
||||||
|
comment: _comment.text.isNotEmpty ? _comment.text : null,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
final invoice = await ndk.zaps.fetchInvoice(
|
||||||
|
lud16Link: Lnurl.getLud16LinkFromLud16(profile!.lud16!)!,
|
||||||
|
amountSats: _amount!,
|
||||||
|
zapRequest: zapRequest,
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_pr = invoice?.invoice;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _zapAmount(int n) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap:
|
||||||
|
() => setState(() {
|
||||||
|
_amount = n;
|
||||||
|
}),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: n == _amount ? LAYER_2 : LAYER_1,
|
||||||
|
borderRadius: DEFAULT_BR,
|
||||||
|
),
|
||||||
|
alignment: AlignmentDirectional.center,
|
||||||
|
child: Text(
|
||||||
|
formatSats(n),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||||
#include <objectbox_flutter_libs/objectbox_flutter_libs_plugin.h>
|
#include <objectbox_flutter_libs/objectbox_flutter_libs_plugin.h>
|
||||||
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||||
@ -16,4 +17,7 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||||||
g_autoptr(FlPluginRegistrar) objectbox_flutter_libs_registrar =
|
g_autoptr(FlPluginRegistrar) objectbox_flutter_libs_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "ObjectboxFlutterLibsPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "ObjectboxFlutterLibsPlugin");
|
||||||
objectbox_flutter_libs_plugin_register_with_registrar(objectbox_flutter_libs_registrar);
|
objectbox_flutter_libs_plugin_register_with_registrar(objectbox_flutter_libs_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
flutter_secure_storage_linux
|
flutter_secure_storage_linux
|
||||||
objectbox_flutter_libs
|
objectbox_flutter_libs
|
||||||
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
@ -10,6 +10,7 @@ import objectbox_flutter_libs
|
|||||||
import package_info_plus
|
import package_info_plus
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
|
import url_launcher_macos
|
||||||
import video_player_avfoundation
|
import video_player_avfoundation
|
||||||
import wakelock_plus
|
import wakelock_plus
|
||||||
|
|
||||||
@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
|
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
|
||||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||||
}
|
}
|
||||||
|
129
pubspec.lock
129
pubspec.lock
@ -89,6 +89,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
chewie:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: chewie
|
||||||
|
sha256: "4d9554a8f87cc2dc6575dfd5ad20a4375015a29edd567fd6733febe6365e2566"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.11.3"
|
||||||
|
clipboard:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: clipboard
|
||||||
|
sha256: "2ec38f0e59878008ceca0ab122e4bfde98847f88ef0f83331362ba4521f565a9"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.3"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -137,6 +153,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
version: "1.0.2"
|
||||||
|
cupertino_icons:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cupertino_icons
|
||||||
|
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.8"
|
||||||
dbus:
|
dbus:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -419,10 +443,11 @@ packages:
|
|||||||
ndk:
|
ndk:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: ndk
|
path: "packages/ndk"
|
||||||
sha256: "386a2e388785960a7e0c1cecf13e4440bcee30d44d07a03b37b8abd76aefafb0"
|
ref: bbf2aa9c2468b2301de65734199649d56bb0fd74
|
||||||
url: "https://pub.dev"
|
resolved-ref: bbf2aa9c2468b2301de65734199649d56bb0fd74
|
||||||
source: hosted
|
url: "https://github.com/relaystr/ndk"
|
||||||
|
source: git
|
||||||
version: "0.3.2"
|
version: "0.3.2"
|
||||||
ndk_amber:
|
ndk_amber:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
@ -448,6 +473,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.1"
|
version: "0.3.1"
|
||||||
|
nested:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: nested
|
||||||
|
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
objectbox:
|
objectbox:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -584,6 +617,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.9.1"
|
version: "3.9.1"
|
||||||
|
provider:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: provider
|
||||||
|
sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.5"
|
||||||
|
qr:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: qr
|
||||||
|
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
|
qr_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: qr_flutter
|
||||||
|
sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.0"
|
||||||
rust_lib_ndk:
|
rust_lib_ndk:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -717,6 +774,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
url_launcher:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: url_launcher
|
||||||
|
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.1"
|
||||||
|
url_launcher_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_android
|
||||||
|
sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.16"
|
||||||
|
url_launcher_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_ios
|
||||||
|
sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.3"
|
||||||
|
url_launcher_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_linux
|
||||||
|
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.1"
|
||||||
|
url_launcher_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_macos
|
||||||
|
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.2"
|
||||||
|
url_launcher_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_platform_interface
|
||||||
|
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
url_launcher_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_web
|
||||||
|
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
url_launcher_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_windows
|
||||||
|
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.4"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
11
pubspec.yaml
11
pubspec.yaml
@ -24,6 +24,17 @@ dependencies:
|
|||||||
convert: ^3.1.2
|
convert: ^3.1.2
|
||||||
collection: ^1.19.1
|
collection: ^1.19.1
|
||||||
video_player: ^2.9.5
|
video_player: ^2.9.5
|
||||||
|
clipboard: ^0.1.3
|
||||||
|
qr_flutter: ^4.1.0
|
||||||
|
url_launcher: ^6.3.1
|
||||||
|
chewie: ^1.11.3
|
||||||
|
|
||||||
|
dependency_overrides:
|
||||||
|
ndk:
|
||||||
|
git:
|
||||||
|
url: https://github.com/relaystr/ndk
|
||||||
|
path: packages/ndk
|
||||||
|
ref: bbf2aa9c2468b2301de65734199649d56bb0fd74
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@ -8,10 +8,13 @@
|
|||||||
|
|
||||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||||
#include <objectbox_flutter_libs/objectbox_flutter_libs_plugin.h>
|
#include <objectbox_flutter_libs/objectbox_flutter_libs_plugin.h>
|
||||||
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||||
ObjectboxFlutterLibsPluginRegisterWithRegistrar(
|
ObjectboxFlutterLibsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("ObjectboxFlutterLibsPlugin"));
|
registry->GetRegistrarForPlugin("ObjectboxFlutterLibsPlugin"));
|
||||||
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
flutter_secure_storage_windows
|
flutter_secure_storage_windows
|
||||||
objectbox_flutter_libs
|
objectbox_flutter_libs
|
||||||
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
Reference in New Issue
Block a user