Channel folder hierarchy (Tartube parity 1.2)
Users can now organize channels into named folders independent of the
underlying platform groupings. v1 ships single-level (no nesting) since
that's where ~90% of the organizational value lives — nested folders +
drag-to-reparent + per-folder cascading options are deferred to v2 once
we have real-world feedback on what the workflow needs.
Data layer
- New `folders` SQLite table: id, name (UNIQUE), position, created_at.
- New `channel_assignments` table: (platform, handle) → folder_id with
ON DELETE CASCADE so deleting a folder cleanly unfiles its channels.
- Database methods: create_folder, rename_folder, delete_folder (with
PRAGMA foreign_keys ON for the cascade), list_folders,
set_channel_folder, get_all_channel_assignments.
- New `FolderRecord` struct (id/name/position) is `Serialize` so it
rides on the /api/library response.
Library
- `Channel` gains `folder_id: Option<i64>` (None = Unfiled).
- New `library::apply_channel_folders` mirror of `apply_channel_options`
populates it from the bulk DB map after every scan / rescan.
Web API
- `POST /api/folders` create with `{name}` → `{id}`.
- `POST /api/folders/:id/rename` rename with `{name}`.
- `DELETE /api/folders/:id` cascade-deletes assignments.
- `POST /api/channels/:platform/:handle/folder` upsert/clear via
`{folder_id: <i64|null>}`. All endpoints bump the library ETag.
- `/api/library` response now carries a `folders: [...]` array so the
client can render the sidebar grouping without a second round trip.
Web UI
- Sidebar gains a "📁 Folders" section above the platform groupings.
Each folder lists its member channels (with platform icon prefix);
channels with `folder_id` are pulled out of their platform section
to avoid duplication. Unfiled channels keep platform grouping.
- "manage" / "new" button next to the Folders heading opens a modal
with the full folder list (member counts inline) plus rename /
delete buttons and a Create input.
- New per-channel sub-action "📁 Move to folder…" opens a small
picker modal with one row per folder + "— Unfiled —". Current
assignment highlighted.
Desktop UI
- Sidebar refactored to build a `Vec<SidebarItem>` ordered list
(FolderHeader / FolderManageBtn / PlatformHeader / Channel) and
iterate it once, instead of the previous flat
`for i in 0..library.len()` loop. Folder section renders above
the platform sections; channels with `folder_id` are skipped from
the platform-grouped loop so they appear in exactly one place.
- New `folder_manager_window` + `move_to_folder_window` egui windows
cover create / rename (via the create-buffer field) / delete and
per-channel folder picking. Right-click context menu on a channel
gains "📁 Move to folder…".
- App struct gains: `folders`, `show_folder_manager`,
`folder_create_buffer`, `show_move_to_folder`, `move_to_folder_target`.
Tartube spec checklist
- Folder hierarchy box ticked (with the v2 deferrals noted in the
inline annotation).
55 unit tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
8281246420
commit
4b0e4b3b07
6 changed files with 652 additions and 21 deletions
|
|
@ -124,6 +124,11 @@ pub struct Channel {
|
|||
/// scan by reading the DB once via
|
||||
/// [`crate::database::Database::get_all_channel_options`].
|
||||
pub download_options: DownloadOptions,
|
||||
/// Optional folder grouping. `None` means the channel is "Unfiled" and
|
||||
/// appears under its platform's heading. Populated post-scan by
|
||||
/// [`apply_channel_folders`] from the
|
||||
/// [`crate::database::Database::get_all_channel_assignments`] map.
|
||||
pub folder_id: Option<i64>,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
|
|
@ -140,6 +145,18 @@ impl Channel {
|
|||
}
|
||||
}
|
||||
|
||||
/// Mutate `channels` in place, filling each one's [`Channel::folder_id`]
|
||||
/// from the supplied `(platform_dir_name, handle) → folder_id` map.
|
||||
pub fn apply_channel_folders(
|
||||
channels: &mut [Channel],
|
||||
folder_map: &std::collections::HashMap<(String, String), i64>,
|
||||
) {
|
||||
for ch in channels {
|
||||
let key = (ch.platform.dir_name().to_string(), ch.name.clone());
|
||||
ch.folder_id = folder_map.get(&key).copied();
|
||||
}
|
||||
}
|
||||
|
||||
/// Mutate `channels` in place, filling each one's [`Channel::download_options`]
|
||||
/// from the supplied `(platform_dir_name, handle) → options_json` map.
|
||||
///
|
||||
|
|
@ -222,6 +239,7 @@ pub fn scan_channels(youtube_root: &Path) -> Vec<Channel> {
|
|||
total_videos_cached,
|
||||
total_size_cached,
|
||||
download_options: DownloadOptions::default(),
|
||||
folder_id: None,
|
||||
})
|
||||
})
|
||||
.into_iter()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue