瀏覽代碼

Add favorite folders to Musicify

Add support for pinning folders as Favorites in Musicify. This introduces a sidebar Favorites section, favorite/unfavorite controls in folder views, persistence through module storage, and direct navigation that preserves folder backtracking from a pinned folder.
Toby Chui 3 周之前
父節點
當前提交
b30e98a1ea
共有 2 個文件被更改,包括 127 次插入 和 3 次删除
  1. 40 3
      src/web/Musicify/index.html
  2. 87 0
      src/web/Musicify/musicify.js

+ 40 - 3
src/web/Musicify/index.html

@@ -636,6 +636,7 @@
         .btn-primary:hover { background: var(--accent2); }
         .btn-secondary { background: var(--bg3); color: var(--text2); border: 1px solid var(--border); }
         .btn-secondary:hover { background: var(--hover); color: var(--text); }
+        .btn-secondary.fav-active { color: var(--accent); border-color: var(--accent); }
         .btn-danger    { background: rgba(239,68,68,.15); color: #f87171; border: 1px solid rgba(239,68,68,.25); }
         .btn-danger:hover { background: rgba(239,68,68,.25); }
 
@@ -957,6 +958,20 @@
                     <i class="ui history icon"></i> Recently Added
                 </a>
 
+                <template x-if="favoriteFolders.length > 0">
+                    <div>
+                        <div class="sidebar-section-label" style="margin-top:8px;">Favorites</div>
+                        <template x-for="fav in favoriteFolders" :key="fav.path">
+                            <div class="playlist-item" :class="{active: view==='folders' && folderPath===fav.path}"
+                                 x-on:click="openFavoriteFolder(fav)" :title="fav.path">
+                                <i class="ui heart icon" style="flex-shrink:0;"></i>
+                                <span class="pl-name" x-text="fav.name"></span>
+                                <span class="pl-del" x-on:click.stop="removeFavoriteFolder(fav.path, $event)">&#10005;</span>
+                            </div>
+                        </template>
+                    </div>
+                </template>
+
                 <div class="sidebar-section-label" style="margin-top:8px;">Playlists</div>
 
                 <template x-for="pl in playlists" :key="pl.name">
@@ -1128,11 +1143,23 @@
                 </div>
                 <div class="content-body">
                     <!-- Back button -->
-                    <template x-if="folderStack.length > 0">
-                        <div style="margin-bottom:12px;">
-                            <button class="btn btn-secondary" x-on:click="folderBack()">
+                    <template x-if="folderStack.length > 0 || (canFavoriteCurrentFolder() && !(folderContents.songs && folderContents.songs.length))">
+                        <div style="margin-bottom:12px;display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;">
+                            <button class="btn btn-secondary" x-show="folderStack.length > 0" x-on:click="folderBack()">
                                 <i class="ui arrow left icon"></i> Back
                             </button>
+                            <template x-if="!(folderContents.songs && folderContents.songs.length)">
+                                <span style="margin-left:auto;">
+                                    <button class="btn btn-secondary" style="padding:6px 14px;font-size:12px;"
+                                    :class="isFolderFavorited(folderPath) ? 'fav-active' : ''"
+                                    x-show="canFavoriteCurrentFolder()"
+                                    :title="isFolderFavorited(folderPath) ? 'Remove this folder from Favorites' : 'Pin this folder to the Favorites list'"
+                                    x-on:click="toggleFavoriteFolder()">
+                                    <i class="ui icon" :class="isFolderFavorited(folderPath) ? 'heart' : 'heart outline'"></i>
+                                    <span x-text="isFolderFavorited(folderPath) ? 'Favorited' : 'Mark as Favorite'"></span>
+                                </button>
+                                </span>
+                            </template>
                         </div>
                     </template>
 
@@ -1158,10 +1185,20 @@
                                 <div class="section-title" style="font-size:13px;margin-bottom:0;">
                                     <span x-text="folderContents.songs.length"></span> tracks
                                 </div>
+                                <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
+                                <button class="btn btn-secondary" style="padding:6px 14px;font-size:12px;"
+                                    :class="isFolderFavorited(folderPath) ? 'fav-active' : ''"
+                                    x-show="canFavoriteCurrentFolder()"
+                                    :title="isFolderFavorited(folderPath) ? 'Remove this folder from Favorites' : 'Pin this folder to the Favorites list'"
+                                    x-on:click="toggleFavoriteFolder()">
+                                    <i class="ui icon" :class="isFolderFavorited(folderPath) ? 'heart' : 'heart outline'"></i>
+                                    <span x-text="isFolderFavorited(folderPath) ? 'Favorited' : 'Mark as Favorite'"></span>
+                                </button>
                                 <button class="btn btn-primary" style="padding:6px 14px;font-size:12px;"
                                     x-on:click="playList(folderContents.songs, 0)">
                                     <i class="ui play icon"></i> Play all
                                 </button>
+                                </div>
                             </div>
                             <div class="song-list">
                                 <div class="song-list-header">

+ 87 - 0
src/web/Musicify/musicify.js

@@ -28,6 +28,9 @@ function musicifyApp() {
         folderContents: { folders: [], songs: [] },
         musicLibraries: [],     // [ { label, root } ] from listRoots.js
 
+        // ── Favorite (pinned) folders ───────────────────────────────────────
+        favoriteFolders: [],    // [ { path, name, root } ] persisted per user
+
         // ── Artists ─────────────────────────────────────────────────────────
         artists: [],
         selectedArtist: null,   // full artist object for dedicated artist songs view
@@ -280,6 +283,9 @@ function musicifyApp() {
             // Load playlists for sidebar
             this._loadPlaylists();
 
+            // Load pinned folders for the sidebar Favorites section
+            this._loadFavoriteFolders();
+
             // Pre-load available music library roots for the folder-view switcher
             this._loadMusicLibraries();
 
@@ -445,6 +451,87 @@ function musicifyApp() {
             return crumbs;
         },
 
+        // ════════════════════════════════════════════════════════════════════
+        //  FAVORITE (PINNED) FOLDERS
+        // ════════════════════════════════════════════════════════════════════
+        _loadFavoriteFolders() {
+            const self = this;
+            ao_module_storage.loadStorage("Musicify", "favorites", function(val) {
+                if (!val) return;
+                try {
+                    var list = JSON.parse(val);
+                    if (Array.isArray(list)) {
+                        self.favoriteFolders = list.filter(f => f && f.path);
+                    }
+                } catch(e) {}
+            });
+        },
+
+        _saveFavoriteFolders() {
+            ao_module_storage.setStorage("Musicify", "favorites", JSON.stringify(this.favoriteFolders));
+        },
+
+        // The library root itself is always reachable from the Folders tab, so
+        // there is nothing to gain from pinning it.
+        canFavoriteCurrentFolder() {
+            return !!this.folderPath && this.folderPath !== this.folderRoot;
+        },
+
+        isFolderFavorited(path) {
+            var p = path || this.folderPath;
+            return this.favoriteFolders.some(f => f.path === p);
+        },
+
+        toggleFavoriteFolder() {
+            if (!this.canFavoriteCurrentFolder()) return;
+            var path = this.folderPath;
+            var name = path.split('/').filter(s => s !== '').pop() || path;
+            if (this.isFolderFavorited(path)) {
+                this.favoriteFolders = this.favoriteFolders.filter(f => f.path !== path);
+                this._saveFavoriteFolders();
+                this._showToast('Removed "' + name + '" from Favorites');
+            } else {
+                this.favoriteFolders.push({ path: path, name: name, root: this.folderRoot });
+                this._saveFavoriteFolders();
+                this._showToast('Added "' + name + '" to Favorites');
+            }
+        },
+
+        removeFavoriteFolder(path, event) {
+            if (event) event.stopPropagation();
+            var fav = this.favoriteFolders.find(f => f.path === path);
+            this.favoriteFolders = this.favoriteFolders.filter(f => f.path !== path);
+            this._saveFavoriteFolders();
+            this._showToast('Removed "' + ((fav && fav.name) || 'folder') + '" from Favorites');
+        },
+
+        // Open the Folders view straight at a pinned folder. The ancestor paths
+        // between the library root and the target are pushed onto the folder
+        // stack so the Back button still walks back up the tree.
+        openFavoriteFolder(fav) {
+            if (!fav || !fav.path) return;
+            this.view = 'folders';
+            this.searchQuery = '';
+            this.artistDetailOpen = false;
+            this.selectedArtist = null;
+            if (window.innerWidth <= 768) this.sidebarOpen = false;
+            if (this.showNowPlaying) this.closeNowPlaying();
+
+            var root = fav.root || this.folderRoot;
+            this.folderRoot = root;
+            this.folderStack = [];
+            if (fav.path.indexOf(root + '/') === 0) {
+                var rest = fav.path.substring(root.length + 1).split('/');
+                var acc = root;
+                this.folderStack.push(acc);
+                for (var i = 0; i < rest.length - 1; i++) {
+                    acc = acc + '/' + rest[i];
+                    this.folderStack.push(acc);
+                }
+            }
+            this.loadFolder(fav.path);
+        },
+
         // ════════════════════════════════════════════════════════════════════
         //  ARTISTS
         // ════════════════════════════════════════════════════════════════════