mirror of
https://github.com/meditohq/medito-app.git
synced 2024-09-19 07:57:33 +03:00
New Folder screen and player screen
This commit is contained in:
parent
214a674feb
commit
5e83cad40c
Binary file not shown.
Before Width: | Height: | Size: 167 KiB |
@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:Medito/utils/utils.dart';
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
@ -68,7 +69,7 @@ class MeditoAudioHandler extends BaseAudioHandler
|
||||
@override
|
||||
Future<void> play() async {
|
||||
try {
|
||||
if (_currentlyPlayingBGSound.isNotEmptyAndNotNull()) {
|
||||
if (_currentlyPlayingBGSound.isNotEmptyAndNotNull() && mediaItemHasBGSound()) {
|
||||
unawaited(_bgPlayer.play());
|
||||
unawaited(_bgPlayer.setLoopMode(LoopMode.all));
|
||||
}
|
||||
@ -203,7 +204,26 @@ class MeditoAudioHandler extends BaseAudioHandler
|
||||
Future<void> _setBgVolumeFadeAtEnd(int timeLeft) async {
|
||||
print(_bgPlayer.volume - (_bgVolume / FADE_DURATION));
|
||||
if (_bgPlayer.volume > 0) {
|
||||
unawaited(_bgPlayer.setVolume(_bgPlayer.volume - (_bgVolume / FADE_DURATION)));
|
||||
unawaited(
|
||||
_bgPlayer.setVolume(_bgPlayer.volume - (_bgVolume / FADE_DURATION)));
|
||||
}
|
||||
}
|
||||
|
||||
void skipForward30Secs() {
|
||||
var seekDuration = min(_duration?.inMilliseconds ?? 0,
|
||||
_player.position.inMilliseconds + Duration(seconds: 30).inMilliseconds);
|
||||
_player.seek(Duration(milliseconds: seekDuration));
|
||||
}
|
||||
|
||||
void skipBackward10Secs() {
|
||||
var seekDuration = max(0,
|
||||
_player.position.inMilliseconds - Duration(seconds: 10).inMilliseconds);
|
||||
_player.seek(Duration(milliseconds: seekDuration));
|
||||
}
|
||||
|
||||
void setPlayerSpeed(double speed) => _player.setSpeed(speed);
|
||||
|
||||
bool mediaItemHasBGSound() => mediaItem.value?.extras?[HAS_BG_SOUND];
|
||||
|
||||
}
|
||||
|
||||
|
@ -24,12 +24,12 @@ import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'audioplayer/audio_inherited_widget.dart';
|
||||
import 'network/auth.dart';
|
||||
import 'utils/colors.dart';
|
||||
|
||||
late SharedPreferences sharedPreferences;
|
||||
|
||||
@ -62,7 +62,12 @@ Future<void> main() async {
|
||||
}
|
||||
}
|
||||
|
||||
void _runApp(MeditoAudioHandler _audioHandler) => runApp(AudioHandlerInheritedWidget(audioHandler: _audioHandler, child: ParentWidget()));
|
||||
void _runApp(MeditoAudioHandler _audioHandler) => runApp(ProviderScope(
|
||||
child: AudioHandlerInheritedWidget(
|
||||
audioHandler: _audioHandler,
|
||||
child: ParentWidget(),
|
||||
),
|
||||
));
|
||||
|
||||
/// This Widget is the main application widget.
|
||||
class ParentWidget extends StatefulWidget {
|
||||
|
@ -26,6 +26,7 @@ class FolderItemsRepository {
|
||||
try {
|
||||
final response = await httpGet(BASE_URL + ext + id + folderParameters,
|
||||
fileNameForCache: BASE_URL + id + '/' + ext, skipCache: skipCache);
|
||||
if(response == null) return null;
|
||||
return FolderResponse.fromJson(response);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
|
41
lib/network/folder/folder_provider.dart
Normal file
41
lib/network/folder/folder_provider.dart
Normal file
@ -0,0 +1,41 @@
|
||||
import 'package:Medito/network/http_get.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../auth.dart';
|
||||
import 'new_folder_response.dart';
|
||||
|
||||
var folderParameters =
|
||||
'?fields=*,items.item:folders.id,items.item:folders.type,items.item:folders.title,items.item:folders.subtitle,items.item:sessions.id,items.item:sessions.type,items.item:sessions.title,items.item:sessions.subtitle,items.item:sessions.old_id,items.item:dailies.id,items.item:dailies.type,items.item:dailies.title,items.item:dailies.subtitle,items.item:articles.id,items.item:articles.type,items.item:articles.title,items.item:articles.subtitle&deep[items][_sort]=position';
|
||||
var ext = 'items/folders/';
|
||||
|
||||
final folderProvider =
|
||||
FutureProvider.family<NewFolderResponse?, String?>((ref, id) async {
|
||||
if (id == null) {
|
||||
throw Exception('Id is null');
|
||||
}
|
||||
final skipCache = ref.watch(folderSkipCacheProvider);
|
||||
final content = await _httpGet(id, skipCache);
|
||||
|
||||
if(content == null) return null;
|
||||
return NewFolderResponse.fromJson(content);
|
||||
});
|
||||
|
||||
final folderProviderSkipCache =
|
||||
FutureProvider.family<NewFolderResponse?, String>((ref, id) async {
|
||||
final content = await _httpGet(id, true);
|
||||
|
||||
if(content == null) return null;
|
||||
|
||||
return NewFolderResponse.fromJson(content);
|
||||
});
|
||||
|
||||
Future<Map<String,Object?>?> _httpGet(String id, bool skipCache) {
|
||||
return httpGet(
|
||||
BASE_URL + ext + id + folderParameters,
|
||||
fileNameForCache: BASE_URL + id + '/' + ext,
|
||||
skipCache: skipCache,
|
||||
);
|
||||
}
|
||||
|
||||
final folderSkipCacheProvider = StateProvider<bool>((ref) => false);
|
45
lib/network/folder/new_folder_response.dart
Normal file
45
lib/network/folder/new_folder_response.dart
Normal file
@ -0,0 +1,45 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'new_folder_response.freezed.dart';
|
||||
part 'new_folder_response.g.dart';
|
||||
|
||||
@freezed
|
||||
class NewFolderResponse with _$NewFolderResponse {
|
||||
const factory NewFolderResponse({
|
||||
required Data? data,
|
||||
}) = _NewFolderResponse;
|
||||
|
||||
factory NewFolderResponse.fromJson(Map<String, Object?> json) =>
|
||||
_$NewFolderResponseFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Data with _$Data {
|
||||
const factory Data(
|
||||
{int? id,
|
||||
String? title,
|
||||
String? subtitle,
|
||||
String? description,
|
||||
List<FolderItem>? items}) = _Data;
|
||||
|
||||
factory Data.fromJson(Map<String, Object?> json) => _$DataFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class FolderItem with _$FolderItem {
|
||||
const factory FolderItem({
|
||||
Item? item,
|
||||
}) = _FolderItem;
|
||||
|
||||
factory FolderItem.fromJson(Map<String, Object?> json) =>
|
||||
_$FolderItemFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Item with _$Item {
|
||||
const factory Item({int? id, String? type, String? title, String? subtitle}) =
|
||||
_Item;
|
||||
|
||||
factory Item.fromJson(Map<String, Object?> json) => _$ItemFromJson(json);
|
||||
}
|
765
lib/network/folder/new_folder_response.freezed.dart
Normal file
765
lib/network/folder/new_folder_response.freezed.dart
Normal file
@ -0,0 +1,765 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
|
||||
|
||||
part of 'new_folder_response.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
|
||||
NewFolderResponse _$NewFolderResponseFromJson(Map<String, dynamic> json) {
|
||||
return _NewFolderResponse.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$NewFolderResponse {
|
||||
Data? get data => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$NewFolderResponseCopyWith<NewFolderResponse> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $NewFolderResponseCopyWith<$Res> {
|
||||
factory $NewFolderResponseCopyWith(
|
||||
NewFolderResponse value, $Res Function(NewFolderResponse) then) =
|
||||
_$NewFolderResponseCopyWithImpl<$Res, NewFolderResponse>;
|
||||
@useResult
|
||||
$Res call({Data? data});
|
||||
|
||||
$DataCopyWith<$Res>? get data;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$NewFolderResponseCopyWithImpl<$Res, $Val extends NewFolderResponse>
|
||||
implements $NewFolderResponseCopyWith<$Res> {
|
||||
_$NewFolderResponseCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? data = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
data: freezed == data
|
||||
? _value.data
|
||||
: data // ignore: cast_nullable_to_non_nullable
|
||||
as Data?,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$DataCopyWith<$Res>? get data {
|
||||
if (_value.data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $DataCopyWith<$Res>(_value.data!, (value) {
|
||||
return _then(_value.copyWith(data: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$_NewFolderResponseCopyWith<$Res>
|
||||
implements $NewFolderResponseCopyWith<$Res> {
|
||||
factory _$$_NewFolderResponseCopyWith(_$_NewFolderResponse value,
|
||||
$Res Function(_$_NewFolderResponse) then) =
|
||||
__$$_NewFolderResponseCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({Data? data});
|
||||
|
||||
@override
|
||||
$DataCopyWith<$Res>? get data;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$_NewFolderResponseCopyWithImpl<$Res>
|
||||
extends _$NewFolderResponseCopyWithImpl<$Res, _$_NewFolderResponse>
|
||||
implements _$$_NewFolderResponseCopyWith<$Res> {
|
||||
__$$_NewFolderResponseCopyWithImpl(
|
||||
_$_NewFolderResponse _value, $Res Function(_$_NewFolderResponse) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? data = freezed,
|
||||
}) {
|
||||
return _then(_$_NewFolderResponse(
|
||||
data: freezed == data
|
||||
? _value.data
|
||||
: data // ignore: cast_nullable_to_non_nullable
|
||||
as Data?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$_NewFolderResponse
|
||||
with DiagnosticableTreeMixin
|
||||
implements _NewFolderResponse {
|
||||
const _$_NewFolderResponse({required this.data});
|
||||
|
||||
factory _$_NewFolderResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$$_NewFolderResponseFromJson(json);
|
||||
|
||||
@override
|
||||
final Data? data;
|
||||
|
||||
@override
|
||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||
return 'NewFolderResponse(data: $data)';
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'NewFolderResponse'))
|
||||
..add(DiagnosticsProperty('data', data));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$_NewFolderResponse &&
|
||||
(identical(other.data, data) || other.data == data));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, data);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$_NewFolderResponseCopyWith<_$_NewFolderResponse> get copyWith =>
|
||||
__$$_NewFolderResponseCopyWithImpl<_$_NewFolderResponse>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$_NewFolderResponseToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _NewFolderResponse implements NewFolderResponse {
|
||||
const factory _NewFolderResponse({required final Data? data}) =
|
||||
_$_NewFolderResponse;
|
||||
|
||||
factory _NewFolderResponse.fromJson(Map<String, dynamic> json) =
|
||||
_$_NewFolderResponse.fromJson;
|
||||
|
||||
@override
|
||||
Data? get data;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$_NewFolderResponseCopyWith<_$_NewFolderResponse> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
Data _$DataFromJson(Map<String, dynamic> json) {
|
||||
return _Data.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$Data {
|
||||
int? get id => throw _privateConstructorUsedError;
|
||||
String? get title => throw _privateConstructorUsedError;
|
||||
String? get subtitle => throw _privateConstructorUsedError;
|
||||
String? get description => throw _privateConstructorUsedError;
|
||||
List<FolderItem>? get items => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$DataCopyWith<Data> get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $DataCopyWith<$Res> {
|
||||
factory $DataCopyWith(Data value, $Res Function(Data) then) =
|
||||
_$DataCopyWithImpl<$Res, Data>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int? id,
|
||||
String? title,
|
||||
String? subtitle,
|
||||
String? description,
|
||||
List<FolderItem>? items});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$DataCopyWithImpl<$Res, $Val extends Data>
|
||||
implements $DataCopyWith<$Res> {
|
||||
_$DataCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = freezed,
|
||||
Object? title = freezed,
|
||||
Object? subtitle = freezed,
|
||||
Object? description = freezed,
|
||||
Object? items = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: freezed == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
title: freezed == title
|
||||
? _value.title
|
||||
: title // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
subtitle: freezed == subtitle
|
||||
? _value.subtitle
|
||||
: subtitle // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
description: freezed == description
|
||||
? _value.description
|
||||
: description // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
items: freezed == items
|
||||
? _value.items
|
||||
: items // ignore: cast_nullable_to_non_nullable
|
||||
as List<FolderItem>?,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$_DataCopyWith<$Res> implements $DataCopyWith<$Res> {
|
||||
factory _$$_DataCopyWith(_$_Data value, $Res Function(_$_Data) then) =
|
||||
__$$_DataCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int? id,
|
||||
String? title,
|
||||
String? subtitle,
|
||||
String? description,
|
||||
List<FolderItem>? items});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$_DataCopyWithImpl<$Res> extends _$DataCopyWithImpl<$Res, _$_Data>
|
||||
implements _$$_DataCopyWith<$Res> {
|
||||
__$$_DataCopyWithImpl(_$_Data _value, $Res Function(_$_Data) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = freezed,
|
||||
Object? title = freezed,
|
||||
Object? subtitle = freezed,
|
||||
Object? description = freezed,
|
||||
Object? items = freezed,
|
||||
}) {
|
||||
return _then(_$_Data(
|
||||
id: freezed == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
title: freezed == title
|
||||
? _value.title
|
||||
: title // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
subtitle: freezed == subtitle
|
||||
? _value.subtitle
|
||||
: subtitle // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
description: freezed == description
|
||||
? _value.description
|
||||
: description // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
items: freezed == items
|
||||
? _value._items
|
||||
: items // ignore: cast_nullable_to_non_nullable
|
||||
as List<FolderItem>?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$_Data with DiagnosticableTreeMixin implements _Data {
|
||||
const _$_Data(
|
||||
{this.id,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.description,
|
||||
final List<FolderItem>? items})
|
||||
: _items = items;
|
||||
|
||||
factory _$_Data.fromJson(Map<String, dynamic> json) => _$$_DataFromJson(json);
|
||||
|
||||
@override
|
||||
final int? id;
|
||||
@override
|
||||
final String? title;
|
||||
@override
|
||||
final String? subtitle;
|
||||
@override
|
||||
final String? description;
|
||||
final List<FolderItem>? _items;
|
||||
@override
|
||||
List<FolderItem>? get items {
|
||||
final value = _items;
|
||||
if (value == null) return null;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||
return 'Data(id: $id, title: $title, subtitle: $subtitle, description: $description, items: $items)';
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'Data'))
|
||||
..add(DiagnosticsProperty('id', id))
|
||||
..add(DiagnosticsProperty('title', title))
|
||||
..add(DiagnosticsProperty('subtitle', subtitle))
|
||||
..add(DiagnosticsProperty('description', description))
|
||||
..add(DiagnosticsProperty('items', items));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$_Data &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.title, title) || other.title == title) &&
|
||||
(identical(other.subtitle, subtitle) ||
|
||||
other.subtitle == subtitle) &&
|
||||
(identical(other.description, description) ||
|
||||
other.description == description) &&
|
||||
const DeepCollectionEquality().equals(other._items, _items));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id, title, subtitle, description,
|
||||
const DeepCollectionEquality().hash(_items));
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$_DataCopyWith<_$_Data> get copyWith =>
|
||||
__$$_DataCopyWithImpl<_$_Data>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$_DataToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _Data implements Data {
|
||||
const factory _Data(
|
||||
{final int? id,
|
||||
final String? title,
|
||||
final String? subtitle,
|
||||
final String? description,
|
||||
final List<FolderItem>? items}) = _$_Data;
|
||||
|
||||
factory _Data.fromJson(Map<String, dynamic> json) = _$_Data.fromJson;
|
||||
|
||||
@override
|
||||
int? get id;
|
||||
@override
|
||||
String? get title;
|
||||
@override
|
||||
String? get subtitle;
|
||||
@override
|
||||
String? get description;
|
||||
@override
|
||||
List<FolderItem>? get items;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$_DataCopyWith<_$_Data> get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
FolderItem _$FolderItemFromJson(Map<String, dynamic> json) {
|
||||
return _FolderItem.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$FolderItem {
|
||||
Item? get item => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$FolderItemCopyWith<FolderItem> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $FolderItemCopyWith<$Res> {
|
||||
factory $FolderItemCopyWith(
|
||||
FolderItem value, $Res Function(FolderItem) then) =
|
||||
_$FolderItemCopyWithImpl<$Res, FolderItem>;
|
||||
@useResult
|
||||
$Res call({Item? item});
|
||||
|
||||
$ItemCopyWith<$Res>? get item;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$FolderItemCopyWithImpl<$Res, $Val extends FolderItem>
|
||||
implements $FolderItemCopyWith<$Res> {
|
||||
_$FolderItemCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? item = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
item: freezed == item
|
||||
? _value.item
|
||||
: item // ignore: cast_nullable_to_non_nullable
|
||||
as Item?,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$ItemCopyWith<$Res>? get item {
|
||||
if (_value.item == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $ItemCopyWith<$Res>(_value.item!, (value) {
|
||||
return _then(_value.copyWith(item: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$_FolderItemCopyWith<$Res>
|
||||
implements $FolderItemCopyWith<$Res> {
|
||||
factory _$$_FolderItemCopyWith(
|
||||
_$_FolderItem value, $Res Function(_$_FolderItem) then) =
|
||||
__$$_FolderItemCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({Item? item});
|
||||
|
||||
@override
|
||||
$ItemCopyWith<$Res>? get item;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$_FolderItemCopyWithImpl<$Res>
|
||||
extends _$FolderItemCopyWithImpl<$Res, _$_FolderItem>
|
||||
implements _$$_FolderItemCopyWith<$Res> {
|
||||
__$$_FolderItemCopyWithImpl(
|
||||
_$_FolderItem _value, $Res Function(_$_FolderItem) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? item = freezed,
|
||||
}) {
|
||||
return _then(_$_FolderItem(
|
||||
item: freezed == item
|
||||
? _value.item
|
||||
: item // ignore: cast_nullable_to_non_nullable
|
||||
as Item?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$_FolderItem with DiagnosticableTreeMixin implements _FolderItem {
|
||||
const _$_FolderItem({this.item});
|
||||
|
||||
factory _$_FolderItem.fromJson(Map<String, dynamic> json) =>
|
||||
_$$_FolderItemFromJson(json);
|
||||
|
||||
@override
|
||||
final Item? item;
|
||||
|
||||
@override
|
||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||
return 'FolderItem(item: $item)';
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'FolderItem'))
|
||||
..add(DiagnosticsProperty('item', item));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$_FolderItem &&
|
||||
(identical(other.item, item) || other.item == item));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, item);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$_FolderItemCopyWith<_$_FolderItem> get copyWith =>
|
||||
__$$_FolderItemCopyWithImpl<_$_FolderItem>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$_FolderItemToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _FolderItem implements FolderItem {
|
||||
const factory _FolderItem({final Item? item}) = _$_FolderItem;
|
||||
|
||||
factory _FolderItem.fromJson(Map<String, dynamic> json) =
|
||||
_$_FolderItem.fromJson;
|
||||
|
||||
@override
|
||||
Item? get item;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$_FolderItemCopyWith<_$_FolderItem> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
Item _$ItemFromJson(Map<String, dynamic> json) {
|
||||
return _Item.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$Item {
|
||||
int? get id => throw _privateConstructorUsedError;
|
||||
String? get type => throw _privateConstructorUsedError;
|
||||
String? get title => throw _privateConstructorUsedError;
|
||||
String? get subtitle => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$ItemCopyWith<Item> get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $ItemCopyWith<$Res> {
|
||||
factory $ItemCopyWith(Item value, $Res Function(Item) then) =
|
||||
_$ItemCopyWithImpl<$Res, Item>;
|
||||
@useResult
|
||||
$Res call({int? id, String? type, String? title, String? subtitle});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$ItemCopyWithImpl<$Res, $Val extends Item>
|
||||
implements $ItemCopyWith<$Res> {
|
||||
_$ItemCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = freezed,
|
||||
Object? type = freezed,
|
||||
Object? title = freezed,
|
||||
Object? subtitle = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: freezed == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
type: freezed == type
|
||||
? _value.type
|
||||
: type // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
title: freezed == title
|
||||
? _value.title
|
||||
: title // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
subtitle: freezed == subtitle
|
||||
? _value.subtitle
|
||||
: subtitle // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$_ItemCopyWith<$Res> implements $ItemCopyWith<$Res> {
|
||||
factory _$$_ItemCopyWith(_$_Item value, $Res Function(_$_Item) then) =
|
||||
__$$_ItemCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({int? id, String? type, String? title, String? subtitle});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$_ItemCopyWithImpl<$Res> extends _$ItemCopyWithImpl<$Res, _$_Item>
|
||||
implements _$$_ItemCopyWith<$Res> {
|
||||
__$$_ItemCopyWithImpl(_$_Item _value, $Res Function(_$_Item) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = freezed,
|
||||
Object? type = freezed,
|
||||
Object? title = freezed,
|
||||
Object? subtitle = freezed,
|
||||
}) {
|
||||
return _then(_$_Item(
|
||||
id: freezed == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
type: freezed == type
|
||||
? _value.type
|
||||
: type // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
title: freezed == title
|
||||
? _value.title
|
||||
: title // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
subtitle: freezed == subtitle
|
||||
? _value.subtitle
|
||||
: subtitle // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$_Item with DiagnosticableTreeMixin implements _Item {
|
||||
const _$_Item({this.id, this.type, this.title, this.subtitle});
|
||||
|
||||
factory _$_Item.fromJson(Map<String, dynamic> json) => _$$_ItemFromJson(json);
|
||||
|
||||
@override
|
||||
final int? id;
|
||||
@override
|
||||
final String? type;
|
||||
@override
|
||||
final String? title;
|
||||
@override
|
||||
final String? subtitle;
|
||||
|
||||
@override
|
||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||
return 'Item(id: $id, type: $type, title: $title, subtitle: $subtitle)';
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'Item'))
|
||||
..add(DiagnosticsProperty('id', id))
|
||||
..add(DiagnosticsProperty('type', type))
|
||||
..add(DiagnosticsProperty('title', title))
|
||||
..add(DiagnosticsProperty('subtitle', subtitle));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$_Item &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.type, type) || other.type == type) &&
|
||||
(identical(other.title, title) || other.title == title) &&
|
||||
(identical(other.subtitle, subtitle) ||
|
||||
other.subtitle == subtitle));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id, type, title, subtitle);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$_ItemCopyWith<_$_Item> get copyWith =>
|
||||
__$$_ItemCopyWithImpl<_$_Item>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$_ItemToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _Item implements Item {
|
||||
const factory _Item(
|
||||
{final int? id,
|
||||
final String? type,
|
||||
final String? title,
|
||||
final String? subtitle}) = _$_Item;
|
||||
|
||||
factory _Item.fromJson(Map<String, dynamic> json) = _$_Item.fromJson;
|
||||
|
||||
@override
|
||||
int? get id;
|
||||
@override
|
||||
String? get type;
|
||||
@override
|
||||
String? get title;
|
||||
@override
|
||||
String? get subtitle;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$_ItemCopyWith<_$_Item> get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
64
lib/network/folder/new_folder_response.g.dart
Normal file
64
lib/network/folder/new_folder_response.g.dart
Normal file
@ -0,0 +1,64 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'new_folder_response.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$_NewFolderResponse _$$_NewFolderResponseFromJson(Map<String, dynamic> json) =>
|
||||
_$_NewFolderResponse(
|
||||
data: json['data'] == null
|
||||
? null
|
||||
: Data.fromJson(json['data'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_NewFolderResponseToJson(
|
||||
_$_NewFolderResponse instance) =>
|
||||
<String, dynamic>{
|
||||
'data': instance.data,
|
||||
};
|
||||
|
||||
_$_Data _$$_DataFromJson(Map<String, dynamic> json) => _$_Data(
|
||||
id: json['id'] as int?,
|
||||
title: json['title'] as String?,
|
||||
subtitle: json['subtitle'] as String?,
|
||||
description: json['description'] as String?,
|
||||
items: (json['items'] as List<dynamic>?)
|
||||
?.map((e) => FolderItem.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_DataToJson(_$_Data instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'title': instance.title,
|
||||
'subtitle': instance.subtitle,
|
||||
'description': instance.description,
|
||||
'items': instance.items,
|
||||
};
|
||||
|
||||
_$_FolderItem _$$_FolderItemFromJson(Map<String, dynamic> json) =>
|
||||
_$_FolderItem(
|
||||
item: json['item'] == null
|
||||
? null
|
||||
: Item.fromJson(json['item'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_FolderItemToJson(_$_FolderItem instance) =>
|
||||
<String, dynamic>{
|
||||
'item': instance.item,
|
||||
};
|
||||
|
||||
_$_Item _$$_ItemFromJson(Map<String, dynamic> json) => _$_Item(
|
||||
id: json['id'] as int?,
|
||||
type: json['type'] as String?,
|
||||
title: json['title'] as String?,
|
||||
subtitle: json['subtitle'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_ItemToJson(_$_Item instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'type': instance.type,
|
||||
'title': instance.title,
|
||||
'subtitle': instance.subtitle,
|
||||
};
|
205
lib/network/folder/new_folder_screen.dart
Normal file
205
lib/network/folder/new_folder_screen.dart
Normal file
@ -0,0 +1,205 @@
|
||||
import 'package:Medito/network/folder/new_folder_response.dart';
|
||||
import 'package:Medito/utils/navigation_extra.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../utils/strings.dart';
|
||||
import '../../utils/utils.dart';
|
||||
import 'folder_provider.dart';
|
||||
|
||||
class NewFolderScreen extends ConsumerWidget {
|
||||
const NewFolderScreen({Key? key, required this.id}) : super(key: key);
|
||||
|
||||
final String? id;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
var value = ref.watch(folderProvider(id));
|
||||
return value.when(
|
||||
data: (data) => buildScaffoldWithData(data, ref),
|
||||
error: (err, stack) => Text(err.toString()),
|
||||
loading: () => _buildLoadingWidget());
|
||||
}
|
||||
|
||||
Widget _buildLoadingWidget() =>
|
||||
const Center(child: CircularProgressIndicator());
|
||||
|
||||
Scaffold buildScaffoldWithData(NewFolderResponse? folder, WidgetRef ref) {
|
||||
return Scaffold(
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
ref.read(folderSkipCacheProvider.notifier).state = true;
|
||||
},
|
||||
child: ListView(
|
||||
children: [
|
||||
HeaderWidget(folder?.data?.title, folder?.data?.description, ''),
|
||||
ListView.builder(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemCount: folder?.data?.items?.length ?? 0,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
var title = folder?.data?.items?[i].item?.title;
|
||||
var subtitle = folder?.data?.items?[i].item?.subtitle;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => _onListItemTap(folder?.data?.items?[i].item?.id,
|
||||
folder?.data?.items?[i].item?.type, ref.context),
|
||||
child: _buildListTile(title, subtitle, true),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListTile(String? title, String? subtitle, bool showIcon) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (title != null) Text(title),
|
||||
if (subtitle != null)
|
||||
Column(children: [
|
||||
Container(height: 8),
|
||||
Text(subtitle),
|
||||
])
|
||||
],
|
||||
),
|
||||
showIcon ? Icon(_getIcon(), color: Colors.white) : Container()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onListItemTap(int? id, String? type, BuildContext context) {
|
||||
checkConnectivity().then((value) {
|
||||
if (value) {
|
||||
var location = GoRouter.of(context).location;
|
||||
if (type == 'folder') {
|
||||
if (location.contains('folder2')) {
|
||||
context.go(getPathFromString(Folder3Path,
|
||||
[location.split('/')[2], this.id, id.toString()]));
|
||||
} else {
|
||||
context.go(
|
||||
getPathFromString(Folder2Path, [this.id, id.toString()]));
|
||||
}
|
||||
} else {
|
||||
context.go(location + getPathFromString(type, [id.toString()]));
|
||||
}
|
||||
} else {
|
||||
createSnackBar(CHECK_CONNECTION, context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
IconData _getIcon() {
|
||||
return Icons.check_circle_outline_sharp;
|
||||
// return Icons.article_outlined;
|
||||
// return Icons.arrow_forward_ios_sharp;
|
||||
}
|
||||
}
|
||||
|
||||
class HeaderWidget extends StatelessWidget {
|
||||
const HeaderWidget(this.title, this.description, this.imageUrl, {Key? key})
|
||||
: super(key: key);
|
||||
|
||||
final title;
|
||||
final description;
|
||||
final imageUrl;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 1.3,
|
||||
child: Stack(
|
||||
children: [
|
||||
buildHeroImage(imageUrl),
|
||||
_buildMeditoRoundCloseButton(),
|
||||
_buildTitleText(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (description != null && description != '') _buildDescription(context)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDescription(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.grey.shade900,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
description ?? '',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: Colors.white),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMeditoRoundCloseButton() {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 20.0),
|
||||
child: MeditoRoundCloseButton(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTitleText(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 20.0,
|
||||
right: 20.0,
|
||||
bottom: 12.0,
|
||||
),
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).primaryTextTheme.headline4,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Image buildHeroImage(String imageUrl) {
|
||||
return Image.asset(
|
||||
'assets/images/dalle.png',
|
||||
fit: BoxFit.fill,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MeditoRoundCloseButton extends StatelessWidget {
|
||||
const MeditoRoundCloseButton({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialButton(
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onPressed: () {
|
||||
router.pop();
|
||||
},
|
||||
color: Colors.black38,
|
||||
padding: EdgeInsets.all(16),
|
||||
shape: CircleBorder(),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -20,8 +20,9 @@ import 'package:Medito/network/http_get.dart';
|
||||
class CoursesRepo {
|
||||
final _ext = 'items/courses';
|
||||
|
||||
Future<CoursesResponse> fetchCourses([bool skipCache = false]) async {
|
||||
Future<CoursesResponse?> fetchCourses([bool skipCache = false]) async {
|
||||
final response = await httpGet(BASE_URL + _ext, skipCache: skipCache);
|
||||
if(response == null) return null;
|
||||
return CoursesResponse.fromJson(response);
|
||||
}
|
||||
}
|
||||
|
@ -20,8 +20,9 @@ import 'package:Medito/network/http_get.dart';
|
||||
class DailyMessageRepo {
|
||||
final _ext = 'items/daily_message';
|
||||
|
||||
Future<DailyMessageResponse>? getMessage([bool skipCache = false]) async {
|
||||
Future<DailyMessageResponse?>? getMessage([bool skipCache = false]) async {
|
||||
final response = await httpGet(BASE_URL + _ext, skipCache: skipCache);
|
||||
if(response == null) return null;
|
||||
return DailyMessageResponse.fromJson(response);
|
||||
}
|
||||
}
|
||||
|
@ -13,15 +13,16 @@ Affero GNU General Public License for more details.
|
||||
You should have received a copy of the Affero GNU General Public License
|
||||
along with Medito App. If not, see <https://www.gnu.org/licenses/>.*/
|
||||
|
||||
import 'package:Medito/network/home/menu_response.dart';
|
||||
import 'package:Medito/network/auth.dart';
|
||||
import 'package:Medito/network/home/menu_response.dart';
|
||||
import 'package:Medito/network/http_get.dart';
|
||||
|
||||
class HomeRepo {
|
||||
final _ext = 'items/menu';
|
||||
|
||||
Future<MenuResponse> fetchMenu(bool skipCache) async {
|
||||
Future<MenuResponse?> fetchMenu(bool skipCache) async {
|
||||
final response = await httpGet(BASE_URL + _ext, skipCache: skipCache);
|
||||
if (response == null) return null;
|
||||
return MenuResponse.fromJson(response);
|
||||
}
|
||||
}
|
||||
|
@ -13,15 +13,16 @@ Affero GNU General Public License for more details.
|
||||
You should have received a copy of the Affero GNU General Public License
|
||||
along with Medito App. If not, see <https://www.gnu.org/licenses/>.*/
|
||||
|
||||
import 'package:Medito/network/home/shortcuts_response.dart';
|
||||
import 'package:Medito/network/auth.dart';
|
||||
import 'package:Medito/network/home/shortcuts_response.dart';
|
||||
import 'package:Medito/network/http_get.dart';
|
||||
|
||||
class ShortcutsRepo {
|
||||
final _ext = 'items/shortcuts';
|
||||
|
||||
Future<ShortcutsResponse> fetchShortcuts({bool skipCache = false}) async {
|
||||
Future<ShortcutsResponse?> fetchShortcuts({bool skipCache = false}) async {
|
||||
final response = await httpGet(BASE_URL + _ext, skipCache: skipCache);
|
||||
if (response == null) return null;
|
||||
return ShortcutsResponse.fromJson(response);
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ import 'package:pedantic/pedantic.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
//move this to network package later
|
||||
Future httpGet(String url,
|
||||
Future<Map<String, dynamic>?> httpGet(String url,
|
||||
{bool skipCache = false, String? fileNameForCache}) async {
|
||||
var cache;
|
||||
|
||||
|
@ -34,6 +34,7 @@ class AnnouncementBloc {
|
||||
try {
|
||||
var data = await _repo.fetchAnnouncements(
|
||||
skipCache: skipCache, hasOpened: hasOpened);
|
||||
if (data == null) return;
|
||||
announcementController.add(data);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
|
@ -21,10 +21,11 @@ class AnnouncementRepository {
|
||||
var ext = 'items/announcement';
|
||||
var welcome_ext = 'items/welcome_announcement';
|
||||
|
||||
Future<AnnouncementResponse> fetchAnnouncements(
|
||||
Future<AnnouncementResponse?> fetchAnnouncements(
|
||||
{bool skipCache = false, bool hasOpened = false}) async {
|
||||
final response =
|
||||
await httpGet(BASE_URL + getExt(hasOpened), skipCache: skipCache);
|
||||
if (response == null) return null;
|
||||
return AnnouncementResponse.fromJson(response);
|
||||
}
|
||||
|
||||
|
@ -13,14 +13,17 @@ Affero GNU General Public License for more details.
|
||||
You should have received a copy of the Affero GNU General Public License
|
||||
along with Medito App. If not, see <https://www.gnu.org/licenses/>.*/
|
||||
|
||||
import 'package:Medito/network/packs/packs_response.dart';
|
||||
import 'package:Medito/network/auth.dart';
|
||||
import 'package:Medito/network/http_get.dart';
|
||||
import 'package:Medito/network/packs/packs_response.dart';
|
||||
|
||||
class PacksRepository {
|
||||
|
||||
Future<List<PacksData>?> fetchPacks(bool skipCache) async {
|
||||
final response = await httpGet(BASE_URL + 'items/packs', skipCache: skipCache);
|
||||
final response =
|
||||
await httpGet(BASE_URL + 'items/packs', skipCache: skipCache);
|
||||
if (response == null) return null;
|
||||
|
||||
return PacksResponse.fromJson(response).data;
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import 'package:audio_service/audio_service.dart';
|
||||
class PlayerBloc {
|
||||
PlayerRepository? _repo;
|
||||
PlayerCopyData? version;
|
||||
@Deprecated('Use backgroundSoundsProvider instead')
|
||||
StreamController<ApiResponse<BackgroundSoundsResponse>>?
|
||||
bgSoundsListController;
|
||||
|
||||
@ -44,6 +45,7 @@ class PlayerBloc {
|
||||
setVersionCopySeen(version?.id ?? 0);
|
||||
}
|
||||
|
||||
@Deprecated('Use backgroundSoundsProvider instead')
|
||||
Future<void> fetchBackgroundSounds() async {
|
||||
try {
|
||||
var sounds = await _repo?.fetchBackgroundSounds(true);
|
||||
@ -70,3 +72,4 @@ class PlayerBloc {
|
||||
bgSoundsListController?.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,22 +19,41 @@ import 'package:Medito/network/http_get.dart';
|
||||
import 'package:Medito/network/player/audio_complete_copy_response.dart';
|
||||
import 'package:Medito/network/session_options/background_sounds.dart';
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
|
||||
import '../user/user_utils.dart';
|
||||
|
||||
var ext = 'items/player_copy?fields=*.*';
|
||||
var bgSoundsUrl = '${BASE_URL}items/background_sounds';
|
||||
|
||||
final backgroundSoundsProvider =
|
||||
FutureProvider<BackgroundSoundsResponse?>((ref) async {
|
||||
final skipCache = ref.watch(bgSoundsSkipCacheProvider);
|
||||
final content = await httpGet(bgSoundsUrl, skipCache: skipCache);
|
||||
|
||||
if (content == null) return null;
|
||||
|
||||
return BackgroundSoundsResponse.fromJson(content);
|
||||
});
|
||||
final bgSoundsSkipCacheProvider = StateProvider<bool>((ref) => false);
|
||||
|
||||
class PlayerRepository {
|
||||
final _ext = 'items/player_copy?fields=*.*';
|
||||
final _ratingExt = 'items/rating';
|
||||
var bgSoundsUrl = '${BASE_URL}items/background_sounds';
|
||||
|
||||
Future<PlayerCopyResponse> fetchCopyData() async {
|
||||
@Deprecated('Do not use')
|
||||
Future<PlayerCopyResponse?> fetchCopyData() async {
|
||||
final response = await httpGet(BASE_URL + _ext);
|
||||
if (response == null) return null;
|
||||
return PlayerCopyResponse.fromJson(response);
|
||||
}
|
||||
|
||||
Future<BackgroundSoundsResponse> fetchBackgroundSounds(bool skipCache) async {
|
||||
@Deprecated('Use backgroundSoundsProvider instead')
|
||||
Future<BackgroundSoundsResponse?> fetchBackgroundSounds(
|
||||
bool skipCache) async {
|
||||
final response = await httpGet(bgSoundsUrl, skipCache: skipCache);
|
||||
if (response == null) return null;
|
||||
return BackgroundSoundsResponse.fromJson(response);
|
||||
}
|
||||
|
||||
|
@ -129,8 +129,8 @@ class SessionOptionsBloc {
|
||||
Tracking.postUsage(
|
||||
Tracking.AUDIO_STARTED,
|
||||
{
|
||||
Tracking.SESSION_ID: mediaItem.extras?[SESSION_ID],
|
||||
Tracking.SESSION_DURATION: mediaItem.extras?[LENGTH],
|
||||
Tracking.SESSION_ID: mediaItem.extras?[SESSION_ID].toString() ?? '',
|
||||
Tracking.SESSION_DURATION: mediaItem.extras?[LENGTH].toString() ?? '',
|
||||
Tracking.SESSION_GUIDE: mediaItem.artist ?? ''
|
||||
},
|
||||
),
|
||||
|
@ -39,6 +39,8 @@ class SessionOptionsRepository {
|
||||
|
||||
final response = await httpGet(url, skipCache: skipCache);
|
||||
|
||||
if (response == null) return null;
|
||||
|
||||
return SessionOptionsResponse.fromJson(response).data;
|
||||
}
|
||||
|
||||
|
@ -27,10 +27,10 @@ class TextBloc {
|
||||
var data = await _repo.fetchData(id, skipCache);
|
||||
|
||||
if (!titleController.isClosed) {
|
||||
titleController.sink.add(data.title ?? '');
|
||||
titleController.sink.add(data?.title ?? '');
|
||||
}
|
||||
if (!bodyController.isClosed) {
|
||||
bodyController.sink.add(data.body ?? '');
|
||||
bodyController.sink.add(data?.body ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,8 +20,9 @@ import 'package:Medito/network/http_get.dart';
|
||||
class TextRepository {
|
||||
var ext = 'items/articles/';
|
||||
|
||||
Future<TextResponse> fetchData(String id, bool skipCache) async {
|
||||
Future<TextResponse?> fetchData(String id, bool skipCache) async {
|
||||
final response = await httpGet(BASE_URL + ext + id, skipCache: skipCache);
|
||||
if(response == null) return null;
|
||||
return TextResponse.fromJson(response);
|
||||
}
|
||||
}
|
||||
|
@ -13,11 +13,25 @@ Affero GNU General Public License for more details.
|
||||
You should have received a copy of the Affero GNU General Public License
|
||||
along with Medito App. If not, see <https://www.gnu.org/licenses/>.*/
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
const DEFAULT_VOLUME = 0.65;
|
||||
|
||||
@Deprecated('Use volume provider instead')
|
||||
Future<dynamic> retrieveSavedBgVolume() async {
|
||||
return await _getSavedVolume();
|
||||
}
|
||||
|
||||
final volumeProvider = StateProvider((_) => Volume());
|
||||
|
||||
class Volume {
|
||||
Future<double> getVolume() async {
|
||||
return await _getSavedVolume();
|
||||
}
|
||||
}
|
||||
|
||||
Future<double> _getSavedVolume() async {
|
||||
var prefs = await SharedPreferences.getInstance();
|
||||
var vol;
|
||||
try {
|
||||
|
@ -21,6 +21,9 @@ class MeditoColors {
|
||||
static var darkBGColor = Color(0xff22282D);
|
||||
static var almostBlack = Color(0xEE000000);
|
||||
|
||||
static var greyIsTheNewBlack = Color(0xFF1C1C1E);
|
||||
static var greyIsTheNewGrey = Color(0xFF2C2C2E);
|
||||
|
||||
static const newGrey = Color(0xffb6b6b8);
|
||||
static const midnight = Color(0xff111213);
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:Medito/network/folder/new_folder_screen.dart';
|
||||
import 'package:Medito/utils/utils.dart';
|
||||
import 'package:Medito/widgets/btm_nav/downloads_widget.dart';
|
||||
import 'package:Medito/widgets/session_options/session_options_screen.dart';
|
||||
@ -5,9 +6,8 @@ import 'package:Medito/widgets/text/text_file_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../widgets/folders/folder_nav_widget.dart';
|
||||
import '../widgets/home/home_wrapper_widget.dart';
|
||||
import '../widgets/player/player2/player_widget_2.dart';
|
||||
import '../widgets/player/player2/new_player_widget.dart';
|
||||
|
||||
const String SessionPath = '/session/:sid';
|
||||
const String DailyPath = '/daily/:did';
|
||||
@ -101,7 +101,9 @@ GoRoute _getSessionRoute() {
|
||||
GoRoute _getPlayerRoute() {
|
||||
return GoRoute(
|
||||
path: 'player',
|
||||
pageBuilder: (context, state) => getPlayerMaterialPage(state),
|
||||
pageBuilder: (context, state) {
|
||||
return getPlayerMaterialPage(state);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -146,48 +148,50 @@ MaterialPage<void> getCollectionMaterialPage(GoRouterState state) {
|
||||
}
|
||||
|
||||
MaterialPage<void> getPlayerMaterialPage(GoRouterState state) {
|
||||
return MaterialPage(key: state.pageKey, child: PlayerWidget());
|
||||
return MaterialPage(key: state.pageKey, child: NewPlayerWidget());
|
||||
}
|
||||
|
||||
MaterialPage<void> getFolderMaterialPage(GoRouterState state) {
|
||||
if (state.params.length == 1) {
|
||||
return MaterialPage(
|
||||
key: state.pageKey,
|
||||
child: FolderNavWidget(id: state.params['fid'] ?? ''));
|
||||
child: NewFolderScreen(id: state.params['fid']));
|
||||
} else if (state.params.length == 2) {
|
||||
return MaterialPage(
|
||||
key: state.pageKey,
|
||||
child: FolderNavWidget(id: state.params['f2id'] ?? ''));
|
||||
child: NewFolderScreen(id: state.params['f2id']));
|
||||
} else {
|
||||
return MaterialPage(
|
||||
key: state.pageKey,
|
||||
child: FolderNavWidget(id: state.params['f3id'] ?? ''));
|
||||
child: NewFolderScreen(id: state.params['f3id']));
|
||||
}
|
||||
}
|
||||
|
||||
String getPathFromString(String? place, List<String> ids) {
|
||||
String getPathFromString(String? place, List<String?> ids) {
|
||||
ids.removeWhere((element) => element == null);
|
||||
|
||||
if (place == 'session') {
|
||||
return SessionPath.replaceAll(':sid', ids.first);
|
||||
return SessionPath.replaceAll(':sid', ids.first!);
|
||||
}
|
||||
if (place == 'daily') {
|
||||
return DailyPath.replaceAll(':did', ids.first);
|
||||
return DailyPath.replaceAll(':did', ids.first!);
|
||||
}
|
||||
if (place == 'donation') {
|
||||
return DonationPath;
|
||||
}
|
||||
if (place == 'article') {
|
||||
return ArticlePath.replaceAll(':aid', ids.first);
|
||||
return ArticlePath.replaceAll(':aid', ids.first!);
|
||||
}
|
||||
if (place != null && place.contains('folder3')) {
|
||||
return Folder3Path.replaceAll(':fid', ids.first).replaceAll(':f2id', ids[1])
|
||||
.replaceAll(':f3id', ids[2]);
|
||||
return Folder3Path.replaceAll(':fid', ids.first!).replaceAll(':f2id', ids[1]!)
|
||||
.replaceAll(':f3id', ids[2]!);
|
||||
}
|
||||
if (place != null && place.contains('folder2')) {
|
||||
return Folder2Path.replaceAll(':fid', ids.first)
|
||||
.replaceAll(':f2id', ids[1]);
|
||||
return Folder2Path.replaceAll(':fid', ids.first!)
|
||||
.replaceAll(':f2id', ids[1]!);
|
||||
}
|
||||
if (place == 'folder') {
|
||||
return FolderPath.replaceAll(':fid', ids.first);
|
||||
return FolderPath.replaceAll(':fid', ids.first!);
|
||||
}
|
||||
if (place == 'url') {
|
||||
launchUrl(ids.first);
|
||||
|
@ -21,7 +21,6 @@ class _HomeWrapperWidgetState extends State<HomeWrapperWidget> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
||||
firstOpenOperations().then((hasOpened) {
|
||||
_hasOpened = hasOpened;
|
||||
setState(() {
|
||||
@ -62,27 +61,27 @@ class _HomeWrapperWidgetState extends State<HomeWrapperWidget> {
|
||||
?.copyWith(fontSize: 12),
|
||||
selectedItemColor: MeditoColors.walterWhite,
|
||||
unselectedItemColor: MeditoColors.newGrey,
|
||||
currentIndex: _currentIndex,
|
||||
onTap: _onTabTapped,
|
||||
items: [
|
||||
BottomNavigationBarItem(
|
||||
tooltip: 'Home',
|
||||
icon: Icon(
|
||||
Icons.home_outlined,
|
||||
size: 20,
|
||||
currentIndex: _currentIndex,
|
||||
onTap: _onTabTapped,
|
||||
items: [
|
||||
BottomNavigationBarItem(
|
||||
tooltip: 'Home',
|
||||
icon: Icon(
|
||||
Icons.home_outlined,
|
||||
size: 20,
|
||||
),
|
||||
label: 'Home',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(
|
||||
Icons.format_list_bulleted_outlined,
|
||||
size: 20,
|
||||
),
|
||||
tooltip: 'Packs',
|
||||
label: 'Packs',
|
||||
),
|
||||
],
|
||||
),
|
||||
label: 'Home',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(
|
||||
Icons.format_list_bulleted_outlined,
|
||||
size: 20,
|
||||
),
|
||||
tooltip: 'Packs',
|
||||
label: 'Packs',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -81,8 +81,9 @@ class _ChooseBackgroundSoundDialogState
|
||||
_handler = AudioHandlerInheritedWidget.of(context).audioHandler;
|
||||
|
||||
return StreamBuilder(
|
||||
stream: Stream.fromFuture(Connectivity()
|
||||
.checkConnectivity()),
|
||||
stream: Stream.fromFuture(
|
||||
Connectivity().checkConnectivity(),
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
if (snapshot.data == ConnectivityResult.none) {
|
||||
|
327
lib/widgets/player/player2/new_player_widget.dart
Normal file
327
lib/widgets/player/player2/new_player_widget.dart
Normal file
@ -0,0 +1,327 @@
|
||||
/*This file is part of Medito App.
|
||||
|
||||
Medito App is free software: you can redistribute it and/or modify
|
||||
it under the terms of the Affero GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Medito App is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
Affero GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the Affero GNU General Public License
|
||||
along with Medito App. If not, see <https://www.gnu.org/licenses/>.*/
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:Medito/audioplayer/media_lib.dart';
|
||||
import 'package:Medito/audioplayer/medito_audio_handler.dart';
|
||||
import 'package:Medito/network/player/player_bloc.dart';
|
||||
import 'package:Medito/utils/colors.dart';
|
||||
import 'package:Medito/utils/shared_preferences_utils.dart';
|
||||
import 'package:Medito/utils/strings.dart';
|
||||
import 'package:Medito/utils/utils.dart';
|
||||
import 'package:Medito/widgets/main/app_bar_widget.dart';
|
||||
import 'package:Medito/widgets/player/player2/audio_complete_dialog.dart';
|
||||
import 'package:Medito/widgets/player/position_indicator_widget.dart';
|
||||
import 'package:Medito/widgets/player/subtitle_text_widget.dart';
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../../../audioplayer/audio_inherited_widget.dart';
|
||||
import '../../../tracking/tracking.dart';
|
||||
import '../../../utils/bgvolume_utils.dart';
|
||||
|
||||
class NewPlayerWidget extends StatefulWidget {
|
||||
final normalPop;
|
||||
|
||||
NewPlayerWidget({this.normalPop});
|
||||
|
||||
@override
|
||||
_NewPlayerWidgetState createState() => _NewPlayerWidgetState();
|
||||
}
|
||||
|
||||
class _NewPlayerWidgetState extends State<NewPlayerWidget> {
|
||||
MeditoAudioHandler? _handler;
|
||||
late PlayerBloc _bloc;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_handler?.stop();
|
||||
_bloc.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startTimeout();
|
||||
_bloc = PlayerBloc();
|
||||
|
||||
Future.delayed(Duration(milliseconds: 200)).then((value) async {
|
||||
var bgSound = await getBgSoundNameFromSharedPrefs();
|
||||
var volume = await retrieveSavedBgVolume();
|
||||
await _handler
|
||||
?.customAction(SET_BG_SOUND_VOL, {SET_BG_SOUND_VOL: volume});
|
||||
return _handler?.customAction(SEND_BG_SOUND, {SEND_BG_SOUND: bgSound});
|
||||
});
|
||||
}
|
||||
|
||||
void _startTimeout() {
|
||||
var timerMaxSeconds = 20;
|
||||
Timer.periodic(Duration(seconds: timerMaxSeconds), (timer) {
|
||||
if (_handler?.playbackState.value.processingState ==
|
||||
AudioProcessingState.loading &&
|
||||
mounted) {
|
||||
createSnackBar(TIMEOUT, context);
|
||||
}
|
||||
timer.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_handler = AudioHandlerInheritedWidget.of(context).audioHandler;
|
||||
var mediaItem = _handler?.mediaItem.value;
|
||||
|
||||
try {
|
||||
if (_hasBGSound() == true) {
|
||||
getSavedBgSoundData();
|
||||
}
|
||||
} on Exception catch (e, s) {
|
||||
unawaited(
|
||||
Sentry.captureException(e,
|
||||
stackTrace: s, hint: 'extras[HAS_BG_SOUND]: ${_hasBGSound()}'),
|
||||
);
|
||||
}
|
||||
|
||||
_handler?.customEvent.stream.listen((event) {
|
||||
if (mounted &&
|
||||
event[AUDIO_COMPLETE] is bool &&
|
||||
event[AUDIO_COMPLETE] == true) {
|
||||
_trackSessionEnd(_handler?.mediaItem.value);
|
||||
showGeneralDialog(
|
||||
transitionDuration: Duration(milliseconds: 400),
|
||||
context: context,
|
||||
barrierColor: MeditoColors.darkMoon,
|
||||
pageBuilder: (_, __, ___) {
|
||||
return AudioCompleteDialog(
|
||||
bloc: _bloc, mediaItem: _handler?.mediaItem.value);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return Material(
|
||||
color: MeditoColors.greyIsTheNewBlack,
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_getAppBar(),
|
||||
_buildImage(mediaItem?.artUri?.toString()),
|
||||
Container(height: 24),
|
||||
_buildTitleRows(),
|
||||
Expanded(child: SizedBox.shrink()),
|
||||
_buildPlayerButtonRow(mediaItem),
|
||||
Expanded(child: SizedBox.shrink()),
|
||||
// A seek bar.
|
||||
PositionIndicatorWidget(
|
||||
handler: _handler,
|
||||
bgSoundsStream: _bloc.bgSoundsListController?.stream),
|
||||
Container(height: 24)
|
||||
])
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool? _hasBGSound() => _handler?.mediaItemHasBGSound();
|
||||
|
||||
StreamBuilder<bool> _buildPlayPauseButtons(MediaItem? mediaItem) {
|
||||
return StreamBuilder<bool>(
|
||||
stream: _handler?.playbackState.map((state) => state.playing).distinct(),
|
||||
builder: (context, snapshot) {
|
||||
final playing = snapshot.data ?? false;
|
||||
if (playing) {
|
||||
return _pauseButton();
|
||||
} else {
|
||||
return _playButton(mediaItem);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
StreamBuilder<MediaItem> _buildTitleRows() {
|
||||
return StreamBuilder<MediaItem>(
|
||||
stream: _handler?.mediaItem.cast(),
|
||||
builder: (context, snapshot) {
|
||||
final mediaItem = snapshot.data;
|
||||
return Column(
|
||||
children: [_getTitleRow(mediaItem), _getSubtitleWidget(mediaItem)],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
MeditoAppBarWidget _getAppBar() {
|
||||
return MeditoAppBarWidget(
|
||||
transparent: true,
|
||||
hasCloseButton: true,
|
||||
closePressed: _onBackPressed,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getTitleRow(MediaItem? mediaItem) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
mediaItem?.title ?? 'Loading...',
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: buildTitleTheme(),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TextStyle? buildTitleTheme() {
|
||||
return Theme.of(context).textTheme.headline1;
|
||||
}
|
||||
|
||||
Widget _getSubtitleWidget(MediaItem? mediaItem) {
|
||||
var attr = mediaItem?.extras != null ? (mediaItem?.extras?['attr']) : '';
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: SubtitleTextWidget(body: attr),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _playButton(MediaItem? mediaItem) => Semantics(
|
||||
label: 'Play button',
|
||||
child: IconButton(
|
||||
color: MeditoColors.walterWhite,
|
||||
icon: Icon(Icons.play_circle_fill),
|
||||
iconSize: 80,
|
||||
onPressed: () =>
|
||||
_playPressed(mediaItem?.extras?[HAS_BG_SOUND] ?? true),
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> _playPressed(bool hasBgSound) async {
|
||||
await _handler?.play();
|
||||
if (hasBgSound) await getSavedBgSoundData();
|
||||
}
|
||||
|
||||
Widget _pauseButton() => Semantics(
|
||||
label: 'Pause button',
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.pause_circle_filled),
|
||||
iconSize: 80,
|
||||
color: MeditoColors.walterWhite,
|
||||
onPressed: _handler?.pause,
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> getSavedBgSoundData() async {
|
||||
var file = await getBgSoundFileFromSharedPrefs();
|
||||
var name = await getBgSoundNameFromSharedPrefs();
|
||||
unawaited(_handler?.customAction(SEND_BG_SOUND, {SEND_BG_SOUND: name}));
|
||||
unawaited(_handler?.customAction(PLAY_BG_SOUND, {PLAY_BG_SOUND: file}));
|
||||
}
|
||||
|
||||
void _onBackPressed() {
|
||||
GoRouter.of(context).pop();
|
||||
}
|
||||
|
||||
Widget buildButtonLabel(MediaItem mediaItem) {
|
||||
var label = _bloc.version?.buttonLabel;
|
||||
|
||||
if (label == null) return Container();
|
||||
|
||||
return Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.subtitle2?.copyWith(
|
||||
color: parseColor(mediaItem.extras?[SECONDARY_COLOUR])),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildSvgPicture(Color secondaryColor) {
|
||||
var icon = _bloc.version?.buttonIcon;
|
||||
|
||||
if (icon == null) return Container();
|
||||
|
||||
return SvgPicture.asset(
|
||||
'assets/images/' + icon + '.svg',
|
||||
color: secondaryColor,
|
||||
);
|
||||
}
|
||||
|
||||
void _trackSessionEnd(MediaItem? mediaItem) {
|
||||
if (mediaItem == null) return;
|
||||
unawaited(
|
||||
Tracking.postUsage(
|
||||
Tracking.AUDIO_COMPLETED,
|
||||
{
|
||||
Tracking.SESSION_ID: mediaItem.extras?[SESSION_ID].toString() ?? '',
|
||||
Tracking.SESSION_DURATION: mediaItem.extras?[LENGTH].toString() ?? '',
|
||||
Tracking.SESSION_GUIDE: mediaItem.artist ?? ''
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPlayerButtonRow(MediaItem? mediaItem) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_buildRewindButton(),
|
||||
Container(width: 38),
|
||||
_buildPlayPauseButtons(mediaItem),
|
||||
Container(width: 38),
|
||||
_buildForwardButton()
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRewindButton() {
|
||||
return IconButton(
|
||||
iconSize: 40,
|
||||
color: MeditoColors.walterWhite,
|
||||
onPressed: () => _handler?.skipBackward10Secs(),
|
||||
icon: Icon(Icons.replay_10));
|
||||
}
|
||||
|
||||
Widget _buildForwardButton() {
|
||||
return IconButton(
|
||||
iconSize: 40,
|
||||
color: MeditoColors.walterWhite,
|
||||
onPressed: () => _handler?.skipForward30Secs(),
|
||||
icon: Icon(Icons.forward_30));
|
||||
}
|
||||
|
||||
Widget _buildImage(String? currentImage) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40.0),
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(6))),
|
||||
child: getNetworkImageWidget(currentImage ?? ''),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,380 +0,0 @@
|
||||
/*This file is part of Medito App.
|
||||
|
||||
Medito App is free software: you can redistribute it and/or modify
|
||||
it under the terms of the Affero GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Medito App is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
Affero GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the Affero GNU General Public License
|
||||
along with Medito App. If not, see <https://www.gnu.org/licenses/>.*/
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:Medito/audioplayer/media_lib.dart';
|
||||
import 'package:Medito/audioplayer/medito_audio_handler.dart';
|
||||
import 'package:Medito/network/player/player_bloc.dart';
|
||||
import 'package:Medito/utils/colors.dart';
|
||||
import 'package:Medito/utils/shared_preferences_utils.dart';
|
||||
import 'package:Medito/utils/strings.dart';
|
||||
import 'package:Medito/utils/utils.dart';
|
||||
import 'package:Medito/widgets/main/app_bar_widget.dart';
|
||||
import 'package:Medito/widgets/player/player2/audio_complete_dialog.dart';
|
||||
import 'package:Medito/widgets/player/player_button.dart';
|
||||
import 'package:Medito/widgets/player/position_indicator_widget.dart';
|
||||
import 'package:Medito/widgets/player/subtitle_text_widget.dart';
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../../../audioplayer/audio_inherited_widget.dart';
|
||||
import '../../../tracking/tracking.dart';
|
||||
import '../../../utils/bgvolume_utils.dart';
|
||||
import '../background_sounds_sheet_widget.dart';
|
||||
|
||||
class PlayerWidget extends StatefulWidget {
|
||||
final normalPop;
|
||||
|
||||
PlayerWidget({this.normalPop});
|
||||
|
||||
@override
|
||||
_PlayerWidgetState createState() => _PlayerWidgetState();
|
||||
}
|
||||
|
||||
class _PlayerWidgetState extends State<PlayerWidget> {
|
||||
MeditoAudioHandler? _handler;
|
||||
late PlayerBloc _bloc;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_handler?.stop();
|
||||
_bloc.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startTimeout();
|
||||
_bloc = PlayerBloc();
|
||||
|
||||
Future.delayed(Duration(milliseconds: 200)).then((value) async {
|
||||
var bgSound = await getBgSoundNameFromSharedPrefs();
|
||||
var volume = await retrieveSavedBgVolume();
|
||||
await _handler
|
||||
?.customAction(SET_BG_SOUND_VOL, {SET_BG_SOUND_VOL: volume});
|
||||
return _handler?.customAction(SEND_BG_SOUND, {SEND_BG_SOUND: bgSound});
|
||||
});
|
||||
}
|
||||
|
||||
void _startTimeout() {
|
||||
var timerMaxSeconds = 20;
|
||||
Timer.periodic(Duration(seconds: timerMaxSeconds), (timer) {
|
||||
if (_handler?.playbackState.value.processingState ==
|
||||
AudioProcessingState.loading &&
|
||||
mounted) {
|
||||
createSnackBar(TIMEOUT, context);
|
||||
}
|
||||
timer.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_handler = AudioHandlerInheritedWidget.of(context).audioHandler;
|
||||
var mediaItem = _handler?.mediaItem.value;
|
||||
|
||||
try {
|
||||
if (_handler?.mediaItem.value?.extras?[HAS_BG_SOUND]) {
|
||||
getSavedBgSoundData();
|
||||
}
|
||||
} on Exception catch (e, s) {
|
||||
unawaited(Sentry.captureException(e,
|
||||
stackTrace: s,
|
||||
hint:
|
||||
'extras[HAS_BG_SOUND]: ${_handler?.mediaItem.value?.extras?[HAS_BG_SOUND]}'));
|
||||
}
|
||||
|
||||
_handler?.customEvent.stream.listen((event) {
|
||||
if (mounted &&
|
||||
event[AUDIO_COMPLETE] is bool &&
|
||||
event[AUDIO_COMPLETE] == true) {
|
||||
_trackSessionEnd(_handler?.mediaItem.value);
|
||||
showGeneralDialog(
|
||||
transitionDuration: Duration(milliseconds: 400),
|
||||
context: context,
|
||||
barrierColor: MeditoColors.darkMoon,
|
||||
pageBuilder: (_, __, ___) {
|
||||
return AudioCompleteDialog(
|
||||
bloc: _bloc, mediaItem: _handler?.mediaItem.value);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return Material(
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Stack(
|
||||
children: [
|
||||
_getGradientWidget(mediaItem, context),
|
||||
_getGradientOverlayWidget(),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_getAppBar(),
|
||||
// Show media item title
|
||||
StreamBuilder<MediaItem>(
|
||||
stream: _handler?.mediaItem.cast(),
|
||||
builder: (context, snapshot) {
|
||||
final mediaItem = snapshot.data;
|
||||
return Column(
|
||||
children: [
|
||||
_getTitleRow(mediaItem, false),
|
||||
_getSubtitleWidget(mediaItem, false)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(child: SizedBox.shrink()),
|
||||
StreamBuilder<bool>(
|
||||
stream: _handler?.playbackState
|
||||
.map((state) => state.playing)
|
||||
.distinct(),
|
||||
builder: (context, snapshot) {
|
||||
final playing = snapshot.data ?? false;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (playing)
|
||||
_pauseButton(mediaItem)
|
||||
else
|
||||
_playButton(mediaItem),
|
||||
],
|
||||
),
|
||||
_getBgMusicIconButton(
|
||||
mediaItem?.extras?[HAS_BG_SOUND] ?? true)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(child: SizedBox.shrink()),
|
||||
// A seek bar.
|
||||
PositionIndicatorWidget(
|
||||
handler: _handler,
|
||||
color: parseColor(mediaItem?.extras?[PRIMARY_COLOUR]),
|
||||
),
|
||||
Container(height: 24)
|
||||
])
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getGradientOverlayWidget() {
|
||||
return Image.asset(
|
||||
'assets/images/texture.png',
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
fit: BoxFit.fill,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getGradientWidget(MediaItem? mediaItem, BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
parseColor(mediaItem?.extras?[PRIMARY_COLOUR]).withAlpha(100),
|
||||
parseColor(mediaItem?.extras?[PRIMARY_COLOUR]).withAlpha(0),
|
||||
],
|
||||
radius: 1.0,
|
||||
)),
|
||||
height: MediaQuery.of(context).size.height,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
));
|
||||
}
|
||||
|
||||
MeditoAppBarWidget _getAppBar() {
|
||||
return MeditoAppBarWidget(
|
||||
transparent: true,
|
||||
hasCloseButton: true,
|
||||
closePressed: _onBackPressed,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getTitleRow(MediaItem? mediaItem, bool complete) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: !complete
|
||||
? Text(
|
||||
mediaItem?.title ?? 'Loading...',
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: buildTitleTheme(),
|
||||
)
|
||||
: FutureBuilder<String>(
|
||||
future: _bloc.getVersionTitle(),
|
||||
builder: (context, snapshot) {
|
||||
return Text(
|
||||
snapshot.hasData ? (snapshot.data ?? '') : 'Loading...',
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: buildTitleTheme(),
|
||||
);
|
||||
})),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TextStyle? buildTitleTheme() {
|
||||
return Theme.of(context).textTheme.headline1;
|
||||
}
|
||||
|
||||
Widget _getSubtitleWidget(MediaItem? mediaItem, bool complete) {
|
||||
var attr = '';
|
||||
if (complete) {
|
||||
attr = _bloc.version?.body ?? WELL_DONE_SUBTITLE;
|
||||
} else {
|
||||
attr = mediaItem?.extras != null ? (mediaItem?.extras?['attr']) : '';
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: SubtitleTextWidget(body: attr),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getBgMusicIconButton(bool visible) {
|
||||
return Visibility(
|
||||
visible: visible,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 32),
|
||||
child: MaterialButton(
|
||||
enableFeedback: true,
|
||||
textColor: MeditoColors.walterWhite,
|
||||
color: MeditoColors.walterWhiteTrans,
|
||||
onPressed: _onBgMusicPressed,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 4),
|
||||
child: Row(children: [
|
||||
Icon(
|
||||
Icons.music_note_outlined,
|
||||
color: MeditoColors.walterWhite,
|
||||
),
|
||||
Container(width: 8),
|
||||
Text(SOUNDS),
|
||||
Container(width: 8),
|
||||
]))),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _playButton(MediaItem? mediaItem) => Semantics(
|
||||
label: 'Play button',
|
||||
child: PlayerButton(
|
||||
icon: Icons.play_arrow,
|
||||
onPressed: () => _playPressed(mediaItem?.extras?[HAS_BG_SOUND] ?? true),
|
||||
secondaryColor: parseColor(mediaItem?.extras?[SECONDARY_COLOUR]),
|
||||
primaryColor: parseColor(mediaItem?.extras?[PRIMARY_COLOUR]),
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> _playPressed(bool hasBgSound) async {
|
||||
await _handler?.play();
|
||||
if (hasBgSound) await getSavedBgSoundData();
|
||||
}
|
||||
|
||||
Widget _pauseButton(MediaItem? mediaItem) => Semantics(
|
||||
label: 'Pause button',
|
||||
child: PlayerButton(
|
||||
icon: Icons.pause,
|
||||
secondaryColor: parseColor(mediaItem?.extras?[SECONDARY_COLOUR]),
|
||||
primaryColor: parseColor(mediaItem?.extras?[PRIMARY_COLOUR]),
|
||||
onPressed: _handler?.pause,
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> getSavedBgSoundData() async {
|
||||
var file = await getBgSoundFileFromSharedPrefs();
|
||||
var name = await getBgSoundNameFromSharedPrefs();
|
||||
unawaited(_handler?.customAction(SEND_BG_SOUND, {SEND_BG_SOUND: name}));
|
||||
unawaited(_handler?.customAction(PLAY_BG_SOUND, {PLAY_BG_SOUND: file}));
|
||||
}
|
||||
|
||||
void _onBackPressed() {
|
||||
GoRouter.of(context).pop();
|
||||
}
|
||||
|
||||
Widget buildButtonLabel(MediaItem mediaItem) {
|
||||
var label = _bloc.version?.buttonLabel;
|
||||
|
||||
if (label == null) return Container();
|
||||
|
||||
return Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.subtitle2?.copyWith(
|
||||
color: parseColor(mediaItem.extras?[SECONDARY_COLOUR])),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildSvgPicture(Color secondaryColor) {
|
||||
var icon = _bloc.version?.buttonIcon;
|
||||
|
||||
if (icon == null) return Container();
|
||||
|
||||
return SvgPicture.asset(
|
||||
'assets/images/' + icon + '.svg',
|
||||
color: secondaryColor,
|
||||
);
|
||||
}
|
||||
|
||||
void _trackSessionEnd(MediaItem? mediaItem) {
|
||||
if (mediaItem == null) return;
|
||||
unawaited(
|
||||
Tracking.postUsage(
|
||||
Tracking.AUDIO_COMPLETED,
|
||||
{
|
||||
Tracking.SESSION_ID: mediaItem.extras?[SESSION_ID],
|
||||
Tracking.SESSION_DURATION: mediaItem.extras?[LENGTH],
|
||||
Tracking.SESSION_GUIDE: mediaItem.artist ?? ''
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onBgMusicPressed() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => ChooseBackgroundSoundDialog(
|
||||
handler: _handler, stream: _bloc.bgSoundsListController?.stream),
|
||||
);
|
||||
// slight delay in case the cache returns before the sheet opens
|
||||
Future.delayed(Duration(milliseconds: 50))
|
||||
.then((value) => _bloc.fetchBackgroundSounds());
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
import 'package:Medito/utils/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class PlayerButton extends StatelessWidget {
|
||||
final Widget? child;
|
||||
final IconData? icon;
|
||||
final Future<void> Function()? onPressed;
|
||||
final Color? primaryColor;
|
||||
final Color? secondaryColor;
|
||||
final String? text;
|
||||
final SvgPicture? image;
|
||||
|
||||
PlayerButton(
|
||||
{Key? key,
|
||||
this.child,
|
||||
this.icon,
|
||||
this.onPressed,
|
||||
this.primaryColor,
|
||||
this.text,
|
||||
this.secondaryColor,
|
||||
this.image})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onPressed,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'assets/images/blurb.svg',
|
||||
height: 88,
|
||||
width: 88,
|
||||
color: primaryColor,
|
||||
),
|
||||
image ??
|
||||
Icon(
|
||||
icon,
|
||||
size: 24,
|
||||
color: secondaryColor ?? MeditoColors.darkMoon,
|
||||
),
|
||||
child ?? Container()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -2,16 +2,19 @@ import 'dart:math';
|
||||
|
||||
import 'package:Medito/audioplayer/media_lib.dart';
|
||||
import 'package:Medito/audioplayer/medito_audio_handler.dart';
|
||||
import 'package:Medito/network/player/player_bloc.dart';
|
||||
import 'package:Medito/utils/colors.dart';
|
||||
import 'package:Medito/utils/duration_ext.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
class PositionIndicatorWidget extends StatefulWidget {
|
||||
final Color? color;
|
||||
final MeditoAudioHandler? handler;
|
||||
import 'background_sounds_sheet_widget.dart';
|
||||
|
||||
PositionIndicatorWidget({Key? key, this.handler, this.color})
|
||||
class PositionIndicatorWidget extends StatefulWidget {
|
||||
final MeditoAudioHandler? handler;
|
||||
final Stream? bgSoundsStream;
|
||||
|
||||
PositionIndicatorWidget({Key? key, this.handler, this.bgSoundsStream})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@ -20,6 +23,8 @@ class PositionIndicatorWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PositionIndicatorWidgetState extends State<PositionIndicatorWidget> {
|
||||
final _speedList = ['X1', 'X1.25', 'X1.5', 'X2', 'X0.6'];
|
||||
var _currentSpeed = 'X1';
|
||||
|
||||
/// Tracks the position while the user drags the seek bar.
|
||||
final BehaviorSubject<double?> _dragPositionSubject =
|
||||
@ -27,13 +32,13 @@ class _PositionIndicatorWidgetState extends State<PositionIndicatorWidget> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
double? _seekPos;
|
||||
|
||||
return StreamBuilder(
|
||||
stream: Rx.combineLatest2<double?, double, double?>(
|
||||
_dragPositionSubject.stream,
|
||||
Stream.periodic(Duration(milliseconds: 200), (count) => count.toDouble()),
|
||||
Stream.periodic(
|
||||
Duration(milliseconds: 200), (count) => count.toDouble()),
|
||||
(dragPosition, _) => dragPosition),
|
||||
builder: (context, AsyncSnapshot<double?> snapshot) {
|
||||
//snapshot.data will be non null if slider was dragged
|
||||
@ -51,14 +56,14 @@ class _PositionIndicatorWidgetState extends State<PositionIndicatorWidget> {
|
||||
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
|
||||
child: SliderTheme(
|
||||
data: SliderThemeData(
|
||||
trackHeight: 4,
|
||||
trackHeight: 8,
|
||||
trackShape: CustomTrackShape(),
|
||||
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 12.0),
|
||||
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 0.0),
|
||||
),
|
||||
child: Slider(
|
||||
min: 0.0,
|
||||
activeColor: widget.color,
|
||||
inactiveColor: MeditoColors.meditoTextGrey,
|
||||
activeColor: MeditoColors.walterWhite,
|
||||
inactiveColor: MeditoColors.greyIsTheNewGrey,
|
||||
max: duration.toDouble(),
|
||||
value: _seekPos ??
|
||||
max(0.0, min(position.toDouble(), duration.toDouble())),
|
||||
@ -66,7 +71,8 @@ class _PositionIndicatorWidgetState extends State<PositionIndicatorWidget> {
|
||||
_dragPositionSubject.add(value);
|
||||
},
|
||||
onChangeEnd: (value) {
|
||||
widget.handler?.seek(Duration(milliseconds: value.toInt()));
|
||||
widget.handler
|
||||
?.seek(Duration(milliseconds: value.toInt()));
|
||||
_seekPos = value;
|
||||
_dragPositionSubject.add(null); // todo is this ok?
|
||||
},
|
||||
@ -84,26 +90,157 @@ class _PositionIndicatorWidgetState extends State<PositionIndicatorWidget> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
Duration(milliseconds: position).toMinutesSeconds(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle2
|
||||
?.copyWith(color: MeditoColors.meditoTextGrey),
|
||||
),
|
||||
Text(
|
||||
Duration(milliseconds: duration).toMinutesSeconds(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle2
|
||||
?.copyWith(color: MeditoColors.meditoTextGrey),
|
||||
),
|
||||
_getCurrentPositionLabel(position, context),
|
||||
Container(width: 8),
|
||||
_getSpeedLabel(duration, context),
|
||||
Container(width: 8),
|
||||
if (_hasBGSound()) _getSoundLabel(),
|
||||
if (_hasBGSound()) Container(width: 8),
|
||||
_getFullDurationLabel(duration, context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _hasBGSound() => widget.handler?.mediaItemHasBGSound() == true;
|
||||
|
||||
Expanded _getFullDurationLabel(int duration, BuildContext context) {
|
||||
return Expanded(
|
||||
flex: 25,
|
||||
child: Container(
|
||||
height: 40,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: MeditoColors.greyIsTheNewGrey,
|
||||
borderRadius: BorderRadius.all(Radius.circular(3)),
|
||||
),
|
||||
child: Text(
|
||||
Duration(milliseconds: duration).toMinutesSeconds(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle2
|
||||
?.copyWith(color: MeditoColors.walterWhite),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Expanded _getCurrentPositionLabel(int position, BuildContext context) {
|
||||
return Expanded(
|
||||
flex: 25,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: MeditoColors.greyIsTheNewGrey,
|
||||
borderRadius: BorderRadius.all(Radius.circular(3)),
|
||||
),
|
||||
child: Text(
|
||||
Duration(milliseconds: position).toMinutesSeconds(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle2
|
||||
?.copyWith(color: MeditoColors.walterWhite),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getSpeedLabel(int duration, BuildContext context) {
|
||||
return Expanded(
|
||||
flex: 25,
|
||||
child: GestureDetector(
|
||||
onTap: () => {widget.handler?.setPlayerSpeed(_getNextSpeed())},
|
||||
child: Container(
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: MeditoColors.walterWhite,
|
||||
borderRadius: BorderRadius.all(Radius.circular(3)),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
_currentSpeed,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle2
|
||||
?.copyWith(color: MeditoColors.greyIsTheNewGrey),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getSoundLabel() {
|
||||
return Builder(builder: (context) {
|
||||
return GestureDetector(
|
||||
onTap: () => _onBgMusicPressed(context),
|
||||
child: Container(
|
||||
height: 40,
|
||||
padding: EdgeInsets.symmetric(horizontal: 24),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: MeditoColors.walterWhite,
|
||||
borderRadius: BorderRadius.all(Radius.circular(3)),
|
||||
),
|
||||
child: Text(
|
||||
'SOUND',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle2
|
||||
?.copyWith(color: MeditoColors.greyIsTheNewGrey),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
double _getNextSpeed() {
|
||||
var nextIndex = _speedList.indexOf(_currentSpeed) + 1;
|
||||
if (nextIndex >= _speedList.length) {
|
||||
_currentSpeed = _speedList[0];
|
||||
} else {
|
||||
_currentSpeed = _speedList[nextIndex];
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_currentSpeed;
|
||||
});
|
||||
return _getSpeedDoubleFromString(_currentSpeed);
|
||||
}
|
||||
|
||||
double _getSpeedDoubleFromString(String current) {
|
||||
if (current == 'X1') {
|
||||
return 1;
|
||||
} else if (current == 'X1.25') {
|
||||
return 1.25;
|
||||
} else if (current == 'X1.5') {
|
||||
return 1.5;
|
||||
} else if (current == 'X2') {
|
||||
return 2;
|
||||
} else if (current == 'X0.6') {
|
||||
return 0.6;
|
||||
} else if (current == 'X0.75') {
|
||||
return 1;
|
||||
} else {
|
||||
return 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
void _onBgMusicPressed(BuildContext context) {
|
||||
var bloc = PlayerBloc();
|
||||
|
||||
// slight delay in case the cache returns before the sheet opens
|
||||
Future.delayed(Duration(milliseconds: 50))
|
||||
.then((value) => bloc.fetchBackgroundSounds());
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => ChooseBackgroundSoundDialog(
|
||||
handler: widget.handler, stream: bloc.bgSoundsListController?.stream),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomTrackShape extends RoundedRectSliderTrackShape {
|
||||
@ -123,7 +260,7 @@ class CustomTrackShape extends RoundedRectSliderTrackShape {
|
||||
final trackLeft = offset.dx;
|
||||
var trackTop;
|
||||
if (addTopPadding) {
|
||||
trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2 + 8;
|
||||
trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2 + 12;
|
||||
} else {
|
||||
trackTop = parentBox.size.height / 2 - 2;
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
http: ^0.13.3
|
||||
google_fonts: ^2.3.1
|
||||
google_fonts: ^3.0.1
|
||||
flutter_svg: ^1.0.3
|
||||
path_provider: ^2.0.1
|
||||
url_launcher: ^6.0.3
|
||||
@ -40,10 +40,17 @@ dependencies:
|
||||
sentry_flutter: ^6.9.1
|
||||
back_button_interceptor: ^5.0.0
|
||||
go_router: ^3.0.4
|
||||
flutter_riverpod: ^2.1.1
|
||||
freezed: ^2.2.0
|
||||
freezed_annotation: ^2.2.0
|
||||
json_annotation: ^4.7.0
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.3.0
|
||||
riverpod_generator: ^1.0.4
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
json_serializable: ^6.5.4
|
||||
mocktail: ^0.3.0
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user