Compare commits

..

3 commits

Author SHA1 Message Date
mikes222
c36081eddf Stub for creating and queriing from server implemented 2025-08-16 15:54:36 +02:00
tk
b427b44e4c rename android pkg, ignore client secrets 2025-08-16 15:47:46 +02:00
tk
707dfbd76c add sample http client call logic 2025-08-16 15:47:04 +02:00
22 changed files with 226 additions and 36 deletions

View file

@ -42,6 +42,21 @@ class EndpointQuote extends _i1.EndpointRef {
@override @override
String get name => 'quote'; String get name => 'quote';
_i2.Future<void> updateQuote(_i4.Quote quote) =>
caller.callServerEndpoint<void>(
'quote',
'updateQuote',
{'quote': quote},
);
_i2.Stream<_i4.Quote> quoteUpdates() =>
caller.callStreamingServerEndpoint<_i2.Stream<_i4.Quote>, _i4.Quote>(
'quote',
'quoteUpdates',
{},
{},
);
_i2.Future<_i4.Quote> createQuote(_i5.CreateQuoteRequest req) => _i2.Future<_i4.Quote> createQuote(_i5.CreateQuoteRequest req) =>
caller.callServerEndpoint<_i4.Quote>( caller.callServerEndpoint<_i4.Quote>(
'quote', 'quote',

View file

@ -6,7 +6,7 @@ plugins {
} }
android { android {
namespace = "com.example.wien_talks_flutter" namespace = "com.wien_talks"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion ndkVersion = flutter.ndkVersion
@ -21,7 +21,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.wien_talks_flutter" applicationId = "com.wien_talks"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion

View file

@ -1,4 +1,4 @@
package com.example.wien_talks_flutter package com.wien_talks
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity

View file

@ -15,7 +15,7 @@ class CreateEventScreen extends StatelessWidget {
return ScreenWidget( return ScreenWidget(
child: Column( child: Column(
children: [ children: [
NewsInputForm(onSubmit: (newsEventModel) {}), NewsInputForm(),
StreamBuilder(stream: LocationMgr().stream, builder: (BuildContext context, AsyncSnapshot<LocationData> snapshot) => Text(snapshot.data.toString())), StreamBuilder(stream: LocationMgr().stream, builder: (BuildContext context, AsyncSnapshot<LocationData> snapshot) => Text(snapshot.data.toString())),
Expanded( Expanded(
child: GetLocationWidget( child: GetLocationWidget(

View file

@ -93,6 +93,7 @@ class LocationMgr {
iconMarker = null; iconMarker = null;
viewModel?.dispose(); viewModel?.dispose();
viewModel = null; viewModel = null;
_lastLocationData = null;
} }
Stream<LocationData> get stream => _subject.stream; Stream<LocationData> get stream => _subject.stream;

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:serverpod_flutter/serverpod_flutter.dart'; import 'package:serverpod_flutter/serverpod_flutter.dart';
import 'package:wien_talks_client/wien_talks_client.dart'; import 'package:wien_talks_client/wien_talks_client.dart';
import 'package:wien_talks_flutter/go_router.dart'; import 'package:wien_talks_flutter/helper/go_router.dart';
/// Sets up a global client object that can be used to talk to the server from /// Sets up a global client object that can be used to talk to the server from
/// anywhere in our app. The client is generated from your server code /// anywhere in our app. The client is generated from your server code
@ -21,9 +21,13 @@ void main() {
// You can set the variable when running or building your app like this: // You can set the variable when running or building your app like this:
// E.g. `flutter run --dart-define=SERVER_URL=https://api.example.com/` // E.g. `flutter run --dart-define=SERVER_URL=https://api.example.com/`
const serverUrlFromEnv = String.fromEnvironment('SERVER_URL'); const serverUrlFromEnv = String.fromEnvironment('SERVER_URL');
final serverUrl = serverUrlFromEnv.isEmpty ? 'http://$localhost:8080/' : serverUrlFromEnv; final serverUrl =
serverUrlFromEnv.isEmpty ? 'http://$localhost:8080/' : serverUrlFromEnv;
client = Client(serverUrl)..connectivityMonitor = FlutterConnectivityMonitor(); client = Client(serverUrl)
..connectivityMonitor = FlutterConnectivityMonitor();
client.openStreamingConnection();
runApp(const MyApp()); runApp(const MyApp());
} }
@ -34,7 +38,7 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp.router( return MaterialApp.router(
title: 'Serverpod Demo', title: 'Wien Talks',
theme: ThemeData(primarySwatch: Colors.blue), theme: ThemeData(primarySwatch: Colors.blue),
routerConfig: router, routerConfig: router,
//home: NewsScreen(), //home: NewsScreen(),

View file

@ -1,10 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:wien_talks_flutter/models/news_event_model.dart'; import 'package:loader_overlay/loader_overlay.dart';
import 'package:location/location.dart';
import 'package:wien_talks_client/wien_talks_client.dart';
import 'package:wien_talks_flutter/location_mgr.dart';
import 'package:wien_talks_flutter/main.dart';
import 'package:wien_talks_flutter/widgets/error_snackbar.dart';
class NewsInputForm extends StatefulWidget { class NewsInputForm extends StatefulWidget {
final Function(NewsEventModel) onSubmit; const NewsInputForm({super.key});
const NewsInputForm({Key? key, required this.onSubmit}) : super(key: key);
@override @override
_NewsInputFormState createState() => _NewsInputFormState(); _NewsInputFormState createState() => _NewsInputFormState();
@ -20,16 +23,29 @@ class _NewsInputFormState extends State<NewsInputForm> {
super.dispose(); super.dispose();
} }
void _submitForm() { void _submitForm() async {
LocationData? locationData = LocationMgr().lastLocation;
if (locationData == null || locationData.latitude == null || locationData.longitude == null) {
ErrorSnackbar().show(context, "No location available, please retry later");
return;
}
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
final newsData = NewsEventModel( var handler = context.loaderOverlay..show();
content: _newsController.text.trim(), try {
timestamp: DateTime.now(), final newsData = CreateQuoteRequest(
latitude: 0.0, text: _newsController.text.trim(),
longitude: 0.0, lat: LocationMgr().lastLocation!.latitude!,
); lng: LocationMgr().lastLocation!.longitude!,
widget.onSubmit(newsData); );
_newsController.clear(); await client.quote.createQuote(newsData);
_newsController.clear();
} catch (error) {
if (mounted) {
ErrorSnackbar().show(context, error.toString());
}
} finally {
handler.hide();
}
} }
} }

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:wien_talks_flutter/show_latest_news_widget.dart';
import 'package:wien_talks_flutter/widgets/heading_text.dart'; import 'package:wien_talks_flutter/widgets/heading_text.dart';
import 'package:wien_talks_flutter/widgets/screen_widget.dart'; import 'package:wien_talks_flutter/widgets/screen_widget.dart';
@ -10,17 +11,32 @@ class NewsScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var column = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
HeadingText(text: "Latest news"),
...[Text("News 1"), Text("News 2")],
SizedBox(
height: 30,
),
OutlinedButton(
onPressed: () {
context.pushNamed("create_event");
},
child: Text("Submit your own event")),
],
);
return ScreenWidget( return ScreenWidget(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
HeadingText(text: "Latest news"), HeadingText(text: "Latest news"),
...[Text("News 1"), Text("News 2")], ShowLatestNewsWidget(),
SizedBox( SizedBox(
height: 30, height: 30,
), ),
OutlinedButton( ElevatedButton(
onPressed: () { onPressed: () {
context.pushNamed("create_event"); context.pushNamed("create_event");
}, },

View file

@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:wien_talks_flutter/main.dart';
class ShowLatestNewsWidget extends StatelessWidget {
const ShowLatestNewsWidget({super.key});
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: client.quote.stream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
return Text(snapshot.data ?? "Be the first to submit amazing news!", style: TextStyle(fontSize: 20, color: Theme.of(context).colorScheme.error));
return Placeholder();
});
}
}

View file

@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
import 'package:wien_talks_client/wien_talks_client.dart';
import 'package:wien_talks_flutter/main.dart';
class AddQuoteFab extends StatelessWidget {
const AddQuoteFab({
super.key,
});
@override
Widget build(BuildContext context) {
return FloatingActionButton(onPressed: () {
client.quote.createQuote(
CreateQuoteRequest(text: 'Quote Text', lat: 22, lng: 140));
});
}
}

View file

@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
class ErrorSnackbar {
void show(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(message, style: TextStyle(color: Theme.of(context).colorScheme.onError)),
showCloseIcon: true,
duration: Duration(seconds: 30),
backgroundColor: Theme.of(context).colorScheme.error,
));
}
}

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:loader_overlay/loader_overlay.dart';
class ScreenWidget extends StatelessWidget { class ScreenWidget extends StatelessWidget {
final Widget child; final Widget child;
@ -11,10 +12,11 @@ class ScreenWidget extends StatelessWidget {
appBar: AppBar( appBar: AppBar(
title: const Text('News'), title: const Text('News'),
), ),
// floatingActionButton: AddQuoteFab(),
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: child, child: LoaderOverlay(child: child),
)), )),
); );
} }

View file

@ -7,7 +7,7 @@ project(runner LANGUAGES CXX)
set(BINARY_NAME "wien_talks_flutter") set(BINARY_NAME "wien_talks_flutter")
# The unique GTK application identifier for this application. See: # The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID # https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.wien_talks_flutter") set(APPLICATION_ID "com.wien_talks")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent # Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake. # versions of CMake.

View file

@ -10,11 +10,8 @@ import file_picker
import file_selector_macos import file_selector_macos
import location import location
import path_provider_foundation import path_provider_foundation
<<<<<<< Updated upstream
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin import sqflite_darwin
=======
>>>>>>> Stashed changes
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
@ -22,9 +19,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
LocationPlugin.register(with: registry.registrar(forPlugin: "LocationPlugin")) LocationPlugin.register(with: registry.registrar(forPlugin: "LocationPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
<<<<<<< Updated upstream
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
=======
>>>>>>> Stashed changes
} }

View file

@ -27,6 +27,8 @@ dependencies:
go_router: ^16.1.0 go_router: ^16.1.0
loader_overlay: ^5.0.0
location: ^8.0.1 location: ^8.0.1
mapsforge_flutter: ^3.0.2 mapsforge_flutter: ^3.0.2

View file

@ -16,3 +16,4 @@ config/firebase_service_account_key.json
.env .env
*.env *.env
config/

View file

@ -12,9 +12,10 @@
import 'package:serverpod/serverpod.dart' as _i1; import 'package:serverpod/serverpod.dart' as _i1;
import '../greeting_endpoint.dart' as _i2; import '../greeting_endpoint.dart' as _i2;
import '../quotes/quotes_endpoint.dart' as _i3; import '../quotes/quotes_endpoint.dart' as _i3;
import 'package:wien_talks_server/src/generated/quotes/quote.dart' as _i4;
import 'package:wien_talks_server/src/generated/quotes/create_quote.dart' import 'package:wien_talks_server/src/generated/quotes/create_quote.dart'
as _i4; as _i5;
import 'package:serverpod_auth_server/serverpod_auth_server.dart' as _i5; import 'package:serverpod_auth_server/serverpod_auth_server.dart' as _i6;
class Endpoints extends _i1.EndpointDispatch { class Endpoints extends _i1.EndpointDispatch {
@override @override
@ -61,12 +62,30 @@ class Endpoints extends _i1.EndpointDispatch {
name: 'quote', name: 'quote',
endpoint: endpoints['quote']!, endpoint: endpoints['quote']!,
methodConnectors: { methodConnectors: {
'updateQuote': _i1.MethodConnector(
name: 'updateQuote',
params: {
'quote': _i1.ParameterDescription(
name: 'quote',
type: _i1.getType<_i4.Quote>(),
nullable: false,
)
},
call: (
_i1.Session session,
Map<String, dynamic> params,
) async =>
(endpoints['quote'] as _i3.QuoteEndpoint).updateQuote(
session,
params['quote'],
),
),
'createQuote': _i1.MethodConnector( 'createQuote': _i1.MethodConnector(
name: 'createQuote', name: 'createQuote',
params: { params: {
'req': _i1.ParameterDescription( 'req': _i1.ParameterDescription(
name: 'req', name: 'req',
type: _i1.getType<_i4.CreateQuoteRequest>(), type: _i1.getType<_i5.CreateQuoteRequest>(),
nullable: false, nullable: false,
) )
}, },
@ -106,8 +125,20 @@ class Endpoints extends _i1.EndpointDispatch {
) async => ) async =>
(endpoints['quote'] as _i3.QuoteEndpoint).getAllQuotes(session), (endpoints['quote'] as _i3.QuoteEndpoint).getAllQuotes(session),
), ),
'quoteUpdates': _i1.MethodStreamConnector(
name: 'quoteUpdates',
params: {},
streamParams: {},
returnType: _i1.MethodStreamReturnType.streamType,
call: (
_i1.Session session,
Map<String, dynamic> params,
Map<String, Stream> streamParams,
) =>
(endpoints['quote'] as _i3.QuoteEndpoint).quoteUpdates(session),
),
}, },
); );
modules['serverpod_auth'] = _i5.Endpoints()..initializeEndpoints(server); modules['serverpod_auth'] = _i6.Endpoints()..initializeEndpoints(server);
} }
} }

View file

@ -1,6 +1,8 @@
greeting: greeting:
- hello: - hello:
quote: quote:
- updateQuote:
- quoteUpdates:
- createQuote: - createQuote:
- getQuoteById: - getQuoteById:
- getAllQuotes: - getAllQuotes:

View file

@ -1,6 +1,6 @@
import 'package:serverpod/serverpod.dart'; import 'package:serverpod/serverpod.dart';
import 'package:wien_talks_server/src/generated/protocol.dart'; import 'package:wien_talks_server/src/generated/protocol.dart';
import 'package:wien_talks_server/src/quotes/quote_controller.dart'; import 'package:wien_talks_server/src/quotes/quote_util.dart';
class QuoteEndpoint extends Endpoint { class QuoteEndpoint extends Endpoint {
static const _channelQuoteUpdates = 'quote-updates'; static const _channelQuoteUpdates = 'quote-updates';

View file

@ -177,6 +177,64 @@ class _QuoteEndpoint {
final _i2.SerializationManager _serializationManager; final _i2.SerializationManager _serializationManager;
_i3.Future<void> updateQuote(
_i1.TestSessionBuilder sessionBuilder,
_i5.Quote quote,
) async {
return _i1.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _i1.InternalTestSessionBuilder).internalBuild(
endpoint: 'quote',
method: 'updateQuote',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'quote',
methodName: 'updateQuote',
parameters: _i1.testObjectToJson({'quote': quote}),
serializationManager: _serializationManager,
);
var _localReturnValue = await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
) as _i3.Future<void>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_i3.Stream<_i5.Quote> quoteUpdates(_i1.TestSessionBuilder sessionBuilder) {
var _localTestStreamManager = _i1.TestStreamManager<_i5.Quote>();
_i1.callStreamFunctionAndHandleExceptions(
() async {
var _localUniqueSession =
(sessionBuilder as _i1.InternalTestSessionBuilder).internalBuild(
endpoint: 'quote',
method: 'quoteUpdates',
);
var _localCallContext =
await _endpointDispatch.getMethodStreamCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'quote',
methodName: 'quoteUpdates',
arguments: {},
requestedInputStreams: [],
serializationManager: _serializationManager,
);
await _localTestStreamManager.callStreamMethod(
_localCallContext,
_localUniqueSession,
{},
);
},
_localTestStreamManager.outputStreamController,
);
return _localTestStreamManager.outputStreamController.stream;
}
_i3.Future<_i5.Quote> createQuote( _i3.Future<_i5.Quote> createQuote(
_i1.TestSessionBuilder sessionBuilder, _i1.TestSessionBuilder sessionBuilder,
_i6.CreateQuoteRequest req, _i6.CreateQuoteRequest req,