Skip to content

Commit

Permalink
feat: channel subpage view #1
Browse files Browse the repository at this point in the history
  • Loading branch information
MSOB7YY committed Jan 5, 2024
1 parent 0aaba54 commit db22eaa
Show file tree
Hide file tree
Showing 15 changed files with 1,145 additions and 406 deletions.
19 changes: 18 additions & 1 deletion lib/controller/edit_delete_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class EditDeleteController {
if (!await requestManageStoragePermission()) {
return null;
}
final saveDir = await Directory(AppDirs.SAVED_ARTWORKS).create();
final saveDir = await Directory(AppDirs.SAVED_ARTWORKS).create(recursive: true);
final saveDirPath = saveDir.path;
final newPath = "$saveDirPath${Platform.pathSeparator}${track.filenameWOExt}.png";
final imgFiles = await Indexer.inst.extractTracksArtworks(
Expand All @@ -72,6 +72,23 @@ class EditDeleteController {
}
}

/// returns save directory path if saved successfully
Future<String?> saveImageToStorage(File imageFile) async {
if (!await requestManageStoragePermission()) {
return null;
}
final saveDir = await Directory(AppDirs.SAVED_ARTWORKS).create(recursive: true);
final saveDirPath = saveDir.path;
final newPath = "$saveDirPath${Platform.pathSeparator}${imageFile.path.getFilenameWOExt}.png";
try {
await imageFile.copy(newPath);
return saveDirPath;
} catch (e) {
printy(e, isError: true);
return null;
}
}

Future<void> updateTrackPathInEveryPartOfNamida(Track oldTrack, String newPath) async {
final newtrlist = await Indexer.inst.convertPathToTrack([newPath]);
if (newtrlist.isEmpty) return;
Expand Down
1 change: 1 addition & 0 deletions lib/core/translations/keys.dart
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ abstract class LanguageKeys {
String get LINK => _getKey('LINK');
String get LIST_OF_FOLDERS => _getKey('LIST_OF_FOLDERS');
String get LOADING_FILE => _getKey('LOADING_FILE');
String get LOAD_ALL => _getKey('LOAD_ALL');
String get LOCAL => _getKey('LOCAL');
String get LOCAL_VIDEO_MATCHING => _getKey('LOCAL_VIDEO_MATCHING');
String get LOST_MEMORIES => _getKey('LOST_MEMORIES');
Expand Down
10 changes: 10 additions & 0 deletions lib/ui/widgets/artwork.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class ArtworkWidget extends StatefulWidget {
final bool displayIcon;
final IconData icon;
final bool isCircle;
final VoidCallback? onError;

const ArtworkWidget({
required super.key,
Expand All @@ -67,6 +68,7 @@ class ArtworkWidget extends StatefulWidget {
this.displayIcon = true,
this.icon = Broken.musicnote,
this.isCircle = false,
this.onError,
});

@override
Expand Down Expand Up @@ -253,6 +255,14 @@ class _ArtworkWidgetState extends State<ArtworkWidget> with LoadingItemsDelayMix
);
}),
errorBuilder: (context, error, stackTrace) {
if (error.toString().contains('Invalid image data')) {
final fp = widget.path;
if (fp != null) {
File(fp).tryDeleting();
FileImage(File(fp)).evict();
}
}
widget.onError?.call();
return getStockWidget(
stackWithOnTopWidgets: false,
);
Expand Down
192 changes: 192 additions & 0 deletions lib/youtube/controller/youtube_channel_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:newpipeextractor_dart/newpipeextractor_dart.dart';

import 'package:namida/controller/connectivity.dart';
import 'package:namida/controller/current_color.dart';
import 'package:namida/core/extensions.dart';
import 'package:namida/core/icon_fonts/broken_icons.dart';
import 'package:namida/core/translations/language.dart';
import 'package:namida/ui/widgets/custom_widgets.dart';
import 'package:namida/youtube/class/youtube_subscription.dart';
import 'package:namida/youtube/controller/youtube_controller.dart';
import 'package:namida/youtube/controller/youtube_subscriptions_controller.dart';

enum VideosSorting {
date,
views,
duration,
}

mixin YoutubeChannelController<T extends StatefulWidget> on State<T> {
late final ScrollController uploadsScrollController = ScrollController();
YoutubeSubscription? channel;
final streamsList = <StreamInfoItem>[];
({DateTime oldest, DateTime newest})? streamsPeakDates;

late final _defaultSorting = VideosSorting.date;
late final _defaultSortingByTop = true;
late final sorting = _defaultSorting.obs;
late final sortingByTop = _defaultSortingByTop.obs;

bool isLoadingInitialStreams = true;
final isLoadingMoreUploads = false.obs;
final lastLoadingMoreWasEmpty = false.obs;

late final sortWidget = SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Obx(
() => Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
...VideosSorting.values.map(
(e) {
final details = sortToTextAndIcon(e);
final enabled = sorting.value == e;
final itemsColor = enabled ? Colors.white.withOpacity(0.8) : null;
return NamidaInkWell(
animationDurationMS: 200,
borderRadius: 6.0,
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
margin: const EdgeInsets.symmetric(horizontal: 3.0),
bgColor: enabled ? CurrentColor.inst.color : context.theme.cardColor,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
enabled
? Obx(
() => StackedIcon(
baseIcon: details.$2,
secondaryIcon: sortingByTop.value ? Broken.arrow_down_2 : Broken.arrow_up_3,
iconSize: 20.0,
secondaryIconSize: 10.0,
blurRadius: 4.0,
baseIconColor: itemsColor,
// secondaryIconColor: enabled ? context.theme.colorScheme.background : null,
),
)
: Icon(
details.$2,
size: 20.0,
color: null,
),
const SizedBox(width: 4.0),
Text(
details.$1,
style: context.textTheme.displayMedium?.copyWith(color: itemsColor),
),
],
),
onTap: () => setState(
() => sortStreams(sort: e, sortingByTop: enabled ? !sortingByTop.value : null),
),
);
},
),
],
),
),
);

@override
void dispose() {
uploadsScrollController.dispose();
isLoadingMoreUploads.close();
lastLoadingMoreWasEmpty.close();
sorting.close();
super.dispose();
}

void updatePeakDates(List<StreamInfoItem> streams) {
int oldest = (streamsPeakDates?.oldest ?? DateTime.now()).millisecondsSinceEpoch;
int newest = (streamsPeakDates?.newest ?? DateTime(0)).millisecondsSinceEpoch;
streams.loop((e, _) {
final d = e.date;
if (d != null) {
final ms = d.millisecondsSinceEpoch;
if (ms < oldest) {
oldest = ms;
} else if (ms > newest) {
newest = ms;
}
}
});
streamsPeakDates = (oldest: DateTime.fromMillisecondsSinceEpoch(oldest), newest: DateTime.fromMillisecondsSinceEpoch(newest));
}

Future<void> fetchChannelStreams(YoutubeSubscription sub) async {
final st = await YoutubeController.inst.getChannelStreams(sub.channelID);
updatePeakDates(st);
YoutubeSubscriptionsController.inst.refreshLastFetchedTime(sub.channelID);
setState(() {
isLoadingInitialStreams = false;
if (sub.channelID == channel?.channelID) {
streamsList.addAll(st);
trySortStreams();
}
});
}

Future<void> fetchStreamsNextPage(YoutubeSubscription? sub) async {
if (isLoadingMoreUploads.value) return;
if (lastLoadingMoreWasEmpty.value) return;

isLoadingMoreUploads.value = true;
final st = await YoutubeController.inst.getChannelStreamsNextPage();
updatePeakDates(st);
isLoadingMoreUploads.value = false;
if (st.isEmpty) {
if (ConnectivityController.inst.hasConnection) lastLoadingMoreWasEmpty.value = true;
return;
}
if (sub?.channelID == channel?.channelID) {
setState(() {
streamsList.addAll(st);
trySortStreams();
});
}
}

void trySortStreams() {
if (sorting.value != _defaultSorting || sortingByTop.value != _defaultSortingByTop) {
sortStreams(jumpToZero: false);
}
}

void sortStreams({List<StreamInfoItem>? streams, VideosSorting? sort, bool? sortingByTop, bool jumpToZero = true}) {
sort ??= sorting.value;
streams ??= streamsList;
sortingByTop ??= this.sortingByTop.value;
switch (sort) {
case VideosSorting.date:
sortingByTop ? streams.sortByReverse((e) => e.date ?? DateTime(0)) : streams.sortBy((e) => e.date ?? DateTime(0));
break;

case VideosSorting.views:
sortingByTop ? streams.sortByReverse((e) => e.viewCount ?? 0) : streams.sortBy((e) => e.viewCount ?? 0);
break;

case VideosSorting.duration:
sortingByTop ? streams.sortByReverse((e) => e.duration ?? Duration.zero) : streams.sortBy((e) => e.duration ?? Duration.zero);
break;

default:
null;
}
sorting.value = sort;
this.sortingByTop.value = sortingByTop;

if (jumpToZero && uploadsScrollController.hasClients) uploadsScrollController.jumpTo(0);
}

(String, IconData) sortToTextAndIcon(VideosSorting sort) {
switch (sort) {
case VideosSorting.date:
return (lang.DATE, Broken.calendar);
case VideosSorting.views:
return (lang.VIEWS, Broken.eye);
case VideosSorting.duration:
return (lang.DURATION, Broken.timer_1);
}
}
}
10 changes: 6 additions & 4 deletions lib/youtube/controller/youtube_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,12 @@ class YoutubeController {
if (checkFromStorage) {
final file = File('${AppDirs.YT_METADATA_TEMP}$id.txt');
final res = file.readAsJsonSync();
try {
final strInfo = StreamInfoItem.fromMap(res);
return strInfo;
} catch (_) {}
if (res != null) {
try {
final strInfo = StreamInfoItem.fromMap(res);
return strInfo;
} catch (_) {}
}
}
return null;
}
Expand Down
2 changes: 1 addition & 1 deletion lib/youtube/controller/youtube_import_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class YoutubeImportController {
res.loop((e, index) {
final valInMap = YoutubeSubscriptionsController.inst.subscribedChannels[e.id];
YoutubeSubscriptionsController.inst.subscribedChannels[e.id] = YoutubeSubscription(
title: valInMap != null && valInMap.title == '' ? e.title : valInMap?.title,
title: valInMap != null && valInMap.title == '' ? e.title : valInMap?.title ?? e.title,
channelID: e.id,
subscribed: true,
lastFetched: valInMap?.lastFetched,
Expand Down
Loading

0 comments on commit db22eaa

Please sign in to comment.