mirror of
https://github.com/nostrlabs-io/zap-stream-flutter.git
synced 2025-06-15 11:48:21 +00:00
feat: zaps / profiles
This commit is contained in:
@ -4,4 +4,5 @@
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
</manifest>
|
||||
|
@ -31,6 +31,10 @@
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</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:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer' as developer;
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
@ -19,14 +20,18 @@ class Account {
|
||||
Nip19.isKey("nsec", key) ? Bip340.getPublicKey(keyData) : keyData;
|
||||
final privateKey = Nip19.isKey("npub", key) ? null : keyData;
|
||||
return Account._(
|
||||
type: AccountType.privateKey, pubkey: pubkey, privateKey: privateKey);
|
||||
type: AccountType.privateKey,
|
||||
pubkey: pubkey,
|
||||
privateKey: privateKey,
|
||||
);
|
||||
}
|
||||
|
||||
static Account privateKeyHex(String key) {
|
||||
return Account._(
|
||||
type: AccountType.privateKey,
|
||||
privateKey: key,
|
||||
pubkey: Bip340.getPublicKey(key));
|
||||
type: AccountType.privateKey,
|
||||
privateKey: key,
|
||||
pubkey: Bip340.getPublicKey(key),
|
||||
);
|
||||
}
|
||||
|
||||
static Account externalPublicKeyHex(String key) {
|
||||
@ -34,18 +39,20 @@ class Account {
|
||||
}
|
||||
|
||||
static Map<String, dynamic> toJson(Account? acc) => {
|
||||
"type": acc?.type.name,
|
||||
"pubKey": acc?.pubkey,
|
||||
"privateKey": acc?.privateKey
|
||||
};
|
||||
"type": acc?.type.name,
|
||||
"pubKey": acc?.pubkey,
|
||||
"privateKey": acc?.privateKey,
|
||||
};
|
||||
|
||||
static Account? fromJson(Map<String, dynamic> json) {
|
||||
if (json.length > 2 && json.containsKey("pubKey")) {
|
||||
return Account._(
|
||||
type: AccountType.values
|
||||
.firstWhere((v) => v.toString().endsWith(json["type"] as String)),
|
||||
pubkey: json["pubKey"],
|
||||
privateKey: json["privateKey"]);
|
||||
type: AccountType.values.firstWhere(
|
||||
(v) => v.toString().endsWith(json["type"] as String),
|
||||
),
|
||||
pubkey: json["pubKey"],
|
||||
privateKey: json["privateKey"],
|
||||
);
|
||||
}
|
||||
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 {
|
||||
final acc = await _storage.read(key: _storageKey);
|
||||
if (acc != null) {
|
||||
super.value = Account.fromJson(json.decode(acc));
|
||||
if (acc?.isNotEmpty ?? false) {
|
||||
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_rust_verifier/ndk_rust_verifier.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/theme.dart';
|
||||
import 'package:zap_stream_flutter/utils.dart';
|
||||
@ -42,26 +43,29 @@ Future<void> main() async {
|
||||
// reload / cache login data
|
||||
loginData.addListener(() {
|
||||
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) {
|
||||
case AccountType.privateKey:
|
||||
ndk.accounts.loginPrivateKey(
|
||||
pubkey: loginData.value!.pubkey,
|
||||
pubkey: pubkey,
|
||||
privkey: loginData.value!.privateKey!,
|
||||
);
|
||||
case AccountType.externalSigner:
|
||||
ndk.accounts.loginExternalSigner(
|
||||
signer: AmberEventSigner(
|
||||
publicKey: loginData.value!.pubkey,
|
||||
publicKey: pubkey,
|
||||
amberFlutterDS: AmberFlutterDS(Amberflutter()),
|
||||
),
|
||||
);
|
||||
case AccountType.publicKey:
|
||||
ndk.accounts.loginPublicKey(pubkey: loginData.value!.pubkey);
|
||||
ndk.accounts.loginPublicKey(pubkey: pubkey);
|
||||
}
|
||||
}
|
||||
ndk.metadata.loadMetadata(loginData.value!.pubkey);
|
||||
ndk.follows.getContactList(loginData.value!.pubkey);
|
||||
ndk.metadata.loadMetadata(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: [
|
||||
HeaderWidget(),
|
||||
RxFilter<Nip01Event>(
|
||||
filter: Filter(kinds: [30_311], limit: 10),
|
||||
filters: [
|
||||
Filter(kinds: [30_311], limit: 50),
|
||||
],
|
||||
builder: (ctx, state) {
|
||||
if (state == null) {
|
||||
return SizedBox.shrink();
|
||||
|
@ -4,10 +4,11 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:ndk/shared/nips/nip19/nip19.dart';
|
||||
import 'package:zap_stream_flutter/login.dart';
|
||||
import 'package:zap_stream_flutter/main.dart';
|
||||
import 'package:zap_stream_flutter/theme.dart';
|
||||
import 'package:zap_stream_flutter/widgets/button.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _LoginPage();
|
||||
}
|
||||
@ -42,7 +43,7 @@ class _LoginPage extends State<LoginPage> {
|
||||
}
|
||||
},
|
||||
),
|
||||
BasicButton.text("Login with Key"),
|
||||
/*BasicButton.text("Login with Key"),
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(vertical: 20),
|
||||
height: 1,
|
||||
@ -50,7 +51,7 @@ class _LoginPage extends State<LoginPage> {
|
||||
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:wakelock_plus/wakelock_plus.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/chat.dart';
|
||||
import 'package:zap_stream_flutter/widgets/pill.dart';
|
||||
import 'package:zap_stream_flutter/widgets/profile.dart';
|
||||
import 'package:zap_stream_flutter/widgets/zap.dart';
|
||||
|
||||
class StreamPage extends StatefulWidget {
|
||||
final StreamEvent stream;
|
||||
@ -19,6 +22,7 @@ class StreamPage extends StatefulWidget {
|
||||
|
||||
class _StreamPage extends State<StreamPage> {
|
||||
VideoPlayerController? _controller;
|
||||
ChewieController? _chewieController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -34,10 +38,15 @@ class _StreamPage extends State<StreamPage> {
|
||||
_controller = VideoPlayerController.networkUrl(
|
||||
Uri.parse(url),
|
||||
httpHeaders: Map.from({"user-agent": userAgent}),
|
||||
videoPlayerOptions: VideoPlayerOptions(allowBackgroundPlayback: true),
|
||||
);
|
||||
() async {
|
||||
await _controller!.initialize();
|
||||
await _controller!.play();
|
||||
|
||||
_chewieController = ChewieController(
|
||||
videoPlayerController: _controller!,
|
||||
autoPlay: true,
|
||||
);
|
||||
setState(() {
|
||||
// nothing
|
||||
});
|
||||
@ -64,8 +73,8 @@ class _StreamPage extends State<StreamPage> {
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child:
|
||||
_controller != null
|
||||
? VideoPlayer(_controller!)
|
||||
_chewieController != null
|
||||
? Chewie(controller: _chewieController!)
|
||||
: Container(color: LAYER_1),
|
||||
),
|
||||
Text(
|
||||
@ -76,15 +85,45 @@ class _StreamPage extends State<StreamPage> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ProfileWidget.pubkey(widget.stream.info.host),
|
||||
PillWidget(
|
||||
color: LAYER_1,
|
||||
child: Text(
|
||||
"${widget.stream.info.participants} viewers",
|
||||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
|
||||
),
|
||||
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(
|
||||
color: LAYER_1,
|
||||
child: Text(
|
||||
"${widget.stream.info.participants} viewers",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
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
|
||||
class RxFilter<T> extends StatefulWidget {
|
||||
final Filter filter;
|
||||
final List<Filter> filters;
|
||||
final bool leaveOpen;
|
||||
final Widget Function(BuildContext, List<T>?) builder;
|
||||
final T Function(Nip01Event)? mapper;
|
||||
@ -16,7 +16,7 @@ class RxFilter<T> extends StatefulWidget {
|
||||
|
||||
const RxFilter({
|
||||
super.key,
|
||||
required this.filter,
|
||||
required this.filters,
|
||||
required this.builder,
|
||||
this.mapper,
|
||||
this.leaveOpen = true,
|
||||
@ -34,16 +34,16 @@ class _RxFilter<T> extends State<RxFilter<T>> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
developer.log("RX:SEDNING ${widget.filter}");
|
||||
developer.log("RX:SEDNING ${widget.filters}");
|
||||
_response = ndk.requests.subscription(
|
||||
filters: [widget.filter],
|
||||
filters: widget.filters,
|
||||
cacheRead: true,
|
||||
cacheWrite: true,
|
||||
explicitRelays: widget.relays,
|
||||
);
|
||||
if (!widget.leaveOpen) {
|
||||
_response.future.then((_) {
|
||||
developer.log("RX:CLOSING ${widget.filter}");
|
||||
developer.log("RX:CLOSING ${widget.filters}");
|
||||
ndk.requests.closeSubscription(_response.requestId);
|
||||
});
|
||||
}
|
||||
@ -56,7 +56,7 @@ class _RxFilter<T> extends State<RxFilter<T>> {
|
||||
.listen((events) {
|
||||
setState(() {
|
||||
_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);
|
||||
});
|
||||
});
|
||||
@ -85,7 +85,7 @@ class _RxFilter<T> extends State<RxFilter<T>> {
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
|
||||
developer.log("RX:CLOSING ${widget.filter}");
|
||||
developer.log("RX:CLOSING ${widget.filters}");
|
||||
ndk.requests.closeSubscription(_response.requestId);
|
||||
}
|
||||
|
||||
@ -98,7 +98,7 @@ class _RxFilter<T> extends State<RxFilter<T>> {
|
||||
|
||||
/// An async filter loader into [RxFilter]
|
||||
class RxFutureFilter<T> extends StatelessWidget {
|
||||
final Future<Filter> Function() filterBuilder;
|
||||
final Future<List<Filter>> Function() filterBuilder;
|
||||
final bool leaveOpen;
|
||||
final Widget Function(BuildContext, List<T>?) builder;
|
||||
final Widget? loadingWidget;
|
||||
@ -115,12 +115,12 @@ class RxFutureFilter<T> extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<Filter>(
|
||||
return FutureBuilder<List<Filter>>(
|
||||
future: filterBuilder(),
|
||||
builder: (ctx, data) {
|
||||
if (data.hasData) {
|
||||
return RxFilter<T>(
|
||||
filter: data.data!, mapper: mapper, builder: builder);
|
||||
filters: data.data!, mapper: mapper, builder: builder);
|
||||
} else {
|
||||
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_3 = Color.fromARGB(255, 50, 50, 50);
|
||||
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 SECONDARY_1 = Color.fromARGB(255, 52, 210, 254);
|
||||
Color NEUTRAL_500 = Color.fromARGB(255, 155, 155, 155);
|
||||
Color NEUTRAL_800 = Color.fromARGB(255, 32, 32, 32);
|
||||
Color ZAP_1 = Color.fromARGB(255, 255, 141, 43);
|
||||
|
@ -114,7 +114,11 @@ StreamInfo extractStreamInfo(Nip01Event ev) {
|
||||
matchTag(t, 'recording', (v) => ret.recording = v);
|
||||
matchTag(t, 'url', (v) => ret.recording = 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, 'starts', (v) => ret.starts = int.tryParse(v));
|
||||
matchTag(t, 'ends', (v) => ret.ends = int.tryParse(v));
|
||||
@ -135,6 +139,10 @@ StreamInfo extractStreamInfo(Nip01Event ev) {
|
||||
} else {
|
||||
ret.stream = ret.streams.firstWhereOrNull((a) => a.contains('.m3u8'));
|
||||
}
|
||||
if (ret.status == StreamStatus.ended &&
|
||||
(ret.recording?.isNotEmpty ?? false)) {
|
||||
ret.stream = ret.recording;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
@ -213,3 +221,20 @@ class Category {
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
class BasicButton extends StatelessWidget {
|
||||
@ -35,7 +35,7 @@ class BasicButton extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
decoration: decoration,
|
||||
padding: padding ?? EdgeInsets.symmetric(vertical: 10),
|
||||
padding: padding ?? EdgeInsets.symmetric(vertical: 4, horizontal: 12),
|
||||
margin: margin,
|
||||
onTap: onTap,
|
||||
);
|
||||
@ -43,6 +43,7 @@ class BasicButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final defaultBr = BorderRadius.all(Radius.circular(100));
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
@ -50,10 +51,7 @@ class BasicButton extends StatelessWidget {
|
||||
margin: margin,
|
||||
decoration:
|
||||
decoration ??
|
||||
BoxDecoration(
|
||||
color: LAYER_2,
|
||||
borderRadius: BorderRadius.all(Radius.circular(100)),
|
||||
),
|
||||
BoxDecoration(color: LAYER_2, borderRadius: defaultBr),
|
||||
child: Center(child: child),
|
||||
),
|
||||
);
|
||||
|
@ -24,7 +24,9 @@ class ChatWidget extends StatelessWidget {
|
||||
child: SingleChildScrollView(
|
||||
reverse: true,
|
||||
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) {
|
||||
return Column(
|
||||
spacing: 8,
|
||||
@ -32,14 +34,105 @@ class ChatWidget extends StatelessWidget {
|
||||
children:
|
||||
(state ?? [])
|
||||
.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(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
WriteMessageWidget(stream: stream),
|
||||
if (stream.info.status == StreamStatus.live)
|
||||
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(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
WidgetSpan(child: AvatarWidget(profile: profile, size: 20)),
|
||||
WidgetSpan(
|
||||
child: AvatarWidget(profile: profile, size: 24),
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
),
|
||||
TextSpan(text: " "),
|
||||
TextSpan(
|
||||
text: ProfileNameWidget.nameFromProfile(profile),
|
||||
style: TextStyle(
|
||||
color: msg.pubKey == stream.info.host ? PRIMARY_1 : SECONDARY_1,
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: ProfileNameWidget(
|
||||
profile: profile,
|
||||
style: TextStyle(
|
||||
color:
|
||||
msg.pubKey == stream.info.host ? PRIMARY_1 : SECONDARY_1,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(text: " "),
|
||||
@ -120,6 +220,7 @@ class _WriteMessageWidget extends State<WriteMessageWidget> {
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
onSubmitted: (_) => _sendMessage(),
|
||||
decoration: InputDecoration(
|
||||
labelText: "Write message",
|
||||
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(
|
||||
onPressed: () {
|
||||
_sendMessage();
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:ndk/shared/nips/nip19/nip19.dart';
|
||||
import 'package:zap_stream_flutter/main.dart';
|
||||
import 'package:zap_stream_flutter/theme.dart';
|
||||
import 'package:zap_stream_flutter/widgets/avatar.dart';
|
||||
@ -20,7 +21,10 @@ class _HeaderWidget extends State<HeaderWidget> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SvgPicture.asset("assets/svg/logo.svg", height: 23),
|
||||
GestureDetector(
|
||||
onTap: () => context.go("/"),
|
||||
child: SvgPicture.asset("assets/svg/logo.svg", height: 23),
|
||||
),
|
||||
LoginButtonWidget(),
|
||||
],
|
||||
),
|
||||
@ -29,10 +33,18 @@ class _HeaderWidget extends State<HeaderWidget> {
|
||||
}
|
||||
|
||||
class LoginButtonWidget extends StatelessWidget {
|
||||
const LoginButtonWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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 {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
|
@ -1,4 +1,5 @@
|
||||
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/main.dart';
|
||||
@ -48,7 +49,14 @@ class ProfileNameWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
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,
|
||||
});
|
||||
|
||||
static Widget pubkey(String pubkey) {
|
||||
static Widget pubkey(String pubkey, {double? size}) {
|
||||
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:ndk/ndk.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 {
|
||||
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
|
||||
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);
|
||||
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) {
|
||||
@ -30,7 +53,7 @@ class StreamGrid extends StatelessWidget {
|
||||
child: Container(
|
||||
height: 1,
|
||||
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:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:ndk/shared/nips/nip19/nip19.dart';
|
||||
import 'package:zap_stream_flutter/imgproxy.dart';
|
||||
@ -34,15 +35,31 @@ class StreamTileWidget extends StatelessWidget {
|
||||
aspectRatio: 16 / 9,
|
||||
child: Stack(
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
imageUrl: proxyImg(context, stream.info.image ?? ""),
|
||||
Center(
|
||||
child: CachedNetworkImage(
|
||||
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)
|
||||
Positioned(
|
||||
right: 8,
|
||||
top: 8,
|
||||
child: PillWidget(
|
||||
color: Theme.of(context).highlightColor,
|
||||
color: switch (stream.info.status) {
|
||||
StreamStatus.live => Theme.of(context).highlightColor,
|
||||
_ => LAYER_3,
|
||||
},
|
||||
child: Text(
|
||||
stream.info.status!.name.toUpperCase(),
|
||||
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 <objectbox_flutter_libs/objectbox_flutter_libs_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
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 =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "ObjectboxFlutterLibsPlugin");
|
||||
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
|
||||
flutter_secure_storage_linux
|
||||
objectbox_flutter_libs
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
@ -10,6 +10,7 @@ import objectbox_flutter_libs
|
||||
import package_info_plus
|
||||
import path_provider_foundation
|
||||
import sqflite_darwin
|
||||
import url_launcher_macos
|
||||
import video_player_avfoundation
|
||||
import wakelock_plus
|
||||
|
||||
@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
|
||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||
}
|
||||
|
129
pubspec.lock
129
pubspec.lock
@ -89,6 +89,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -137,6 +153,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -419,10 +443,11 @@ packages:
|
||||
ndk:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: ndk
|
||||
sha256: "386a2e388785960a7e0c1cecf13e4440bcee30d44d07a03b37b8abd76aefafb0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: "packages/ndk"
|
||||
ref: bbf2aa9c2468b2301de65734199649d56bb0fd74
|
||||
resolved-ref: bbf2aa9c2468b2301de65734199649d56bb0fd74
|
||||
url: "https://github.com/relaystr/ndk"
|
||||
source: git
|
||||
version: "0.3.2"
|
||||
ndk_amber:
|
||||
dependency: "direct main"
|
||||
@ -448,6 +473,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
objectbox:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -584,6 +617,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -717,6 +774,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
11
pubspec.yaml
11
pubspec.yaml
@ -24,6 +24,17 @@ dependencies:
|
||||
convert: ^3.1.2
|
||||
collection: ^1.19.1
|
||||
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:
|
||||
flutter_test:
|
||||
|
@ -8,10 +8,13 @@
|
||||
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <objectbox_flutter_libs/objectbox_flutter_libs_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
ObjectboxFlutterLibsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ObjectboxFlutterLibsPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
flutter_secure_storage_windows
|
||||
objectbox_flutter_libs
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
Reference in New Issue
Block a user