This commit is contained in:
2026-02-13 15:53:22 +01:00
commit 25271189b5
639 changed files with 49083 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
import 'package:hiddingsel_app/appflow/controller/network/wordpress.dart';
import 'package:hiddingsel_app/appflow/model/articles.dart';
import 'package:hiddingsel_app/appflow/model/represented_organisations.dart';
import 'package:hiddingsel_app/constants/constant.dart';
import '../../services/storage.dart';
class ArticleController {
static int defaultSortCompare(ArticleModel a, ArticleModel b) => b.publishedAt.compareTo(a.publishedAt);
static Stream<List<ArticleModel>> getArticleListStream({int Function(ArticleModel, ArticleModel) sortCompare = defaultSortCompare}) async*{
List<ArticleModel> yieldArticles = [];
List<ArticleModel> allWordpressArticles = [];
List<ArticleModel> savedArticles = await DataConnector.getJsonEncodableList<ArticleModel>(SystemStrings.dataArticles, [], ArticleModel.fromJson);
yield yieldArticles..addAll(savedArticles)..sort(sortCompare);
for (var page = 0, perPage = 10; page < 10; page++) {
var wordpressArticles = await WordpressController.getArticles(page, perPage);
var newWordpressArticles = wordpressArticles.where((a) => !yieldArticles.contains(a)).toList();
if (newWordpressArticles.isNotEmpty) {
yield yieldArticles..addAll(newWordpressArticles)..sort(sortCompare);
}
allWordpressArticles = allWordpressArticles..addAll(wordpressArticles)..sort(sortCompare);
DataConnector.saveJsonEncodable(SystemStrings.dataArticles, allWordpressArticles);
}
}
static Stream<List<ArticleModel>> getSearchResultListStream(String searchWord, {int Function(ArticleModel, ArticleModel) sortCompare = defaultSortCompare}) async*{
List<ArticleModel> yieldArticles = [];
for (var page = 0, perPage = 10; page < 3; page++) {
var wordpressArticles = await WordpressController.searchArticles(page, perPage, searchWord);
var newWordpressArticles = wordpressArticles.where((a) => !yieldArticles.contains(a)).toList();
if (newWordpressArticles.isNotEmpty) {
yield yieldArticles..addAll(newWordpressArticles)..sort(sortCompare);
}
}
}
static Stream<List<ArticleModel>> getOrganisationArticleListStream(RepresentedOrganisationModel organisation, {int Function(ArticleModel, ArticleModel) sortCompare = defaultSortCompare}) async*{
List<ArticleModel> yieldArticles = [];
List<ArticleModel> allWordpressArticles = [];
List<ArticleModel> savedArticles = await DataConnector.getJsonEncodableList<ArticleModel>(SystemStrings.dataArticles + organisation.id, [], ArticleModel.fromJson);
yield yieldArticles..addAll(savedArticles)..sort(sortCompare);
for (var page = 0, perPage = 10; page < 10; page++) {
var wordpressArticles = await WordpressController.getArticlesFromOrganisations(page, perPage, [organisation]);
var newWordpressArticles = wordpressArticles.where((a) => !yieldArticles.contains(a)).toList();
if (newWordpressArticles.isNotEmpty) {
yield yieldArticles..addAll(newWordpressArticles)..sort(sortCompare);
}
allWordpressArticles = allWordpressArticles..addAll(wordpressArticles)..sort(sortCompare);
DataConnector.saveJsonEncodable(SystemStrings.dataArticles + organisation.id, allWordpressArticles);
}
}
static Stream<List<ArticleModel>> getFavoriteArticleListStream({int Function(ArticleModel, ArticleModel) sortCompare = defaultSortCompare}) async*{
var favoriteOrganisations = List<RepresentedOrganisationModel>.empty(growable: true);
for (RepresentedOrganisationModel organisation in RepresentedOrganisationModel.values) {
if(await organisation.favorized) favoriteOrganisations.add(organisation);
}
List<ArticleModel> yieldArticles = [];
List<ArticleModel> allWordpressArticles = [];
List<ArticleModel> savedArticles = await DataConnector.getJsonEncodableList<ArticleModel>(SystemStrings.dataFavoriteArticles, [], ArticleModel.fromJson);
List<ArticleModel> savedFavoriteArticles = savedArticles.where((e) => e.categories.any((orga) => favoriteOrganisations.contains(orga))).toList();
yield yieldArticles..addAll(savedFavoriteArticles)..sort(sortCompare);
for (var page = 0, perPage = 10; page < 10; page++) {
var wordpressArticles = await WordpressController.getArticlesFromOrganisations(page, perPage, favoriteOrganisations);
var newWordpressArticles = wordpressArticles.where((a) => !yieldArticles.contains(a)).toList();
if (newWordpressArticles.isNotEmpty) {
yield yieldArticles..addAll(newWordpressArticles)..sort(sortCompare);
}
allWordpressArticles = allWordpressArticles..addAll(wordpressArticles)..sort(sortCompare);
DataConnector.saveJsonEncodable(SystemStrings.dataFavoriteArticles, allWordpressArticles);
}
}
}

View File

@@ -0,0 +1,68 @@
import 'package:hiddingsel_app/appflow/controller/network/calendar.dart';
import 'package:hiddingsel_app/appflow/model/event.dart';
import 'package:hiddingsel_app/appflow/model/event_organisations.dart';
import '../../constants/constant.dart';
import '../../services/storage.dart';
import '../../todo/scheduled_notification.dart';
class EventController {
static int defaultSortCompare(EventModel a, EventModel b) => a.schedule.startTime.compareTo(b.schedule.startTime);
static Stream<List<EventModel>> getEventListStream({int Function(EventModel, EventModel) sortCompare = defaultSortCompare}) async* {
List<EventModel> yieldEvents = [];
List<EventModel> allEvents = [];
List<EventModel> newEvents = [];
List<EventModel> organisationEvents = [];
List<EventModel> savedEvents = await DataConnector.getJsonEncodableList<
EventModel>(SystemStrings.dataEvents, [], EventModel.fromJson);
yield yieldEvents = yieldEvents..addAll(savedEvents)..sort(sortCompare);
for (EventOrganisationModel eventOrganisation in EventOrganisationModel.values){
organisationEvents = await CalendarController
.getOrganisationEvents(eventOrganisation);
newEvents = organisationEvents.where((a) => !yieldEvents.contains(a)).toList();
if (newEvents.isNotEmpty) {
yield yieldEvents
..addAll(newEvents)
..sort(sortCompare);
}
allEvents = allEvents..addAll(organisationEvents)..sort(sortCompare);
}
DataConnector.saveJsonEncodable(SystemStrings.dataEvents, allEvents);
}
static Future<void> scheduleNotificationsBySettings() async {
List<EventOrganisationModel> notifiableOrganisations = await _getNotifiableOrganisations();
final duration = Duration(milliseconds: await SettingsConnector.getInt(SystemStrings.keyAlertTime, 1*60*60*1000));
await ScheduledNotificationConnector.cancelAllScheduledNotification();
EventController.getEventListStream().forEach((List<EventModel> list) async{
var eventsToSchedule = list
.where((e) => e.schedule.startTime.isAfter(DateTime.now()))
.where((e) => notifiableOrganisations.any((o) => e.eventOrganizer.contains(o)))
.toList();
_scheduleNotifications(eventsToSchedule, duration);
});
}
static _getNotifiableOrganisations() async {
var notifiableOrganisations = List<EventOrganisationModel>.empty(growable: true);
for (EventOrganisationModel event in EventOrganisationModel.values) {
if(await event.eventNotificationAllowed) notifiableOrganisations.add(event);
}
return notifiableOrganisations;
}
static _scheduleNotifications(List<EventModel> eventsToSchedule,
Duration duration) {
eventsToSchedule.sort((a, b) => a.schedule.startTime.compareTo(b.schedule.startTime));
Future.forEach(eventsToSchedule.take(50), (EventModel element) async {
ScheduledNotificationConnector.scheduleNotification(
element.id.hashCode,
element.schedule.startTime.subtract(duration),
element.name,
'${element.eventOrganizer.first.name} - Der Termin beginnt bald.');
});
}
}

View File

@@ -0,0 +1,65 @@
import 'package:flutter/services.dart';
import 'package:get_it/get_it.dart';
import 'package:hiddingsel_app/appflow/controller/events.dart';
import 'package:hiddingsel_app/appflow/model/pois.dart';
import 'package:hiddingsel_app/appflow/model/represented_organisations.dart';
import 'package:hiddingsel_app/constants/constant.dart';
import 'package:hiddingsel_app/appflow/model/event_organisations.dart';
import 'package:hiddingsel_app/services/storage.dart';
import 'package:hiddingsel_app/todo/scheduled_notification.dart';
import '../model/companies.dart';
import '../model/topics.dart';
import '../../services/notification.dart';
import 'package:timezone/data/latest_all.dart' as tz;
class InitialStartController {
static Future<void> ensureInitialized() async {
await initializeAlways();
SettingsConnector.getBool(SystemStrings.keyFirstTime, true).then((firstTime)
{if(firstTime) {
initializeFirstTime();
SettingsConnector.setBool(SystemStrings.keyFirstTime, false);
}});
}
static Future<void> initializeAlways() async {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: UIColors.white,
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.light,
));
//RemoteConfigConnector.initialize();
ExternalNotificationConnector.initialize();
await ScheduledNotificationConnector.initialize();
tz.initializeTimeZones();
await load<PointOfInterestModel>(SystemStrings.assetPois, PointOfInterestModel.fromJson);
await load<CompanyModel>(SystemStrings.assetCompanies, CompanyModel.fromJson);
await load<RepresentedOrganisationModel>(SystemStrings.assetRepresentedOrganisations, RepresentedOrganisationModel.fromJson);
await load<EventOrganisationModel>(SystemStrings.assetEventOrganisations, EventOrganisationModel.fromJson);
await load<PushNotificationTopicModel>(SystemStrings.assetTopics, PushNotificationTopicModel.fromJson);
EventController.scheduleNotificationsBySettings();
}
static Future<void> load<T>(asset, fromJson) async {
final jsonData = await AssetConnector.getJson(asset);
List<T> list = jsonData.map((json) => fromJson(json)).toList().cast<T>();
GetIt.I.registerSingleton<List<T>>(list);
}
static void initializeFirstTime() async {
PushNotificationTopicModel.values.forEach((e) => e.subscribe(false));
CompanyModel.values.forEach((e) => e.subscribe(false));
RepresentedOrganisationModel.values.forEach((e) => e.favorize(false));
EventOrganisationModel.values.forEach((e) => e.allowEventNotification(false));
EventOrganisationModel.values.where((e) => e.id == "garbage").forEach((e) => e.allowEventNotification(true));
EventOrganisationModel.values.where((e) => e.id == "dorfgemeinschaft").forEach((e) => e.allowEventNotification(true));
EventController.scheduleNotificationsBySettings();
}
}

View File

@@ -0,0 +1,21 @@
import 'package:hiddingsel_app/appflow/model/event_organisations.dart';
import '../../../services/network.dart';
import '../../../todo/parser.dart';
import '../../model/event.dart';
import 'package:http/http.dart';
import 'dart:convert' show utf8;
class CalendarController {
static Future<List<EventModel>> getOrganisationEvents(EventOrganisationModel eventOrganisation) {
if(eventOrganisation.ical == null) return Future.value(List<EventModel>.empty());
return BaseService.getSaveFromServer(
() => _getEvents(eventOrganisation.ical), List<EventModel>.empty());
}
static Future<List<EventModel>> _getEvents(icalUri) async {
final response = await get(icalUri);
return IcalParser.parse(utf8.decode(response.bodyBytes));
}
}

View File

@@ -0,0 +1,64 @@
import 'dart:convert';
import 'package:hiddingsel_app/appflow/model/represented_organisations.dart';
import '../../../services/network.dart';
import '../../model/articles.dart';
import 'package:http/http.dart';
class WordpressController {
static const String _baseUrl = 'www.hiddingsel.de';
static const String _path = 'wp-json/wp/v2/posts';
static const String _parameterPage = 'page';
static const String _parameterPerPage = 'per_page';
static const String _parameterSearch = 'search';
static const String _parameterCategories = 'categories';
static const String _parameterEmbed = '_embed';
static const int _httpCodeKeineWeiterenArticel = 400;
static Future<List<ArticleModel>> getArticles(int page, int itemCount) =>
BaseService.getSaveFromServer(
() => _getArticles(page, itemCount),
List<ArticleModel>.empty()
);
static Future<List<ArticleModel>> searchArticles(int page, int itemCount, String searchWord) =>
BaseService.getSaveFromServer(
() => _getArticles(page, itemCount, searchWord: searchWord),
List<ArticleModel>.empty()
);
static Future<List<ArticleModel>> getArticlesFromOrganisations(int page, int itemCount, List<RepresentedOrganisationModel> organisations) =>
BaseService.getSaveFromServer(
() => _getArticles(page, itemCount, organisations: organisations),
List<ArticleModel>.empty()
);
static Future<List<ArticleModel>> _getArticles(int page, int itemCount, {String? searchWord, List<RepresentedOrganisationModel>? organisations}) async {
final parameters = <String, dynamic> {
_parameterPage: (page + 1).toString(),
_parameterPerPage: itemCount.toString(),
_parameterSearch: searchWord,
_parameterCategories: organisations?.map((e) => e.wordpressId).join(','),
_parameterEmbed: null,
};
if(searchWord?.isEmpty ?? true) {
parameters.remove(_parameterSearch);
}
if(organisations?.isEmpty ?? true) {
parameters.remove(_parameterCategories);
}
final uri = Uri.https(_baseUrl, _path, parameters);
final response = await get(uri);
if (response.statusCode == _httpCodeKeineWeiterenArticel) {
return [];
}
final jsonData = json.decode(response.body);
return jsonData.map((article) => ArticleModel.fromWordpressJson(article)).cast<ArticleModel>().toList();
}
}

View File

@@ -0,0 +1,45 @@
import 'package:hiddingsel_app/appflow/model/event_organisations.dart';
import 'package:hiddingsel_app/appflow/model/topics.dart';
import 'package:hiddingsel_app/services/storage.dart';
import '../../constants/constant.dart';
import 'events.dart';
class SettingsController {
static const subscribtion = SystemStrings.keyPreSubscribtion;
static const eventNotification = SystemStrings.keyPreEventNotification;
static const favorization = SystemStrings.keyPreFavorization;
static void subscribeTopic(PushNotificationTopicModel topic) =>
SettingsConnector.setBool(subscribtion + topic.id, true);
static void unsubscribeTopic(PushNotificationTopicModel topic) =>
SettingsConnector.setBool(subscribtion + topic.id, false);
static Future<bool> subscribed(PushNotificationTopicModel topic) =>
SettingsConnector.getBool(subscribtion + topic.id, false);
static void allowEventNotification(EventOrganisationModel eventOrganisation) async {
await SettingsConnector.setBool(eventNotification + eventOrganisation.id, true);
EventController.scheduleNotificationsBySettings();
}
static void unallowEventNotification(EventOrganisationModel eventOrganisation) async {
await SettingsConnector.setBool(eventNotification + eventOrganisation.id, false);
EventController.scheduleNotificationsBySettings();
}
static Future<bool> eventNotificationAllowed(EventOrganisationModel eventOrganisation) =>
SettingsConnector.getBool(eventNotification + eventOrganisation.id, false);
static void favorize(PushNotificationTopicModel topic) =>
SettingsConnector.setBool(favorization + topic.id, true);
static void unfavorize(PushNotificationTopicModel topic) =>
SettingsConnector.setBool(favorization + topic.id, false);
static Future<bool> favorized(PushNotificationTopicModel topic) =>
SettingsConnector.getBool(favorization + topic.id, false);
}

View File

@@ -0,0 +1,86 @@
import 'package:hiddingsel_app/appflow/model/images.dart';
import 'package:hiddingsel_app/appflow/model/represented_organisations.dart';
import 'package:hiddingsel_app/services/storage.dart';
import 'package:html/parser.dart' show parse;
import '../../todo/WebConnector.dart';
import 'event_organisations.dart';
import 'interfaces.dart';
class ArticleModel with IsShareable, JsonEncodable {
final int id;
final String title;
final String htmlContent;
final DateTime publishedAt;
final ImageModel image;
final Uri url;
final List<RepresentedOrganisationModel> categories;
String get content => WebViewHelper.getHtmlContentWithoutMedia(htmlContent);
List<ImageModel> get otherImages => WebViewHelper.getImageMedia(htmlContent)
.map((url) => ImageModel(url)).toList();
ArticleModel.withImageModel(this.id, this.title, this.htmlContent, this.publishedAt,
this.image, this.url,
{categories, otherImages})
: this.categories = categories ?? [];
ArticleModel(this.id, this.title, this.htmlContent, this.publishedAt,
String thumbnailImageResource, this.url,
{categories, otherImages})
: this.image = ImageModel(thumbnailImageResource), this.categories = categories ?? List<RepresentedOrganisationModel>.empty();
factory ArticleModel.fromWordpressJson(Map<String, dynamic> json) => ArticleModel(
json['id'] as int,
parse(json['title']['rendered'] as String).documentElement!.text,
json['content']['rendered'] as String,
DateTime.parse(json['date'] as String),
json['_embedded']['wp:featuredmedia'][0]['source_url'] as String,
Uri.parse(json['link'] as String),
categories: (json['categories'] as List<dynamic>)
.cast<int>()
.map((id) => RepresentedOrganisationModel.fromWordpressId(id))
.where((e) => e != null)
.cast<RepresentedOrganisationModel>()
.toList(),
);
factory ArticleModel.fromJson(Map<String, dynamic> json) => ArticleModel(
json['id'] as int,
json['title'] as String,
json['htmlContent'] as String,
DateTime.parse(json['publishedAt'] as String),
json['image'] as String,
Uri.parse(json['url'] as String),
categories: (json['categories'] as List<dynamic>)
.cast<String>()
.map((id) => EventOrganisationModel.fromId(id))
.where((e) => e != null)
.cast<RepresentedOrganisationModel>()
.toList(),
otherImages: (json['otherImages'] as List<dynamic>)
.cast<String>()
.map((resouce) => ImageModel(resouce))
.toList(),
);
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'htmlContent': htmlContent,
'publishedAt': publishedAt.toIso8601String(),
'image': image.resource,
'url': url.toString(),
'categories': categories.map((c) => c.id).toList(),
'otherImages': otherImages.map((i) => i.resource).toList(),
};
@override
bool operator ==(Object other) =>
other is ArticleModel &&
runtimeType == other.runtimeType &&
id == other.id;
@override
int get hashCode => id.hashCode;
}

View File

@@ -0,0 +1,48 @@
import 'package:flutter/cupertino.dart';
import 'package:get_it/get_it.dart';
import 'package:hiddingsel_app/appflow/model/images.dart';
import 'package:hiddingsel_app/appflow/model/persons.dart';
import '../../constants/constant.dart';
import 'contacts.dart';
import 'interfaces.dart';
import 'package:hiddingsel_app/appflow/model/topics.dart';
class CompanyModel extends PushNotificationTopicModel
with Contact, Person {
final ImageModel image;
final PersonModel person;
final ContactModel contact;
final String? products;
final String? connectionToHiddingsel;
InlineSpan get text => TextSpan(text: 'Produkte/Dienstleistungen:\n', style: UITheme.theme.textTheme.headlineSmall, children:
[
TextSpan(text: '$products\n', style: UITheme.theme.textTheme.bodyLarge),
TextSpan(text: 'Was verbindet Euch mit Hiddingsel?\n', style: UITheme.theme.textTheme.headlineSmall),
TextSpan(text: '$connectionToHiddingsel\n', style: UITheme.theme.textTheme.bodyLarge),],);
const CompanyModel.withImageModel(id, name, this.image, this.person, this.contact, this.products, this.connectionToHiddingsel) : super(id, name);
CompanyModel(id, name, String imageResource, this.person, this.contact, this.products, this.connectionToHiddingsel) : this.image = ImageModel(imageResource), super(id, name);
factory CompanyModel.fromJson(Map<String, dynamic> json) {
return CompanyModel(
json['id'] as String,
json['name'] as String,
json['image'] as String,
PersonModel.fromJson(json['person'] as Map<String, dynamic>),
ContactModel.fromJson(json['contact'] as Map<String, dynamic>),
json['products'] as String?,
json['connectionToHiddingsel'] as String?,
);
}
static final List<CompanyModel> values = GetIt.I<List<CompanyModel>>()..sort((a, b) => a.name.compareTo(b.name));
bool operator ==(Object other) =>
other is CompanyModel &&
runtimeType == other.runtimeType &&
id == other.id;
int get hashCode => id.hashCode;
}

View File

@@ -0,0 +1,55 @@
import 'package:hiddingsel_app/appflow/model/interfaces.dart';
import 'package:hiddingsel_app/appflow/model/location.dart';
class ContactModel with IsSearchable{
final String? phone;
final String? mail;
final LocationModel? location;
final Uri? website;
final String? facebook;
final String? instagram;
final String? twitter;
get tags => [phone, mail, location?.address, website?.host, facebook, instagram].where((e) => e != null).toList();
const ContactModel(
{this.phone,
this.mail,
this.location,
this.website,
this.facebook,
this.instagram,
this.twitter
});
factory ContactModel.fromJson(Map<String, dynamic> json) {
return ContactModel(
phone: (json['phone'] as String?) != null ? json['phone'] as String: null,
mail: (json['mail'] as String?) != null ? json['mail'] as String: null,
location: (json['location'] as Map<String, dynamic>?) != null
? LocationModel.fromJson(json['location'] as Map<String, dynamic>)
: null,
website: (json['website'] as String?) != null
? Uri.parse(json['website'] as String)
: null,
facebook: (json['facebook'] as String?) != null
? json['facebook'] as String
: null,
instagram: (json['instagram'] as String?) != null
? json['instagram'] as String
: null,
twitter: (json['twitter'] as String?) != null
? json['twitter'] as String
: null,
);
}
toJson() =>{
'phone': phone,
'mail': mail,
'location': location?.toJson(),
'website': website?.toString(),
'facebook': facebook,
'instagram': instagram,
'twitter': twitter,
};
}

View File

@@ -0,0 +1,47 @@
import 'package:hiddingsel_app/appflow/model/schedules.dart';
import 'package:hiddingsel_app/services/storage.dart';
import 'contacts.dart';
import 'event_organisations.dart';
import 'interfaces.dart';
class EventModel with Name, Contact, JsonEncodable {
final String id;
final ScheduleModel schedule;
final String name;
final ContactModel contact;
final String? description;
List<EventOrganisationModel> eventOrganizer;
EventModel(this.id, this.schedule, this.name, this.contact,
{this.description, List<EventOrganisationModel>? eventOrganizer})
: this.eventOrganizer = eventOrganizer ?? [];
factory EventModel.fromJson(Map<String, dynamic> json) => EventModel(
json['id'] as String,
ScheduleModel.fromJson(json['schedule'] as Map<String, dynamic>),
json['name'] as String,
ContactModel.fromJson(json['contact'] as Map<String, dynamic>),
description: json['description'] as String?,
eventOrganizer: (json['eventOrganizer'] as List<dynamic>).map((id) => EventOrganisationModel.fromId(id)).cast<EventOrganisationModel>().toList(),
);
Map<String, dynamic> toJson() => {
'id': id,
'schedule': schedule.toJson(),
'name': name,
'contact': contact.toJson(),
'description': description,
'eventOrganizer': eventOrganizer.map((e) => e.id).toList(),
};
@override
bool operator ==(Object other) =>
other is EventModel &&
runtimeType == other.runtimeType &&
(id == other.id || (schedule.startTime == other.schedule.startTime && name == other.name));
@override
int get hashCode => id.hashCode;
}

View File

@@ -0,0 +1,47 @@
import 'package:get_it/get_it.dart';
import 'package:hiddingsel_app/appflow/model/represented_organisations.dart';
import 'package:hiddingsel_app/appflow/model/topics.dart';
import '../controller/settings.dart';
import 'interfaces.dart';
class EventOrganisationModel extends PushNotificationTopicModel with AllowsEventNotifications {
final Uri? ical;
Future<bool> get eventNotificationAllowed => SettingsController.eventNotificationAllowed(this);
allowEventNotification(bool allowed) {
if (allowed) {
SettingsController.allowEventNotification(this);
} else {
SettingsController.unallowEventNotification(this);
}
}
const EventOrganisationModel(id, name, this.ical) : super(id, name);
factory EventOrganisationModel.fromJson(Map<String, dynamic> json) {
return EventOrganisationModel(
json['id'] as String,
json['name'] as String,
(json['ical'] as String?) != null
? Uri.parse(json['ical'] as String)
: null,
);
}
static EventOrganisationModel? fromId(String id) =>
EventOrganisationModel.values.cast<EventOrganisationModel?>().firstWhere((topic) => topic?.id == id, orElse: () => null);
static List<EventOrganisationModel> values = GetIt.I<List<EventOrganisationModel>>()
..addAll(RepresentedOrganisationModel.values)
..sort((a, b) => a.name.compareTo(b.name));
bool operator == (Object other) =>
other is EventOrganisationModel &&
runtimeType == other.runtimeType &&
id == other.id;
int get hashCode => id.hashCode;
}

View File

@@ -0,0 +1,18 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/cupertino.dart';
class ImageModel {
final String resource;
ImageProvider get image {
if (resource.startsWith('http')) {
return CachedNetworkImageProvider(resource);
} else if (resource.startsWith('assets')) {
return AssetImage(resource);
} else {
throw Exception('Invalid image resource: $resource');
}
}
const ImageModel(this.resource);
}

View File

@@ -0,0 +1,42 @@
import 'package:hiddingsel_app/appflow/model/persons.dart';
import 'images.dart';
import 'contacts.dart';
mixin Name {
String get name;
}
mixin Contact {
ContactModel get contact;
}
mixin Person {
PersonModel get person;
}
mixin Image {
ImageModel get image;
}
mixin IsShareable {
Uri get url;
}
mixin IsSearchable {
get tags;
}
mixin IsFavorable {
get favorized;
favorize(bool favorize);
}
mixin IsSubscribable {
get subscribed;
subscribe(bool subscribe);
}
mixin AllowsEventNotifications {
get eventNotificationAllowed;
allowEventNotification(bool allowed);
}

View File

@@ -0,0 +1,20 @@
class LocationModel {
final double? latitude;
final double? longitude;
final String? address;
LocationModel({this.latitude, this.longitude, this.address});
factory LocationModel.fromJson(Map<String, dynamic> json) => LocationModel(
latitude: json["latitude"] as double?,
longitude: json["longitude"] as double?,
address: json["address"] as String?,
);
Map<String, dynamic> toJson() => {
"latitude": latitude,
"longitude": longitude,
"address": address,
};
}

View File

@@ -0,0 +1,23 @@
import 'package:hiddingsel_app/appflow/model/images.dart';
import 'package:hiddingsel_app/appflow/model/interfaces.dart';
class PersonModel with Name, IsSearchable{
final String name;
final ImageModel image;
final String? position;
get tags => [name];
const PersonModel.withImageModel(this.name, this.image, {this.position});
PersonModel(this.name, String imageResource, {this.position}) : image = ImageModel(imageResource);
factory PersonModel.fromJson(Map<String, dynamic> json) => PersonModel(
json['name'] as String,
json['image'] as String,
position: json['position'] as String?,
);
}

View File

@@ -0,0 +1,31 @@
import 'package:get_it/get_it.dart';
import 'package:hiddingsel_app/appflow/model/images.dart';
import 'interfaces.dart';
class PointOfInterestModel with Name {
final String name;
final String? subtitle;
final String text;
final ImageModel image;
final List<ImageModel> otherImages;
const PointOfInterestModel.withImageModel(this.name, this.subtitle, this.text, this.image, this.otherImages);
PointOfInterestModel(this.name, this.subtitle, this.text, String imageResource, List<String> otherImagesResources) : image = ImageModel(imageResource), otherImages = otherImagesResources.map((e) => ImageModel(e)).toList();
factory PointOfInterestModel.fromJson(Map<String, dynamic> json) {
return PointOfInterestModel(
json['name'] as String,
json['subtitle'] as String?,
json['text'] as String,
json['assetThumbnailImage'] as String,
(json['assetsOtherImages'] as List<dynamic>).cast<String>(),
);
}
static final List<PointOfInterestModel> values = GetIt.I<List<PointOfInterestModel>>();
}

View File

@@ -0,0 +1,79 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:get_it/get_it.dart';
import 'package:hiddingsel_app/appflow/model/images.dart';
import 'package:hiddingsel_app/appflow/model/persons.dart';
import 'package:hiddingsel_app/appflow/model/event_organisations.dart';
import '../../constants/constant.dart';
import '../../services/environment.dart';
import '../controller/settings.dart';
import 'contacts.dart';
import 'interfaces.dart';
class RepresentedOrganisationModel extends EventOrganisationModel
with Contact, Person, IsFavorable, IsSearchable {
final int wordpressId;
final ImageModel image;
final PersonModel person;
final ContactModel contact;
InlineSpan get text => stringToInlineSpan(info);
final String? info;
final List<String> _tags;
List<String> get tags => [name]..addAll(_tags)..addAll(person.tags)..addAll(contact.tags);
Future<bool> get favorized => SettingsController.favorized(this);
favorize(bool favorize) {
if (favorize) {
SettingsController.favorize(this);
} else {
SettingsController.unfavorize(this);
}
}
InlineSpan stringToInlineSpan(String? text) {
final textblocks = text?.split('\\');
return TextSpan(children: textblocks?.map((textblock)
{
if (textblock.startsWith('link=')) {
String link = textblock.substring(5);
return TextSpan(text: link,
style: UITheme.theme.textTheme.bodyLarge,
recognizer: TapGestureRecognizer()..onTap = () => EnvironmentConnector.openBrowser(Uri.parse(link)));
} else {
return TextSpan(text: textblock,
style: UITheme.theme.textTheme.bodyLarge);
}
}).toList());
}
const RepresentedOrganisationModel.withImageModel(id, name, this.wordpressId, this.image, this.person, this.contact, this.info, this._tags, Uri? ical) : super(id, name, ical);
RepresentedOrganisationModel(id, name, this.wordpressId, String imageResource, this.person, this.contact, this.info, this._tags, Uri? ical) : image = ImageModel(imageResource), super(id, name, ical);
factory RepresentedOrganisationModel.fromJson(Map<String, dynamic> json) {
return RepresentedOrganisationModel(
json['id'] as String,
json['name'] as String,
json['wordpress_id'] as int,
json['image'] as String,
PersonModel.fromJson(json['person'] as Map<String, dynamic>),
ContactModel.fromJson(json['contact'] as Map<String, dynamic>),
json['text'] as String?,
(json['tags'] as List<dynamic>).cast<String>(),
(json['ical'] as String?) != null
? Uri.parse(json['ical'] as String)
: null,
);
}
static RepresentedOrganisationModel? fromWordpressId(int wordpressId) =>
RepresentedOrganisationModel.values.cast<RepresentedOrganisationModel?>().firstWhere((o) => o?.wordpressId == wordpressId, orElse: () => null);
static List<RepresentedOrganisationModel> values = GetIt.I<List<RepresentedOrganisationModel>>()..sort((a, b) => a.name.compareTo(b.name));
bool operator ==(Object other) =>
other is RepresentedOrganisationModel &&
runtimeType == other.runtimeType &&
id == other.id;
int get hashCode => id.hashCode;
}

View File

@@ -0,0 +1,32 @@
import 'package:hiddingsel_app/constants/enums.dart';
class ScheduleModel {
final DateTime startTime;
final DateTime endTime;
final Frequency frequency;
final int? interval;
final DateTime? until;
ScheduleModel(
this.startTime, this.endTime, this.frequency, this.interval, this.until);
bool get isAllDay =>
startTime == endTime || (startTime.difference(endTime).inDays != 0 && startTime.difference(endTime).inDays%1 == 0);
toJson() => {
'startTime': startTime.toIso8601String(),
'endTime': endTime.toIso8601String(),
'frequency': frequency.toString(),
'interval': interval,
'until': until?.toIso8601String(),
};
static ScheduleModel fromJson(Map<String, dynamic> json) => ScheduleModel(
DateTime.parse(json['startTime'] as String),
DateTime.parse(json['endTime'] as String),
Frequency.values
.firstWhere((f) => f.toString() == json['frequency']),
json['interval'] as int?,
(json['until'] as String?) != null ? DateTime.parse(json['until'] as String) : null,
);
}

View File

@@ -0,0 +1,41 @@
import 'package:get_it/get_it.dart';
import '../controller/settings.dart';
import '../../services/notification.dart';
import 'companies.dart';
import 'event_organisations.dart';
import 'interfaces.dart';
class PushNotificationTopicModel with Name, IsSubscribable {
final String id;
final String name;
get subscribed => SettingsController.subscribed(this);
subscribe(bool subscribe) {
if(subscribe) {
SettingsController.subscribeTopic(this);
ExternalNotificationConnector.subscribe(this);
} else {
SettingsController.unsubscribeTopic(this);
ExternalNotificationConnector.unsubscribe(this);
}
}
const PushNotificationTopicModel(this.id, this.name);
factory PushNotificationTopicModel.fromJson(Map<String, dynamic> json) {
return PushNotificationTopicModel(
json['id'] as String,
json['name'] as String
);
}
static List<PushNotificationTopicModel> values = GetIt.I<List<PushNotificationTopicModel>>()..addAll(EventOrganisationModel.values)..addAll(CompanyModel.values)..sort((a, b) => a.name.compareTo(b.name));
bool operator ==(Object other) =>
other is PushNotificationTopicModel &&
runtimeType == other.runtimeType &&
id == other.id;
int get hashCode => id.hashCode;
}

View File

@@ -0,0 +1,31 @@
import 'package:flutter/cupertino.dart';
import 'package:hiddingsel_app/appflow/model/articles.dart';
import '../widgets/card.dart';
import '../navigation_drawer.dart';
class ArticleThumbnailView extends StatelessWidget with NavigationDrawerItem {
String get title => _article.title;
final ArticleModel _article;
final Function(PreferredSizeWidget appBar, Widget body)? _onChange;
ArticleThumbnailView(this._article, this._onChange);
@override
Widget build(BuildContext context) => ItemWithTitle.fromArticle(_article, _onChange);
}
class ArticleView extends StatelessWidget with NavigationDrawerItem {
String get title => _article.title;
final ArticleModel _article;
ArticleView(this._article);
@override
Widget build(BuildContext context) => ListView(
children: [
ItemWithWebView.fromArticle(_article)
]);
}

View File

@@ -0,0 +1,272 @@
import 'package:flutter/material.dart';
import 'package:hiddingsel_app/constants/constant.dart';
import '../navigation_drawer.dart';
class GeschichteView extends StatelessWidget with NavigationDrawerItem {
//TODO Low-Prio: Beim nächsten Anfassen durch RichText ersetzen
@override
String get title => 'Geschichte';
@override
Widget build(BuildContext context) => ListView(
children: [
Padding(
padding: EdgeInsets.fromLTRB(
UIShapes.paddingMax,
UIShapes.paddingMax,
UIShapes.paddingMax,
UIShapes.paddingSimple),
child: Text(
'Über unser Dorf',
style: UITheme.theme.textTheme.headlineSmall,
),
),
Container(
decoration: BoxDecoration(
color: UIColors.grey5,
),
padding: EdgeInsets.fromLTRB(
0,
UIShapes.paddingSimple,
UIShapes.paddingMax,
UIShapes.paddingSimple,
),
child: ClipRRect(
borderRadius: UIShapes.rightRoundedRectangle,
child: Image(
image: AssetImage(
'assets/images/geschichte/img_geschichte.png',
),
fit: BoxFit.cover,
),
),
),
Padding(
padding: EdgeInsets.all(UIShapes.paddingMax),
child: Text(
'Entstehung und Geschichte unseres Dorfes',
style: UITheme.theme.textTheme.headlineSmall,
),
),
Padding(
padding: EdgeInsets.fromLTRB(
UIShapes.paddingMax,
0,
UIShapes.paddingMax,
UIShapes.paddingMax,
),
child: Text(
'Der Name Hiddingsel wurde erstmals im Jahre 1032 erwähnt. Der Name entstammt dem angelsächsischen Männernamen „Hiddo“. Die Endung auf „sel“ deutet auf Saal-Scheune-Sitz hin. Einer zweiten Version zufolge leitet sich die Silbe „Hid“ von dem Eigennamen „Hildi“ her. Zu dieser Zeit gab es verschiedene Haupthöfe mit zugeordneten Unterhöfen. Einer dieser Haupthöfe war der Hof Hiddingsel. Der Hof des Hiddo hat an der westlichen Seite des Dorfes Hiddingsel gelegen. Hier stand bis 1893 noch eine Burg mit einer breiten Schutzmauer. Um die Burg führte eine Gräfte. Eine Senke deutetenoch darauf hin. Die Senke und die alte Umflut des Kleuterbachs wurde durch ein 12 m langes Schemm überbrückt. 1905 wurden die Schemms abgebrochen und dafür der Landweg Hiddingsel-Rödder hergerichtet.\n\nDer Hof Hiddingsel befand sich im Besitz des Domkapitels zu Münster. Auf dem Grund und Boden dieses Hofs ließ das Domkapitel eine kleine Kapelle errichten, die in der Folgezeit Anlass für die Entstehung des Dorfes wurde. Nach 1240 wurde Hiddingsel eine Pfarrei. Jedoch schon Anfang des 16. Jahrhunderts verlor die Pfarrei ihre Selbständigkeit und wurde Rektorat. Erst im Jahre 1861 wurde wieder eine eigene Pfarrei eingerichtet. Der erste Pfarrer war der damalige Rektor Hoffschläger.\n\nDer Hof Hiddingsel wurde im 13. Jahrhundert nach und nach zerstückelt und den sich ansiedelnden Bewohnern überlassen. Als Ersatz kaufte am 18. August 1331 der münsterische Domherr Burkhardt das in der Bauernschaft Rödder gelegene und an Hiddingsel anliegende Lehngut „umso die Rechte der Gutsherrschaft über die Hörigen des Dorfes nach wie vor ausüben zu können.“\n\nDas Kirchdorf Hiddingsel mag im 15. und 16. Jahrhundert etwa 30 bis 40 Häuser gehabt haben. Es blieb in dieser Zeit nicht von Schicksalsschlägen wie Brand, Pest, Krieg und Überflutungen verschont. Innerhalb von 115 Jahren wurde es viermal vollständig vom Feuer zerstört, und zwar in den Jahren 1587 während des spanisch-niederländischen Krieges (1568 bis 1609) wurde das Dorf „von den Flämischen“ wie von den Holländern in Brand gesteckt , 1606, 1639 und 1702. Für das Feuer vom 31. Juli 1606 ist folgende Begebenheit übermittelt: Grund des Brandes war diesmal keine kriegerische Auseinandersetzung, sondern ein Ehestreit. Die Frau des Johann Stoberts wollte ihren trunksüchtigen Mann nicht in ihrem Bett dulden, weil sie glaubte, dass er im Wirtshaus zu viel Geld vertrunken hätte. Es gab einen handfesten Krach, der darin gipfelte, dass der Mann aus seiner Schlafkammer vertrieben sich mit einer brennenden Kerze in den Hühnerstall setzte und dort einschlief. Die Kerze setzte sein eigenes Haus und in der Folge das ganze Dorf in Brand. Am 8. Dezember 1703 vernichtete ein orkanartiger Sturm die soeben neu erbauten Häuser und den Kirchturm.',
style: UITheme.theme.textTheme.bodyLarge,
),
),
SizedBox(
height: 200,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
Image(
image: AssetImage(
'assets/images/geschichte/img_geschichte_01.jpg',
),
fit: BoxFit.cover,
width: 200,
height: 200,
),
VerticalDivider(),
Image(
image: AssetImage(
'assets/images/geschichte/img_geschichte_02.jpg',
),
fit: BoxFit.cover,
width: 200,
height: 200,
),
VerticalDivider(),
Image(
image: AssetImage(
'assets/images/geschichte/img_geschichte_03.jpg',
),
fit: BoxFit.cover,
width: 200,
height: 200,
),
VerticalDivider(),
Image(
image: AssetImage(
'assets/images/geschichte/img_geschichte_04.jpg',
),
fit: BoxFit.cover,
width: 200,
height: 200,
),
VerticalDivider(),
Image(
image: AssetImage(
'assets/images/geschichte/img_geschichte_05.jpg',
),
fit: BoxFit.cover,
width: 200,
height: 200,
),
VerticalDivider(),
Image(
image: AssetImage(
'assets/images/geschichte/img_geschichte_06.jpg',
),
fit: BoxFit.cover,
width: 200,
height: 200,
),
VerticalDivider(),
Image(
image: AssetImage(
'assets/images/geschichte/img_geschichte_07.jpg',
),
fit: BoxFit.cover,
width: 200,
height: 200,
),
VerticalDivider(),
Image(
image: AssetImage(
'assets/images/geschichte/img_geschichte_08.jpg',
),
fit: BoxFit.cover,
width: 200,
height: 200,
),
VerticalDivider(),
Image(
image: AssetImage(
'assets/images/geschichte/img_geschichte_09.jpg',
),
fit: BoxFit.cover,
width: 200,
height: 200,
),
],
),
),
Padding(
padding: EdgeInsets.all(UIShapes.paddingMax),
child: Text(
'Rund 250 Menschen wurden Opfer der Pest von 1636. Nur drei Frauen, die sich zum Schutz gegen die Seuche unter einer kleinen Brücke versteckt hielten, bis die Gefahr vorüber war, sollen von der verheerenden Seuche verschont geblieben sein. Die Brücke heißt noch heute aus dieser Begebenheit „Frauenschemm“. Ein Überbleibsel dieser Zeit ist die noch heute jährlich stattfindende Pestprozession der katholischen Pfarrgemeinde St. Georg zu Hiddingsel. Darüber hinaus erinnert eine im Jahr 2013 errichtete Gedenkstatue an der „Frauenschemm“ Brücke an die vorgenannte Begebenheit.\n\nIm Jahre 1803 fiel das Kirchengut in Hiddingsel der Säkularisation zum Opfer und kam so an die Krone Preußens. Im Jahre 1837 trat die preußische Regierung das Gut an den Herzog von Croÿ ab.\n\In den Jahren 1875, 1881, 1890, 1932, 1945 und 1963 überschwemmte der durch das Dorf fließende Kleuterbach das gesamte Dorf und die Umgebung. Hiddingsel war von der Außenwelt vollkommen abgeschnitten. Die neue Umflut bewahrte das Dorf 1981 vor einer weiteren Hochwasserkatastrophe.\n\nHiddingsel erlebte zwei Gebietsreformen. So wurde das Dorf am 1. Juli 1969 dem Nachbardorf Buldern eingemeindet ehe es im Rahmen einer weiteren Gebietsreform unter Datum vom 1. Januar 1975 ein Ortsteil der Stadt Dülmen wurde.',
style: UITheme.theme.textTheme.bodyLarge,
),
),
SizedBox(
height: 200,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
Image(
image: AssetImage(
'assets/images/geschichte/img_geschichte_10.jpg',
),
fit: BoxFit.cover,
width: 200,
height: 200,
),
VerticalDivider(),
Image(
image: AssetImage(
'assets/images/geschichte/img_geschichte_11.jpg',
),
fit: BoxFit.cover,
width: 200,
height: 200,
),
VerticalDivider(),
Image(
image: AssetImage(
'assets/images/geschichte/img_geschichte_12.jpg',
),
fit: BoxFit.cover,
width: 200,
height: 200,
),
VerticalDivider(),
Image(
image: AssetImage(
'assets/images/geschichte/img_geschichte_13.jpg',
),
fit: BoxFit.cover,
width: 200,
height: 200,
),
VerticalDivider(),
Image(
image: AssetImage(
'assets/images/geschichte/img_geschichte_14.jpg',
),
fit: BoxFit.cover,
width: 200,
height: 200,
),
VerticalDivider(),
Image(
image: AssetImage(
'assets/images/geschichte/img_geschichte_15.jpg',
),
fit: BoxFit.cover,
width: 200,
height: 200,
),
VerticalDivider(),
Image(
image: AssetImage(
'assets/images/geschichte/img_geschichte_16.jpg',
),
fit: BoxFit.cover,
width: 200,
height: 200,
),
VerticalDivider(),
Image(
image: AssetImage(
'assets/images/geschichte/img_geschichte_17.jpg',
),
fit: BoxFit.cover,
width: 200,
height: 200,
),
VerticalDivider(),
Image(
image: AssetImage(
'assets/images/geschichte/img_geschichte_18.jpg',
),
fit: BoxFit.cover,
width: 200,
height: 200,
),
],
),
),
Padding(
padding: EdgeInsets.all(UIShapes.paddingMax),
child: Text(
'Seit 1940 verfügt Hiddingsel auch über ein eigenes Ortswappen. „In Silber (Weiß) ein schrägrechter fünflätziger schwarzer Turnierkragen, darüber und darunter je ein schrägrechtes rotes Schwert.“ Der Turnierkragen wurde dem Wappen der Herren „von Tuchdorp“ entnommen. Es handelt sich hier um ein altes Rittergeschlecht. „Bruno von Tuchthorpe“ nennt sich 1330 „Pfarrer von Hiddingsel“, weil er zum Unterhalt der Pfarrstelle verpflichtet war. Die Besitzung der „von Tuchdorp“ ging 1331 durch Kauf auf den Hiddo-Hof über. Die Schwerter im Wappen deuten auf eine alte Gerichtsstätte hin, heute „Dingelke“ genannt. So stehen die im Wappenschild dargestellten Symbole in enger Beziehung zur Gemeinde.',
style: UITheme.theme.textTheme.bodyLarge,
),
),
Row(
children: [
Spacer(),
Image(
image: AssetImage(
'assets/images/geschichte/img_geschichte_19.png',
),
fit: BoxFit.cover,
width: 200,
height: 200,
),
Spacer(),
],
),
],
);
}

View File

@@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:hiddingsel_app/constants/constant.dart';
import 'package:hiddingsel_app/appflow/view/navigation_drawer.dart';
class VorwortView extends StatelessWidget with NavigationDrawerItem {
//TODO Low-Prio: Beim nächsten Anfassen durch RichText ersetzen
@override
String get title => 'Vorwort';
@override
Widget build(BuildContext context) => ListView(
children: [
Padding(
padding: EdgeInsets.fromLTRB(UIShapes.paddingMax, UIShapes.paddingMax, UIShapes.paddingMax, UIShapes.paddingSimple),
child: Text(
'Ein Gruß des Ortsvorstehers',
style: UITheme.theme.textTheme.headlineSmall,
),
),
Container(
decoration: BoxDecoration(
color: UIColors.grey5,
),
padding: EdgeInsets.fromLTRB(
0,
UIShapes.paddingSimple,
UIShapes.paddingMax,
UIShapes.paddingSimple,
),
child: ClipRRect(
borderRadius: UIShapes.rightRoundedRectangle,
child: Image(
image: AssetImage(
'assets/images/vorwort/img_vorwort.png',
),
fit: BoxFit.cover,
),
),
),
Padding(
padding: EdgeInsets.fromLTRB(UIShapes.paddingMax, UIShapes.paddingMax, UIShapes.paddingMax, 0,),
child: Text(
'Liebe Gäste,',
style: UITheme.theme.textTheme.displayLarge,
),
),
Padding(
padding: EdgeInsets.all(UIShapes.paddingMax),
child: Text(
'\nals Ortsvorsteher begrüße ich Sie herzlich auf der Internetseite von Hiddingsel. Gerne stellen wir Ihnen unser Heimatdorf, einen Ortsteil der Stadt Dülmen, näher vor.\n\nMit dieser Internetseite wollen wir sowohl den Bewohnern unseres Dorfes als auch Gästen und allen Interessierten möglichst viele Informationen über unser Dorf anbieten. Sie werden auf unserer Internetseite Interessantes und Wissenswertes sowie Beschauliches und Wichtiges aus dem historischen und aktuellen Dorfleben finden.\n\nDie eher ländlich geprägte westfälische Parklandschaft unseres rund 1.700 Einwohner zählenden Dorfes lädt zum Wandern, Rad fahren, Erholen aber auch zum Staunen ein. Das rege und abwechslungsreiche Dorfleben schöpft seine Kraft aus vielen engagierten und kreativen Mitbürgern, die in einer bunten Vereinslandschaft, in bürgerlichen oder kirchlichen Initiativen und Einrichtungen aktiv sind.\n\nNeben einer guten Grundversorgung für das tägliche Leben, besteht das gastronomische Angebot derzeit aus zwei Restaurants. Ein ortsansässiger Kindergarten, eine Grundschule mit Mehrzweckhalle für den Schul-und Vereinssport sowie zahlreiche lebendige Vereine mit unterschiedlichsten Betätigungsfeldern tragen zu einer hervorragenden Wohnqualität bei. Die Hiddingsler verstehen im Übrigen auch zu feiern. Neben zahlreichen Veranstaltungen sind hier besonders das traditionelle jährliche Schützenfest sowie der am 1. Advent stattfindende Weihnachtsmarkt in unserem Dorf zu erwähnen. Selbstverständlich sind zu den verschiedenen Veranstaltungen im Jahresverlauf Gäste aus Nah und Fern immer sehr herzlich eingeladen.\n\nIch bedanke mich für Ihr Interesse und wünsche allen Besuchern viel Spaß beim „Surfen“ durch Hiddingsel. Wenn Sie uns nach Ihrem virtuellen Rundgang durch unser lebenswertes Dorf auch einmal persönlich in Hiddingsel besuchen möchten, würde ich mich darüber sehr freuen. Gerne stehe ich Ihnen für einen Besuch unseres Dorfes mit Rat und Tat zur Seite.\n\nAlso, bis bald bei uns in Hiddingsel', style: UITheme.theme.textTheme.bodyLarge,),
),
Padding(
padding: EdgeInsets.all(UIShapes.paddingMax),
child: Row(
children: [
ClipRRect(
borderRadius:
BorderRadius.circular(UIShapes.paddingDouble),
child: Image(
image: AssetImage(
'assets/images/vorwort/img_vorwort_01.png',
),
fit: BoxFit.cover,
width: 110,
height: 110,
),
),
VerticalDivider(),
RichText(
text: TextSpan(
children: [
TextSpan(
text: 'Ihr',
style: UITheme.theme.textTheme.bodyLarge,
),
TextSpan(
text: '\nHendrik Clodius\n',
style: UITheme.theme.textTheme.headlineSmall?.apply(fontWeightDelta: 1),
),
TextSpan(
text: 'Ortsvorsteher',
style: UITheme.theme.textTheme.bodyLarge,
)
],
),
),
Spacer(),
],
),
),
],
);
}

View File

@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:hiddingsel_app/appflow/view/menu_pages/start.dart';
import 'package:hiddingsel_app/appflow/view/navigation_drawer.dart' as nd;
import '../../main.dart';
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
PreferredSizeWidget? _appBar;
Widget? _body;
return StatefulBuilder(
builder: (context, setState) {
Function(PreferredSizeWidget appBar, Widget body) onChange =
(appBar, newBody) => setState(() {
_appBar = appBar;
_body = newBody;
},);
if(_appBar == null) {
StartView item = StartView(onChange);
_appBar = item.getAppBar();
_body = item;
}
return Scaffold(
appBar: _appBar,
body: _body,
drawer: nd.NavigationDrawer(onChange),
bottomNavigationBar: nd.NavigationBottomBar(onChange),
);
},
);
}
static void openPage(nd.NavigationDrawerItem item, onChange, {String? optionalTitle}) {
navigatorKey.currentState?.push(
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: item.getAppBar(optionalTitle: optionalTitle),
body: item,
bottomNavigationBar: nd.NavigationBottomBar((PreferredSizeWidget appBar, Widget body) {
Navigator.pop(context);
onChange(appBar, body);
}),
),
),
);
}
}

View File

View File

@@ -0,0 +1,31 @@
import 'package:flutter/cupertino.dart';
import 'package:hiddingsel_app/appflow/controller/articles.dart';
import 'package:hiddingsel_app/appflow/model/represented_organisations.dart';
import 'package:hiddingsel_app/appflow/view/navigation_drawer.dart';
import '../../model/articles.dart';
import '../further_pages/article.dart';
class ArticleListView extends StatelessWidget with NavigationDrawerItem{
final String title;
final Stream<List<ArticleModel>> _articleStream;
final Function(PreferredSizeWidget appBar, Widget body)? _onChange;
ArticleListView(this._onChange) : title = 'News', _articleStream = ArticleController.getArticleListStream();
ArticleListView.favorites(this._onChange) : title = 'Favoriten', _articleStream = ArticleController.getFavoriteArticleListStream();
ArticleListView.search(String searchWord, this._onChange) : title = 'Suche', _articleStream = ArticleController.getSearchResultListStream(searchWord);
ArticleListView.organisation(RepresentedOrganisationModel organisation, this._onChange, {scrollController}) : title = 'Organisation', _articleStream = ArticleController.getOrganisationArticleListStream(organisation);
@override
Widget build(BuildContext context) => StreamBuilder(
initialData: List<ArticleModel>.empty(),
stream: _articleStream,
builder: (context, AsyncSnapshot<List<ArticleModel>> snapshot) => buildArticleListView(context, snapshot.data??[])
);
Widget buildArticleListView(BuildContext context, List<ArticleModel> list) => ListView.builder(
itemCount: list.length,
itemBuilder: (context, index) => ArticleThumbnailView(list[index], _onChange),
);
}

View File

@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:hiddingsel_app/constants/constant.dart';
import 'package:hiddingsel_app/services/environment.dart';
import '../navigation_drawer.dart';
class BuergerFuerBuergerView extends StatelessWidget with NavigationDrawerItem {
//TODO Low-Prio: Beim nächsten Anfassen durch RichText ersetzen
@override
String get title => 'Bürger für Bürger';
@override
Widget build(BuildContext context) => Container(
padding: EdgeInsets.all(UIShapes.paddingMax),
decoration: BoxDecoration(
gradient: UIGradiants.red,
),
child: Column(
children: [
Spacer(),
Text(
'KLICK MICH!',
style: UITheme.theme.textTheme.displayMedium,
textAlign: TextAlign.center,
),
Spacer(),
FloatingActionButton(
child: ShaderMask(
shaderCallback: (Rect bounds) {
return UIGradiants.red.createShader(bounds);
},
child: Icon(
Icons.mail,
),
),
tooltip: 'Mail an Hidd-Box',
backgroundColor: UIColors.white,
onPressed: (){
EnvironmentConnector.openMail('Hidd-Box@hiddingsel.de');
},
),
Spacer(),
Text(
'HIDD-BOX',
style: UITheme.theme.textTheme.titleLarge,
),
Text(
'H\t\tiddingseler\nI\t\tdeen und\nD\t\tdenkanstöße für unser\nD\t\torf\n\nHaben sie Anregungen, Ideen oder Wünsche, die das Dorf betreffen? Dann teilen sie uns diese doch bitte mit! Wir nehmen ihr Anliegen auf und werden es kurzfristig im Vorstand der Dorfgemeinschaft erörtern und ggf. der Delegiertenversammlung als Idee vorstellen. Jeder ernst gemeinte Vorschlag bekommt auch eine entsprechende Rückmeldung.\n\nMit ihrem Vorschlag unterstützen sie uns dabei unser Dorf ein bisschen schöner, attraktiver oder liebenswerter zu machen.\n\nVielen Dank für ihre Unterstützung!\n\nIhr Vorstand der Dorfgemeinschaft Hiddingsel',
style: UITheme.theme.textTheme.bodyMedium,
),
Spacer(),
],
));
}

View File

@@ -0,0 +1,54 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:hiddingsel_app/appflow/model/companies.dart';
import 'package:hiddingsel_app/appflow/view/navigation_drawer.dart';
import '../widgets/string_list.dart';
import '../../../constants/constant.dart';
import '../widgets/card.dart';
final List<CompanyModel> _companies = CompanyModel.values;
class CompanyView extends StatelessWidget {
final CompanyModel _company;
CompanyView(this._company);
@override
Widget build(BuildContext context) => ListView(
children: [ItemWithPersonAndText.fromCompany(_company),],
);
}
class CompanyPagesView extends StatelessWidget with NavigationDrawerItem {
String get title => 'Dienstleistungen | Firmen';
final CompanyModel _company;
CompanyPagesView(this._company);
Widget build(BuildContext context) => PageView.builder(
controller: PageController(initialPage: _companies.indexOf(_company)),
itemCount: _companies.length,
itemBuilder: (context, index) => CompanyView(_companies[index]),
);
}
class CompanyListView extends StatelessWidget with NavigationDrawerItem {
@override
String get title => 'Dienstleistungen | Firmen';
final Function(PreferredSizeWidget appBar, Widget body)? _onChange;
CompanyListView(this._onChange);
@override
Widget build(BuildContext context) => HiddingselMenu(
_companies.map((e) => CompanyPagesView(e)).toList(),
_onChange,
names: _companies.map((e) => e.name).toList(),
gradient: UIGradiants.purple
);
}

View File

@@ -0,0 +1,101 @@
import 'package:flutter/material.dart';
import 'package:hiddingsel_app/constants/constant.dart';
import '../../../services/environment.dart';
import '../navigation_drawer.dart';
class ContactView extends StatelessWidget with NavigationDrawerItem {
//TODO Low-Prio: Beim nächsten Anfassen durch RichText ersetzen
@override
String get title => 'Kontakt';
@override
Widget build(BuildContext context) => Container(
padding: EdgeInsets.all(UIShapes.paddingMax),
decoration: BoxDecoration(
gradient: UIGradiants.black,
),
child: ListView(
children: <Widget>[
Divider(),
Text(
'Ihr Verein möchte auch in der Hiddingsel-App sichtbar sein? Ein Termin ihres Vereins soll im Dorfkalender erscheinen? Oder sie haben eine generelle Anmerkung zur App?\nDann melden sie sich gerne mit ihrem Anliegen bei uns unter den foldenen E-Mail-Adressen.',
textAlign: TextAlign.justify,
style: UITheme.theme.textTheme.bodyMedium,
),
Divider(height: UIShapes.paddingMax),
Divider(height: UIShapes.paddingMax),
FloatingActionButton(
child: Icon(
Icons.mail,
color: UIColors.white,
),
tooltip: 'news@hiddingsel.de',
backgroundColor: UIColors.black,
onPressed: (){
EnvironmentConnector.openMail('news@hiddingsel.de');
},
),
Text(
'NEWS',
textAlign: TextAlign.center,
style: UITheme.theme.textTheme.headlineMedium,
),
Text(
'news@hiddingsel.de',
textAlign: TextAlign.center,
style: UITheme.theme.textTheme.bodyMedium,
),
Divider(height: UIShapes.paddingMax),
Divider(height: UIShapes.paddingMax),
FloatingActionButton(
child: Icon(
Icons.mail,
color: UIColors.white,
),
tooltip: 'termine@hiddingsel.de',
backgroundColor: UIColors.black,
onPressed: (){
EnvironmentConnector.openMail('termine@hiddingsel.de');
},
),
Text(
'TERMINE',
textAlign: TextAlign.center,
style: UITheme.theme.textTheme.headlineMedium,
),
Text(
'termine@hiddingsel.de',
textAlign: TextAlign.center,
style: UITheme.theme.textTheme.bodyMedium,
),
Divider(height: UIShapes.paddingMax),
Divider(height: UIShapes.paddingMax),
FloatingActionButton(
child: Icon(
Icons.mail,
color: UIColors.white,
),
tooltip: 'app@hiddingsel.de',
backgroundColor: UIColors.black,
onPressed: (){
EnvironmentConnector.openMail('app@hiddingsel.de');
},
),
Text(
'APP',
textAlign: TextAlign.center,
style: UITheme.theme.textTheme.headlineMedium,
),
Text(
'app@hiddingsel.de',
textAlign: TextAlign.center,
style: UITheme.theme.textTheme.bodyMedium,
),
Divider(),
],
),
);
}

View File

@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:hiddingsel_app/appflow/view/navigation_drawer.dart';
import 'package:hiddingsel_app/appflow/view/further_pages/vorwort.dart';
import '../further_pages/geschichte.dart';
import '../widgets/string_list.dart';
import '../../../constants/constant.dart';
List<NavigationDrawerItem> _items = [VorwortView(), GeschichteView()];
class DasDorfPagesView extends StatelessWidget with NavigationDrawerItem {
String get title => 'Das Dorf';
final NavigationDrawerItem _item;
DasDorfPagesView(this._item);
Widget build(BuildContext context) =>
PageView.builder(
controller: PageController(initialPage: _items.indexOf(_item)),
itemCount: _items.length,
itemBuilder: (context, index) => _items[index],
);
}
class DasDorfListView extends StatelessWidget with NavigationDrawerItem {
String get title => 'Das Dorf';
final Function(PreferredSizeWidget appBar, Widget body)? _onChange;
DasDorfListView(this._onChange);
@override
Widget build(BuildContext context) => HiddingselMenu(
_items,
_onChange,
names: _items.map((e) => e.title).toList(),
gradient: UIGradiants.green,
);
}

View File

@@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:hiddingsel_app/constants/constant.dart';
import '../navigation_drawer.dart';
class ImprintView extends StatelessWidget with NavigationDrawerItem {
//TODO Low-Prio: Beim nächsten Anfassen durch RichText ersetzen
@override
String get title => 'Impressum';
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.symmetric(horizontal: UIShapes.paddingMax,),
child: ListView(
children: <Widget>[
Divider(),
GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (context) => AboutDialog(
//applicationIcon: ImageIcon(AssetImage(Constants.hiddingselIconAsset)),
applicationName: 'Hiddingsel-App: Lizenzen',
applicationVersion: 'April 2021',
),
);
},
child: Text(
'Klicken Sie hier, um die verwendeten Lizenzen anzuzeigen.',
style: UITheme.theme.textTheme.headlineSmall
?.apply(color: UIColors.grey4),
),
),
Divider(),
Text(
'Diese App wurde vom Projektteam "Digitales Hiddingsel" konzipiert, inhaltlich ausgestaltet und umgesetzt.\n\nBei inhaltlichen und technischen Fragen wenden Sie sich bitte an diese Mail:\n\napp@hiddingsel.de\n',
textAlign: TextAlign.justify,
style: UITheme.theme.textTheme.bodyLarge,
),
Text(
'\nProjektleitung:\nTim Marquardt\nSoftwareentwicklung:\nNils Weber\n',
style: UITheme.theme.textTheme.bodyLarge,
),
Text(
'\nUrheberrecht\n',
style:
UITheme.theme.textTheme.bodyLarge?.apply(fontWeightDelta: 1),
),
Text(
'Diese App ist urheberrechtlich geschützt. Es ist verboten diese App oder Teile von ihr (wie Bilder und Texte) zu kopieren und zu vervielfältigen. Das Urheberrecht für die Gestaltung, Fotos, Grafiken und Texte liegt bei dem Verein, wenn nicht für Teile der App an anderer Stelle ein anderer Urheber genannt wird. Alle Rechte, auch die der fotomechanischen Wiedergabe, der Vervielfältigung und der Verbreitung mittels besonderer Verfahren (zum Beispiel Datenverarbeitung, Datenträger und Datennetze), auch teilweise, behält sich der Urheber vor.\n',
textAlign: TextAlign.justify,
style: UITheme.theme.textTheme.bodyLarge,
),
Text(
'\nInhalt des Webangebotes\n',
style:
UITheme.theme.textTheme.bodyLarge?.apply(fontWeightDelta: 1),
),
Text(
'Der Appbetreiber ist bemüht, sein Webangebot stets aktuell und inhaltlich richtig sowie vollständig anzubieten. Dennoch ist das Auftreten von Fehlern nicht völlig auszuschließen. Er übernimmt keine Haftung für die Aktualität, die inhaltliche Richtigkeit sowie für die Vollständigkeit der im Webangebot eingestellten Informationen, es sei denn die Fehler wurden vorsätzlich oder grob fahrlässig aufgenommen. Dies bezieht sich auf eventuelle Schäden materieller oder ideeller Art Dritter, die durch die Nutzung dieses Webangebotes verursacht wurden.\n',
textAlign: TextAlign.justify,
style: UITheme.theme.textTheme.bodyLarge,
),
Text(
'\nExterne Verweise und Links\n',
style:
UITheme.theme.textTheme.bodyLarge?.apply(fontWeightDelta: 1),
),
Text(
'Der Appbetreiber hat in seiner App Links zu Seiten im Internet gelegt, deren Inhalt und Aktualisierung nicht seinem Einflussbereich unterliegt. Für alle diese Links gilt:\n„Der Appbetreiber hat keinen Einfluss auf Gestaltung und Inhalte fremder Internetseiten. Er distanziert sich daher von allen fremden Inhalten, auch wenn von Seiten des Betreibers auf diese externen Seiten ein Link gesetzt wurde.“\nDiese Erklärung gilt für alle in dieser App angezeigten Links und für alle Inhalte der Seiten, zu denen Links führen.\n',
textAlign: TextAlign.justify,
style: UITheme.theme.textTheme.bodyLarge,
),
Text(
'\nDatenschutz\n',
style: UITheme.theme.textTheme.bodyLarge,
),
Text(
'Sofern innerhalb des Internetangebotes die Möglichkeit der Eingabe von persönlichen Daten (E-Mailadressen, Namen, Anschriften) besteht, erfolgt diese freiwillig. Der Seitenbetreiber erklärt ausdrücklich, dass er diese Daten nicht an Dritte weitergibt. Sie werden nur für den vom Benutzer bestimmten Zweck erhoben, wie z.B. Zusenden von Infomaterial per Mail oder Post oder zur Beantwortung von Anfragen an den Verein.\n',
textAlign: TextAlign.justify,
style: UITheme.theme.textTheme.bodyLarge,
),
Text(
'\nRechtswirksamkeit\n',
style:
UITheme.theme.textTheme.bodyLarge?.apply(fontWeightDelta: 1),
),
Text(
'Dieser Haftungsausschluss ist Teil des Internetangebotes www.hiddingsel.de. Sofern einzelne Formulierungen oder Teile dieses Textes der geltenden Rechtslage nicht mehr oder nicht mehr vollständig entsprechen, bleiben die übrigen Teile dieser Erklärung davon unberührt.',
textAlign: TextAlign.justify,
style: UITheme.theme.textTheme.bodyLarge,
),
Divider(),
],
),
);
}

View File

@@ -0,0 +1,101 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:hiddingsel_app/appflow/model/represented_organisations.dart';
import 'package:hiddingsel_app/appflow/view/navigation_drawer.dart';
import '../../../constants/constant.dart';
import '../../controller/articles.dart';
import '../../model/articles.dart';
import '../further_pages/article.dart';
import '../widgets/card.dart';
import '../widgets/string_list.dart';
final List<RepresentedOrganisationModel> _organisations =
RepresentedOrganisationModel.values;
class OrganisationView extends StatelessWidget {
final RepresentedOrganisationModel _organisation;
final Function(PreferredSizeWidget appBar, Widget body)? _onChange;
OrganisationView(this._organisation, this._onChange, {Key? key})
: super(key: key);
@override
Widget build(BuildContext context) => StreamBuilder(
initialData: List<ArticleModel>.empty(),
stream:
ArticleController.getOrganisationArticleListStream(_organisation),
builder: (context, AsyncSnapshot<List<ArticleModel>> snapshot) =>
CustomScrollView(
slivers: [
SliverPersistentHeader(
delegate: OrganisationSliver(_organisation),
floating: true,
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ArticleThumbnailView(
(snapshot.data ?? [])[index], _onChange),
childCount: snapshot.data?.length ?? 0,
),
)
],
),
);
}
class OrganisationSliver extends SliverPersistentHeaderDelegate {
final RepresentedOrganisationModel _organisation;
OrganisationSliver(this._organisation, {Key? key});
@override
double get maxExtent => 550;
@override
double get minExtent => 550;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => true;
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) =>
Container(
color: UIColors.white,
child: ItemWithPersonAndText.fromOrganisation(_organisation),
);
}
class OrganisationPagesView extends StatelessWidget with NavigationDrawerItem {
@override
String get title => 'Organisationen | Vereine';
final RepresentedOrganisationModel _organisation;
final Function(PreferredSizeWidget appBar, Widget body)? _onChange;
OrganisationPagesView(this._organisation, this._onChange);
Widget build(BuildContext context) => PageView.builder(
controller:
PageController(initialPage: _organisations.indexOf(_organisation)),
itemCount: _organisations.length,
itemBuilder: (context, index) =>
OrganisationView(_organisations[index], _onChange),
);
}
class OrganisationListView extends StatelessWidget with NavigationDrawerItem {
@override
String get title => 'Organisationen | Vereine';
final Function(PreferredSizeWidget appBar, Widget body)? _onChange;
OrganisationListView(this._onChange);
@override
Widget build(BuildContext context) => HiddingselMenu(
_organisations.map((e) => OrganisationPagesView(e, _onChange)).toList(),
_onChange,
names: _organisations.map((e) => e.name).toList(),
gradient: UIGradiants.yellow,
);
}

View File

@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:hiddingsel_app/appflow/model/pois.dart';
import 'package:hiddingsel_app/appflow/view/widgets/gallery.dart';
import 'package:hiddingsel_app/appflow/view/widgets/string_list.dart';
import 'package:hiddingsel_app/appflow/view/navigation_drawer.dart';
import '../../../constants/constant.dart';
import '../widgets/card.dart';
final List<PointOfInterestModel> _pois = PointOfInterestModel.values;
class PoiView extends StatelessWidget {
final PointOfInterestModel _poi;
const PoiView(this._poi, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) => ListView(
children: [
ItemWithText.fromPoi(_poi),
SizedBox(
height: 200,
child: HiddingselGallery(
_poi.otherImages.map((e) => e.image).toList()),
),
],
);
}
class PoiPagesView extends StatelessWidget with NavigationDrawerItem {
final PointOfInterestModel _poi;
PoiPagesView(this._poi);
@override
String get title => 'Sehenswertes';
Widget build(BuildContext context) => PageView.builder(
controller: PageController(initialPage: _pois.indexOf(_poi)),
itemCount: _pois.length,
itemBuilder: (context, index) => PoiView(_pois[index]),
);
}
class PoiListView extends StatelessWidget with NavigationDrawerItem {
@override
String get title => 'Sehenswertes';
final Function(PreferredSizeWidget appBar, Widget body)? _onChange;
PoiListView(this._onChange);
@override
Widget build(BuildContext context) => HiddingselMenu(
_pois.map((e) => PoiPagesView(e)).toList(),
_onChange,
names: _pois.map((e) => e.name).toList(),
gradient: UIGradiants.blue);
}

View File

@@ -0,0 +1,283 @@
import 'package:flutter/material.dart';
import 'package:hiddingsel_app/appflow/controller/articles.dart';
import 'package:hiddingsel_app/appflow/model/companies.dart' as enu;
import 'package:hiddingsel_app/appflow/view/further_pages/article.dart';
import 'package:hiddingsel_app/constants/constant.dart';
import 'package:hiddingsel_app/appflow/model/represented_organisations.dart' as enu2;
import 'package:hiddingsel_app/appflow/model/articles.dart';
import 'package:hiddingsel_app/appflow/view/menu_pages/companies.dart';
import 'package:hiddingsel_app/appflow/view/menu_pages/organisations.dart';
import 'package:hiddingsel_app/appflow/view/menu_pages/das_dorf.dart';
import 'package:hiddingsel_app/appflow/view/menu_pages/pois.dart';
import '../../model/pois.dart';
import '../home.dart';
import 'article_list.dart';
import '../navigation_drawer.dart';
class Search extends StatelessWidget with NavigationDrawerItem {
String get title => 'Suche';
final Function(PreferredSizeWidget appBar, Widget body) _onChange;
const Search(this._onChange);
@override
Widget build(BuildContext context) {
TextEditingController controller = new TextEditingController();
return StatefulBuilder(
builder: (context, setState) {
if (controller.value.text.isEmpty) {
return buildSearch(controller, setState, context);
} else {
return buildResults(controller, setState, context);
}
},
);
}
Widget buildSearch(
TextEditingController controller, setState, BuildContext context) =>
LayoutBuilder(
builder: (context, constraints) => Stack(
alignment: Alignment.center,
children: [
Container(
decoration: BoxDecoration(gradient: UIGradiants.red),
width: constraints.maxWidth,
height: constraints.maxHeight,
),
Positioned(
top: constraints.maxHeight * 0.6,
child: Column(
children: [
SizedBox(
width: constraints.maxWidth * 0.8,
height: UIShapes.paddingMax,
child: TextField(
onSubmitted: (s) {
setState(() {
controller = controller;
});
},
textInputAction: TextInputAction.search,
controller: controller,
autofocus: true,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'SUCHEN',
hintStyle: UITheme.theme.textTheme.displayMedium?.apply(
color: UIColors.white.withAlpha(100),
),
),
textAlign: TextAlign.center,
style: TextStyle(
color: UIColors.white,
fontSize: UIShapes.paddingMax,
),
),
),
Divider(),
FloatingActionButton(
backgroundColor: UIColors.white,
child: Icon(
Icons.search,
color: UIColors.grey5,
),
onPressed: () {
setState(() {
controller = controller;
});
},
),
],
),
),
],
),
);
Widget buildResults(
TextEditingController controller, setState, BuildContext context) {
String searchWord = controller.value.text;
List<enu2.RepresentedOrganisationModel> orgas = enu2.RepresentedOrganisationModel.values
.where((element) =>
element.tags.any((alias) => alias.toLowerCase().contains(searchWord.toLowerCase())))
.toList();
List<enu.CompanyModel> comps = enu.CompanyModel.values
.where((element) =>
element.name.toLowerCase().contains(searchWord.toLowerCase()))
.toList();
List<PointOfInterestModel> pois = PointOfInterestModel.values
.where((element) =>
element.name.toLowerCase().contains(searchWord.toLowerCase()))
.toList();
List others = ['Vorwort', 'Geschichte']
.where((element) =>
element.toLowerCase().contains(searchWord.toLowerCase()))
.toList();
List<Widget> children = [];
if (orgas.isNotEmpty) {
children.addAll([
Text(
OrganisationListView(null).title.toUpperCase(),
style: UITheme.theme.textTheme.headlineMedium,
),
Divider(),
]);
for (enu2.RepresentedOrganisationModel o in orgas) {
children.addAll([
GestureDetector(
child: Text(
'... ' + o.name.toUpperCase(),
style: UITheme.theme.textTheme.titleLarge,
),
onTap: () {
Home.openPage(OrganisationPagesView(o, _onChange), _onChange);
},
),
Divider()
]);
}
}
if (comps.isNotEmpty) {
children.addAll([
Text(
CompanyListView(null).title.toUpperCase(),
style: UITheme.theme.textTheme.headlineMedium,
),
Divider(),
]);
for (enu.CompanyModel o in comps) {
children.addAll([
GestureDetector(
child: Text(
'... ' + o.name.toUpperCase(),
style: UITheme.theme.textTheme.titleLarge,
),
onTap: () {
Home.openPage(CompanyPagesView(o), _onChange);
},
),
Divider()
]);
}
}
if (pois.isNotEmpty) {
children.addAll([
Text(
PoiListView(null).title.toUpperCase(),
style: UITheme.theme.textTheme.headlineMedium,
),
Divider(),
]);
for (PointOfInterestModel o in pois) {
children.addAll([
GestureDetector(
child: Text(
'... ' + o.name.toUpperCase(),
style: UITheme.theme.textTheme.titleLarge,
),
onTap: () {
Home.openPage(PoiPagesView(o), _onChange);
},
),
Divider()
]);
}
}
if (others.isNotEmpty) {
children.addAll([
Text(
DasDorfListView(null).title.toUpperCase(),
style: UITheme.theme.textTheme.headlineMedium,
),
Divider(),
]);
for (String o in others) {
children.addAll([
GestureDetector(
child: Text(
'... ' + o.toUpperCase(),
style: UITheme.theme.textTheme.titleLarge,
),
onTap: () {
Home.openPage(DasDorfListView(_onChange), _onChange);
},
),
Divider()
]);
}
}
return FutureBuilder(
future: ArticleController.getSearchResultListStream(
searchWord).first,
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.data != null) {
List<ArticleModel> arts = snapshot.data;
if (arts.isNotEmpty) {
List<Widget> news = [
Text(
ArticleListView(null).title.toUpperCase(),
style: UITheme.theme.textTheme.headlineMedium,
),
Divider(),
];
for (ArticleModel o in arts) {
news.addAll([
GestureDetector(
child: Text(
'... ' + o.title.toUpperCase(),
style: UITheme.theme.textTheme.titleLarge,
),
onTap: () {
Home.openPage(ArticleView(o), _onChange);
},
),
Divider(),
]);
}
news.addAll(children);
children = news;
}
}
return Container(
padding: EdgeInsets.all(UIShapes.paddingMax),
decoration: BoxDecoration(gradient: UIGradiants.red),
child: ListView(
children: [
SizedBox(
height: UIShapes.paddingMax,
child: Text(searchWord.toUpperCase(),
textAlign: TextAlign.center,
style: UITheme.theme.textTheme.displayMedium),
),
Divider(),
FloatingActionButton(
backgroundColor: UIColors.white,
child: Icon(
Icons.cancel_outlined,
color: UIColors.grey5,
),
onPressed: () {
setState(() {
controller.clear();
});
},
),
Divider(),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
],
),
);
});
}
}

View File

@@ -0,0 +1,135 @@
import 'package:flutter/material.dart';
import 'package:hiddingsel_app/appflow/controller/events.dart';
import 'package:hiddingsel_app/constants/constant.dart';
import 'package:hiddingsel_app/appflow/model/event_organisations.dart';
import 'package:hiddingsel_app/appflow/model/represented_organisations.dart';
import 'package:hiddingsel_app/packages/list_view_extension/colum_extension.dart';
import 'package:hiddingsel_app/services/storage.dart';
import 'package:hiddingsel_app/packages/icon_switch/icon_switch.dart';
import '../navigation_drawer.dart';
import '../../model/topics.dart';
import '../widgets/time_picker.dart';
class SettingsView extends StatelessWidget with NavigationDrawerItem {
@override
String get title => 'Einstellungen';
@override
Widget build(BuildContext context) {
return ListView(
padding: EdgeInsets.all(UIShapes.paddingDouble),
children: [
buildNewsNotificationTile(RepresentedOrganisationModel.values),
//buildFavorizationTile(RepresentedOrganisationModel.values),
buildEventNotificationTile(EventOrganisationModel.values),
buildNotificationTimeTile(),
],
);
}
ExpansionTile buildNewsNotificationTile(List<PushNotificationTopicModel> topics) {
return ExpansionTile(
title: Text(
'News-Benachrichtigungen'.toUpperCase(),
style: UITheme.theme.textTheme.headlineSmall,
),
children: [
ColumnExtension.builder(
itemBuilder: (context, index) => ListTile(
leading: IconSwitch(
topics[index].subscribe,
initFavorite: topics[index].subscribed,
gradientColor:UIGradiants.yellow,
iconFavorite: Icons.notifications_active,
iconDefavorite: Icons.notifications_outlined,
),
title: Text(
topics[index].name.toUpperCase(),
style: UITheme.theme.textTheme.headlineSmall,
),
),
itemCount: topics.length,
),
],
);
}
/*
ExpansionTile buildFavorizationTile(
List<RepresentedOrganisationModel> organisations) {
return ExpansionTile(
title: Text(
'Favoriten bearbeiten'.toUpperCase(),
style: UITheme.theme.textTheme.headline5,
),
children: [
ColumnExtension.builder(
itemBuilder: (context, index) => ListTile(
leading: IconSwitch(
organisations[index].favorize,
initFavorite: organisations[index].favorized,
gradientColor: UIGradiants.yellow,
),
title: Text(
organisations[index].name.toUpperCase(),
style: UITheme.theme.textTheme.headline5,
),
),
itemCount: organisations.length,
),
],
);
}*/
ExpansionTile buildEventNotificationTile(
List<EventOrganisationModel> organisations) {
return ExpansionTile(
title: Text(
'Termin-Benachrichtigungen'.toUpperCase(),
style: UITheme.theme.textTheme.headlineSmall,
),
children: [
ColumnExtension.builder(
itemBuilder: (context, index) => ListTile(
leading: IconSwitch(
(b) => organisations[index].allowEventNotification(b),
initFavorite: organisations[index].eventNotificationAllowed,
gradientColor: UIGradiants.yellow,
iconFavorite: Icons.notifications_active,
iconDefavorite: Icons.calendar_today,
),
title: Text(
organisations[index].name.toUpperCase(),
style: UITheme.theme.textTheme.headlineSmall,
),
),
itemCount: organisations.length,
),
],
);
}
Widget buildNotificationTimeTile() {
return ExpansionTile(
title: Text(
'Benachrichtungszeit Termine'.toUpperCase(),
style: UITheme.theme.textTheme.headlineSmall,
),
children: [
LayoutBuilder(builder: (context, constraints) => FutureBuilder(
future: SettingsConnector.getInt(SystemStrings.keyAlertTime, 1*60*60*1000),
builder: (context, AsyncSnapshot snapshot) => HiddingselTimePicker(
Duration(milliseconds: snapshot.data??1*60*60*1000), (duration) async {
await SettingsConnector.setInt(SystemStrings.keyAlertTime, duration.inMilliseconds);
await EventController.scheduleNotificationsBySettings();
}, width: constraints.maxWidth, key: UniqueKey())
),
),
],
);
}
}

View File

@@ -0,0 +1,59 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:hiddingsel_app/constants/constant.dart';
import 'package:hiddingsel_app/appflow/view/menu_pages/search.dart';
import 'package:intl/intl.dart';
import '../navigation_drawer.dart';
class StartView extends StatelessWidget with NavigationDrawerItem {
String get title => 'Start';
final Function(PreferredSizeWidget appBar, Widget body) _onChange;
const StartView(onChange) : _onChange = onChange;
@override
Widget build(BuildContext context) => LayoutBuilder(
builder: (context, constraints) => Stack(
alignment: Alignment.center,
children: [
Container(
width: constraints.maxWidth,
height: constraints.maxHeight,
child: Image(
image: AssetImage('assets/images/start/img_start_${NumberFormat("00", "en_US").format(Random().nextInt(12)+1)}.png'),
fit: BoxFit.cover,
),
),
Positioned(
top: constraints.maxHeight * 0.6,
child: Column(
children: [
Text(
'WILLKOMMEN!',
style: UITheme.theme.textTheme.displayMedium,
),
Divider(),
FloatingActionButton(
backgroundColor: UIColors.white,
child: Icon(
Icons.search,
color: UIColors.grey5,
),
onPressed: () {
_onChange(
Search(_onChange).getAppBar(),
Search(_onChange),
);
},
),
],
),
),
],
),
);
}

View File

@@ -0,0 +1,157 @@
import 'package:flutter/material.dart';
import 'package:hiddingsel_app/appflow/view/menu_pages/article_list.dart';
import 'package:hiddingsel_app/appflow/view/menu_pages/pois.dart';
import 'package:hiddingsel_app/appflow/view/menu_pages/companies.dart';
import 'package:hiddingsel_app/appflow/view/menu_pages/das_dorf.dart';
import 'package:hiddingsel_app/todo/calendar.dart';
import 'package:hiddingsel_app/constants/constant.dart';
import 'menu_pages/contact.dart';
import 'menu_pages/search.dart';
import 'menu_pages/buerger_fuer_buerger.dart';
import 'menu_pages/organisations.dart';
import 'menu_pages/settings.dart';
import 'widgets/app_bar.dart';
import 'menu_pages/imprint.dart';
class NavigationDrawer extends StatelessWidget with NavigationDrawerItem {
final Function(PreferredSizeWidget appBar, Widget body) _onChange;
const NavigationDrawer(this._onChange);
@override
String get title => 'Menü';
@override
Widget build(BuildContext context) => Scaffold(
appBar: this.getAppBar(),
body: Container(
padding: EdgeInsets.all(UIShapes.paddingMax),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_getListTile(Search(_onChange), null, context),
Spacer(
flex: 1,
),
_getListTile(ArticleListView(_onChange), null, context),
Spacer(
flex: 1,
),
_getListTile(CalendarListPage(), null, context),
Spacer(
flex: 3,
),
_getListTile(DasDorfListView(_onChange), Icons.arrow_forward_ios, context),
Spacer(
flex: 1,
),
_getListTile(PoiListView(_onChange), Icons.arrow_forward_ios, context),
Spacer(
flex: 1,
),
_getListTile(OrganisationListView(_onChange), Icons.arrow_forward_ios, context),
Spacer(
flex: 1,
),
_getListTile(CompanyListView(_onChange), Icons.arrow_forward_ios, context),
Spacer(
flex: 1,
),
_getListTile(BuergerFuerBuergerView(), null, context),
Spacer(
flex: 3,
),
_getListTile(SettingsView(), null, context),
Spacer(
flex: 1,
),
_getListTile(ImprintView(), null, context),
Spacer(
flex: 1,
),
_getListTile(ContactView(), null, context),
Spacer(
flex: 7,
),
],
),
),
);
Widget _getListTile(NavigationDrawerItem item, IconData? icon, BuildContext context) =>
GestureDetector(
child: Row(children:
[
Expanded(
child:Text(
item.title.toUpperCase(),
style:
UITheme.theme.textTheme.headlineSmall?.apply(color: UIColors.grey4),
),),
Icon(icon, color: UIColors.grey4,),
]
),
onTap: () {
_onChange(
item.getAppBar(),
item,
);
Navigator.pop(context);
},
);
}
class NavigationBottomBar extends StatelessWidget {
final Function(PreferredSizeWidget appBar, Widget body) _onChange;
const NavigationBottomBar(onChange) : _onChange = onChange;
@override
Widget build(BuildContext context) => Container(
padding: EdgeInsets.fromLTRB(UIShapes.paddingMax, UIShapes.paddingMax,
UIShapes.paddingMax, UIShapes.paddingMax),
color: UIColors.grey6,
child: SafeArea( child: Row(
children: [
Spacer(),
_getListTile(ArticleListView(_onChange)),
Spacer(),
Text(
'|',
style: UITheme.theme.textTheme.titleLarge,
),
Spacer(),
_getListTile(CalendarListPage()),
Spacer(),
Text(
'|',
style: UITheme.theme.textTheme.titleLarge,
),
Spacer(),
_getListTile(ContactView()),
Spacer(),
],
),
),);
Widget _getListTile(NavigationDrawerItem item) =>
GestureDetector(
child: Text(
item.title.toUpperCase(),
style: UITheme.theme.textTheme.titleLarge,
),
onTap: () {
_onChange(
item.getAppBar(),
item,
);
},
);
}
mixin NavigationDrawerItem on Widget {
String get title;
PreferredSizeWidget getAppBar({String? optionalTitle}) => HiddingselAppBar(Text(optionalTitle??title.toUpperCase()));
}

View File

@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:hiddingsel_app/constants/constant.dart';
import '../home.dart';
class HiddingselAppBar extends PreferredSize {
final Widget title;
HiddingselAppBar(this.title)
: super(child: title, preferredSize: Size.fromHeight(kToolbarHeight));
@override
Size get preferredSize => Size.fromHeight(88);
@override
Widget build(BuildContext context) => SafeArea(
child: Container(
height: preferredSize.height,
color: UIColors.white,
child: Column(
children: [
Flexible(
child: GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Home()),
);
},
child: Image(
image: AssetImage(
'assets/other/appbar.png',
),
),
),
),
Divider(),
AppBar(
title: title,
backgroundColor: UIColors.grey6,
)
],
),
),
);
}

View File

@@ -0,0 +1,19 @@
import 'package:flutter/cupertino.dart';
import '../../../constants/constant.dart';
class HiddingselButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;
HiddingselButton(this.text, this.onPressed, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) => GestureDetector(
child: Text(
text.toUpperCase(),
style: UITheme.theme.textTheme.headlineSmall,
),
onTap: onPressed,
);
}

View File

@@ -0,0 +1,673 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:hiddingsel_app/appflow/model/location.dart';
import 'package:hiddingsel_app/appflow/view/widgets/gallery.dart';
import 'package:hiddingsel_app/packages/html_view/html_view.dart';
import 'package:hiddingsel_app/constants/constant.dart';
import '../further_pages/article.dart';
import '../../../services/environment.dart';
import '../home.dart';
import '../../model/articles.dart';
import '../../model/companies.dart';
import '../../model/pois.dart';
import '../../model/represented_organisations.dart';
import '../../../packages/icon_switch/icon_switch.dart';
class ItemCard extends StatelessWidget {
final Widget _title;
final ImageProvider? _imageProvider;
final Widget _child;
final Function()? _onShare;
final Function(bool favorite) _onFavorite;
final Future<bool> _initFavorite;
final Function(bool notify) _onNotify;
final Future<bool> _initNotify;
final Gradient _gradientColor;
final bool _right;
ItemCard(this._title, this._imageProvider, this._child, this._onShare,
{onFavorite, initFavorite, onNotify, initNotify, gradientColor, right = true})
: _onFavorite = onFavorite,
_initFavorite = initFavorite ?? Future.value(false),
_onNotify = onNotify,
_initNotify = initNotify ?? Future.value(false),
_gradientColor = gradientColor ??
LinearGradient(
colors: [
UIColors.white,
UIColors.white,
],
),
_right = right;
@override
Widget build(BuildContext context) {
int space = 20;
List<Widget> buttons = [
IconButton(
icon: Icon(Icons.share, color: UIColors.white),
tooltip: 'Share',
onPressed: _onShare,
),
];
buttons.add(Spacer());
/*buttons.add(
IconSwitch(
_onFavorite,
initFavorite: _initFavorite,
gradientColor: _gradientColor,
defaultColor: UIColors.white,
),
);
buttons.add(Spacer());*/
buttons.add(
IconSwitch(
_onNotify,
initFavorite: _initNotify,
gradientColor: _gradientColor,
defaultColor: UIColors.white,
iconFavorite: Icons.notifications_active,
iconDefavorite: Icons.notifications_outlined,
),
);
return LayoutBuilder(
builder: (context, constraints) {
Widget image;
if (_imageProvider != null) {
image = Image(
image: _imageProvider,
fit: BoxFit.cover,
width: constraints.maxWidth - 48 - space,
);
} else {
image = Container(
width: constraints.maxWidth - 48 - space,
child: Center(
child: CircularProgressIndicator(),
),
);
}
RoundedRectangleBorder border;
double paddingLeft = 0;
double paddingRight = 0;
if (_right) {
border = UIShapes.leftRoundedRectangleWithBorder;
paddingLeft = UIShapes.paddingSimple + space / 2;
} else {
border = UIShapes.rightRoundedRectangleWithBorder;
paddingRight = UIShapes.paddingSimple + space / 2;
}
return Container(
margin: EdgeInsets.symmetric(vertical: UIShapes.paddingSimple),
child: Stack(
alignment: Alignment.topRight,
children: [
Positioned.fill(
left: paddingLeft,
right: paddingRight,
child: Container(
decoration: ShapeDecoration(shape: border),
width: constraints.maxWidth - UIShapes.paddingDouble,
),
),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.fromLTRB(
_right? UIShapes.paddingMax + UIShapes.paddingDouble : UIShapes.paddingMax,
UIShapes.paddingDouble,
UIShapes.paddingSimple,
UIShapes.paddingSimple),
child: _title,
),
Container(
height: constraints.maxWidth * 0.6,
decoration: BoxDecoration(
color: UIColors.grey5,
),
child: Row(
children: [
Padding(
padding: EdgeInsets.fromLTRB(
0,
UIShapes.paddingSimple,
0,
UIShapes.paddingSimple,
),
child: ClipRRect(
borderRadius: UIShapes.rightRoundedRectangle,
child: image,
),
),
Padding(
padding: EdgeInsets.fromLTRB(
0,
UIShapes.paddingSimple,
0,
UIShapes.paddingSimple,
),
child: Column(children: buttons),
),
VerticalDivider(
color: _right ? Colors.transparent : UIColors.white,
width: 2,
)
],
),
),
Padding(
padding: EdgeInsets.fromLTRB(
UIShapes.paddingMax + paddingLeft,
UIShapes.paddingSimple,
UIShapes.paddingSimple + paddingRight,
UIShapes.paddingDouble),
child: _child,
),
],
),
],
),
);
},
);
}
}
class ItemWithPersonAndText extends StatelessWidget {
final String _title;
final String _assetLogo;
final String _assetPerson;
final String _personName;
final String? _personText;
final InlineSpan _text;
final String? _mail;
final String? _phone;
final Uri? _website;
final LocationModel? _location;
final String? _facebook;
final String? _instagram;
final Function() _onShare;
final Function(bool favorite) _onFavorite;
final Future<bool> _initFavorite;
final Function(bool notify) _onNotify;
final Future<bool> _initNotify;
final Gradient _gradientColor;
ItemWithPersonAndText.fromOrganisation(RepresentedOrganisationModel _organisation)
: _title = _organisation.name,
_assetLogo = _organisation.image.resource,
_assetPerson = _organisation.person.image.resource,
_personName = _organisation.person.name,
_personText = _organisation.person.position,
_mail = _organisation.contact.mail,
_phone = _organisation.contact.phone,
_website = _organisation.contact.website,
_location = _organisation.contact.location,
_facebook = _organisation.contact.facebook,
_instagram = _organisation.contact.instagram,
_text = _organisation.text,
_onShare = shareOrganisation(_organisation),
_onFavorite = _organisation.favorize,
_initFavorite = _organisation.favorized,
_onNotify = _organisation.subscribe,
_initNotify = _organisation.subscribed,
_gradientColor = UIGradiants.yellow;
ItemWithPersonAndText.fromCompany(CompanyModel _company)
: _title = _company.name,
_assetLogo = _company.image.resource,
_assetPerson = _company.person.image.resource,
_personName = _company.person.name,
_personText = _company.person.position,
_text = _company.text,
_mail = _company.contact.mail,
_phone = _company.contact.phone,
_website = _company.contact.website,
_location = _company.contact.location,
_facebook = _company.contact.facebook,
_instagram = _company.contact.instagram,
_onShare = shareCompany(_company),
_onFavorite = doNothing,
_initFavorite = Future(() => false),
_onNotify = _company.subscribe,
_initNotify = _company.subscribed,
_gradientColor = UIGradiants.purple;
static doNothing(s) {}
@override
Widget build(
BuildContext context) {
Text title = Text(
_title,
style: UITheme.theme.textTheme.displaySmall,
);
ImageProvider image = AssetImage(_assetLogo);
List<Widget> iconButtons = [];
if (_mail != null) {
iconButtons.add(
IconButton(
icon: const Icon(
Icons.mail,
color: UIColors.grey5,
),
tooltip: 'Mail',
onPressed: () {
EnvironmentConnector.openMail(_mail);
},
),
);
}
if (_phone != null) {
iconButtons.add(
IconButton(
icon: const Icon(
Icons.phone,
color: UIColors.grey5,
),
tooltip: 'Phone',
onPressed: () {
EnvironmentConnector.openPhone(_phone);
},
),
);
}
if (_website != null) {
iconButtons.add(
IconButton(
icon: const Icon(
Icons.web,
color: UIColors.grey5,
),
tooltip: 'Website',
onPressed: () {
EnvironmentConnector.openBrowser(_website);
},
),
);
}
if (_location != null) {
iconButtons.add(
IconButton(
icon: const Icon(
Icons.place,
color: UIColors.grey5,
),
tooltip: 'Location',
onPressed: () {EnvironmentConnector.openMaps(_location);},
),
);
}
if (_facebook != null) {
iconButtons.add(
IconButton(
icon: const Icon(
Icons.facebook,
color: UIColors.grey5,
),
tooltip: 'Facebook',
onPressed: () {EnvironmentConnector.openFacebook(_facebook);},
),
);
}
if (_instagram != null) {
iconButtons.add(
IconButton(
icon: const Icon(
FontAwesomeIcons.instagram,
color: UIColors.grey5,
),
tooltip: 'Instagram',
onPressed: () {EnvironmentConnector.openInstagram(_instagram);},
),
);
}
Widget child = Column(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(UIShapes.paddingDouble),
child: Image(
image: AssetImage(_assetPerson),
fit: BoxFit.cover,
width: 100,
height: 100,
),
),
VerticalDivider(width: UIShapes.paddingSimple),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_personName,
style: UITheme.theme.textTheme.displaySmall,
),
Text(
_personText??'',
style: UITheme.theme.textTheme.bodyLarge,
),
],
),
),
Column(
children: iconButtons,
),
],
),
RichText(text: _text,),
],
);
return ItemCard(
title,
image,
child,
_onShare,
onFavorite: _onFavorite,
initFavorite: _initFavorite,
onNotify: _onNotify,
initNotify: _initNotify,
gradientColor: _gradientColor,
);
}
}
class ItemWithText extends StatelessWidget {
final String _title;
final String _assetTitle;
final String? _subtitle;
final String _text;
final Function() _onShare;
final Function()? _onFavorite;
final Function()? _onNotify;
ItemWithText.fromPoi(PointOfInterestModel _poi)
: _title = _poi.name,
_assetTitle = _poi.image.resource,
_subtitle = _poi.subtitle,
_text = _poi.text,
_onShare = sharePoi(_poi),
_onFavorite = null,
_onNotify = null;
@override
Widget build(BuildContext context) {
Text title = Text(
_title,
style: UITheme.theme.textTheme.headlineSmall,
);
ImageProvider image = AssetImage(_assetTitle);
Column child = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_subtitle ?? '',
style: UITheme.theme.textTheme.displaySmall,
),
Text(
_text,
style: UITheme.theme.textTheme.bodyLarge,
),
],
);
return ItemCard(
title,
image,
child,
_onShare,
onFavorite: _onFavorite,
onNotify: _onNotify,
);
}
}
class ItemWithTitle extends StatelessWidget {
final ArticleModel _article;
final String _title;
final String? _thumbnailPictureUrl;
final String _subtitle;
final Function()? _onShare;
final Function(bool favorite)? _onFavorite;
final Future<bool>? _initFavorite;
final Function(bool notify)? _onNotify;
final Future<bool>? _initNotify;
final bool _right;
final Function(PreferredSizeWidget appBar, Widget body)? _onChange;
ItemWithTitle.fromNull()
: _title = '',
_thumbnailPictureUrl = null,
_subtitle = '',
_onShare = null,
_onFavorite = null,
_initFavorite = null,
_onNotify = null,
_initNotify = null,
_right = false,
_onChange = null,
_article = ArticleModel(-1, '', '', DateTime.now(), '', Uri());
ItemWithTitle.fromArticle(this._article, this._onChange)
: _title =
'${_article.publishedAt.year}-${_article.publishedAt.month}-${_article.publishedAt.day}',
_thumbnailPictureUrl = _article.image.resource,
_subtitle = _article.title,
_onShare = shareArticle(_article),
_onFavorite = favorizeAll(_article.categories),
_initFavorite = allFavorized(_article.categories),
_onNotify = subscribeAll(_article.categories),
_initNotify = allSubscribed(_article.categories),
_right = false;
static favorizeAll(List<RepresentedOrganisationModel> list) {
return (fav) => list.forEach((e) => e.favorize(fav));
}
static Future<bool> allFavorized(List<RepresentedOrganisationModel> list) async {
bool favorized = false;
for(RepresentedOrganisationModel e in list) {
favorized = favorized || await e.favorized;
}
return favorized;
}
static subscribeAll(List<RepresentedOrganisationModel> list) {
return (sub) => list.forEach((e) => e.subscribe(sub));
}
static Future<bool> allSubscribed(List<RepresentedOrganisationModel> list) async {
bool subscribed = false;
for(RepresentedOrganisationModel e in list) {
subscribed = subscribed || await e.subscribed;
}
return subscribed;
}
@override
Widget build(BuildContext context) {
Text title = Text(
_title,
style: UITheme.theme.textTheme.headlineSmall,
);
ImageProvider? image = _thumbnailPictureUrl != null ? CachedNetworkImageProvider(_thumbnailPictureUrl) : null;
Widget child = Row(
children: [
Expanded(
child: Text(
_subtitle,
style: UITheme.theme.textTheme.displaySmall,
),
),
Icon(
Icons.arrow_forward_ios,
color: UIColors.grey4,
),
],
);
var onTap;
if (_article.id != -1) {
onTap = () {
Home.openPage(ArticleView(_article), _onChange);
};
}
return GestureDetector(
child: ItemCard(
title,
image,
child,
_onShare,
onFavorite: _onFavorite,
initFavorite: _initFavorite,
onNotify: _onNotify,
initNotify: _initNotify,
right: _right,
gradientColor: UIGradiants.yellow,
),
onTap: onTap,
);
}
}
class ItemWithWebView extends StatelessWidget {
final ArticleModel _article;
final String _title;
final String _thumbnailPictureUrl;
final String _subtitle;
final Function() _onShare;
final Function(bool favorite) _onFavorite;
final Future<bool> _initFavorite;
final Function(bool notify) _onNotify;
final Future<bool> _initNotify;
final bool _right;
ItemWithWebView.fromArticle(this._article)
: _title =
'${_article.publishedAt.year}-${_article.publishedAt.month}-${_article.publishedAt.day}',
_thumbnailPictureUrl = _article.image.resource,
_subtitle = _article.title,
_onShare = shareArticle(_article),
_onFavorite = favorizeAll(_article.categories),
_initFavorite = allFavorized(_article.categories),
_onNotify = subscribeAll(_article.categories),
_initNotify = allSubscribed(_article.categories),
_right = true;
static favorizeAll(List<RepresentedOrganisationModel> list) {
return (fav) => list.forEach((e) => e.favorize(fav));
}
static Future<bool> allFavorized(List<RepresentedOrganisationModel> list) async {
bool favorized = false;
for(RepresentedOrganisationModel e in list) {
favorized = favorized || await e.favorized;
}
return favorized;
}
static subscribeAll(List<RepresentedOrganisationModel> list) {
return (sub) => list.forEach((e) => e.subscribe(sub));
}
static Future<bool> allSubscribed(List<RepresentedOrganisationModel> list) async {
bool subscribed = false;
for(RepresentedOrganisationModel e in list) {
subscribed = subscribed || await e.subscribed;
}
return subscribed;
}
@override
Widget build(BuildContext context) {
Text title = Text(
_title,
style: UITheme.theme.textTheme.headlineSmall,
);
ImageProvider image = CachedNetworkImageProvider(_thumbnailPictureUrl);
Widget child = Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_subtitle,
style: UITheme.theme.textTheme.displaySmall,
textAlign: TextAlign.left,
),
HtmlView(
_article.content,
),
],
);
SizedBox otherImages = SizedBox(
height: 200,
child: HiddingselGallery(_article.otherImages.map((e) => e.image).toList()),
);
return ListView(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
children: [
ItemCard(
title,
image,
child,
_onShare,
onFavorite: _onFavorite,
initFavorite: _initFavorite,
onNotify: _onNotify,
initNotify: _initNotify,
right: _right,
gradientColor: UIGradiants.yellow,
),
otherImages
],
);
}
}
Function() sharePoi(PointOfInterestModel poi) => () {
EnvironmentConnector.share(
'Ich habe eine interessante Sehenswürdigkeit auf hiddingsel.de gefunden:\n${poi.name}');
};
Function() shareOrganisation(RepresentedOrganisationModel organisation) => () {
EnvironmentConnector.share(
'Ich habe einen interessanten Verein auf hiddingsel.de gefunden:\n${organisation.name}');
};
Function() shareCompany(CompanyModel company) => () {
String text = 'Ich habe ein interessantes Unternehmen auf hiddingsel.de gefunden:\n${company.name}';
if(company.contact.mail != null) {
text = text + '\nE-Mail: ${company.contact.mail}';
}
if(company.contact.phone != null) {
text = text + '\nTelefon: ${company.contact.phone}';
}
if(company.contact.website != null) {
text = text + '\nWebseite: ${company.contact.website}';
}
if(company.contact.instagram != null) {
text = text + '\nInstagram: ${company.contact.instagram}';
}
if(company.contact.facebook != null) {
text = text + '\nFacebook: ${company.contact.facebook}';
}
if(company.contact.twitter != null) {
text = text + '\nTwitter: ${company.contact.twitter}';
}
if(company.contact.location?.address != null) {
text = text + '\nAdresse: ${company.contact.location?.address}';
}
EnvironmentConnector.share(text);
};
Function() shareArticle(ArticleModel article) => () {
EnvironmentConnector.share(
'Ich habe einen interessanten Artikel auf hiddingsel.de gefunden:\n${article.url}');
};

View File

@@ -0,0 +1,36 @@
import 'dart:math';
import 'package:flutter/material.dart';
import '../../../packages/image/zoomable_image.dart';
class HiddingselGallery extends StatelessWidget {
final int _itemCount; //_poi.otherImages.length
final IndexedWidgetBuilder _itemBuilder;
static IndexedWidgetBuilder getItemBuilder(List<ImageProvider> images) => (context, index) => ZoomableImage(
images[index],
fit: BoxFit.cover,
width: 200,
height: 200,
);
HiddingselGallery(List<ImageProvider> images) : _itemCount = images.length, _itemBuilder = getItemBuilder(images);
HiddingselGallery.builder(this._itemCount, this._itemBuilder);
@override
Widget build(BuildContext context) => ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: max(0, 2 * _itemCount - 1),
itemBuilder: (BuildContext context, int index) {
if (index % 2 == 0) {
return _itemBuilder(context, index ~/ 2);
} else {
return VerticalDivider();
}
},
);
}

View File

@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:hiddingsel_app/appflow/view/widgets/button.dart';
import '../../../constants/constant.dart';
import '../home.dart';
import '../navigation_drawer.dart';
class HiddingselDecoratedList extends StatelessWidget {
final int _itemCount;
final IndexedWidgetBuilder _itemBuilder;
final Gradient? _gradient;
HiddingselDecoratedList(int itemCount, IndexedWidgetBuilder itemBuilder, {Gradient? gradient, Key? key})
: _itemCount = itemCount,
_itemBuilder = itemBuilder,
_gradient = gradient, super(key: key);
@override
Widget build(BuildContext context) => Container(
decoration: BoxDecoration(gradient: _gradient),
child: ListView.separated(
padding: EdgeInsets.all(UIShapes.paddingMax),
itemBuilder: _itemBuilder,
separatorBuilder: (context, i) => Divider(),
itemCount: _itemCount,
),
);
}
class HiddingselMenu extends StatelessWidget {
final List<NavigationDrawerItem> _items;
final List<String> _names;
final Function(NavigationDrawerItem) onPressed;
final Gradient? _gradient;
HiddingselMenu(List<NavigationDrawerItem> items, Function(PreferredSizeWidget appBar, Widget body)? _onChange, {List<String>? names, Function(NavigationDrawerItem)? onPressed, Gradient? gradient, Key? key})
: _items = items,
onPressed = onPressed??((NavigationDrawerItem item) => Home.openPage(
item,
_onChange,
)),
_names = names??items.map((e) => e.title).toList(),
_gradient = gradient, super(key: key);
@override
Widget build(BuildContext context) => HiddingselDecoratedList(
_items.length,
(context, index) => HiddingselButton(_names[index], () => onPressed(_items[index])),
gradient: _gradient,);
}

View File

@@ -0,0 +1,123 @@
import 'package:flutter/material.dart';
import 'package:hiddingsel_app/constants/constant.dart';
class HiddingselTimePicker extends StatelessWidget {
final int _height;
final double _width;
final Duration _initTime;
final Function(Duration dateTime) _onSelected;
HiddingselTimePicker(this._initTime, this._onSelected,
{width = 300, height = 100, key})
: _width = width,
_height = height,
super(key: key);
@override
Widget build(BuildContext context) {
int days = _initTime.inDays % (365);
int hours = _initTime.inHours % 24;
int minutes = _initTime.inMinutes % 60;
var daysWheel = _buildWheel(7, days, (d) {
days = d;
_onSelected(Duration(days: days, hours: hours, minutes: minutes));
}, UITheme.theme.textTheme.bodyLarge);
var hoursWheel = _buildWheel(24, hours, (h) {
hours = h;
_onSelected(Duration(days: days, hours: hours, minutes: minutes));
}, UITheme.theme.textTheme.bodyLarge);
var minutesWheel = _buildWheel(60, minutes, (m) {
minutes = m;
_onSelected(Duration(days: days, hours: hours, minutes: minutes));
}, UITheme.theme.textTheme.bodyLarge);
return Container(
height: _height.toDouble(),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Spacer(flex: 5),
_stackWheelWithText(daysWheel, 'Tage'),
Spacer(flex: 1),
_stackWheelWithText(hoursWheel, 'Stunden'),
Spacer(flex: 1),
_stackWheelWithText(minutesWheel, 'Minuten'),
Spacer(flex: 5),
],
),
);
}
_buildWheel(int maxValue, initValue, onSelected, textStyle) {
double width = textStyle.fontSize;
FixedExtentScrollController controller =
FixedExtentScrollController(initialItem: initValue);
var valList = List<Widget>.generate(
maxValue,
(int index) => Text(index.toString(), style: textStyle),
);
bool autoScroll = false;
return NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
if (!autoScroll && scrollNotification is ScrollEndNotification) {
autoScroll = true;
int index;
if (scrollNotification.metrics.pixels % width > width / 2) {
index = scrollNotification.metrics.pixels ~/ width + 1;
} else {
index = scrollNotification.metrics.pixels ~/ width;
}
controller.jumpToItem(index);
onSelected(index % maxValue);
autoScroll = false;
return true;
}
return false;
},
child: ListWheelScrollView.useDelegate(
controller: controller,
itemExtent: width,
diameterRatio: 1,
childDelegate: ListWheelChildLoopingListDelegate(
children: valList,
),
),
);
}
_stackWheelWithText(wheel, String unit) {
return Container(
width: _width / 3,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Spacer(flex: 1),
Stack(
alignment: Alignment.center,
children: [
Container(
width: 40,
child: wheel,
),
Container(
decoration: BoxDecoration(
border: Border.all(color: UIColors.grey5, width: 1),
borderRadius: BorderRadius.circular(UIShapes.paddingSimple),
),
child: SizedBox(
width: 30,
height: UITheme.theme.textTheme.bodyLarge?.fontSize),
),
],
),
//Spacer(flex: 1),
Text(
unit,
style: UITheme.theme.textTheme.bodyLarge,
),
Spacer(flex: 1)
],
),
);
}
}