Skip to content

Commit

Permalink
core: folders logic rewrite
Browse files Browse the repository at this point in the history
+ deep directories extraction is now dropped, more convenient
+ pressing folder menu will show dialog with direct tracks only
+ long pressing folder tile or menu will show dialog with recursive tracks
ref: #151
  • Loading branch information
MSOB7YY committed May 22, 2024
1 parent a7a7bd4 commit a8932c0
Show file tree
Hide file tree
Showing 12 changed files with 170 additions and 132 deletions.
98 changes: 33 additions & 65 deletions lib/class/folder.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
// ignore_for_file: unnecessary_this

import 'dart:io';

import 'package:namida/class/track.dart';
import 'package:namida/controller/indexer_controller.dart';
import 'package:namida/core/constants.dart';
import 'package:namida/core/extensions.dart';

final _pathSeparator = Platform.pathSeparator;

class Folder {
final String path;
late final String folderName;

const Folder(this.path);
Folder(this.path) : folderName = path.pathReverseSplitter(_pathSeparator);

@override
bool operator ==(other) {
Expand All @@ -27,95 +31,59 @@ class Folder {

extension FolderUtils on Folder {
String get parentPath => path.withoutLast(Platform.pathSeparator);
String get folderNameRaw => path.split(Platform.pathSeparator).last;

/// Checks if any other folders inside library have the same name.
///
/// Can be heplful to display full path in such case.
bool get hasSimilarFolderNames {
return Indexer.inst.mainMapFolders.keys.where((element) => element.folderNameRaw == folderNameRaw).length > 1;
}

String get folderName {
if (kStoragePaths.contains(path)) {
return path.formatPath();
}

final parentFolder = getParentFolder()?.path;
if (parentFolder != null) {
final parts = path.replaceFirst(parentFolder, '').split(Platform.pathSeparator);
parts.removeWhere((element) => element == '');
final isNested = parts.length > 1;
if (isNested) {
final nestedPath = parts.join(Platform.pathSeparator);
return nestedPath.formatPath();
int count = 0;
for (final k in Indexer.inst.mainMapFolders.keys) {
if (k.folderName == folderName) {
count++;
if (count > 1) return true;
}
}

return folderNameRaw;
return false;
}

List<Track> get tracks => Indexer.inst.mainMapFolders[this] ?? [];
Iterable<Track> get tracksRecusive sync* {
for (final e in Indexer.inst.mainMapFolders.entries) {
if (e.key.path.startsWith(this.path)) {
yield* e.value;
}
}
}

/// [fullyFunctional] returns the first parent folder that has different subfolders obtained by [getDirectoriesInside].
///
/// Otherwise, it checks for the first parent folder that exists in [Indexer.inst.mainMapFolders].
/// less accurate but more performant, since its being used by [folderName].
Folder? getParentFolder({bool fullyFunctional = false}) {
/// checks for the first parent folder that exists in [Indexer.mainMapFolders].
Folder? getParentFolder() {
final parts = path.split(Platform.pathSeparator);
parts.removeLast();

while (parts.isNotEmpty) {
parts.removeLast();
final f = Folder(parts.join(Platform.pathSeparator));
if (f.tracks.isNotEmpty) return f;
}
if (fullyFunctional) {
if (!kStoragePaths.contains(path)) {
if (Folder(parentPath).getDirectoriesInside().length > 1) {
return Folder(parentPath);
}
}
if (Indexer.inst.mainMapFolders[f] != null) return f;
parts.removeLast();
}

// if (fullyFunctional) {
// final newParts = path.split(Platform.pathSeparator);
// final currentDirs = getDirectoriesInside();

// while (newParts.isNotEmpty) {
// newParts.removeLast();
// final f = Folder(newParts.join(Platform.pathSeparator));

// if (!f.getDirectoriesInside().isEqualTo(currentDirs)) {
// return f;
// }
// }
// }

return null;
}

/// Gets directories inside [this] folder, automatically handles nested folders.
List<Folder> getDirectoriesInside() {
final allFolders = Indexer.inst.mainMapFolders.keys;
final foldersMap = Indexer.inst.mainMapFolders;
final allInside = <Folder>[];

allInside.addAll(
allFolders.where((key) {
final f = key.path.split(Platform.pathSeparator);
f.removeLast();
String newPath() => f.join(Platform.pathSeparator);
bool isSamePath() => newPath() == path;
final splitsCount = this.path.split(Platform.pathSeparator).length;

/// maintains nested loops (folder doesnt exist in library but subfolder exists).
while (f.isNotEmpty && !isSamePath() && Folder(newPath()).tracks.isEmpty) {
f.removeLast();
for (final folder in foldersMap.keys) {
if (folder.path.startsWith(this.path)) {
final folderSplitsCount = folder.path.split(Platform.pathSeparator).length;
if (folderSplitsCount == splitsCount + 1) {
allInside.add(folder);
}

return isSamePath();
}),
);

allInside.sortBy((e) => e.folderName.toLowerCase());
}
}

return allInside;
}
Expand Down
17 changes: 2 additions & 15 deletions lib/controller/file_browser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -269,25 +269,12 @@ class _NamidaFileBrowserState<T extends FileSystemEntity> extends State<_NamidaF
late final _scrollController = ScrollController();
late final _pathSplitsScrollController = ScrollController();

static String _pathReverseSplitter(String path, String until) {
String extension = ''; // represents the latest part
int latestIndex = path.length - 1;

while (latestIndex > 0) {
final char = path[latestIndex];
if (char == until) break;
extension = char + extension;
latestIndex--;
}
return extension;
}

static String _pathToName(String path) {
return _pathReverseSplitter(path, _pathSeparator);
return path.pathReverseSplitter(_pathSeparator);
}

static String _pathToExtension(String path) {
return _pathReverseSplitter(path, '.').toLowerCase();
return path.pathReverseSplitter('.').toLowerCase();
}

Isolate? _isolate;
Expand Down
95 changes: 75 additions & 20 deletions lib/controller/folders_controller.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import 'dart:io';

import 'package:get/get.dart';

import 'package:namida/class/folder.dart';
import 'package:namida/class/track.dart';
import 'package:namida/controller/scroll_search_controller.dart';
import 'package:namida/controller/search_sort_controller.dart';
import 'package:namida/controller/settings_controller.dart';
import 'package:namida/core/constants.dart';
import 'package:namida/core/enums.dart';
import 'package:namida/core/extensions.dart';

class Folders {
static Folders get inst => _instance;
static final Folders _instance = Folders._internal();
Folders._internal();
static final Folders inst = Folders._();
Folders._();

final Rxn<Folder> currentFolder = Rxn<Folder>();

Expand All @@ -28,6 +29,8 @@ class Folders {
/// Highlights the track that is meant to be navigated to after calling [goToFolder].
final RxnInt indexToScrollTo = RxnInt();

double _latestScrollOffset = 0;

/// Indicates wether the navigator can go back at this point.
/// Returns true only if at home, otherwise will call [stepOut] and return false.
bool onBackButton() {
Expand All @@ -38,45 +41,97 @@ class Folders {
return true;
}

void stepIn(Folder? folder, {Track? trackToScrollTo}) {
void stepIn(Folder? folder, {Track? trackToScrollTo, double jumpTo = 0}) {
if (folder == null || folder.path == '') {
isHome.value = true;
isInside.value = false;
currentFolder.value = null;
_scrollJump(jumpTo);
return;
}
isHome.value = false;
isInside.value = true;

final dirInside = folder.getDirectoriesInside();
if (isHome.value != false) isHome.value = false;
if (isInside.value != true) isInside.value = true;

if (dirInside.length == 1 && folder.tracks.isEmpty) {
stepIn(dirInside.first);
return;
}
_saveScrollOffset();

currentFolderslist.value = dirInside;
final dirInside = folder.getDirectoriesInside();

currentFolderslist.value = dirInside;
currentFolder.value = folder;

if (trackToScrollTo != null) {
indexToScrollTo.value = folder.tracks.indexOf(trackToScrollTo);
}
if (LibraryTab.folders.scrollController.hasClients) {
LibraryTab.folders.scrollController.jumpTo(0);
}
currentFolder.value?.tracks.sortByAlts(SearchSortController.inst.getMediaTracksSortingComparables(MediaType.folder));
_scrollJump(jumpTo);
}

void stepOut() {
Folder? folder;
if (settings.enableFoldersHierarchy.value) {
folder = currentFolder.value?.getParentFolder(fullyFunctional: true);
folder = currentFolder.value?.getParentFolder();
}
indexToScrollTo.value = null;
stepIn(folder);
stepIn(folder, jumpTo: _latestScrollOffset);
}

void onFirstLoad() {
stepIn(Folder(settings.defaultFolderStartupLocation.value));
if (settings.enableFoldersHierarchy.value) {
final startupPath = settings.defaultFolderStartupLocation.value;
stepIn(Folder(startupPath));
}
}

void onFoldersHierarchyChanged(bool enabled) {
Folders.inst.isHome.value = true;
Folders.inst.isInside.value = false;
}

void _saveScrollOffset() {
try {
_latestScrollOffset = LibraryTab.folders.scrollController.offset;
} catch (_) {
_latestScrollOffset = 0;
}
}

void _scrollJump(double to) {
if (LibraryTab.folders.scrollController.hasClients) {
try {
LibraryTab.folders.scrollController.jumpTo(to);
} catch (_) {}
}
}

/// Generates missing folders in between
void onMapChanged(Map<Folder, List<Track>> map) {
final newFolders = <MapEntry<Folder, List<Track>>>[];

void recursiveIf(bool Function() fn) {
if (fn()) recursiveIf(fn);
}

for (final k in map.keys) {
final f = k.path.split(Platform.pathSeparator);
f.removeLast();

recursiveIf(() {
if (f.length > 3) {
final newPath = f.join(Platform.pathSeparator);
if (kStoragePaths.contains(newPath)) {
f.removeLast();
return true;
}
if (map[Folder(newPath)] == null) {
newFolders.add(MapEntry(Folder(newPath), []));
f.removeLast();
return true;
}
}
return false;
});
}
map.addEntries(newFolders);
map.sortBy((e) => e.key.folderName.toLowerCase());
}
}
6 changes: 4 additions & 2 deletions lib/controller/indexer_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import 'package:namida/class/split_config.dart';
import 'package:namida/class/track.dart';
import 'package:namida/class/video.dart';
import 'package:namida/controller/current_color.dart';
import 'package:namida/controller/folders_controller.dart';
import 'package:namida/controller/navigator_controller.dart';
import 'package:namida/controller/player_controller.dart';
import 'package:namida/controller/scroll_search_controller.dart';
Expand All @@ -38,7 +39,6 @@ class Indexer {

final RxBool isIndexing = false.obs;

final currentTracksPathsBeingExtracted = <String>[].obs;
final RxSet<String> allAudioFiles = <String>{}.obs;
final RxInt filteredForSizeDurationTracks = 0.obs;
final RxInt duplicatedTracksLength = 0.obs;
Expand All @@ -56,7 +56,7 @@ class Indexer {
final mainMapAlbumArtists = LibraryItemMap();
final mainMapComposer = LibraryItemMap();
final mainMapGenres = LibraryItemMap();
final RxMap<Folder, List<Track>> mainMapFolders = <Folder, List<Track>>{}.obs;
final mainMapFolders = <Folder, List<Track>>{}.obs;

final RxList<Track> tracksInfoList = <Track>[].obs;

Expand Down Expand Up @@ -238,6 +238,7 @@ class Indexer {
mainMapFolders.addForce(tr.folder, tr);
});

Folders.inst.onMapChanged(mainMapFolders);
_sortAll();
sortMediaTracksSubLists(MediaType.values);
}
Expand Down Expand Up @@ -393,6 +394,7 @@ class Indexer {
mainMapFolders[e]?.sortByAlts(folderSorters);
});

Folders.inst.onMapChanged(mainMapFolders);
_sortAll();
}

Expand Down
19 changes: 19 additions & 0 deletions lib/core/extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -710,3 +710,22 @@ extension StatefulWUtils<T extends StatefulWidget> on State<T> {
}
}
}

extension StringPathUtils on String {
/// keeps reverse collecting string until [until] is matched.
/// useful to exract extensions or filenames.
String pathReverseSplitter(String until) {
String extension = ''; // represents the latest part
final path = this;
int latestIndex = path.length - 1;
if (latestIndex >= 0 && path[latestIndex] == until) latestIndex - 1;

while (latestIndex > 0) {
final char = path[latestIndex];
if (char == until) break;
extension = char + extension;
latestIndex--;
}
return extension;
}
}
Loading

0 comments on commit a8932c0

Please sign in to comment.