Giter Club home page Giter Club logo

timetable's Introduction

πŸ“… Customizable, animated calendar widget including day, week, and month views.

Navigation Animation
Callbacks Changing the VisibleDateRange

Available Layouts

A Timetable widget that displays multiple consecutive days.

Light Mode Dark Mode

A Timetable widget that displays multiple consecutive days without their dates and without a week indicator.

Light Mode Dark Mode

A Timetable widget that displays MonthWidgets in a page view.

Light Mode Dark Mode

Getting started

0. General Information

Timetable doesn't care about any time-zone related stuff. All supplied DateTimes must have isUtc set to true, but the actual time zone is then ignored when displaying events.

Some date/time-related parameters also have special suffixes:

  • date: A DateTime with a time of zero.
  • month: A DateTime with a time of zero and a day of one.
  • timeOfDay: A Duration between zero and 24 hours.
  • dayOfWeek: An int between one and seven (DateTime.monday through DateTime.sunday).

Timetable currently offers localizations for Chinese, English, French, German, Hungarian, Italian, Japanese, Portuguese, and Spanish. Even if you're just supporting English in your app, you have to add Timetable's localization delegate to your MaterialApp/CupertinoApp/WidgetsApp:

MaterialApp(
  localizationsDelegates: [
    TimetableLocalizationsDelegate(),
    // Other delegates, e.g., `GlobalMaterialLocalizations.delegate`
  ],
  // ...
);

You want to contribute a new localization? Awesome! Please follow the steps listed in the doc comment of TimetableLocalizationsDelegate.

1. Define your Events

Events are provided as instances of Event. To get you started, there's the subclass BasicEvent, which you can instantiate directly. If you want to be more specific, you can also implement your own class extending Event.

⚠️ Most of Timetable's classes accept a type-parameter E extends Event. Please set it to your chosen Event-subclass (e.g., BasicEvent) to avoid runtime exceptions.

In addition, you also need a Widget to display your events. When using BasicEvent, this can simply be BasicEventWidget.

2. Create a DateController (optional)

Similar to a ScrollController or a TabController, a DateController is responsible for interacting with Timetable's widgets and managing their state. As the name suggests, you can use a DateController to access the currently visible dates, and also animate or jump to different days. And by supplying a VisibleDateRange, you can also customize how many days are visible at once and whether they, e.g., snap to weeks.

final myDateController = DateController(
  // All parameters are optional and displayed with their default value.
  initialDate: DateTimeTimetable.today(),
  visibleRange: VisibleDateRange.week(startOfWeek: DateTime.monday),
);

Don't forget to dispose your controller, e.g., in State.dispose!

Here are some of the available VisibleDateRanges:

  • VisibleDateRange.days: displays visibleDayCount consecutive days, snapping to every swipeRange days (aligned to alignmentDate) in the range from minDate to maxDate
  • VisibleDateRange.week: displays and snaps to whole weeks with a customizable startOfWeek in the range from minDate to maxDate
  • VisibleDateRange.weekAligned: displays visibleDayCount consecutive days while snapping to whole weeks with a customizable firstDay in the range from minDate to maxDate – can be used, e.g., to display a five-day workweek

3. Create a TimeController (optional)

Similar to the DateController above, a TimeController is also responsible for interacting with Timetable's widgets and managing their state. More specifically, it controls the visible time range and zoom factor in a MultiDateTimetable or RecurringMultiDateTimetable. You can also programmatically change those and, e.g., animate out to reveal the full day.

final myTimeController = TimeController(
  // All parameters are optional. By default, the whole day is revealed
  // initially and you can zoom in to view just a single minute.
  minDuration: 15.minutes, // The closest you can zoom in.
  maxDuration: 23.hours, // The farthest you can zoom out.
  initialRange: TimeRange(9.hours, 17.hours),
  maxRange: TimeRange(0.hours, 24.hours),
);

This example uses some of time's extension methods on int to create a Duration more concisely.

Don't forget to dispose your controller, e.g., in State.dispose!

4. Create your Timetable widget

The configuration for Timetable's widgets is provided via inherited widgets. You can use a TimetableConfig<E> to provide all at once:

TimetableConfig<BasicEvent>(
  // Required:
  dateController: _dateController,
  timeController: _timeController,
  eventBuilder: (context, event) => BasicEventWidget(event),
  child: MultiDateTimetable<BasicEvent>(),
  // Optional:
  eventProvider: (date) => someListOfEvents,
  allDayEventBuilder: (context, event, info) =>
      BasicAllDayEventWidget(event, info: info),
  allDayOverflowBuilder: (date, overflowedEvents) => /* … */,
  callbacks: TimetableCallbacks(
    // onWeekTap, onDateTap, onDateBackgroundTap, onDateTimeBackgroundTap, and
    // onMultiDateHeaderOverflowTap
  ),
  theme: TimetableThemeData(
    context,
    // startOfWeek: DateTime.monday,
    // See the "Theming" section below for more options.
  ),
)

And you're done πŸŽ‰

Theming

Timetable already supports light and dark themes out of the box, adapting to the ambient ThemeData. You can, however, customize the styles of almost all components by providing a custom TimetableThemeData.

To apply your own theme, specify it in the TimetableConfig<E> (or directly in a TimetableTheme):

TimetableConfig<BasicEvent>(
  theme: TimetableThemeData(
    context,
    startOfWeek: DateTime.monday,
    dateDividersStyle: DateDividersStyle(
      context,
      color: Colors.blue.withOpacity(.3),
      width: 2,
    ),
    dateHeaderStyleProvider: (date) =>
        DateHeaderStyle(context, date, tooltip: 'My custom tooltip'),
    nowIndicatorStyle: NowIndicatorStyle(
      context,
      lineColor: Colors.green,
      shape: TriangleNowIndicatorShape(color: Colors.green),
    ),
    // See the "Theming" section below for more.
  ),
  // Other properties...
)

TimetableThemeData and all component styles provide two constructors each:

  • The default constructor takes a BuildContext and sometimes a day or month, using information from the ambient theme and locale to generate default values. You can still override all options via optional, named parameters.
  • The named raw constructor is usually const and has required parameters for all options.

Advanced Features

Drag and Drop

Drag and Drop demo

You can easily make events inside the content area of MultiDateTimetable or RecurringMultiDateTimetable draggable by wrapping them in a PartDayDraggableEvent:

PartDayDraggableEvent(
  // The user started dragging this event.
  onDragStart: () {},
  // The event was dragged to the given [DateTime].
  onDragUpdate: (dateTime) {},
  // The user finished dragging the event and landed on the given [DateTime].
  onDragEnd: (dateTime) {},
  child: MyEventWidget(),
  // By default, the child is displayed with a reduced opacity when it's
  // dragged. But, of course, you can customize this:
  childWhileDragging: OptionalChildWhileDragging(),
)

Timetable doesn't automatically show a moving feedback widget at the current pointer position. Instead, you can customize this and, e.g., snap event starts to multiples of 15Β minutes. Have a look at the included example app where we implemented exactly that by displaying the drag feedback as a time overlay.

If you have widgets outside of timetable that can be dragged into timetable, you have to give your MultiDateContent and each PartDayDraggableEvent a geometryKey. A geometryKey is a GlobalKey<MultiDateContentGeometry> with which the current drag offset can be converted to a DateTime.

final geometryKey = GlobalKey<MultiDateContentGeometry>();

final timetable = MultiDateTimetable(contentGeometryKey: geometryKey);
// Or `MultiDateContent(geometryKey: geometryKey)` if you build your timetable
// from the provided, modular widgets.

final draggableEvent = PartDayDraggableEvent.forGeometryKeys(
  {geometryKey},
  // `child`, `childWhileDragging`, and the callbacks are available here as
  // well.
);

// Alternatively, you can manually convert an offset to a `DateTime:`
final dateTime = geometryKey.currentState!.resolveOffset(globalOffset);

You could even offer to drag the event into one of multiple timetables: Give each timetable its own geometryKey and pass all of them to PartDayDraggableEvent.forGeometryKeys. In the callbacks, you receive the geometryKey of the timetable that the event is currently being dragged over. See PartDayDraggableEvent.geometryKeys for the exact behavior.

Time Overlays

Drag and Drop demo

In addition to displaying events, MultiDateTimetable and RecurringMultiDateTimetable can display overlays for time ranges on every day. In the screenshot above, a light gray overlay is displayed on weekdays before 8β€―a.m. and after 8β€―p.m., and over the full day for weekends. Time overlays are provided similarly to events: Just add a timeOverlayProvider to your TimetableConfig<E> (or use a DefaultTimeOverlayProvider directly).

TimetableConfig<MyEvent>(
  timeOverlayProvider: (context, date) => <TimeOverlay>[
    TimeOverlay(
      start: 0.hours,
      end: 8.hours,
      widget: ColoredBox(color: Colors.black12),
      position: TimeOverlayPosition.behindEvents, // the default, alternatively `inFrontOfEvents`
    ),
    TimeOverlay(
      start: 20.hours,
      end: 24.hours,
      widget: ColoredBox(color: Colors.black12),
    ),
  ],
  // Other properties...
)

The provider is just a function that receives a date and returns a list of TimeOverlay for that date. The example above therefore draws a light gray background before 8β€―a.m. and after 8β€―p.m. on every day.

timetable's People

Contributors

dependabot[bot] avatar gergoedu avatar jonaswanke avatar jwbot avatar masterhiei avatar paolovalerdi avatar ralaaaa avatar tatsuukraine avatar thexxturboxx avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

timetable's Issues

Customizable event stacking

A few users mentioned the need for customizable event stacking/overlap behavior (#29 (comment), #17 (comment)). I'd like to add two new properties (shown with their default values) to TimetableThemeData:

  • bool enablePartDayEventStacking = true: If set to true, intersecting events may be stacked if their start values differ by at least the following value. If set to false, intersecting events will always be shown next to each other and not overlap.
  • Period partDayEventMinimumDeltaForStacking = Period(minutes: 15): Two events whose start values differ by this value or more can be stacked by the layout algorithm. If the difference is less, they will be shown next to each other.

Change visible range dynamically

I apologize if the feature is already available, but I couldn't find a way to make it work.

I want to give the user the option between 4 visible ranges. 1, 3, 5 and week.

            IconButton(
              icon: Icon(
                Icons.more_vert,
              ),
              onPressed: () {
                showModalBottomSheet(
                  context: context,
                  builder: (context) {
                    return CustomPicker(
                      title: 'Visualização',
                      items: [
                        'Dia',
                        '3 dias',
                        '5 dias',
                        'Semana',
                      ],
                      onSelect: (index) {
                        Navigator.pop(context);
                        setState(() {
                          switch (index) {
                            case 0:
                              visibleRange = VisibleRange.days(1);
                              break;
                            case 1:
                              visibleRange = VisibleRange.days(3);
                              break;
                            case 2:
                              visibleRange = VisibleRange.days(5);
                              break;
                            case 3:
                              visibleRange = VisibleRange.week();
                              break;
                          }
                        });
                      },
                    );
                  },
                  isScrollControlled: false,
                  backgroundColor: Colors.transparent,
                );
              },
            ),

But, the grid does not update after that and keeps the first used VisibleRange (example: if when I created the controller I used VisibleRange.days(5) it always maintain a grid with 5 columns)

My controller code is as follows:

   _controller = TimetableController(
      eventProvider: EventProvider.stream(
        eventGetter: (range) => Stream.periodic(
          Duration(milliseconds: 16),
          (i) {
            final start =
                LocalDate.today().atMidnight() + Period(minutes: i * 2);
            return [
              BasicEvent(
                id: 0,
                title: 'Event',
                color: Colors.blue,
                start: start,
                end: start + Period(hours: 5),
              ),
            ];
          },
        ),
      ),
      initialTimeRange: InitialTimeRange.range(
        startTime: LocalTime(8, 0, 0),
        endTime: LocalTime(20, 0, 0),
      ),
      initialDate: initialDate,
      visibleRange: visibleRange,
      firstDayOfWeek: DayOfWeek.monday,
    );

Below are some screenshots.

Thanks in advance.
Screenshot_1606842184
Screenshot_1606842182
Screenshot_1606842179
Screenshot_1606842176
Screenshot_1606842173

How to change language/name for days

Hi,

How would you change the language/name for the days ? (I want to use the French names for the days).

I've tried many things :

  • modify the _stringRepresentations in dayoftheweek.dart
  • use intl package
  • tried the theme in TimetableThemeData

Thanks, and again great package !

Good idea

Hi, your plugin is so amazing, nice work, but i'd like to see more customization,
Can you add start view time and hide left line?
I attached screen, please check idea, thanks
image

Provide onTap function for BasicEvent type or add a BasicTouchEvent type

Hey there, I think it would be helpful if I could touch the basic widget and provide a function to handle the touch. This would allow me for example to mark events in the timetable or perform any other action while touching on the event body.

You could easily write your own Event to support that functionality how ever I think it's such a common thing that the library maybe should provide either a default event type for that (like for example BasicTouchEvent) or allow to give the BasicEvent an onTap function.

In the following I created a new event type to achieve that (basically taking the BasicEvent and just adding the onTap functionality):

class BasicTouchEvent extends Event {
  const BasicTouchEvent({
    @required Object id,
    @required this.title,
    @required this.color,
    @required this.onTap,
    @required LocalDateTime start,
    @required LocalDateTime end,
  })  : assert(title != null),
        super(id: id, start: start, end: end);

  final String title;
  final Color color;
  final VoidCallback onTap;

  @override
  bool operator ==(dynamic other) =>
      super == other && title == other.title && color == other.color;

  @override
  int get hashCode => hashList([super.hashCode, title, color]);
}

/// A simple [Widget] for displaying a [BasicEvent].
class BasicTouchWidget extends StatelessWidget {
  const BasicTouchWidget(this.event, {Key key})
      : assert(event != null),
        super(key: key);

  /// The [BasicEvent] to be displayed.
  final BasicTouchEvent event;

  @override
  Widget build(BuildContext context) {
    return Material(
      shape: RoundedRectangleBorder(
        side: BorderSide(
          color: context.theme.scaffoldBackgroundColor,
          width: 0.75,
        ),
        borderRadius: BorderRadius.circular(4),
      ),
      color: event.color,
      child: GestureDetector(
          onTap: event.onTap,
          child: Padding(
            padding: EdgeInsets.fromLTRB(4, 2, 4, 0),
            child: DefaultTextStyle(
              style: context.textTheme.bodyText2.copyWith(
                fontSize: 12,
                color: event.color.highEmphasisOnColor,
              ),
              child: Text(event.title),
            ),
          )),
    );
  }
}

From my opinion that would be a good thing to improve the out of the box functionality of the library. Might do a pull request if wished.

Cheers!

Event size doesn't fill all remaining space

Describe the bug
All events in a day column has same size if at least one event overlaps all

Steps to reproduce:

Define events with next hour ranges:
12-16
12-13
12-13
12-13
15-16

You will notice that last item (15-16) will have width equals to thinner event

Screenshots
Π‘Π½ΠΈΠΌΠΎΠΊ экрана 2020-07-11 Π² 00 45 18

Environment:

  • Device: --
  • OS version: --
  • Package version: 0.2.5

Using agenda only

Hi there,
I was wondering if it's possible to use only the agenda widget and then change the content manually via a custom date selection widget.

Edit:
After checking out the source code a little bit, what I want to use is TimetableContent and also be able disabling the horizontal scrolling for the reason I mentioned above.

Thanks!

Select weeks of a specific month only

Hi..... I want to display date range within a month only. Say display dates within 7 month of year. and when it reaches to date 31 it should either scroll back to date 1 or don't scroll at all.
Thanks in advance!

Listener when tapping the background (e.g. for creating an event)

Hello there,

any ideas on when this will be live?
Are you open for foreign contributions?

I could not find a contribution guide but I'd be happy to start working on this one since I could need it for a personal project.

Just wanted to check in whether you are basically open for foreign contributions etc. and what a foreign contributor should consider?

Would be a pity if I start working on this one and it's nearly done on your side or you don't want anyone to contribute to this project.

Interactive scrollbar for vertical scroll on Desktop

With the new version of Flutter 2, stable web apps are created. One of the most interesting things is the interactive scrollbar that can be dragged with the mouse. The idea is to add smart mouse scrolling via Scrollbar for desktop devices and use the default package scrolling for mobile devices.

A simple solution would be to recognize a desktop by screen size.

A more complex solution could be to recognize a desktop from the pointing device via MouseRegion ()

This is a possible implementation via MediaQuery() in vertical_zoom.dart:


import 'package:dartx/dartx.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import '../theme.dart';

@immutable
abstract class InitialZoom {
  const InitialZoom();

  const factory InitialZoom.zoom(double zoom) = _FactorInitialZoom;
  const factory InitialZoom.range({
    double startFraction,
    double endFraction,
  }) = _RangeInitialZoom;

  double getContentHeight(double parentHeight);
  double getOffset(double parentHeight, double contentHeight);
}

class _FactorInitialZoom extends InitialZoom {
  const _FactorInitialZoom(this.zoom)
      : assert(zoom != null),
        assert(zoom > 0);

  final double zoom;

  @override
  double getContentHeight(double parentHeight) => parentHeight * zoom;
  @override
  double getOffset(double parentHeight, double contentHeight) {
    // Center the viewport vertically.
    return (contentHeight - parentHeight) / 2;
  }
}

class _RangeInitialZoom extends InitialZoom {
  const _RangeInitialZoom({
    this.startFraction = 0,
    this.endFraction = 1,
  })  : assert(startFraction != null),
        assert(0 <= startFraction),
        assert(endFraction != null),
        assert(endFraction <= 1),
        assert(startFraction < endFraction);

  final double startFraction;
  final double endFraction;

  @override
  double getContentHeight(double parentHeight) =>
      parentHeight / (endFraction - startFraction);

  @override
  double getOffset(double parentHeight, double contentHeight) =>
      contentHeight * startFraction;
}

class VerticalZoom extends StatefulWidget {
  const VerticalZoom({
    Key key,
    this.initialZoom = const InitialZoom.zoom(1),
    @required this.child,
    this.minChildHeight = 1,
    this.maxChildHeight = double.infinity,
  })  : assert(initialZoom != null),
        assert(child != null),
        assert(minChildHeight != null),
        assert(minChildHeight > 0),
        assert(maxChildHeight != null),
        assert(maxChildHeight > 0),
        assert(minChildHeight <= maxChildHeight),
        super(key: key);

  final InitialZoom initialZoom;

  final Widget child;
  final double minChildHeight;
  final double maxChildHeight;

  @override
  _VerticalZoomState createState() => _VerticalZoomState();
}

class _VerticalZoomState extends State<VerticalZoom> {
  ScrollController _scrollController;
  // We store height i/o zoom factor so our child stays constant when we change
  // height.
  double _contentHeight;
  double _contentHeightUpdateReference;
  double _lastFocus;

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    _scrollController?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final timetableTheme = context.timetableTheme;

    return LayoutBuilder(
      builder: (context, constraints) {
        final height = constraints.maxHeight;

        _contentHeight ??= _coerceContentHeight(
          widget.initialZoom.getContentHeight(height),
          height,
          timetableTheme,
        );
        _scrollController ??= ScrollController(
          initialScrollOffset:
              widget.initialZoom.getOffset(height, _contentHeight),
        );

        return MediaQuery.of(context).size.width > 950
            ? Scrollbar(
                controller: _scrollController,
                isAlwaysShown: true,
                child: SingleChildScrollView(
                  controller: _scrollController,
                  // We handle scrolling manually to improve zoom detection.
                  child: SizedBox(
                    height: _contentHeight,
                    child: widget.child,
                  ),
                ),
              )
            : GestureDetector(
                dragStartBehavior: DragStartBehavior.down,
                onScaleStart: (details) => _onZoomStart(height, details),
                onScaleUpdate: (details) =>
                    _onZoomUpdate(height, details, timetableTheme),
                child: SingleChildScrollView(
                  // We handle scrolling manually to improve zoom detection.
                  physics: NeverScrollableScrollPhysics(),
                  controller: _scrollController,
                  child: SizedBox(
                    height: _contentHeight,
                    child: widget.child,
                  ),
                ),
              );
      },
    );
  }

  void _onZoomStart(double height, ScaleStartDetails details) {
    _contentHeightUpdateReference = _contentHeight;
    _lastFocus = _getFocus(height, details.localFocalPoint);
  }

  void _onZoomUpdate(
    double height,
    ScaleUpdateDetails details,
    TimetableThemeData theme,
  ) {
    setState(() {
      _contentHeight = _coerceContentHeight(
        details.verticalScale * _contentHeightUpdateReference,
        height,
        theme,
      );

      final scrollOffset =
          _lastFocus * _contentHeight - details.localFocalPoint.dy;
      _scrollController.jumpTo(
          scrollOffset.coerceIn(0, (_contentHeight - height).coerceAtLeast(0)));

      _lastFocus = _getFocus(height, details.localFocalPoint);
    });
  }

  double _coerceContentHeight(
    double childHeight,
    double parentHeight,
    TimetableThemeData theme,
  ) {
    return childHeight
        .coerceIn(widget.minChildHeight, widget.maxChildHeight)
        .coerceIn(
          (theme?.minimumHourZoom ?? 0) * parentHeight,
          (theme?.maximumHourZoom ?? double.infinity) * parentHeight,
        );
  }

  double _getFocus(double height, Offset focalPoint) =>
      (_scrollController.offset + focalPoint.dy) / _contentHeight;
}


The result:

2021-03-05 10-13-43_Trim (1)

animateToToday() doesn't work correctly for May 1st 2020

This is reproducible with the example app, by just removing the visibleRange parameter in the controller (so leaving it on the default 7 days).

For some reason, today animateToToday brings the week of May 4th-10th into view instead of the week of April 27th-May 3rd. This is also the default page that the timetable opens on.

Customize scrollable date range

Hey,

is it somehow possible to define the range of date for the timetable widget e.g. the timetable only shows the dates from 22th October to 24th October.

Add BoxDecoration theme to header container and hours container (left side)

Add the ability to define BoxDecoration for the header container and hours container.

It will be useful to keep the timetable up to date with any custom theme (like shadows aka Material elevation, shapes, borders and etc.) that dev is using in his App.

P.S. Sorry that I'm spamming your package with issues)) But everything that I create is something that I need in my current task)

Can't align leadingHeaderBuilder towards bottom

Hey there,

I'm currently trying to create a leadingHeaderBuilder how ever I'm struggling with formatting it correctly.
To be more precise:

image

Basically I want to add a divider to the bottom as I did for the dateHeaderBuilder how ever no matter what I do the widget I add stays at the center of the left box. Only if I add a column with multiple widgets the box get filled. I was playing a little bit around and this is my current code which should work in my opinion:

leadingHeaderBuilder: (context, date) =>  Column(
	mainAxisAlignment: MainAxisAlignment.end,
	crossAxisAlignment: CrossAxisAlignment.end,
        children: [Text("SSS"), _buildDivider()]
,),

At least that's similiar to the way I did for the dateHeaderBuilder.
I'm not sure whether this is a bug or whether I'm doing something wrong or if there is an easier way to do this?

Thanks!


Currently using 0.2.7 of timetable.
Settings of my devices

Name: Pixel_API_R
CPU/ABI: Google Play Intel Atom (x86)
Path: C:\Users.android\avd\Pixel_API_R.avd
Target: google_apis_playstore [Google Play] (API level R)
Skin: pixel_silver
SD Card: 512M
fastboot.chosenSnapshotFile:
runtime.network.speed: full
hw.accelerometer: yes
hw.device.name: pixel
hw.lcd.width: 1080
image.androidVersion.codename: R
hw.initialOrientation: Portrait
image.androidVersion.api: 29
tag.id: google_apis_playstore
hw.mainKeys: no
hw.camera.front: emulated
avd.ini.displayname: Pixel API R
hw.gpu.mode: auto
hw.ramSize: 1536
PlayStore.enabled: true
fastboot.forceColdBoot: no
hw.cpu.ncore: 3
hw.keyboard: yes
hw.sensors.proximity: yes
hw.dPad: no
hw.lcd.height: 1920
vm.heapSize: 228
skin.dynamic: yes
hw.device.manufacturer: Google
hw.gps: yes
hw.audioInput: yes
image.sysdir.1: system-images\android-R\google_apis_playstore\x86
showDeviceFrame: yes
hw.camera.back: virtualscene
AvdId: Pixel_API_R
hw.lcd.density: 420
hw.arc: false
hw.device.hash2: MD5:55acbc835978f326788ed66a5cd4c9a7
fastboot.forceChosenSnapshotBoot: no
fastboot.forceFastBoot: yes
hw.trackBall: no
hw.battery: yes
hw.sdCard: yes
tag.display: Google Play
runtime.network.latency: none
disk.dataPartition.size: 6442450944
hw.sensors.orientation: yes
avd.ini.encoding: UTF-8
hw.gpu.enabled: yes

First Date of the Week

Hi guys, been using your package and it so great, it help me achieve a certain task that needed to be done. Just want to ask if there a way to get the first date of the week in the calendar. Example, im implementing VisibleRange.week(), and initialDate is LocalDate.today() I needed to get the value of the first day of the week, like now October 3-9, 2020 is visible in my timetable so i needed to get Oct. 3, 2020 and then when I scroll horizontally to next week the timetable will now display Oct. 10 - 16, 2020 and now I needed to get the value of Oct 10. is it possible?

Visible time range, not just days

It would be great if there was a way to select not only how many days you want visible, but also which hours (e.g. 8am-8pm).

I know you can just zoom in, but I was thinking there could be a preset default range.

DateHeader overflows on different culture

Describe the bug

Upon changing the time_machine culture to Cultures.getCulture('ro'), the DateHeader overflows because the short week names in Romanian are longer than the ones in English.

image

Environment:

  • Device: Pixel 3 AVD
  • OS version: Android 10
  • Package version: 0.2.8

InitialTimeRange settings

Is there a way to start the Timetable at the current hour?
I've tried the following code:

initialTimeRange: InitialTimeRange.range(
    startTime: LocalTime.currentClockTime().subtract(Period(hours: 1)),
    endTime: LocalTime.currentClockTime().add(Period(hours: 6)),
)

And it works but if i'm testing this code at 9pm it will throw an exception 'startTime < endTime': is not true.
So, for testing purposes i've tried using this code for getting a small range to start the Timetable with:

initialTimeRange: InitialTimeRange.range(
    startTime: LocalTime.currentClockTime(),
    endTime: LocalTime.currentClockTime().add(Period(hours: 3)),
)

This code just throwed another error 'VerticalZoom.zoomMin <= 1 / (endFraction - startFraction) && 1 / (endFraction - startFraction) <= VerticalZoom.zoomMax': is not true.

This is what I basically want to achieve:
image

Thanks!

Why it would be better to get rid of time_machine

So... You use time_machine for datetime usage purpose. I see pros of usage it, but...

  1. time_machine is forbidden by its author. Many PRs with 0 replies, there is no repo activity for MONTHS (even in other projects). Since Your package relies on it, all bugs from time_machine breaks Your package also :/

  2. Dana-Ferguson/time_machine#27
    This one breaks production build for web, so time_machine is not for web in current build and Your package too...

  3. Dana-Ferguson/time_machine#34
    I didn't test it, but I'm using similar version of Flutter build and it probably breaks iOS also...

Since many people works with native types, I think better solution is to use native DateTime.
I'm ready to put my effort to get rid of it, since I really like Your package and it's better to improve current one instead forking new one.

Negative width on all-day event widget

Describe the bug

I have an all-day event with the following time interval:

start: startDate.addWeeks(1),
end: startDate.addWeeks(1).addDays(3)

with startDate = LocalDate.today().subtract(Period(days: LocalDate.today().dayOfWeek.value - 1)).atMidnight(). When scrolling through the timetable, I get the following error:

The following assertion was thrown during performLayout():
BoxConstraints has a negative minimum width.

These invalid constraints were provided to RenderPadding's layout() function by the following function, which probably computed the invalid constraints in question:
  _EventsLayout._positionEvents (package:timetable/src/header/all_day_events.dart:400:13)
The offending constraints were: BoxConstraints(w=-179.9, h=24.0; NOT NORMALIZED)

Screenshots
image

This is easy to fix, so I'll submit a PR shortly.

I can't make it work

Hello,
thanks for your package, it seems great !

However i can't make it work. Whatever i do (use it on my code, or just only try the exemple), nothing seems to work. It give me either a white screen, or a black one if i start modifying things.

Anyone had the same issue ?

Thanks for your help !

  • Language/Framework : Dart/Flutter
  • Device: Android (Simulated)
  • OS version : Android 8,1 "Oreo"
  • Package version:
    timetable: ^0.2.4
    time_machine: ^0.9.12

screenshot

TimetableController's scrollController does not update its page value

Description

Scrolling on the calendar doesn't seem to change the value of the current page. I added the following listener:

timetableController.scrollControllers.addPageChangedListener(() {
      print('Page changed to ${timetableController.scrollControllers.page}');
    });

This causes it to print a message every time I try to scroll, but the value of the page printed always remains the same.

As a side-effect of this, other listeners that depend on the page value, such as TimetableController.dateListenable and TimetableController.currentlyVisibleDatesListenable are also triggering their listeners, but their values aren't changing, and are remaining the same as the initialDate and initialTimeRange values.

Screenshots

image

Environment:

  • Device: Huawei Honor 9 Lite
  • OS version: Android 9.0.0
  • Package version: 0.2.3

Event drag & drop support

Great package!

I didn't find this feature been listed, but it's good to have drag&drop feature to the event items with the ability to define drag steps. So the event could be dragged on the time that multiples of 15 min like: 10:00, 10:15, 10:30 etc.

Or maybe provide a callback to event instance, that receives current hover date time and expects bool to be returned, and if it's FALSE - event can't be dragged over this time slot.

RenderFlex overflowed by 6.0 pixels on the bottom

Hi, I've installed the package as per the document, it works well but I'm getting a notification in the console as described below:

════════ Exception caught by rendering library ═════════════════════════════════════════════════════
A RenderFlex overflowed by 6.0 pixels on the bottom.
The relevant error-causing widget was: 
  Timetable<BasicEvent>

A screenshots below to help explain my problem

Before
image

so, in order to fix it, I've replaced the following line:

context.timetableTheme?.totalDateIndicatorHeight ?? 72,

with MediaQuery.of(context).size * 0.11 ?? 72

After
image

Environment:

  • Device: Samsung Galaxy Note 5
  • OS version: Android 9.0.0
  • Package version: 0.2.8

Support for latest flutter version

Is there a chance that we get a new version which supports the latest flutter version. Specifically it would be really helpful if you could upgrade dartx to 0.6.0. Otherwise we can't upgrade our code to flutter 2.

Using package example results in todays day name not being displayed

Hey thanks for your awesome library!

I tried to setup your example in my local project and everything works fine so far except for the fact that the name of the current day does not get displayed in the header. A screenshot will clarify things:

image

I basically just copied the code of your example folder. Is this a bug or am I doing something wrong? I also added some events but that doesn't change anything.

This is my controller:

    _controller = TimetableController(
      // A basic EventProvider containing a single event.
      eventProvider: provider,
      initialTimeRange: InitialTimeRange.range(
        startTime: LocalTime(8, 0, 0),
        endTime: LocalTime(20, 0, 0),
      ),
      initialDate: LocalDate.today(),
      visibleRange: VisibleRange.days(3),
      firstDayOfWeek: DayOfWeek.monday,
    );

This is my build:

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: Colors.white,
        appBar: AppBar(
          leading: IconButton(
            icon: Icon(Icons.arrow_back_ios, color: Colors.black),
            onPressed: () => Navigator.of(context).pop(),
          ),
          title: widget.args.roomId == null ? Text('TIMETABLE') : Text('TIMETABLE FOR ' + findRoomById(widget.args.roomId).name.toUpperCase()),
          actions: <Widget>[Padding(
              padding: EdgeInsets.only(top: 20.0),
              child: IconButton(
                icon: Icon(Icons.add),
                onPressed: () {

                },
                alignment: Alignment.center,
              ))],
          elevation: 0,
          centerTitle: true,
        ),
        body: Timetable<BasicEvent>(
          controller: _controller,
          eventBuilder: (event) => BasicEventWidget(event),
          allDayEventBuilder: (context, event, info) =>
              BasicAllDayEventWidget(event, info: info),
        ),
    );
  }

Any ideas what is going on? I might be doing something wrong. Maybe you can help me out here.

Cheers!


Currently using 0.2.1 of timetable.
Settings of my devices

Name: Pixel_API_R
CPU/ABI: Google Play Intel Atom (x86)
Path: C:\Users.android\avd\Pixel_API_R.avd
Target: google_apis_playstore [Google Play] (API level R)
Skin: pixel_silver
SD Card: 512M
fastboot.chosenSnapshotFile:
runtime.network.speed: full
hw.accelerometer: yes
hw.device.name: pixel
hw.lcd.width: 1080
image.androidVersion.codename: R
hw.initialOrientation: Portrait
image.androidVersion.api: 29
tag.id: google_apis_playstore
hw.mainKeys: no
hw.camera.front: emulated
avd.ini.displayname: Pixel API R
hw.gpu.mode: auto
hw.ramSize: 1536
PlayStore.enabled: true
fastboot.forceColdBoot: no
hw.cpu.ncore: 3
hw.keyboard: yes
hw.sensors.proximity: yes
hw.dPad: no
hw.lcd.height: 1920
vm.heapSize: 228
skin.dynamic: yes
hw.device.manufacturer: Google
hw.gps: yes
hw.audioInput: yes
image.sysdir.1: system-images\android-R\google_apis_playstore\x86
showDeviceFrame: yes
hw.camera.back: virtualscene
AvdId: Pixel_API_R
hw.lcd.density: 420
hw.arc: false
hw.device.hash2: MD5:55acbc835978f326788ed66a5cd4c9a7
fastboot.forceChosenSnapshotBoot: no
fastboot.forceFastBoot: yes
hw.trackBall: no
hw.battery: yes
hw.sdCard: yes
tag.display: Google Play
runtime.network.latency: none
disk.dataPartition.size: 6442450944
hw.sensors.orientation: yes
avd.ini.encoding: UTF-8
hw.gpu.enabled: yes

DartX incompatibility

Hi,

I'm getting this error with this package:

[src] flutter pub get
Running "flutter pub get" in src...                             
Because every version of flutter from sdk depends on characters 1.0.0 and dartx >=0.2.0 <0.4.0 depends on characters ^0.3.0, flutter from sdk is incompatible with dartx >=0.2.0 <0.4.0.

And because timetable >=0.1.2 depends on dartx ^0.3.0, flutter from sdk is incompatible with timetable >=0.1.2.

So, because Afalina depends on both flutter any from sdk and timetable ^0.2.4, version solving failed.
pub get failed (1; So, because Afalina depends on both flutter any from sdk and timetable ^0.2.4, version solving failed.)
exit code 1

Environment:

[√] Flutter (Channel master, 1.20.0-3.0.pre.126, on Microsoft Windows [Version 10.0.18363.900], locale de-DE)
    β€’ Flutter version 1.20.0-3.0.pre.126 at D:\flutter
    β€’ Framework revision 462b0ea76e (3 days ago), 2020-07-02 16:13:02 -0400
    β€’ Engine revision 51ca1432b3
    β€’ Dart version 2.9.0 (build 2.9.0-20.0.dev 8afe9875a6)

dependencies:
...
time_machine: ^0.9.12
timetable: ^0.2.4

Workaround
dependency_overrides:
dartx: ^0.4.2

Add a month indicator at the top of the calendar

As a user scrolls across the timetable, the weekday indicator is slightly unintuitive. It would be great if we could add a layer before the day of the week indicators that says "August", "September" etc.

Getting Bottom Overflow error if all day event count is large

Describe the bug
I am working on a flutter app where I need to show all day events in a timetable view. Its working for small number of events but if the event count is large then getting attached error
Screenshot_2021-02-28-16-05-29-09_6dc36f560add45efed6f283f9bcff7de

  • Device: Realme 7
  • OS version: Android 10
  • Package version: 0.2.8(Cannot use 0.2.9 as it has breaking flutter version dependency)

wrong dependencies

I am adding in my projects the timetable in my pubspec, and I got this error

Because no versions of firebase_picture_uploader match >1.2.2 <2.0.0 and firebase_picture_uploader 1.2.2 depends on image ^3.0.1, firebase_picture_uploader ^1.2.2 requires image ^3.0.1.
And because no versions of image match >3.0.1 <4.0.0, firebase_picture_uploader ^1.2.2 requires image 3.0.1.
And because image 3.0.1 depends on archive ^3.0.0 which depends on crypto ^3.0.0, firebase_picture_uploader ^1.2.2 requires crypto ^3.0.0.
And because timetable >=0.2.7 depends on dartx ^0.5.0 which depends on crypto ^2.1.0, firebase_picture_uploader ^1.2.2 is incompatible with timetable >=0.2.7.
So, because remote_students depends on both firebase_picture_uploader ^1.2.2 and timetable ^0.2.8, version solving failed.
pub get failed (1; So, because remote_students depends on both firebase_picture_uploader ^1.2.2 and timetable ^0.2.8, version solving failed.)

This is my pubspec.yaml

  firebase_picture_uploader: ^1.2.2

  timetable: ^0.2.8
  time_machine: ^0.9.16

dependency_overrides:
  time_machine:
    git:
      url: https://github.com/Dana-Ferguson/time_machine
      ref: 7753e0d

I can't use another package lower than firebase_picture_uploader ^1.2.2. Can anyone have an idea how can I solve it?

Thanks

Move position calculation to post frame callback or isolate

Describe the bug
With big amount of events, especially if each of them has some content clipping, render can lead to some frame drops.

Proposition
Move event position calculation from paint method to post framework callback or to isolate, to avoid UI block during this calculations

Controller 'animateTo' doesn't work after event update

In my web app the timetable controller looses the ability to 'animateTo' whenever there is a change in the eventlist. The timetable keeps working correctly otherwise (I can swipe through the weeks manually, updated events show correctly, onTap keeps working, etc.). I can even use the timetableController to get information about the currentstate (e.g. currently displayed week). The only thing that seems to stop working is 'AnimateTo'. If I reload the widget through "popAndPushNamed", it works again, until there is a change in a event. Not a 100% sure this is a bug or an issue with my code, but since everything else seems to work correctly I'm posting it here..

Screenshot:
image

Environment:

  • OS version:
  • Package version:

Code that displays the calendar:

import 'package:flutter/material.dart';
import 'package:flutter_timetable_backend_app/models/event.dart';
import 'package:flutter_timetable_backend_app/screens/home/calendar/src/edit_session.dart';
import 'package:flutter_timetable_backend_app/screens/home/calendar/src/week_list.dart';
import 'package:flutter_timetable_backend_app/screens/home/recurring_sessions/src/location_selection.dart';
import 'package:flutter_timetable_backend_app/services/providers.dart';
import 'package:flutter_timetable_backend_app/shared/loading.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:timetable/timetable.dart';
import 'package:time_machine/time_machine.dart';

class Calendar extends StatefulWidget {
  @override
  _CalendarState createState() => _CalendarState();
}

class _CalendarState extends State<Calendar> {
  static TimetableController<BasicEvent> calController;
  List<BasicEvent> myList = [];
  EventProvider<BasicEvent> myEventProvider;

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    calController.dispose();
    super.dispose();
  }


  @override
  Widget build(BuildContext context) {
    // create list of sessions that can be processed by Timetable EventProvider

    return Consumer(builder: (context, watch, child) {
      final boxData = watch(boxDataProvider).data == null ? null : watch(boxDataProvider).data.value;
      final userData = watch(userDataProvider).data == null ? null : watch(userDataProvider).data.value;
      final sessionEvents = watch(sessionEventsProvider).data == null ? null : watch(sessionEventsProvider).data.value;
      if (boxData == null || sessionEvents == null || userData == null) return Container(alignment: Alignment.topCenter, child: Loading());

      final DateTime monday = DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().add(Duration(days: 1-DateTime.now().weekday)).day, 0, 0);
      SessionEvent sessionEvent;

      myList = [];
      List<int> weekNumCount = [0,0,0,0];

      if (sessionEvents != null) {
        sessionEvents.forEach((session) {
          session.start.compareTo(monday.add(Duration(days: 7))) < 0 ? weekNumCount[0]++ :
            session.start.compareTo(monday.add(Duration(days: 14))) < 0 ? weekNumCount[1]++ :
              session.start.compareTo(monday.add(Duration(days: 21))) < 0 ? weekNumCount[2]++ : weekNumCount[3]++;

          myList.add(
              BasicEvent(
                id: session.id,
                title: '${session.spotsTaken}/${session.spots}',
                color: Color(session.color),
                start: LocalDate.dateTime(session.start).at(LocalTime(session.start.hour, session.start.minute, 0)),//LocalDateTime.dateTime(session.start),
                end: LocalDate.dateTime(session.end).at(LocalTime(session.end.hour, session.end.minute, 0)),//LocalDateTime.dateTime(session.end),
              )
            );
        });
      }
      myEventProvider = EventProvider.list(myList);

      calController = TimetableController(
        eventProvider: myEventProvider,
        // Optional parameters with their default values:
        initialTimeRange: InitialTimeRange.range(
          startTime: LocalTime(6, 0, 0),
          endTime: LocalTime(23, 0, 0),
        ),
        initialDate: LocalDate.today(),
        visibleRange: VisibleRange.week(),
        firstDayOfWeek: DayOfWeek.monday,
      );

      void animateToWeek(int weekNum) {
          calController.animateTo(LocalDate.today().addWeeks(weekNum));
      }

      return Container(
        height: MediaQuery.of(context).size.height-150,
        margin: EdgeInsets.only(left: 40.0, top: 40.0, bottom: 40.0, right: 40.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Calendar',
              style: TextStyle(
                fontWeight: FontWeight.bold,
                fontSize: 40.0,
              ),
            ),
            Expanded(
              child: Row(
                children: [
                  Expanded(
                    flex: 1,
                    child: Padding(
                      padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 40.0),
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.start,
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          SizedBox(height: 40.0,),
                          Text('Select location',
                            style: TextStyle(
                              fontSize: 20.0,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                          SizedBox(height: 15.0,),
                          LocationList(),
                          SizedBox(height: 40.0,),
                          Text('Select week',
                            style: TextStyle(
                              fontSize: 20.0,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                          SizedBox(height: 20.0,),
                          WeekList(weekNumCount: weekNumCount, animateToWeek: animateToWeek),
                          SizedBox(height: 30,),
                          Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: [
                            ],
                          ),
                        ],
                      ),
                    ),
                  ),
                  Container(
                    width: 500,
                    child: Padding(
                      padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 40.0),
                      child: Timetable<BasicEvent>(
                        controller: calController,
                        theme: TimetableThemeData(
                          totalDateIndicatorHeight: 74,
                          primaryColor: boxData.highlight,
                        ),
                        eventBuilder: (event) =>
                            BasicEventWidget(event,
                              onTap: () async {
                                sessionEvent = sessionEvents.firstWhere((item) => item.id == event.id);
                                EditSession.editSessionKey.currentState.showEventDetails(sessionEvent);
                              },
                            ),
                        allDayEventBuilder: (context, event, info) =>
                            BasicAllDayEventWidget(event, info: info),
                      ),
                    ),
                  ),
                  Expanded(
                    flex: 1,
                    child: Padding(
                      padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 40.0),
                      child: Container(
                        alignment: Alignment.topCenter,
                        child: EditSession(boxData: boxData, userData: userData, key: EditSession.editSessionKey),
                      ),
                    ),
                  )
                ],
              ),
            ),
          ],
        ),
      );
    });
  }
}

Child widget with the list of weeks:

import 'package:flutter_timetable_backend_app/models/event.dart';
import 'package:flutter_timetable_backend_app/services/database.dart';
import 'package:flutter/material.dart';
import 'package:flutter_timetable_backend_app/shared/loading.dart';
import 'package:flutter_timetable_backend_app/services/providers.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';

class WeekList extends StatefulWidget {

  final List<int> weekNumCount;
  final Function animateToWeek;

  WeekList({this.weekNumCount, this.animateToWeek});

  @override
  _WeekListState createState() => _WeekListState();
}

class _WeekListState extends State<WeekList> {

  int weekDisplayed;
  List<SessionEvent> sessionsToDelete;

  final int weekNum = weekNumber(DateTime.now());
  final DateTime monday = DateTime.now().add(Duration(days: 1-DateTime.now().weekday));

  @override
  void initState() {
    super.initState();
    weekDisplayed = weekNum;
  }


  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, watch, child) {
        final userData = watch(userDataProvider).data == null ? null : watch(userDataProvider).data.value;
        final recurringSessions = watch(recurringSessionsProvider).data == null ? null : watch(recurringSessionsProvider).data.value;
        final sessionEvents = watch(sessionEventsProvider).data == null ? null : watch(sessionEventsProvider).data.value;

        if (userData == null || recurringSessions == null || sessionEvents == null) {return Loading();
        } else {
          return ListView.builder(
            itemCount: 4,
            scrollDirection: Axis.vertical,
            shrinkWrap: true,
            itemBuilder: (context, index) {
              return Card(
                child: ListTile(
                  leading: Text(
                      (weekNum+index).toString(),
                    style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
                  ),
                  title: Text('Mon ${monday.add(Duration(days: 7*index)).day} - Sun ${monday.add(Duration(days: (7*index)+6)).day}'),
                  subtitle: Text('Scheduled sessions: ${widget.weekNumCount[index]}'),
                  selected: index + weekNum == weekDisplayed,
                  onTap: () {
                    setState(() {
                      weekDisplayed = weekNum + index;
                      widget.animateToWeek(index);
                    });
                  },
                  trailing: PopupMenuButton(
                    onSelected: (selectedValue) async {
                      switch (selectedValue) {
                        case 99:
                          await DatabaseService().scheduleFullWeek(userData, recurringSessions, monday.add(Duration(days: 7 * index)));
                          break;
                        case 100:
                          print('total ${sessionEvents.length}');
                          sessionsToDelete = sessionEvents.where((session) => session.start.day >= monday.add(Duration(days: 7 * index)).day).toList();
                          print('after first filter ${sessionsToDelete.length}');
                          sessionsToDelete = sessionsToDelete.where((session) => session.start.day < monday.add(Duration(days: 7 * index + 7)).day).toList();
                          print('after second filter ${sessionsToDelete.length}');
                          await DatabaseService().deleteSessionEvents(userData, sessionsToDelete);
                          break;
                        default:
                          sessionsToDelete = sessionEvents.where((session) => session.start.day == monday.add(Duration(days: 7 * index + selectedValue)).day).toList();
                          await DatabaseService().deleteSessionEvents(userData, sessionsToDelete);
                          print(sessionsToDelete.length);
                          break;
                      }
                    },
                    itemBuilder: (BuildContext ctx) => [
                      PopupMenuItem(child: Text('Schedule full week'), value: 99),
                      PopupMenuItem(child: Text('Clear all sessions'), value: 100),
                      PopupMenuItem(child: Text('Clear Monday'), value: 0),
                      PopupMenuItem(child: Text('Clear Tuesday'), value: 1),
                      PopupMenuItem(child: Text('Clear Wednesday'), value: 2),
                      PopupMenuItem(child: Text('Clear Thursday'), value: 3),
                      PopupMenuItem(child: Text('Clear Friday'), value: 4),
                      PopupMenuItem(child: Text('Clear Saturday'), value: 5),
                      PopupMenuItem(child: Text('Clear Sunday'), value: 6),
                    ]
                  ),
                ),
              );
            });
          }
        }
      );
    }
}

// Calculates number of weeks for a given year as per https://en.wikipedia.org/wiki/ISO_week_date#Weeks_per_year
int numOfWeeks(int year) {
  DateTime dec28 = DateTime(year, 12, 28);
  int dayOfDec28 = int.parse(DateFormat("D").format(dec28));
  return ((dayOfDec28 - dec28.weekday + 10) / 7).floor();
}

// Calculates week number from a date as per https://en.wikipedia.org/wiki/ISO_week_date#Calculation
int weekNumber(DateTime date) {
  int dayOfYear = int.parse(DateFormat("D").format(date));
  int woy =  ((dayOfYear - date.weekday + 10) / 7).floor();
  if (woy < 1) {
    woy = numOfWeeks(date.year - 1);
  } else if (woy > numOfWeeks(date.year)) {
    woy = 1;
  }
  return woy;
}

Add support to build custom header

Currently, day header and week indicator is static and can't be changed or removed

Suggestion
Add day header and week indicator builders to add the ability build any custom header items OR render empty Container

Why it's useful
Week indicator - can be redundant for some UIs.

Day header - dev may want to change format, styling or layout of the day header

Modular architecture with support for different layouts

I plan to revise the layout and theming possibilities of this package, mainly with the following goals in mind:

  • flexibility: Make it possible to choose between different layouts:
    • agenda (linearly list all events by start time)
    • day list: similar to agenda in that days are in an infinite vertical list, but events are positioned like they are now, so you know their lengths/etc. at a glimpse (TODO: think of a better name)
    • day: a single day displayed like multi-date below but with a modified header layout to better fit a single day
    • multi-date: like the current one
    • month: see all days in a month and the first 3 (or so) events
    • resource timeline: With time only on the horizontal axis, this layout uses the vertical axis for separating events by resources they occupy (such as rooms). The horizontal time should be scalable, probably with different resolutions (i.e., showing individual hours when viewing a single day, but only individual days when viewing a month). Similar to https://fullcalendar.io ("Resource Timeline"-Demo)
  • themeability: All layouts should have lots of themeable properties for easy customization
  • modularity: Expose the building blocks that are currently used so you can easily build your own layout if themeability isn't enough
  • animations: All of the above should implicitly animate between any changes

My idea is to implement the layouts mentioned above as separate, stateful widgets so they can e.g. manage their scroll position on their own. They will have more parameters, e.g. a customizable date header widget builder in the multi-date widget. (Currently you can change the pattern, decoration and text style of the day-of-week/-month indicators individually, but not their padding or disable one of them.

The currently used subcomponents like WeekIndicator, MultiDateBackgroundPainter, etc. should get a stable API and be exported.

Implicit animations are inspired from material's ShapeBorder in that each layout can provide a lerpTo or lerpFrom for layouts it supports animating to/from.

Feel free to add a comment if you have ideas for different layouts, wishes for exposed properties/widgets, or feedback for anything else :)

time range

I want to change the time interval from 1 hour to 30 minutes , is it possible ?
this is an example of what i'm saying

cal

Package breaks with Flutter 1.23

Describe the bug

The timetable widget fails building after upgrading from 1.22.0-12.4.pre to 1.23.0-18.1.pre, on the beta channel. This is the error:

Stack trace
══║ EXCEPTION CAUGHT BY RENDERING LIBRARY β•žβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
The following _CastError was thrown during performLayout():
Null check operator used on a null value

The relevant error-causing widget was:
  Scrollable
  file:///C:/Users/sako_/flutter/.pub-cache/hosted/pub.dartlang.org/timetable-0.2.8/lib/src/date_page_view.dart:46:12

When the exception was thrown, this was the stack:
#0      ScrollPosition.pixels (package:flutter/src/widgets/scroll_position.dart:166:31)
#1      ScrollController.offset (package:flutter/src/widgets/scroll_controller.dart:116:33)
#2      ScrollController.debugFillDescription (package:flutter/src/widgets/scroll_controller.dart:270:45)
#3      ScrollController.toString (package:flutter/src/widgets/scroll_controller.dart:247:5)
#4      _StringBase._interpolate (dart:core-patch/string_patch.dart:848:19)
#5      _LinkedScrollPosition.debugFillDescription (package:timetable/src/utils/scrolling.dart:316:36)
#6      ViewportOffset.toString (package:flutter/src/rendering/viewport_offset.dart:234:5)
#7      DiagnosticsProperty.valueToString (package:flutter/src/foundation/diagnostics.dart:2740:60)
#8      DiagnosticsProperty.toDescription (package:flutter/src/foundation/diagnostics.dart:2754:21)
#9      TextTreeRenderer._debugRender (package:flutter/src/foundation/diagnostics.dart:1193:32)
#10     TextTreeRenderer.render (package:flutter/src/foundation/diagnostics.dart:1126:14)
#11     TextTreeRenderer._debugRender (package:flutter/src/foundation/diagnostics.dart:1303:39)
#12     TextTreeRenderer.render (package:flutter/src/foundation/diagnostics.dart:1126:14)
#13     TextTreeRenderer._debugRender (package:flutter/src/foundation/diagnostics.dart:1317:39)
#14     TextTreeRenderer.render (package:flutter/src/foundation/diagnostics.dart:1126:14)
#15     FlutterError.dumpErrorToConsole (package:flutter/src/foundation/assertions.dart:961:11)
#16     TestWidgetsFlutterBinding._runTest.<anonymous closure> (package:flutter_test/src/binding.dart:662:24)
#17     FlutterError.reportError (package:flutter/src/foundation/assertions.dart:1093:15)
#18     RenderObject._debugReportException (package:flutter/src/rendering/object.dart:1311:18)
#19     RenderObject.layout (package:flutter/src/rendering/object.dart:1784:7)
#20     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#21     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#22     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#23     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#24     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#25     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#26     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#27     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#28     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#29     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#30     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#31     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#32     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#33     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#34     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#35     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#36     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#37     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#38     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#39     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#40     RenderFlex.performLayout (package:flutter/src/rendering/flex.dart:831:17)
#41     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#42     RenderConstrainedBox.performLayout (package:flutter/src/rendering/proxy_box.dart:266:14)
#43     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#44     _RenderSingleChildViewport.performLayout (package:flutter/src/widgets/single_child_scroll_view.dart:544:14)
#45     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#46     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#47     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#48     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#49     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#50     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#51     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#52     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#53     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#54     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#55     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#56     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#57     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#58     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#59     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#60     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#61     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#62     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#63     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#64     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#65     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#66     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#67     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#68     _RenderLayoutBuilder.performLayout (package:flutter/src/widgets/layout_builder.dart:353:14)
#69     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#70     RenderFlex.performLayout (package:flutter/src/rendering/flex.dart:831:17)
#71     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#72     RenderStack.performLayout (package:flutter/src/rendering/stack.dart:565:15)
#73     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#74     MultiChildLayoutDelegate.layoutChild (package:flutter/src/rendering/custom_layout.dart:171:12)
#75     _ScaffoldLayout.performLayout (package:flutter/src/material/scaffold.dart:893:7)
#76     MultiChildLayoutDelegate._callPerformLayout (package:flutter/src/rendering/custom_layout.dart:243:7)
#77     RenderCustomMultiChildLayoutBox.performLayout (package:flutter/src/rendering/custom_layout.dart:402:14)
#78     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#79     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#80     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#81     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#82     _RenderCustomClip.performLayout (package:flutter/src/rendering/proxy_box.dart:1306:11)
#83     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#84     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#85     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#86     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#87     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#88     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#89     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#90     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:111:14)
#91     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#92     RenderSliverMultiBoxAdaptor.insertAndLayoutChild (package:flutter/src/rendering/sliver_multi_box_adaptor.dart:488:13)
#93     RenderSliverFixedExtentBoxAdaptor.performLayout (package:flutter/src/rendering/sliver_fixed_extent_list.dart:242:17)
#94     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#95     RenderSliverEdgeInsetsPadding.performLayout (package:flutter/src/rendering/sliver_padding.dart:139:12)
#96     _RenderSliverFractionalPadding.performLayout (package:flutter/src/widgets/sliver_fill.dart:168:11)
#97     RenderObject.layout (package:flutter/src/rendering/object.dart:1777:7)
#98     RenderViewportBase.layoutChildSequence (package:flutter/src/rendering/viewport.dart:508:13)
#99     RenderViewport._attemptLayout (package:flutter/src/rendering/viewport.dart:1566:12)
#100    RenderViewport.performLayout (package:flutter/src/rendering/viewport.dart:1475:20)
#101    RenderObject._layoutWithoutResize (package:flutter/src/rendering/object.dart:1634:7)
#102    PipelineOwner.flushLayout (package:flutter/src/rendering/object.dart:884:18)
#103    AutomatedTestWidgetsFlutterBinding.drawFrame (package:flutter_test/src/binding.dart:1107:23)
#104    RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:309:5)
#105    SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1117:15)
#106    SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1055:9)
#107    AutomatedTestWidgetsFlutterBinding.pump.<anonymous closure> (package:flutter_test/src/binding.dart:974:9)
#110    TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:72:41)
#111    AutomatedTestWidgetsFlutterBinding.pump (package:flutter_test/src/binding.dart:961:27)
#112    WidgetTester.pumpAndSettle.<anonymous closure> (package:flutter_test/src/widget_tester.dart:640:23)
#123    FakeAsync.flushMicrotasks (package:fake_async/fake_async.dart:193:32)
#124    AutomatedTestWidgetsFlutterBinding.runTest.<anonymous closure> (package:flutter_test/src/binding.dart:1208:17)
#125    AutomatedTestWidgetsFlutterBinding.runTest.<anonymous closure> (package:flutter_test/src/binding.dart:1195:35)
(elided 27 frames from dart:async and package:stack_trace)

Environment:

  • Device: Pixel 4
  • OS version: Android 10.0
  • Package version: 0.2.8

Working hours theming

I will start with a question) If this widget doesn't provide this functionality - I will convert it to feature request)

So the question is: Is it possible to style hour cells, or maybe add some overlay to the date column to highlight time ranges that are available for selection?

For instance, some business or person works on Monday from 9am till 6pm. And he has launch time from 1:30pm till 2pm. So I want to reflect that in the time table, by adding a grey background to all day, except range 9am-1:30pm and 2pm-6pm.

Thanks for the answer.

Target kernel_snapshot failed: Exception: Errors during snapshot creation: null

Describe the bug
After running the example...

  • '_EventParentData' is from 'package:timetable/src/header/all_day_events.dart' ('../../flutter/.pub-cache/hosted/pub.dartlang.org/timetable-0.2.0/lib/src/header/all_day_events.dart').
  • 'RenderObjectWidget' is from 'package:flutter/src/widgets/framework.dart' ('../../flutter/packages/flutter/lib/src/widgets/framework.dart').
    Try changing type arguments so that they conform to the bounds.
    class _EventParentDataWidget
    ^
    ../../flutter/packages/flutter/lib/src/widgets/framework.dart:1439:33: Context: This is the type variable whose bound isn't conformed to.
    abstract class ParentDataWidget extends ProxyWidget {
    ^
    ../../flutter/.pub-cache/hosted/pub.dartlang.org/timetable-0.2.0/lib/src/basic.dart:60:36: Error: The getter 'bodyText2' isn't defined for the class 'TextTheme'.
  • 'TextTheme' is from 'package:flutter/src/material/text_theme.dart' ('../../flutter/packages/flutter/lib/src/material/text_theme.dart').
    Try correcting the name to the name of an existing getter, or defining a getter or field named 'bodyText2'.
    style: context.textTheme.bodyText2.copyWith(
    ^^^^^^^^^
    Target kernel_snapshot failed: Exception: Errors during snapshot creation: null
    Failed to build bundle.
    Error launching application on iPhone 11 Pro Max.
    Exited (sigterm)

Environment:

  • Device: iPhone 11 Pro Max
  • OS: macOS Catalina
  • Package version: timetable: ^0.2.0

New event layout algorithm

The current layout algorithm works and I like the behavior of overlapping events to save space, but there are few cases that aren't optimal. An example of that is #29 which is now fixed, but only with a suboptimal workaround.

I'd like to invest some time into designing a new algorithm that is a bit more flexible than grouping partially intersecting events and dividing them into columns. Some examples from another calendar with better layouts:

image image image

Update events displayed functions of the selected day range

Hello,
Very good job for this plugin!
Do you know how to update the events displayed functions of the visible days?
For example, I only display a day with VisibleRange.days(1) and I would like to automatically get and display events function of the selected day (when the user scrolls horizontally). Is there some callback function when page (relative to one day in my case) changed?
Thanks in advance for your reply.

Events duplicates when using EventProvider.stream

Hi i am using EventProvider.stream(eventGetter (range) => Stream.fromFuture(loadEvents(range.start))). My logic is to provide a event dates from api which i call in loadEvents() method Im using range.start to get the currently visible start date then im using it as a parameter to my api. For example today is Oct. 5 and it is set as my initial date and it is start on currentlyVisibleDates my visible range is set to weekly so I pass Oct 5 to my api to get the events, then when I scroll horizontally the start now must be Oct. 12 and the value must pass to the api was Oct. 12 my problem was when I am scrolling to the next week Im getting all other dates in between ling Oct. 6, 7, 8, 9, 10, 11, and the only thing I needed was Oct. 12. and then it became a problem for building the events see screenshot

Screenshots
Screenshot_20201004_003509_com justlearn

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.