Compare commits

...

591 Commits

Author SHA1 Message Date
Anna Schumaker 45d2422be3 gui: Refilter playlists when adding tracks
I was running into a situation where new tracks added to a playlist were
showing up even if they didn't match the current filter text.  Let's fix
this by refiltering the current playlist when new tracks are added to
it.

Fixes #114: New tracks added to filtered view
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-08-28 08:58:45 -04:00
Anna Schumaker 54138d8814 Ocarina 6.5.10-rc
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-08-20 08:34:15 -04:00
Anna Schumaker 6ab3cff28f gui/playlist: Support drag and drop for adding tracks to playlists
Dragging onto a sidebar row without a valid playlist will prompt the
user to create a new playlist.  I could probably add a "New Playlist"
row at some point, but this is easier for now :)

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-05-16 07:35:28 -04:00
Anna Schumaker 0972e027ed gui/treeview: Connect to the drag-and-drop signals
We only support drag-and-drop one row at a time, even though multiple
rows can be selected in the treeview.  If this is a problem, then we can
figure it out later!

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-05-16 07:35:28 -04:00
Anna Schumaker 77efa0c631 gui/model: Configure the gui model as a drag source
We'll need to do this to enable drag and drop to reorder playlists.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-05-16 07:35:28 -04:00
Anna Schumaker a9970c455f gui/sidebar: Add a function for getting an iter from x, y coordinates
This will be used for drag and drop, so that we can figure out what
playlist tracks are being added to.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-05-16 07:35:28 -04:00
Anna Schumaker fc5e6eb043 gui/treeview: Add a function for accessing the tree selection
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-05-16 07:35:28 -04:00
Anna Schumaker a41652ab28 core/playlist: Add a function for manually rearranging playlists
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-05-16 07:35:28 -04:00
Anna Schumaker eca857cb3b core/playlists: Pass sort fields to playlist_generic_alloc()
This lets us configure sorting individually for each allocated playlist.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-05-16 07:35:28 -04:00
Anna Schumaker d7fb67ed51 core/playlists: Remove unsorted playlist_generic_init()
We can just pass 0 to the sorted function to indicate that the playlist
shouldn't be sorted by default.  Let's also take this chance to rename
playlist_generic_init_sorted() -> playlist_generic_init()

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-05-16 07:35:28 -04:00
Anna Schumaker 7df129d533 core/playlists: Accept an argument list for playlists initial sort order
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-05-16 07:35:28 -04:00
Anna Schumaker a4cdac7f22 core/playlists/user: Don't sort user playlists by default
Users should be able to add tracks in the order they want.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-05-16 07:35:28 -04:00
Anna Schumaker d5b0752497 core/playlists/generic: Add album sort to the default fields
If we have two albums by the same artist in the same year (such as a CD1
or CD2 postfix), then we'll end up mixing them together when sorting.
Fix this by changing the default sort order to
Artist -> Year -> Album -> Track Number

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-05-16 07:35:28 -04:00
Anna Schumaker 5fb46dc663 Ocarina 6.5.9
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-05-11 11:43:17 -04:00
Anna Schumaker d149289e00 gui/audio: Close popover menu after 10 seconds
And don't touch the pause count so we don't surprise the user later.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-05-02 11:13:32 -04:00
Anna Schumaker f167f968ba gui/audio: Add a popover menu to clear automatic pausing
The popover is shown whenever the user pauses manually with automatic
pausing enabled.  This will give the user a chance to disable pausing if
it is no longer needed.

Implements #113: Cancel "pause after" configuration when user manually pauses
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-05-02 10:45:23 -04:00
Anna Schumaker 94f3a7f387 Ocarina 6.5.9-rc
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-04-20 09:16:05 -04:00
Anna Schumaker 60e6e2a9eb gui/audio: Remove old pause_after combobox
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-03-02 16:42:55 -05:00
Anna Schumaker 1836104f40 gui/audio: Respond to the user editing the pause entry
We have to disable the up and down buttons when typing in the entry in
case the user decides to type a minus or plus sign.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-03-02 16:42:31 -05:00
Anna Schumaker cd7364300e gui/audio: Wire up the "spin button" increment and decrement buttons
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-03-02 16:42:21 -05:00
Anna Schumaker 994234caf2 gui/audio: Change down-button sensitivity based on pause count
The pause count can't go below -1, so disable the button once we reach
this limit.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-03-02 16:42:20 -05:00
Anna Schumaker af5bafb03e gui/audio: Set the pause entry text based on remaining tracks
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-03-02 16:40:48 -05:00
Anna Schumaker fd68cdf70a gui/audio: Add basic spin-button-like widgets
I set the "linked" property on the hbox to make everything look like a
single widget.  I'm doing this on my own to cut out the GtkAdjustment
and to just use the counter in the audio layer in its place.
Additionally, this will give me the ability to use "+" and "-" keyboard
shortcuts to activate the widgets.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-03-02 16:31:57 -05:00
Anna Schumaker 8a2c631a9b core/audio: Add a function for getting the current pause count
This will be needed by the gui to find the current count at any time,
without waiting for a callback.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-03-02 16:31:57 -05:00
Anna Schumaker e6ab06cf2b core/audio: Change audio_pause_after() to return a boolean
This will be useful later to let the gui know if their change had an
effect.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-03-02 16:31:57 -05:00
Anna Schumaker e6fb772cad Ocarina 6.5.8
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-03-02 16:18:27 -05:00
Anna Schumaker cce8666140 core/tags/album: Check for empty artist names before fetching album art
Otherwise we might crash when we attempt to look at ar_tokens[0].

Reported-by: Josh Larson <themutatedshrimp@gmail.com>
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-28 10:28:59 -05:00
Anna Schumaker edcba6a353 Ocarina 6.5.8-rc
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-27 08:29:21 -05:00
Anna Schumaker 59506d45e7 core/file: Rename cache_file_import() -> file_import()
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-27 08:25:40 -05:00
Anna Schumaker e1f13a7ef4 core/file: Create new functions for reading data from files
The readd(), readu(), and readhu() functions are all used to read
various forms of integers.  The readl() and readw() are for reading
either lines or individual whitespace-delimited words

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-21 16:01:15 -05:00
Anna Schumaker 111fcd4e72 core/file: Rename data_file_writef() -> file_writef()
For writing to files with printf-style formatting.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-21 08:19:19 -05:00
Anna Schumaker c5494811f4 core/file: Rework binary file IO
I rename cache_file_write() to file_write(), and implement the
corresponding file_read() for binary data.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-20 16:59:42 -05:00
Anna Schumaker 07f832ad26 core/file: Rename data_file_version() -> file_version()
Now it can be used by cache files, too.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-20 16:18:50 -05:00
Anna Schumaker 35d53855f5 core/file: Add write support to file_open()
I also take this opportunity to add a OPEN_WRITE_BINARY mode to support
writing binary data to files.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-20 16:18:49 -05:00
Anna Schumaker a848d5d03c core/file: Create a single file_open() function
This function currently only supports opening files for reading, but it
will soon be expanded to supporting writes as well.  To support binary
reads, I add a new OPEN_READ_BINARY open mode.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-20 16:16:23 -05:00
Anna Schumaker 0aaafcb5f7 core/file: Move versioning info into the main file structure
I'll eventually want to support text and binary files in both locations,
so cache files might need access to this too.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-20 16:16:22 -05:00
Anna Schumaker 8f13765b08 core/file: Make a common file_remove() function
This change means removing cache files is now supported.  As a bonus,
we try to remove empty subdirectories to keep down the clutter.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-20 16:09:42 -05:00
Anna Schumaker 84a1022bdf core/file: Create a single file_close() function
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-20 16:06:50 -05:00
Anna Schumaker 659aaff6a1 core/file: Move f_mode out of data files
This might also be useful for cache files to track their current state.
To help with accurate tracking, I take this opportunity to add a CLOSED
state that files default to.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-20 16:06:49 -05:00
Anna Schumaker 3736b6cf3b core/file: Create a single file_exists() function
We can use the single file_path() function to check if files exist, too.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-20 16:03:32 -05:00
Anna Schumaker 3fdf89c75e core/file: Create a single file_write_path() function
Similar to file_path(), I rely on the file ops struct to put files in
the right place.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-20 16:03:31 -05:00
Anna Schumaker 22854b2f25 core/file: Create a single file_path() function
I also introduce a "file_operations" struct that will be used in the
future to do slightly different things for cache and data files.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-20 16:01:14 -05:00
Anna Schumaker b6d45e666e core/file: Remove struct cache_file
And rework the init functions at the same time to reflect that data
files can now be placed under a subdirectory.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-20 15:39:33 -05:00
Anna Schumaker 842547d735 core/file: Move common items out of cache and data file structs
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-20 15:39:33 -05:00
Anna Schumaker 198fbf7f9b core/file: Create a new, unified file structure
We'll spend the next several patches slowly switching over to the new
way of accessing files.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-20 15:39:33 -05:00
Anna Schumaker 82280edfa2 core/file: Rename struct file -> struct data_file
I'm going to create a unified struct file that should be able to share
code between data and cache files.  The first step is to push the old
structure to the side so it can still be used in other places.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-20 15:39:33 -05:00
Anna Schumaker 48f79bdb49 core/file: Make name and subdir strings const for cache files
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-20 15:39:33 -05:00
Anna Schumaker d7d553b80f core/tags/album: Create a new structure to store name and subdir strings
I eventually want to make these fields const in the file code, so we
need a different way to manage and eventually free these strings.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-20 15:39:33 -05:00
Anna Schumaker a2854ef31e core/file: Remove the CACHE_FILE_INIT macro
It was only used by the testing code, so it's not seeing any use in the
real world.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-20 15:39:33 -05:00
Anna Schumaker d96e8ca1ca core/audio: Don't clear NULL audio_pipeline state
Doing so can cause a deadlock somewhere deep inside gstreamer.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2018-02-20 15:39:33 -05:00
Anna Schumaker 1940a31a77 Ocarina 6.5.7
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-12-21 09:16:15 -05:00
Anna Schumaker 07196a7cc8 gui/sidebar: Conditionally hide the rename menu option
We can only rename user-created playlists, so let's not even show this
option for the other playlist types.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-12-05 15:19:36 -05:00
Anna Schumaker 64e27c1221 gui/sidebar: Add a right click menu
Implements #112: Add sidebar right-click menu
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-12-05 15:19:24 -05:00
Anna Schumaker 31cda0eebd gui/sidebar: Add a function for selecting paths
Normally GTK will take care of this, but we'll need it for the
right-click menu so we might as well implement it.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-12-05 14:58:57 -05:00
Anna Schumaker 9b9be4e322 gui/sidebar: Rename playlists when pressing the Backspace key
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-12-05 14:58:57 -05:00
Anna Schumaker 1374a025e1 gui/sidebar: Split out __gui_sidebar_delete() into a new function
This gives me a function that I can reuse in the sidebar right-click
menu, and it cleans up the main keypress handler so more keys can be
added.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-12-05 14:58:57 -05:00
Anna Schumaker 21e1796b14 gui/sidebar: Add a function for finding the current playlist
This seems like a useful function to have, and we already need it for
deleting playlists.  Let's make it a real function so we don't need to
duplicate code.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-12-05 14:58:57 -05:00
Anna Schumaker b4347d5a34 ocarina.ui: Make sidebar shrinkable
Enabling this option resolves several "Negative content width" warnings
that I was seeing.  Normally I'd just ignore them, but these were
generating SIGTRAP during testing and causing the tests to fail
unnecessarily.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-12-05 14:57:42 -05:00
Anna Schumaker 386514ac5c Ocarina 6.5.7-rc
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-09-05 15:00:01 -04:00
Anna Schumaker 7558a32940 gui/ocarina: Load filepaths from the command line
If multiple paths are passed, then only load the first one to keep
things simple.

Implements #102: Allow outside tracks
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-09-05 14:59:46 -04:00
Anna Schumaker a8f94e9443 core/audio: Add a function to load tracks by filepath
If a matching track isn't found in the track database, then use the new
track_alloc_external() function to allocate an external track with the
correct tags.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-09-05 14:59:43 -04:00
Anna Schumaker cc464ed198 core/tags/track: Add support for allocating external tracks
There might be rare occasions where users want to play a track that
exists outside their music library.  This patch adds support for
allocating and freeing these external tracks without adding them to the
track database.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-09-05 14:59:43 -04:00
Anna Schumaker f9a573b6a3 core/tags/track: Add a function to look up tracks using only the path
I'm struggling to come up with a good way to combine code for this with
code for track_add(), since the lookup mechanism is basically the same.
I'll keep them separate for now and think on it some more.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-09-05 14:59:43 -04:00
Anna Schumaker e7ceed9b5d core/library: Improve subdirectory handling for lookup and find
It's possible that the user may pass us a subdirectory of a path that
has already been added to the library.  We can use this to prevent
double-adding tracks if the user does this.  Additionally, this function
can be used to help look up tracks using only a filepath.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-09-05 14:59:43 -04:00
Anna Schumaker 497ed57057 core/string: Add a function for checking if a string is a subdirectory
I want to use this to prevent users from adding a subdirectory of a path
that has already been added to the library.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-09-05 14:59:43 -04:00
Anna Schumaker e6f34d34f0 core/audio: Rework audio message handling
Let's just do everything inline and cut out the extra function calls to
make the code simpler.  I also created functions to help tests send EOS
or error messages.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-09-05 14:59:43 -04:00
Anna Schumaker 4986bdad13 core/audio: Rework how tracks are loaded
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-09-05 14:59:43 -04:00
Anna Schumaker fc6e3ff464 core/audio: Listen for GstStateChanged messages
And use these to trigger the state changed callback.  Additionally, this
callback can now be used by tests to determine when we're done seeking.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-09-05 14:59:43 -04:00
Anna Schumaker c03530f318 core/audio: Remove playbin-based player
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-09-05 14:59:43 -04:00
Anna Schumaker 0f2e30589d core/audio: Switch controls to the new pipeline player
Implements #103: Switch to custom GstPipeline
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-09-05 14:59:22 -04:00
Anna Schumaker 76e400e156 core/audio: Build up the rest of the pipeline
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-09-05 14:12:13 -04:00
Anna Schumaker 9c6a9f7759 core/audio: Add gstreamer elements for decoding files
We only play local music, so we know that we're using a file and we
don't need a uridecodebin to figure it out for us.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-09-05 14:12:13 -04:00
Anna Schumaker 1182e55df9 core/audio: Fix return type of audio_duration()
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-09-05 14:12:13 -04:00
Anna Schumaker b558a9043c core/audio: Rearrange global variables
Add some whitespace to make it easier to follow what everything is.  I
also rename audio_bus to audio_old_id to make it easier to add in a new
audio bus.  Additionally, I changed test_audio_player() to
test_old_player() to make room for the new pipeline.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-09-05 14:12:13 -04:00
Anna Schumaker d95c693db2 tests: Rename gui.h -> loop.h
I want to run a main loop during the audio tests, which aren't in gui/.
Let's rename this file to make me feel better about using it :)

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-09-05 14:12:13 -04:00
Anna Schumaker 36349e9890 Ocarina 6.5.6
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-09-05 14:09:40 -04:00
Anna Schumaker a7280356c8 gui/sidebar: Start editing on a double-middle click
A single middle click could happen by accident, especially if the user
has a touchpad.  A double click is more likely to be deliberate.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-08-29 09:38:31 -04:00
Anna Schumaker 8b17962b4e gui/sidebar: Don't set user playlists as editable by default
Instead, watch for a middle click and set the editable state manually
from there.  This fixes a bug where double clicking on a user playlist
put us in editing mode instead of selecting the playlist for playback.

Fixes #110: Cannot select user playlists
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-08-25 13:36:52 -04:00
Anna Schumaker 7e303fa2b1 gui/sidebar: Add functions for setting and getting editable state
I want to be able to change this when the user middle-clicks a row, so
let's start by adding in some support functions.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-08-25 13:16:11 -04:00
Anna Schumaker 79accb5bb0 core/playlists/system: Load favorites and hidden playlists first
If we don't, then the Collection playlist will include hidden tracks
when it initializes.

Fixes #109: Hidden tracks showing up in Collection
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-08-22 10:04:02 -04:00
Anna Schumaker 8614aa37cd Ocarina 6.5.6-rc
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-08-22 09:35:13 -04:00
Anna Schumaker 4990d68711 gui/playlists/user: Respond to the "edited" signal
We use this to perform user playlist renames with the user-entered text.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-08-22 09:16:20 -04:00
Anna Schumaker fac383e9fc gui/playlists/user: Respond to the "editing-started" signal
We use this to set the playlist name in the provided GtkEntry, clearing
out the number of tracks

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-08-22 09:16:13 -04:00
Anna Schumaker 466d9ce291 gui/sidebar: Enable editing playlist treeview entries
But only for user playlists.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-08-22 09:16:02 -04:00
Anna Schumaker 3f9372051f gui/sidebar: Add a function for setting an iterator from string
The GtkTreeView editing callbacks give me a path string that I'll need
to convert into an iterator to find the edited playlist.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-08-22 09:15:48 -04:00
Anna Schumaker 2cfccb8177 gui/sidebar: Add a playlist update function that takes a playlist
Once we change the name of a playlist, we won't be able to find it in
the sidebar model.  So let's add a way to update an iterator by passing
the playlist to use for updating.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-08-22 09:15:31 -04:00
Anna Schumaker e773ae6f82 core/playlists/user: Add support for renaming playlists
Users may make typos in naming playlists, or they might think up better
names for playlists later.  Let's be friendly and give users a way to
change playlist names if desired.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-08-22 09:15:22 -04:00
Anna Schumaker 042cddb65b core/idle: Don't allocate the thread pool unless we really need it
This saves ~75MB in my testing, and can be a useful memory savor for the
case where all album art has been downloaded.

Implements #96: Allocate g_thread_pool as needed
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-08-22 09:15:04 -04:00
Anna Schumaker 2a95031ee7 core/idle: Schedule async idle tasks directly
Rather than processing them twice.  This should speed things up a tiny
bit.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-08-22 09:14:18 -04:00
Anna Schumaker df21aa1299 gui: Header file cleanup
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-08-22 09:14:13 -04:00
Anna Schumaker 432d3e5d62 core: Add useful headers to core.h
This way we don't need to keep including the same core libraries in
almost every file.

Implements #83: Include useful headers in core/core.h
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-08-22 09:13:46 -04:00
Anna Schumaker a15ad67029 core: Remove struct core_init_data and pass parameters directly
The core_init_data only held three items, most of which don't get set
during testing.  Removing this struct means several tests no longer need
to define a "dummy" initdata object to pass around during init.

Implements #92: Remove core_init_data
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-08-22 09:13:13 -04:00
Anna Schumaker 5247bf2de0 core/audio: Rename audio_ops -> audio_callbacks
And make them optional, that way tests don't need to keep creating fake
callbacks if they don't need anything from the audio layer.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-08-22 09:12:33 -04:00
Anna Schumaker d3df9a69f2 gui/ocarina: Add a --sync option
Passing this option tells Ocarina that it should only process
synchronous idle tasks, such as library or playlist updates.  This
effectively disables album art fetching (and any async jobs we add in
the future).

Implements #91: Add --no-fetch option
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-08-22 09:10:52 -04:00
Anna Schumaker f46ef37630 core/idle: Change idle_init() to take an idle_sync_t
Rather than offering two init functions for testing.  This lets the UI
select if they want async idle tasks (like Album Art fetching).

Implements #90: Give idle_init an async flag
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-08-22 09:10:08 -04:00
Anna Schumaker 76ebfaa6d4 Ocarina 6.5.5
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-06-19 07:59:30 -04:00
Anna Schumaker cc5f65bf82 core/playlists/system: Reset queued tracks when empty
Users might not remember any sorting or random settings from the last
time they queued tracks, so saving these values could result in
selecting tracks in an unexpected order.  Fix this by resetting the
queued tracks options whenever the playlist is emptied.

Fixes #108: Unset random and sorting when finishing queued tracks
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-06-12 10:40:57 -04:00
Anna Schumaker bb40ef479f core/playlists/generic: Initialize pl_random and pl_length
These fields were working fine for statically initialized playlists, but
they get set to garbage values when g_malloc()-ing a playlist.  Let's
make sure they get initialized properly with the rest of the playlist.

Fixes #107: Initialize all values in playlist_generic_init()
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-06-12 08:19:21 -04:00
Anna Schumaker d1c682501f Ocarina 6.5.5-rc
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 09:31:53 -04:00
Anna Schumaker 448b4a16f4 Remove core/queue.c and associated files
Everything has been merged into the playlist layer to better match how
playlists are actually used.  This means we can remove the queue files.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:05 -04:00
Anna Schumaker 3286b61dcf core/playlist: Add playlist_generic_{alloc,free}() functions
Artist and library playlists are allocated manually, so there should be
generic functions that both can use to get a playlist pointer and free
it when we're done.

I also add a callback for telling the UI when new playlists have been
allocated.  This isn't needed for Library playlists, but this is the
only way the UI can know about new Artists.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker 7f54562b71 core/playlist: Add a playlist_init_sorted() function
Sometimes we want to initialize the playlist with sort data, other times
we don't.  This gives us functions we can call for both!

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker cdbf8b1736 core/playlist: Rename pl_private -> pl_search
And change its type to gchar **.  This lets the playlist code manage
setting and freeing search strings.  The UI is still responsible for
how this string is used.

This patch also  lets me remove the now-unused queue_deinit() function
and associated callback.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker 1dd0b7c2aa core/playlist: Move playlist tracks into the playlist struct
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker a808cac04c gui/model: Store a GList in the GtkTreeIter
Storing the current link from the playlist should give us immediate
access to the track, rather than needing to use playlist_at() all the
time.  Additionally, we can now use g_list_next() whenever we need to do
a gtk_tree_model_iter_next()

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker 1c6305e24e core/playlist: Replace queue_at() with playlist_at()
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker 8bf5aefd1a core/playlist: Replace queue_iter with a playlist_iter
The iterator only needs to point to the current position on the GQueue,
so we can do away with manual position tracking.  This lets us use a
GList pointer as an iterator instead of something more complicated.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker 03e7346900 core/playlist: Move current track into the playlist struct
I keep using the queue_iter struct for now to reduce code churn in this
patch.  I'll be replacing it in the next patch.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker 18f1bfe801 core/playlist: Move playlist iterator functions into the playlist layer
I also take this chance to add extra functions for directly manipulating
the current track, since this is something we do fairly often.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker f25bdab367 core/playlist: Move playlist sort order into the playlist struct
Rather than using a variable from the queue layer.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker 0c197c10f9 core/playlist: Move playlist length into the playlist struct
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker fda29aaf13 core/playlist: Add a playlist_generic_deinit() function
We'll need this to clean up playlists once queue_deinit() goes away.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker 2fb27178ee core/playlist: Implement playlist_add() directly
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker efbbc4ceff core/playlist: Implement playlist_size() directly
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker 92bb742f8f core/playlist: Implement playlist_generic_add_front() directly
This function adds a track directly to the front of the playlist,
without any existence checks.  This lets us use it for the History
playlist, which allows multiple tracks.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker 6dbc574954 core/playlist: Implement playlist_generic_clear() directly
I can reuse the "removed" callback to for this, rather than implementing
a new "cleared" callback directly.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker 859ff8656f core/playlist: Implement playlist_has() directly
We can make use of g_queue_find() to find the track for us, and then
convert its return value into a boolean.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker a45c7d6889 gui/playlist: Implement playlist_generic_remove() directly
Rather than iterating over the entire playlist ourselves, we can instead
use the g_queue_remove_all() function to do most of the work for us and
then send an appropriate number of "removed" callbacks.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker ca6c5293c6 core/playlist: Add a playlist_generic_update() function
This function simply triggers the "updated" callback for the given
playlist and track.  I updated the gui model to handle taking tracks
instead of row indexes, since this lets me reuse the same for-each
function that we do for sorting.  Additionally, this prevents UI updates
for playlists that aren't currently visible.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker 287c6e0e9c core/queue: Remove unused queue_selected()
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker 1d09e967d0 core/playlist: Move random setting into the playlist struct
I move the random variable into the playlist code since it is no longer
used by the queue layer.  This gives me the opportunity to change it
into a boolean rather than a bit flag.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker f670a3796b core/playlist: Implement playlist_next() directly
Let's have the playlist generic functions pick the next track rather
than redirecting to the queue code.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker bd8df2a169 core/playlist: Add playlist_selected() for selected tracks
In most cases this function just triggers a UI update, but system
playlists have a little extra bookkeeping to do to remove the track from
the Queued Tracks playlist.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker 07bf09c2ad core/playlist: Add a playlist_generic_resort() function
This function is called to force-sort the playlist.  Additionally, we
trigger the "playlist-sorted" callback to to notify the gui that
playlist rows need to be updated.

Additionally, I implement the gui_model_update_all() function to loop
over the model and update all rows of the current playlist.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker e74c1c053c core/playlist: Implement sorting directly
Rather than passing through to queue_sort().  Additionally, I remove the
queue_sort() function.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker 793a8a5817 core/playlist: Add a playlist_clear_sort() function
This replaces the "reset" field that had been passed to sort.  I think
this makes things a little more straightforward, and gives us a function
we can call when freeing playlists.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker d2335f5c6e core/playlist: Add a playlist_generic_save() function
This function uses the playlist save flags enum to determine what
exactly to save, including support for backwards compatibility with
6.4.x playlists.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker 37d95656e9 core/playlist: Add playlist_generic_load() function
This function uses the playlist save flags enum to determine what
exactly to load, including support for backwards compatibility with
6.4.x playlists.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker a87373f335 core/playlist: Add playlist_callbacks struct
This will be used to gradually phase out the queue_ops struct over the
next several patches.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker 93cb7145e6 core/playlist: Clean up playlist headers
I renamed "type.h" to "playlist.h" to better match what it does.
Additionally, I moved generic playlist functions into a new header file.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker ed095eb987 core/audio: Clean up loading tracks
This lets us call playlist_played() whenever we change tracks to send UI
updates once, rather than using multiple updates to change playcount and
now-playing status.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:45:04 -04:00
Anna Schumaker ee8825745b Ocarina 6.5.4
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-13 08:42:01 -04:00
Anna Schumaker 4ad9b39398 Ocarina 6.5.4-rc
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-05-01 07:36:53 -04:00
Anna Schumaker ae604ab4a8 core/playlists/system: Remove sys_playlist struct
It isn't needed now that playlist differences are handled in the upper
layer.  This also lets me create a shared set of operations for dynamic
playlists.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 2a8288e1b3 core/playlists/system: Cleanup system playlist updating
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 8af9148606 core/playlists/system: Clean up system playlist initialization
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 8a5163fb68 core/playlists/system: Clean up loading system playlists
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 8220dd9932 core/playlists/system: Clean up saving system playlists
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 0754f10883 core/playlist: Remove playlist_update()
This was only used by system playlists to keep the unplayed, most played,
and least played playlist up to date.  We can handle this internally
through the playlist_played() handler.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker a33decf549 core/queue: Remove Q_REPEAT flag
It is unused now that the queued tracks playlist manually removes tracks
when picked.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 9fa5f0b0db core/queue: Remove unused queue_erase() function
It's just added complexity that we don't need to keep around at this
point.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker b17585237a core/playlist: Add a playlist_played() function
This is used to notify when tracks have been played so dynamic playlists
can be updated, and so the model can display the correct playcount.

The old system playlist tests are mostly unnecessary at this point, so I
remove them as part of this patch.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 7655b0cae8 core/playlist: playlist_{next,prev}() uses a playlist function pointer
Allowing us to access the playlist directly, rather than going through
the playlist-type interface.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 1fa31c51a6 core/playlist: Remove playlist_get_queue()
We can just look up the playlist instead, and then access the queue
variable from there.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 72d0f9a248 core/playlist: Remove playlist_get_id() function
And replace its use with direct access of the playlist->pl_id variable.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker b5c1af263a core/playlist: Rename playlist_cur() -> playlist_current()
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 5c215df0bf core/playlist: playlist_select() takes a playlist pointer
And stores it for future reference, so we don't have to keep looking up
current and previous playlists.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 73825dd916 core/playlist: Add a function to look up playlists by id
This is much more straightforward than converting an id to a name and
then looking up playlists from there.  This patch removes the now-unused
playlist_get_name() function.

This patch also lets me simplify the system playlist unit test, since I
can now check several similar operations using a loop.  Additionally, I
change calls to pl_system_lookup() into pl_system_get() for efficiency.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 09e358b96b core/playlist: Rename playlist_get() -> playlist_lookup()
I think "lookup" is a better name for this function, since it's similar
to performing a dictionary lookup.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker bef0c70e5a core/playlist: playlist_sort() takes a playlist pointer
Additionally, it also uses a playlist-level function pointer to decide
what to do.  In most cases this calls the generic sort function, but the
history playlist should never be sorted.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 61e3137131 core/playlist: playlist_{get,set}_random() take a playlist pointer
Additionally, playlist_set_random() uses a playlist-level function
pointer to decide what to do.  In most cases this will simply toggle the
flag, but the history playlist does not support random playback.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 0c1147513e core/playlist: playlist_size() takes a playlist pointer
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 0e523ed279 core/playlist: playlist_has() takes a playlist pointer
Rather than going through a lookup step.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 3364d4733e core/playlist: playlist_remove() uses a playlist-level function pointer
Rather than going through playlist-type operations.  Additionally, I
take this opportunity to change playlist_remove() to take a playlist
struct directly.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 1b9cdf5a5c core/playlist: playlist_add() uses a playlist-level function pointer
Rather than going through playlist-type operations.  Additionally, I
take this opportunity to change playlist_add() to take a playlist struct
directly.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 066027ecb6 core/playlist: playlist_delete() uses a playlist-level function pointer
Rather than going through the playlist-type operations.  Additionally, I
take this opportunity to change playlist_delete() to take a playlist
struct directly.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker ca5f0701e9 core/playlist: playlist_new() returns a playlist pointer
This is much more useful than a boolean status, since we can use the
playlist pointer right away.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 846f7df9c1 gui/sidebar: Add a gui_sidebar_iter_playlist() function
It's frequently useful to get the playlist directly, rather than looking
up type and name first.  Let's add a function to do just that!

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 4d68ce8ce6 gui/playlists/library: Return a new playlist pointer
This pointer can be used immediately by testing code instead of simply
looking at the return status.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 249de0da21 core/playlists/system: Add tests for new tracks and deleting tracks
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 58cd43c330 core/database: Pass database index when allocating dbes
This is useful for user playlists so they can store their playlist-id
directly, letting us remove the entire playlist_get_id() function.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 9b4a1785a1 core/queue: Remove Q_ADD_FRONT flag
And replace its use with a queue_add_front() function instead.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker c59b097638 core/queue: Remove Q_ENABLED flag
Queues are always enabled now that we can select different playlists as
the default.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 7e3e4194f3 core/queue: Remove Q_NO_SORT
This flag was only used by the History playlist, but we have a noop
function to handle this instead.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 186367afe0 core/queue: Remove qop_save()
This callback is no longer used now that saving happens in the playlist
layer.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 62e494f2af core/queue: Remove Q_SAVE_FLAGS and Q_SAVE_SORT
These flags are unused now that the playlist layer handles saving.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:40:02 -04:00
Anna Schumaker 15ceda1194 Ocarina 6.5.3
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-28 09:38:17 -04:00
Anna Schumaker 638caeaa91 gui/sidebar: Set the playlist before changing the random button
Otherwise we could end up changing the random setting on the previous
playlist when displaying a new one.

Fixes #106: Switching playlists clears random setting
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-26 14:25:03 -04:00
Anna Schumaker a7565340d2 Ocarina 6.5.3-rc
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-14 13:59:08 -04:00
Anna Schumaker 0b231119d2 gui/artwork: Rework importing album art
And add a test to check that everything is set correctly.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-14 13:58:05 -04:00
Anna Schumaker fc1e917aee gui/artwork: Rework setting album art
I cleaned up several of the functions and added tests for making sure
everything is set correctly.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-14 13:58:05 -04:00
Anna Schumaker bc1c462d36 gui/artwork: Add an accessor function for the artwork image
This patch also adds a unit test checking that the image is initialized
properly.  In addition, I simplify things by changing the image widget
to be a direct child of the GtkButton.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-14 13:58:05 -04:00
Anna Schumaker 8fd4e4c637 gui/audio: Add a test for the volume button
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-14 13:58:05 -04:00
Anna Schumaker 96e5749e7f gui/audio: Add a test for seeking
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-14 13:58:05 -04:00
Anna Schumaker 2a845feb38 gui/audio: Move audio_ops struct towards the top
This keeps all the audio operations together, and matches how we define
operations structs throughout the rest of Ocarina.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-14 13:58:05 -04:00
Anna Schumaker f8c0668e5e gui/audio: Add an accessor function for the pause_after combo box
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-14 13:58:05 -04:00
Anna Schumaker 07d735eeee gui/audio: Add accessor functions for control buttons
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-14 13:58:05 -04:00
Anna Schumaker 943ab02aa5 gui/audio: Add position and duration label accessor functions
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-14 13:58:05 -04:00
Anna Schumaker 869e83b7bd gui/audio: Add accessor functions for audio tags
And begin working on new, cleaner unit test code.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-14 13:58:05 -04:00
Anna Schumaker 88bf71ac22 Remove gui/view.c
This code is obsolete now that gui/playlist.c handles keypress and
right-click events on the main treeview.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-14 13:58:05 -04:00
Anna Schumaker 3bf99b12eb gui/playlist: Respond to right-clicking the main treeview
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-14 13:58:05 -04:00
Anna Schumaker e660e3f0b2 gui/treeview: Add a "select path at pos" function
This function isn't easy to test without knowledge of screen
coordinates, so I didn't include a unit test this time.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-14 13:58:05 -04:00
Anna Schumaker 3ae5e0f535 gui/playlist: Handle key press events on the main treeview
Setting up a unit test for this is almost impossible, so we don't do
that here.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-14 13:58:05 -04:00
Anna Schumaker 39bc7492d4 gui/playlist: Clean up file and rewrite unit test
The unit test now checks how we respond to the row-activated signal.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-14 13:58:05 -04:00
Anna Schumaker e7d2fa5c4d gui/treeview: Add a function for listing selected tracks
This will be used by higher layers to add tracks to playlists.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-14 13:58:05 -04:00
Anna Schumaker 155b9c3ee6 gui/treeview: Add a test for the row-activated signal
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-14 13:58:05 -04:00
Anna Schumaker 7fbef057bf core: Add a field to initdata for enabling async idle tasks
I'll use this when testing gui album art to enable fetching the artwork.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-14 13:58:05 -04:00
Anna Schumaker d373c55320 core/queue: Add a queue_erase_track() function
Used to trigger the erase callback on tracks, rather than indexes.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-14 13:58:05 -04:00
Anna Schumaker f147c30c30 Ocarina 6.5.2
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-14 13:56:39 -04:00
Anna Schumaker 1e6ab2e23c gui/playlist: Unbold the previous playlist
Looks like this wasn't happening automatically, so let's be sure to
update the previous playlist's row after selecting a new playlist.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-04 11:27:47 -04:00
Anna Schumaker 83a21863b9 gui/sidebar: Select the current playlist on startup
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-04 11:27:45 -04:00
Anna Schumaker e876f8125f gui/sidebar: Restore expanded rows on startup
Fixes #94: Playlist heading not open by default
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-04 11:27:24 -04:00
Anna Schumaker dc53ae271b gui/sidebar: Store the row expanded setting for later use
I want to use this to track if the user has expanded or collapsed any of
the sidebar rows.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-04 10:04:33 -04:00
Anna Schumaker c9e9e3a340 Ocarina 6.5.2-rc
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-03 10:33:19 -04:00
Anna Schumaker d818688bfd gui/playlist: Update playlist sizes with a function pointer array
This lets us select the appropriate update function directly.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-03 10:33:19 -04:00
Anna Schumaker cebf2069cb gui/playlists/system: Respond to favorite and hide buttons
I also take this opportunity to rename these widgets and add accessor
functions.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-03 10:33:19 -04:00
Anna Schumaker 36f399ecb7 gui/playlists/system: Split into a new file and add a unit test
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-03 10:33:19 -04:00
Anna Schumaker 83db8e4ae7 gui/playlists/user: Add a function for getting a list of playlists
This is used to build a right-click menu with the names of each
playlist.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-03 10:33:19 -04:00
Anna Schumaker ad3e56250e gui/playlists/user: Split into a new file and add a unit test
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-03 10:33:19 -04:00
Anna Schumaker bb673ddb62 gui/playlists/artist: Split into a new file and add a unit test
Putting this code in a new file helps make gui/playlist.c less complex,
and better matches the organization of files in core/

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-03 10:33:19 -04:00
Anna Schumaker 4fee5f85f0 gui/playlists/library: Add a function for updating library paths
Finding the header first makes a lot of sense, and avoids iterating
through every playlist while trying to find the library playlist to
update.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-03 10:33:19 -04:00
Anna Schumaker ab47a7ac88 gui/playlists/library: Add new library paths to the sidebar
... without passing through gui/playlist.c first.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-03 10:33:19 -04:00
Anna Schumaker e550638823 gui/playlists/library: Write unit test for adding file paths
This functionality needs to be tested better.  I can't really test the
dialog, since it runs in the main thread, but I can test adding library
paths with the selected directory.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-03 10:33:19 -04:00
Anna Schumaker 3e17b7bc1f Rename gui/collection.c -> gui/playlists/library.c
This is to match how files are named and organized in core/

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-03 10:33:19 -04:00
Anna Schumaker b4e2770223 Remove gui/queue.c
This code is obsolete now that the sidebar handles setting the random
button when playlists are changed.

Implements #76: Remove struct gui_queue
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-04-03 10:33:05 -04:00
Anna Schumaker df2236db9f gui/sidebar: Add a function for selecting the default playlist
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-31 13:45:55 -04:00
Anna Schumaker 1291a0d139 gui/sidebar: Add a function for updating playlist text
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-31 13:45:55 -04:00
Anna Schumaker b3efd9d84d gui/sidebar: Respond to the Delete key
Gtk doesn't have a simple way to manually trigger this, so I didn't
write a unit test for this feature.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-31 13:45:55 -04:00
Anna Schumaker 828f861d9a gui/sidebar: Handle random button clicks
This makes the most sense here, and lets us remove gui/queue.c at some
point in the future.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-31 13:45:55 -04:00
Anna Schumaker 3562e164b0 gui/sidebar: Implement selection-changed handlers
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-31 13:45:55 -04:00
Anna Schumaker a152ed689f gui/sidebar: Move the visible function into sidebar.c
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-31 13:45:55 -04:00
Anna Schumaker a2113dc378 gui/sidebar: Add a function for inserting a playlist in sorted order
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-31 13:45:55 -04:00
Anna Schumaker 9885c60bff gui/sidebar: Add functions for appending child playlists
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-31 13:45:55 -04:00
Anna Schumaker 831a5379e5 gui/sidebar: Add a function for finding playlists
This function scans through the treestore at the current level, without
descending into children.  This is because we frequently know what
category playlists are under when searching for a child, so it makes
sense to find that first.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-31 13:45:55 -04:00
Anna Schumaker f3f8ad91c6 gui/sidebar: Add a function for adding playlists
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-31 13:45:55 -04:00
Anna Schumaker 4fc8c72ea8 gui/sidebar: Initialize the sidebar with default headers
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-31 13:45:55 -04:00
Anna Schumaker 18e76a7dca gui/sidebar: Add useful iterator functions
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-31 13:45:55 -04:00
Anna Schumaker bbac5e23be gui/sidebar: Add a function for getting the sidebar treeview
And rename the treeview from "o_playlist_view" to "sidebar_treeview"

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-31 13:45:55 -04:00
Anna Schumaker c1c197acb5 gui/sidebar: Add a function for getting the sidebar filter model
And rename the model from "o_playlist_filter" to "sidebar_filter"

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-31 13:45:55 -04:00
Anna Schumaker 4c3405e874 gui/sidebar: Add a function for getting the sidebar treestore
And rename the treestore from "o_playlist_store" to "sidebar_store"

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-31 13:45:55 -04:00
Anna Schumaker b3074979f7 gui/sidebar: Add a gui_sidebar() function
Called to access the GtkPaned widget.  I also rename the widget
"sidebar" to match the new convention.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-31 13:45:55 -04:00
Anna Schumaker fd84222c2b core/playlist: Add a function for getting the current playlist
This is useful to keep playlist settings variables hidden to the GUI.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-31 13:45:55 -04:00
Anna Schumaker 38cd2f761d Ocarina 6.5.1
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-31 13:43:16 -04:00
Anna Schumaker a36fd137d5 gui/audio: Disable the audio timeout function during shutdown
Otherwise we may try to use the gstreamer playbin after destroying it,
leading to various error messages showing up in the console.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-30 09:57:15 -04:00
Anna Schumaker a4049f8d01 gui/playlist: Expand playlists after adding user playlists
This will open up each of the playlist headings except for "Collection",
which I think is the behavior we want.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-30 08:21:09 -04:00
Anna Schumaker 9b04ebcd71 gui/playlist: Block __playlist_update_sizes() during startup
Otherwise we'll get a bunch of messages printed out saying that p_store
isn't a GtkTreeModel

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-27 09:04:02 -04:00
Anna Schumaker d3f505465c gui/view: Initialize all libraries before running the test
Otherwise we'll get a message saying that either the filter or the
treeview is not a GtkTreeModel

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-24 11:39:21 -04:00
Anna Schumaker 1d3438932e gui/window: Store window position
And restore it when restarting Ocarina.

Implements #88: Remember window position
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2017-03-24 11:38:18 -04:00
Anna Schumaker fa96def899 gui: Rename queue_model variable to gui_model
The concept of queues is going away over the next few releases, so we
should rename this variable for consistency.  I considered calling it
"playlist_model", but I think "gui_model" better matches naming
conventions in the gui.

Imlements #77: Rename queue_model to playlist_model
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-11-01 09:18:28 -04:00
Anna Schumaker 2431ad104e Ocarina 6.5.1-rc
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-11-01 08:51:23 -04:00
Anna Schumaker 1c386809d0 gui/treeview: Respond to the notify::width signal
I connect this in code so I can pass the column index as data to the
function, avoding the need for a "column index" lookup function.

I restore saved column widths during startup.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-11-01 08:51:23 -04:00
Anna Schumaker dbc1df154a gui/treeview: Add a row-activated signal handler
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-11-01 08:51:23 -04:00
Anna Schumaker 730395aeff gui/treeview: Add a function for scrolling the treeview
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-11-01 08:51:23 -04:00
Anna Schumaker 9e37062920 gui/treeview: Sort treeview when columns are clicked
I decided to manually connect signals this time so I can pass sort
information instead of needing to look up the column index.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-11-01 08:51:23 -04:00
Anna Schumaker df43010766 gui/treeview: Add an init function
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-11-01 08:51:23 -04:00
Anna Schumaker c49b77a24a gui/treeview: Add a function for accessing the treeview
And rename the widget from "o_treeview" to just "treeview".

I wanted to rename gui/view.c to gui/treeview.c at some point.  I
decided to gradually rework things into a new file to make it easier to
track progress.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-11-01 08:51:23 -04:00
Anna Schumaker 3fc19275f3 gui/filter: Add a function for loading a track from a tree path
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-11-01 08:51:23 -04:00
Anna Schumaker ac3c316d9a gui/filter: Add a function for getting filter treepaths from index
This keeps the treepath conversions contained to the filter code when
finding paths to scroll to.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-11-01 08:51:23 -04:00
Anna Schumaker 32d712c213 gui/filter: Remember search text when switching playlists
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-11-01 08:51:23 -04:00
Anna Schumaker 39d87f98f8 gui/filter: Move playlist filtering into filter.c
Implements #75: Move treeview filter to a new layer
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-11-01 08:51:23 -04:00
Anna Schumaker b9916706f1 gui/filter: Add functions for getting the SearchEntry and ComboBox
And take this chance to rename o_search -> filter_search.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-11-01 08:51:23 -04:00
Anna Schumaker ad29c520d4 gui/filter: Add a function for converting filter paths to indexes
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-11-01 08:51:23 -04:00
Anna Schumaker f550d45811 gui/filter: Add a function for converting GtkTreePaths into tracks
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-11-01 08:51:23 -04:00
Anna Schumaker 6325779062 gui/filter: Create a new file for treeview filter code
Right now filtering is split between queue.c and view.c, which can make
things somewhat complicated.  I think this is a sign that we need a new
file to manage filtering.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-11-01 08:51:23 -04:00
Anna Schumaker 27a1e2e12a gui/model: Add a function for accessing the runtime label
And rename the widget from "o_runtime" to just "runtime"

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-11-01 08:51:23 -04:00
Anna Schumaker 619ea0b559 gui/model: Use queue positions instead of queue iterator
This helps simplify the code, since we can reuse
gui_model_iter_nth_child() here.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-11-01 08:51:23 -04:00
Anna Schumaker 010969c7b3 gui/model: Convert set_queue() -> set_playlist()
Ocarina is moving in a playlist oriented direction, so move away from
using the queue directly in favor of using the playlist.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-11-01 08:51:23 -04:00
Anna Schumaker 82da46365f gui/model: Register the GuiModel type during gui_model_init()
This only needs to happen once, so let's do it when we initialize the
model rather than waiting for the first allocation.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-11-01 08:51:23 -04:00
Anna Schumaker e522475d38 gui/model: Various cleanups to code and test
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-11-01 08:51:23 -04:00
Anna Schumaker cdbe96c575 gui/model: Rename "Queue Model" to "Gui Model"
This is going to represent a playlist soon enough, so let's just name it
something more generic in case it ever has to change again.

Implements #87: Rename GuiQueueModel -> GuiModel
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-11-01 08:51:07 -04:00
Anna Schumaker 453f176d63 gui/idle: Rename widget to "progress_bar"
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-10-31 07:56:38 -04:00
Anna Schumaker 3383f9e32a gui/idle: Add a function for getting the progress bar
And add in various cleanups while we're at it.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-10-31 07:55:40 -04:00
Anna Schumaker f9238c34e4 tests/gui: Add a generic way to run a main loop
I've had to code this in several places, so creating a generic function
is long overdue.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-10-31 07:55:23 -04:00
Anna Schumaker 90b80fc8a7 gui/window: Rename widget to "window"
Drop the "o_" prefix since it doesn't really add anything.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-10-31 07:44:34 -04:00
Anna Schumaker 133efc0515 gui/window: Add a function for getting the window
This is cleaner and easier than calling into the gtk builder directly.
I bumped up the window layer so other gui components can use it.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-10-31 07:44:34 -04:00
Anna Schumaker 85bb67feed core/playlist: Add a private field to struct playlist
I intend to use this for playlist filtering to store filter text.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-10-31 07:44:34 -04:00
Anna Schumaker e7b4973a50 core/playlist: Add playlist_get() function
This is called to get the requested playlist.  I also reworked
playlist_get_queue() to call this function.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-10-31 07:44:34 -04:00
Anna Schumaker 7d0dbcbdc7 gui/view: Replace gtk_menu_popup() with gtk_menu_popup_at_pointer()
The function gtk_menu_popup() is deprecated as of Gtk 3.22, so replace
it with the simplified popup_at_pointer() function.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-10-31 07:44:34 -04:00
Anna Schumaker a89dc49609 Ocarina 6.5
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-10-29 17:35:26 -04:00
Anna Schumaker 34f21bf247 Update PKGBUILD
We build with CMake now, so update the PKGBUILD script to match.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-10-29 17:23:59 -04:00
Anna Schumaker 63951568af Replace README with README.md
The new file is written in the Gitlab markdown language.  I also update
the content to match how things are done now that we use CMake.

Implements #85: Update README
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-10-29 17:17:46 -04:00
Anna Schumaker be3b788380 core/playlists/system: Don't read 6.4.x playlists in new idle tasks
Track database defragmenting can happen before the new tasks are ever
scheduled, causing a crash when we try to find tracks with changed IDs.
After the crash, the Favorites and Hidden playlist data appears to be
lost.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-10-21 11:31:11 -04:00
Anna Schumaker 9df093ec85 tests/gui: Fix gtk css theme parsing errors
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-10-19 08:22:58 -04:00
Anna Schumaker 50207c5b79 core/playlist: Don't reselect the current playlist
We want playback to continue after the queued tracks playlist has run
out.  Allowing us to select the same playlist multiple times can cause
us to get stuck picking the next track.

Fixes #100: Finishing "Queued Tracks" playlist shouldn't stop playback
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-10-19 07:58:10 -04:00
Anna Schumaker 01927cf806 gui/model: Don't add tracks if queue_model is NULL
Otherwise we will crash with a memory error.  This only seems to be a
problem when Ocarina is started up after creating a user playlist.

Fixes #99: Ocarina crashes during startup with user playlists
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-10-17 08:03:57 -04:00
Anna Schumaker 59b2c854e7 Add a valgrind suppression file
I generated this file while hunting memory leaks and errors in core/

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-29 14:51:23 -04:00
Anna Schumaker e4e90f165e tests/core/audio: Process idle queue on EOS
Otherwise idle tasks will register as memory leaks.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-29 14:50:36 -04:00
Anna Schumaker 4250757b83 core/playlists: Don't leak playlist names while loading
We need to free the playlist name after looking up in case we hit the
case where the playlist isn't found.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-29 14:50:36 -04:00
Anna Schumaker cb2af114c5 core/playlists/user: Free playlist name during deinit
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-29 12:44:17 -04:00
Anna Schumaker b4d78b0d3d core/playlists/library: Free scan path when we're done with it
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-29 12:44:17 -04:00
Anna Schumaker d5de8e5dbf tests/core/queue: Deinitialize queues after testing
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-29 12:44:17 -04:00
Anna Schumaker 6120366da3 core/tags/album: __album_query_artist() needs to initialize "found"
Otherwise we could end up checking this variable uninitialized.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-29 12:44:17 -04:00
Anna Schumaker eb16d9c6fb tests/core/tags/album: Deinitialize genre after testing
Otherwise valgrind will report that we are leaking memory.
Additionally, only initialize the idle queue for sync idle tasks until
album artwork needs it.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-28 16:53:20 -04:00
Anna Schumaker 1d7cfa0e6d tests/core/tags: Don't call *_db_init() twice in unit tests
This could lead to a false-positive when looking for memory leaks.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-28 15:55:32 -04:00
Anna Schumaker 99faa0cf39 tests: g_assert_cmpstr_free() stores the lhs in a temporary variable
Valgrind told me that all calls to g_assert_cmpstr_free() weren't
actually freeing the string.  A closer look shows that if we pass a
function as "lhs" then the function will be called twice, allocating
twice as much memory.

Fix this by storing the result of lhs in a temporary variable so
functions are only ever called once.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-28 15:20:24 -04:00
Anna Schumaker 0407486316 tests: Rename CONFIG_TESTING_ALBUM_ARTWORK to CONFIG_TESTING_ARTWORK
This makes the flag a little easier to toggle for testing.  I also
rename the compiler option for consistency.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-28 15:20:19 -04:00
Anna Schumaker 9a216fee1e gui/queue: Remove unused GQ_CAN_REPEAT and GQ_CAN_DISABLE flags
These go unused as a result of our recent playlist changes, so we can
remove them now.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-28 10:04:28 -04:00
Anna Schumaker ae916eaf40 gui/queue: No need to change random image sensitivity
GtkThemes should be taking care of this indicator for us.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-28 10:00:25 -04:00
Anna Schumaker 9d279fe21e gui/playlist: Don't show Collection on startup
__gui_playlist_init_idle() will select the first visible playlist by
default, which is what we want.  Let's not override this by showing the
Collection later in this function.

Fixes #95: Startup shows Collection instead of Queued Tracks
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-28 09:33:27 -04:00
Anna Schumaker e25b8407b0 gui: Widget spacing tweaks
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-28 09:12:17 -04:00
Anna Schumaker d7709fd5fb gui: Replace "Add New ..." button with a single GtkButton
I find that I usually create playlists through either a right click
action or through a keyboard shortcut, so we don't need an extra option
hidden inside a MenuButton.  Let's remove the button and show the "Add
Library Path" option directly instead.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-27 14:12:21 -04:00
Anna Schumaker eb8a23f2ff core/tags/track: Add support for re-keying the track database
This lets up update track keys to prevent duplicates showing up during a
library path update.

Fixes #93: Library sometimes has duplicated tracks
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 15:41:56 -04:00
Anna Schumaker 811509ff80 core/database: Add a db_rekey() function
Sometimes database entry keys change, so this gives us a way to update
them in the database keys hash table.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 14:58:30 -04:00
Anna Schumaker cf4eedb592 core/database: Autosave databases if the file version changes
To make sure the latest changes are recorded to disk.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 12:02:45 -04:00
Anna Schumaker 93d1550763 core/settings: Check for a NULL key
This fixes a problem with restoring treeview columns.  Sometimes
"Played" gets allocated way too much space, so I solve this by changing
it's setting key to NULL so the column always gets whatever space is
leftover.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 12:02:23 -04:00
Anna Schumaker 2e38b3551e tests: Add a switch for disabling GUI tests
NoWheyCreamery.com doesn't run an Xserver, so we can't run UI tests.
Add a switch to disable these so test results can be reported
accurately.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:26:45 -04:00
Anna Schumaker ec3b9f7c8e .gitlab-ci.yml: Update for CMake conversion
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:23:09 -04:00
Anna Schumaker 142af976b3 Ocarina 6.5-rc
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:21:55 -04:00
Anna Schumaker 5a41eef8a2 gui/ocarina: Add basic command line options
These options are mostly used to communicate with a running Ocarina
instance to control playback.

Implements #37: Ocarina Command Line Commands
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:36 -04:00
Anna Schumaker 419d31d4c3 core/playlists/system: Drop support for looking up the "Banned" playlist
This playlist has been the same as the hidden playlist for several
releases now.  Let's make the change official!

Implements #28: Save hidden playlist as "hidden" instead of "banned"
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:36 -04:00
Anna Schumaker afbf9e0b1a gui/playlist: Change headers to "Playlists" and "Dynamic"
One word labels are always better.  I move the "Favorites" and "Hidden"
playlists under the "Playlists" header because they're manually
configured by the user.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:36 -04:00
Anna Schumaker ef8a764780 core/playlists: Allocate artist and library playlists during startup
Rather than waiting for them to load with an idle task.  This speeds up
Ocarina startup dramatically, since playlists can be added to the UI
with the correct size instead of needing extra callbacks to update the
size each time a track is added.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:36 -04:00
Anna Schumaker 27436115c7 core/database: Remove dbe_setup()
This was added so tracks could bump the libary tag size.  Now that the
size field has been removed, we can remove this function.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:36 -04:00
Anna Schumaker 1b18177d0a core/database: Remove db_load_idle()
This was added to help show the window faster.  Recent changes have made
it unnecessary.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker 21a47ec3b8 gui: Rename ocarina6.glade -> ocarina.ui
The convention is for gtk builder files named with a ".ui" extension.
Let's rename our file to match, and drop the 6 while we're at it.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker b39c4c9ba4 core/file: File version is OCARINA_MINOR_VERSION
This makes file versioning way easier, since every file will have the
same version.  I'll still need to manage minimum-supported versions, but
that shouldn't be too difficult going forward.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker 43a0ffd54c ocarina: Add OCARINA_{MAJOR, MINOR}_VERSION constants
These can be used for version number comparisons as integers, which will
be useful once file version numbers are based off of minor versions.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker 0cb44aaf3a gui/queue: Remove Repeat button
Most playlists don't allow changing this, and now that we have user
playlists it doesn't make sense to support repeating queued tracks.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker 4a4ec3fa36 gui/audio: Add GtkVolumeButton for changing volume
Implements #38: Add volume slider
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker d8c3fb9ace core/audio: Add support for changing audio volume
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker f02dff4177 gui/view: Sort the user playlists menu
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker ddb564ed82 gui/view: Create a menu item for adding tracks to user playlists
This has to be created programmatically due to the dynamic nature of
user playlists.  The easiest way to do that is to list user playlists in
a submenu and switch them out as needed.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker ecebd20a0b core/playlists: Select queued tracks playlist when adding tracks
This lets me "pause" playing queued tracks in favor of other playlists.
I remember the previous playlist so we can resume track picking once the
queued tracks playlist is empty.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker f0e9e1f1e4 gui/playlist: Add user playlists during init
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker e2d4dd61fb gui/view: Add "New Playlist" menu options
And pop up a dialog mox to ask the user for the name of the new
playlist.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker 1b9101cf26 gui/collection: Replace Add Library Path GtkButton with a GtkMenuButton
This lets me add new options without needing to clutter the gui with
lots of rarely-used buttons.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker dcbf2dff72 core/playlists/user: Add support for user created playlists
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker d6e5e6c773 core: Defragment databases on startup
Databases can change either as part of an upgrade or through adding and
removing entries.  We can save a bit of memory by removing unallocated
entries during startup.

Implements #47: Automatic database defragment
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker 85bfe717dd core/tags/library: Remove unused li_size variable
This was used to display the number of tracks in each library path.  We
can use playlist functions to get this information now.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker 30cebb4a45 core/tags/library: Remove unused "enabled" field
This was used to enable and disable library paths in previous Ocarina
versions.  This isn't used anymore now that we have library-based
playlists.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker 784cd3eb91 core/tags/track: Remove redundant artist and genre tags
The track tag doesn't need these tags now that they can be found in the
album tag.

Implements #64: Remove Artist and Genre pointers from Tracks
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker 71ee59ae22 core/tags/track: Reduce amount of saved data
Artist and genre information are already saved by the album tag.  In
addition, we can save a single date stamp instead of (year, day, month)
triplets.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker 0cefd158d9 core/tags/album: Add artist and genre information to saved data
And perform an upgrade when reading back in.  After track tags are read,
we can save the database in the new format and remove the old tags.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker 0ea75ccb29 core/tags/album: Add artist and genre arguments to album_find()
And propegate these through the alloc steps to create an album with the
correct artist and genre tags set.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker ffd09d410c core/tags/album: Add Genre ID to album keys
This allows for representing albums with multiple genres.  I know this
doesn't happen too often, but it doesn't hurt to be covered.  At the
very least, now we have a genre id for constructing new albums!

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker a85a1df7de core/tags/album: Add Artist ID to album keys
I need a way to represent the same album with different artists to
account for multi-artist albums.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker 50db0db06a core/date: Add functions for reading and writing date stamps
The date stamp is a single value that represents year, month, and day.
This should be slightly easier than working with three separate values.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker 86d7fe43ed core/database: Add support for defragmenting databases
Removing items from the database leaves a NULL pointer "hole" that is
never filled in.  This doesn't affect correctness, but it could be
wasteful as items are added and removed.  This patch adds a function to
defragment the database without changing the order of items.

Implements #66: Add support for rebalancing databases
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker 8d9139aea5 core/database: Add file versioning to databases
I plan to change the file format of some tags, so add versioning so we
can change things at different times.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker 2f098a2af6 core/file: Set minimum version from passed arguments
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker 939ebeac13 core/audio: Expose the audio_save() function
This will be used to re-save the current track after defragmenting the
track database.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker f726b6994c core/playlist: Add a function to force save playlists
This is needed to handle track database defragmentation, but could also
be expanded on later to save playlists from a generic place.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker 54bb0ffe2b core/playlists/library: Save playlist flags between sessions
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker a4692c0fe0 core/playlists/artist: Save playlist flags between sessions
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:35 -04:00
Anna Schumaker d33245f604 core/playlists/system: Clean up saving playlists
This prevents double-saves in both the playlist entrance function and in
the overridden function.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:34 -04:00
Anna Schumaker 6fc965e80d core/playlists/system: Remove legacy saving code
And delete files as they are upgraded.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:34 -04:00
Anna Schumaker 33894d7068 core/playlists/system: Add a new function for loading playlists
This function loads playlist information from a single file, falling
back to multi-file loading if playlist.system doesn't exist yet.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:34 -04:00
Anna Schumaker 87f0847f91 core/playlists/system: Add a new function for saving playlists
This saves playlists to a single file, rather than splitting them across
several.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:34 -04:00
Anna Schumaker 28e5045975 core/queue: Give queues the option to save and load iterator positions
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:34 -04:00
Anna Schumaker 84661c4797 gui/playlist: Hide empty playlists
I need to disable filtering when adding artist playlists to avoid some
weird memory corruption issues.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:34 -04:00
Anna Schumaker 278a54f7ec gui/playlist: Add a GtkTreeFilter to the playlist treeview
I intend to use this to hide empty playlists, but it could potentially
also be used to search for a specific playlist.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:34 -04:00
Anna Schumaker c6e9c176d8 gui/playlist: Select playlists by double clicking
I make the current playlist bold so the user has an easier time finding
it.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:34 -04:00
Anna Schumaker 7a79ac26b0 core/playlists/system: Make sure Favorites and Hidden repeat
Otherwise playing tracks from these playlists will cause the tracks to
get removed.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:34 -04:00
Anna Schumaker 64ceca84c5 core/playlists: Pick the next track from the currently selected playlist
Unless the user has queued tracks, of course.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:34 -04:00
Anna Schumaker 8985e70439 core/playlists: Add a playlist_select() function
I query the underlying playlist to see if it is selectable, and then
update the settings file if it is.

Implements #10: Select default playlist
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:34 -04:00
Anna Schumaker bfcfaae977 core/playlists: Add functions for converting between names and ids
I need to have integer playlist ids to store into the settings database
once I allow changing the default playlist.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:34 -04:00
Anna Schumaker da19ddd388 gui/playlist: Set playlist size when cleared
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:34 -04:00
Anna Schumaker 5232217eeb core/playlists/system: Overload playlist_delete() function
Instead of deleting a playlist, we can use this to clear
user-configurable playlists.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-26 08:07:34 -04:00
Anna Schumaker e4cd59b895 core/tempq: Remove tempq.c
Multiple temporary queues has been replaced with a single Queued Tracks
playlist.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-26 08:07:34 -04:00
Anna Schumaker 69e628f505 gui/tempq: Remove tempq.c
Temporary queues are being replaced by the Queued Tracks playlist.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-26 08:07:34 -04:00
Anna Schumaker 69b39ea717 gui/sidebar: Remove unused sidebar liststore
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-26 08:07:34 -04:00
Anna Schumaker 017da02827 gui/playlist: Add Queued Tracks playlist
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-26 08:07:34 -04:00
Anna Schumaker eb1361248d core/playlists/system: Add queued tracks playlist
Right now tracks are picked from this playlist whenever possible.  A
system for selecting the default playlist will be added shortly.

Implements #20: Only one queue for "Up Next"
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-26 08:07:20 -04:00
Anna Schumaker a775eeb761 core/playlists: Add a pl_next() playlist operation
This is mostly needed so the queued tracks playlist can be saved after
picking a track.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-23 16:11:08 -04:00
Anna Schumaker 903357395b core/audio: Save current track in settings database
The settings code is designed to map strings to unsigned integers, which
is exactly what we do here.  This lets us cut out an extra file access,
which is always a plus.  We remove the audio file after upgrading to
prevent reading it multiple times.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:08 -04:00
Anna Schumaker 7e00c8ed10 core/settings: Move settings into core/
Implements issue #9: Move settings into core/
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:08 -04:00
Anna Schumaker 524d1886f9 tests: Remove scons-based testing code
Completes issue #4: Investigate CTest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:08 -04:00
Anna Schumaker a8abbdfdcc Remove scons build files
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:08 -04:00
Anna Schumaker 19ca9d932b tests: Build gui audio test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 59839547e2 tests: Build gui playlist test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker dd268a24fe tests: Build sidebar test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 0470c6efa6 tests: Build gui idle test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 4e9c5e0a00 tests: Build window test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 6a1c27e9a4 tests: Build gui queue test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker b2dbbbaae6 tests: Build treeview test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker c671c7aefc tests: Build model test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 3e2ed7761c tests: Build settings test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 1710c8076a tests: Build gtk builder test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 8f8e4a4459 tests: Build audio test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 926ca09275 tests: Build tempq test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 7e09375325 tests: Remove core/playlist.c test
The playlist code is heavily tested by unit tests for the files in
core/playlists/, so we no longer need to have a separate playlist test.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 9b7c45634c tests: Build core/playlists/library test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 487274ff00 tests: Build core/playlists/artist test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker c67d09740c tests: Build core/playlists/system test with ctest
I initially tried making several helper functions to reuse code and make
it easier to add new playlists.  This didn't work all that well, mostly
because glib wasn't able to expand variable values or find the line
number that errors occured on.  Macros don't have this problem, since
they are expanded at compile time.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker f9dd51170d tests: Build queue test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 8327d77ddd tests: Build tags/track test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 5e65414263 build: Generate albums through cmake
This is much more straightforward than having scons call a bash script.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 921a04e28e tests: Build tags/library test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker ca2f35a848 tests: Build tags/genre test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 3b03301e61 tests: Allow disabling the Album Art Fetching test
This test can take a long time to run, especially on slow internet
connections.  Let's add a way to disable it when testing other things.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-23 16:11:02 -04:00
Anna Schumaker 65bbd21669 tests: Build tags/album test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 8c875acec9 tests: Build tags/artist test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 1b83978b09 tests: Build containers/database test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 10f81461d9 tests: Build idle test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 4fb854d0b1 tests: Build date test with ctest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker fc88f68d6b tests: Build file test with ctest
This changes test output slightly, in that files are now placed under
subdirectories of $XDG_USER_DATA_DIR/ocarina-test/ instead of in this
directory directly.  This should help avoid conflicting files, and lets
me use a single "rm" command to remove everything before testing.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 9f2811a343 tests: Build string test with CTest
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 2f559b2fb3 tests: Build version test with CTest
This begins the conversion to CTest and the GLib unit test framework.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 0e171012ce Add CMakeLists.txt
For using cmake to generate a makefile instead of building through
scons.

Implements issue #3: Investigate CMake
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-09-23 16:11:02 -04:00
Anna Schumaker 17150b1791 Ocarina 6.4.20
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-23 16:04:45 -04:00
Anna Schumaker 257ef3612f gui/audio: Enable idle polling when the track changes
Unplayed, Most Played, and Least Played tracks playlists update
themselves with an idle task when tracks have been played.  It looks
like we haven't been processing these tasks, so the queue was just
building up after every track.  Fix this by enabling the GTK idle
callback whenever a track is played.

Fixes #89: Dynamic playlists aren't updating
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-19 14:45:11 -04:00
Anna Schumaker b35b1eb132 Ocarina 6.4.20-rc
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-11 10:52:29 -04:00
Anna Schumaker 1bc43a9e2c gui/queue: Remove unused fields from struct gui_queue
Now that we have a single model and filter there is no need to attach
them to every gui_queue.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-11 10:52:29 -04:00
Anna Schumaker 336c0a01af gui/queue: Only allocate a single GtkTreeModelFilter
The view is now responsible for allocating the filter, but the gui_queue
still needs to set the filter function during an init step.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-11 10:52:29 -04:00
Anna Schumaker 65b43c7ae5 gui/model: Make sure that the queue model can represent a NULL queue
Otherwise this could lead to crashes or buggy behavior.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-11 10:52:29 -04:00
Anna Schumaker 40bce8cc59 gui/model: Don't require a GuiQueueModel for public functions
We can get by without this now that the model is shared across all
queues.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-11 10:52:29 -04:00
Anna Schumaker 5c426fc8d7 gui/model: Update runtime label when the model changes
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-11 10:52:29 -04:00
Anna Schumaker f58cc8da46 gui/model: Add a function for getting the current queue
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-11 10:52:29 -04:00
Anna Schumaker a28b5c4ec5 gui/model: Create a static, shared GuiQueueModel
Now that we can switch between different queues we no longer need to
allocate multiple models.

Implements #72: Only allocate a single GuiQueueModel for all playlists
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-11 10:52:29 -04:00
Anna Schumaker a9aa3c297d gui/model: Add support for changing the represented queue
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-11 10:52:27 -04:00
Anna Schumaker 53285534b9 gui/model: Improve the efficiency of gui_queue_model_clear()
We can cut out several thousand function calls and allocations by
reusing the same GtkTreePath structure.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-11 10:50:50 -04:00
Anna Schumaker c2178bc265 core: Cut back on hardcoded dbe_index uses
The dbe_index of a given database item might change in the future, so we
can't rely on it everywhere.  Let's just use it for saving and loading
files, with the expectation that changes will happen sometime after
startup.

Implements #69: Reduce use of dbe_index in Ocarina code
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-11 10:50:45 -04:00
Anna Schumaker e82beb719f core/tags/track: Reduce the size of the track tag
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-11 10:50:34 -04:00
Anna Schumaker d222c306e0 core/date: Force struct date into 32bits
Three unsigned integers is overkill for handling dates.

Implements #65: Date structure can be represented with a single 32bit
value
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-11 10:50:22 -04:00
Anna Schumaker 65b547f60b core/tags/album: Add pointer to genre tag
Albums tend to have a single genre for all tracks, so it makes more
sense for an album to point to genre information.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-11 10:25:43 -04:00
Anna Schumaker 629f81da17 Ocarina 6.4.19
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-11 10:22:40 -04:00
Anna Schumaker 76d8b00ecc core/playlists/library: Use library tag path when allocating playlists
This isn't as important for established playlists, but this string could
quickly become an invalid pointer for new playlists that are added.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-09-01 12:05:02 -04:00
Anna Schumaker 00c4c8a418 gui/collection: Set dialog default response
This gives users the ability to add new library paths by pressing the
"Enter" key instead of needing to click a button with their mouse.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-31 15:35:10 -04:00
Anna Schumaker ae851ab4ea core/playlists/library: Remove tracks from artist playlist when deleting
Otherwise we could leave artist playlists with invalid pointers.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-31 15:34:29 -04:00
Anna Schumaker 21718c4179 gui/collection: Select user's music directory when creating file chooser
Most users will have their music in their ~/Music directory, so it makes
sense to select this path by default.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-30 07:47:07 -04:00
Anna Schumaker 15807434a4 gui/view: Unit test cleanups
- Define __queue_filter_how_changed() for tests that need queue
  callback functions
- Increase column sizes in the view test to make test more reliable

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-29 07:59:11 -04:00
Anna Schumaker 1d02024505 gui/queue: Check for gq_queue before dereferencing it
Otherwise Ocarina will crash.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-29 07:55:15 -04:00
Anna Schumaker 20f10c163d core/playlists/artist: Add pl_artist_delete_track()
Library playlists call this when deleting tracks, otherwise we could end
up dereferencing invalid pointers the next time we use the artist
playlist.

Fixes #81: Remove deleted tracks from artist playlists
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-29 07:53:13 -04:00
Anna Schumaker 4e5ae5e57c Ocarina 6.4.19-rc
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-29 07:38:49 -04:00
Anna Schumaker d7322c1f07 gui/queue: Add a selector for filtering by different fields
Implements #63: Add field selector for filtering
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-29 07:33:00 -04:00
Anna Schumaker 11ef52b5de core/string: Remove unused string functions
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-29 07:33:00 -04:00
Anna Schumaker aead4939c3 core/filter: Remove filter.c
It is unused now that I have a token matching system in place.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-29 07:33:00 -04:00
Anna Schumaker 2e753b6f52 gui/queue: Switch over to using token matches for filtering
This patch also adds the genre field to the list of fields that can be
searched.

Implements #62: Replace filter layer with token matching
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-29 07:33:00 -04:00
Anna Schumaker d16e06111d core/tags/track: Add track_match_token() function
This is used to check if a track has a token that begins with the
requested string.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-29 07:33:00 -04:00
Anna Schumaker 6ad4325f22 core/tags/track: Convert lowercased string into tokens
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-29 07:33:00 -04:00
Anna Schumaker 2e1c27294b core/tags/genre: Add genre_match_token() function
This is called to check if any string in a genre's token list is
prefixed by the given string.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-29 07:33:00 -04:00
Anna Schumaker 1ca9bb36c1 core/tags/genre: Convert lowercased string into tokens
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-29 07:33:00 -04:00
Anna Schumaker a70b27779f core/tags/artist: Add artist_match_token() function
This is called to check if any string in an artist's token list is
prefixed by the given string.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-29 07:33:00 -04:00
Anna Schumaker b643b532d7 core/tags/artist: Convert lowercased string into tokens
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-29 07:32:59 -04:00
Anna Schumaker be29f53eaa core/tags/album: Add album_match_token() function
This is called to check if any string in an album's token list is
prefixed by the given string.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-29 07:32:59 -04:00
Anna Schumaker c3bc8e9c02 core/tags/album: Convert lowercased string into tokens
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-29 07:32:59 -04:00
Anna Schumaker f70920015c core/string: Add a function for matching tokens
This will be used to replace the current filtering code with a token
comparison for each track.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-29 07:32:59 -04:00
Anna Schumaker de0446120e core/string: Add a comparison function for tokenized strings
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-29 07:32:59 -04:00
Anna Schumaker b3750aa31c core/playlists/system: Compare playlist names in a loop
This is easier to maintain than a giant switch statement, and should
make it easier to add new playlist types in the future.

Implements #61: Clean up system playlists
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-29 07:32:44 -04:00
Anna Schumaker d3e2f069fd core/playlists/system: Load collection in a separate idle task
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-29 07:31:29 -04:00
Anna Schumaker 81aea3017e core/playlists/system: Trigger an update during playlist init
This is a bit cleaner, and helps to simplify the system playlist
initialization process.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-29 07:31:29 -04:00
Anna Schumaker f4b12c5a83 core/playlists/system: Define each playlist near it's associated functions
I think this is a bit cleaner, and helps to keep similar things
together.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-29 07:31:20 -04:00
Anna Schumaker 532d55ba4f core/playlists/system: Clean up collection save and load function
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-28 10:13:03 -04:00
Anna Schumaker 019137e4ed core/playlists/generic: Add a playlist_generic_update() function
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-28 10:13:03 -04:00
Anna Schumaker dddb098354 core/playlists/generic: Add a playlist_generic_init() function
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-28 10:13:03 -04:00
Anna Schumaker 1a29458d7c core/playlists/generic: Add a playlist_generic_remove() function
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-28 10:13:03 -04:00
Anna Schumaker c2a7d0289f core/playlists/generic: Add a playlist_generic_add_track() function
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-28 10:13:02 -04:00
Anna Schumaker 8f7e8be39d core/playlists/generic: Add a playlist_generic_set_flag() function
The history playlist uses a noop function, since changing random or
repeat settings doesn't make sense for this playlist.  The collection
playlist uses a custom function to save changes after setting a flag.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-28 10:13:02 -04:00
Anna Schumaker a80a84a955 core/playlists/generic: Add a playlist_generic_sort() function
I set the history playlist to use a noop function, since changing the
history doesn't really make sense.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-28 10:13:02 -04:00
Anna Schumaker d460bcaee8 core/playlists/system: Add struct sys_playlist
This will contain the playlist and operations for each secific type.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-28 10:13:02 -04:00
Anna Schumaker 3d85f0fdc5 Ocarina 6.4.18
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-28 10:08:10 -04:00
Anna Schumaker 1633946981 core/file: Check if a file is too new to be opened
I'm hitting this problem while developing 6.5, since file formats are
going to change.  Let's handle this situation gracefully rather than
segfaulting.

This patch changes versioning problems into fatal errors to prevent us
from overwriting data already on disk.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-28 10:07:27 -04:00
Anna Schumaker 3806577154 gui/playlists: Add artist playlists when they are created
We only need to do this after Ocarina init has completed, otherwise
artist playlists will be added twice.

Fixes #79: Artist playlists not updated when tracks are added
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-28 09:48:12 -04:00
Anna Schumaker a430c5b117 core/playlists/artist: Add artist playlists as artists are added
Scanning new library paths should also add artist playlists, otherwise
they won't show up until Ocarina is restarted.

Fixes #79: Artist playlists not updated when tracks are added
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-28 09:46:16 -04:00
Anna Schumaker 3d31349cca gui/playlist: Random state isn't saved when random button is clicked
We were calling the queue function directly, rather than passing through
the playlist layer.  This means random state isn't saved when the button
is clicked and Ocarina is closed.

Fixes #78: Call playlist_set_random() when clicking random button
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-28 09:06:33 -04:00
Anna Schumaker f855eaea58 gui/collection: Run FileChooserDialog on startup
But only if the library database is empty.  This patch also changes the
prompt of the dialog to "Add Music" so users know what is going on.

Fixes #80: Pop up FileChooserDialog the first time Ocarina is run
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-16 13:38:58 -04:00
Anna Schumaker 5964c508ce core/playlists/artists: Load each playlist with a different idle task
This seems less efficient overall, since we now need to take several
passes over the track database.  What I've noticed is that the
single-pass option creates a lot of UI notifications that makes the gui
unresponsive for a large amount of time.  Breaking this into smaller
chunks gives us a chance to handle any user actions between loading each
playlist.

Fixes #71: Initalize artist playlists with more idle tasks
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-13 09:50:24 -04:00
Anna Schumaker 73b33e9718 gui: UI spacing improvements
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-13 09:49:21 -04:00
Anna Schumaker f3360f6da5 Ocarina 6.4.18-rc
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-13 09:07:26 -04:00
Anna Schumaker 9733b82ae8 core/collection: Remove file
I have replaced everything in this file with the code in
core/playlists/library.c

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker bddbd04ef5 gui: Remove stack widget
It isn't needed now that we pop up a file choose dialog for selecting
library paths.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker b3476e15e9 gui/collection: Remove unused collection code
And the now unused sidebar widget.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker 73c70678bb gui/collection: add button for adding library paths
This is easier than responding to events from the treeview.  Let's just
pop up a dialog and let the user select from there, rather than doing
something complicated with a stack.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker 62e1b27b6c gui/playlist: Add support for deleting playlists
System playlists cannot be deleted, but library playlists can.  Deleting
a library playlist removes the library and associated tracks from the database.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker 382ee79c2e gui/playlist: Add artist playlists to the playlist sidebar
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker f2597a8e6c gui/playlist: Add library playlists to the playlist sidebar
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker 8abc45b1ae gui/playlist: Store the playlist type in the sidebar treemodel
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker 871bf88b94 gui/queue: Store playlist in the gui queue
I'll need to know the playlist type to set artist information properly.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker f70b2b940f core/playlists/artist: Add basic artist operations
For now, hidden tracks are shown in artist queues.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker 24448aefec core/playlists/artist: Add tracks to the artist playlists
I use a single idle task to scan the track database and add to the
appropriate playlist.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker 12ae7cfee6 core/playlists/artist: Add pl_artist_{init, deinit}() functions
The pl_artist_init() function is used to allocate a playlist for each
artist already in the database, and pl_artist_deinit() is then used to
free up this memory during cleanup.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker 11430f89cf core/playlists/library: Add pl_library_update() function
This is called to find and add new tracks to the library.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker e1a722d04b core/playlists/library: Adding a library starts a directory scan
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker c448db2665 core/playlists: Add playlist_{new, delete}() functions
System playlists cannot be created or deleted, so these functions simply
return false in this case.  Library playlists will use this to add new
library paths to Ocarina.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker 78aa0f9ff6 core/playlists/library: Add pl_library_sort() function
Used to sort library playlists.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker 7f867199cb core/playlists/library: Add pl_library_set_flag() function
I also take the opportunity to add in a generic lookup function to
convert a library path into a playlist.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker 1599897fbf core/playlists/library: Add pl_library_add_rm() function
This function is for both adding and removing tracks, and only returns
false.  Tracks will only be added and removed through the update
function.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker c5a0a3470e core/playlists/library: Add pl_library_get_queue() function
Called to access the track queue associated with the given playlist.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker 4d5569ef7a core/playlists/library: Add tracks to library playlists
This needs to happen in an idle task since the track database is loaded
when idle.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker 261f6d91d7 core/playlists/library: Add pl_library_{init, deinit}() functions
The pl_library_init() function is used to allocate a playlist for each
library path already in the database, and pl_library_deinit() is then
used to free this memory during cleanup.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker e4930704a2 core/playlists/system: Add pl_system_delete_track()
Called to remove a track from all system playlists.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker 03ed7c4b84 core/playlists/system: Add pl_system_new_track()
Called to tell system playlists that a new track has been added to the
database.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker 20e0a85a5d core/tags/artist: Add an artist_db_get() function
This will be called to scan the artist database when setting up
playlists.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker b3922cc731 core/tags/artist: Add an artist_lookup() function
For looking up artist tags without allocating a new one.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker e41f554b2e core/tags/artist: Add a pointer to store a playlist
Similar to the library tag, this is a void pointer that should only be
used by the playlist layer.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker 938bbc92f2 core/tags/library: Add a library_lookup() function
I want a way to lookup library paths without allocating new ones, so
let's add this now.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker 6e02f75262 core/tags/library: Add a pointer to store a playlist
I decided to set this as a void pointer to keep other layers from using
the playlist without our knowledge.  The only user of this variable
should be the playlist code.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker cfeca9ae4b core/playlist: Add playlist_type enum
I'm going to use this to distinguish between various playlist types that
are about to be added.  Let's update the playlist functions first, and
then add more types in a future patch.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-08-13 08:31:30 -04:00
Anna Schumaker 6019f07bd3 Ocarina 6.4.17
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-13 08:27:55 -04:00
Anna Schumaker 004d4578d4 gui/queue: Set gq_queue->gq_search to NULL after freeing
Otherwise we can attempt to free the search text again when the queue is
destroyed.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-13 08:26:00 -04:00
Anna Schumaker 085701ad9d gui/queue: Set gq_queue->gq_visible to NULL after destroying
Otherwise the existance check will still pass the next time
__queue_filter() is called, and we will attempt to destroy it again.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-13 08:21:22 -04:00
Anna Schumaker f7e9e8b321 gui/queue: Remember search text when switching queues
I wasn't doing this, but I also wasn't refiltering queues when they were
changed.  This resulted in showing a filtered queue, but without a way
to clear it.  Remembering the text lets me simply set the text, instead
of refiltering queues whenever they are changed.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-10 07:31:03 -04:00
Anna Schumaker 44a6ad0d78 gui/playlist: Make sure we free text from tree_model_get()
Otherwise this leads to a memory leak.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-08-01 12:06:25 -04:00
Anna Schumaker e578e0e6dd Ocarina 6.4.17-rc
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-07-29 17:09:39 -04:00
Anna Schumaker 34447007f9 core/history: Remove unused queue
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 17:09:38 -04:00
Anna Schumaker 5a54fb69b6 core/collection: Remove unused queue
And remove collection_ops from the core initdata

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 17:09:33 -04:00
Anna Schumaker dc07d637f6 gui/history: Remove unused gui queue code
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 17:06:06 -04:00
Anna Schumaker 64fc5a9a0d gui/collection: Remove unused gui queue code
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 17:06:06 -04:00
Anna Schumaker 15ed068e13 gui/playlist: Select Collection playlist during startup
This preserves the original behavior, where the collection is always
selected from the beginning.  This can be improved upon later, once
queued tracks are implemented as a playlist.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-07-29 17:06:06 -04:00
Anna Schumaker 696173b03e gui/playlist: Add Collection and History playlists to the sidebar
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 17:06:06 -04:00
Anna Schumaker dfae74dd50 gui/playlist: Set gui queue name based on passed playlist
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 17:06:06 -04:00
Anna Schumaker 8eaf6506e8 core/playlist: Add playlist_{next, prev}() functions
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 17:06:06 -04:00
Anna Schumaker efae58c356 core/playlists/system: Add history playlist
Implements #8: Convert history queue into a playlist
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 17:06:04 -04:00
Anna Schumaker 6265b937c7 core/playlists/system: Add collection playlist
Implements #7: Convert collection queue into a playlist
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 17:06:03 -04:00
Anna Schumaker 5d4de9c5b0 core/playlist: Add playlist_sort() function
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 17:06:03 -04:00
Anna Schumaker c7bc2062d2 core/playlist: Add playlist_{get,set}_random()
I'll need this to toggle the random flag for the Collection playlist to
avoid cycling through the GUI in order to save.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 17:06:03 -04:00
Anna Schumaker 62123eb025 core/playlists: Add struct playlist
Containing the playlist name and queue.  I pass this to the gui through
the queue_init() function.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 17:06:03 -04:00
Anna Schumaker 44a57ed863 core/queue: Add extra paramater to queue_init()
This is used to pass through a value to the GUI during queue
initialization.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 17:06:03 -04:00
Anna Schumaker 7f0e1ecc99 core/queue: Add queue_{save, load}_flags() function
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 17:06:03 -04:00
Anna Schumaker 92a53d90b4 core/database: Move database out of containers/ subdirectory
It doesn't make sense to have a subdirectory with a single file.  Let's
move it back to core/

Implements #45: Move database into core/
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 17:06:01 -04:00
Anna Schumaker fa0dee5921 core/containers: Remove unused set class
Implements #48: Remove set code
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 17:06:00 -04:00
Anna Schumaker 98fe6d3bbb core/containers: Remove unused index class
Implements #44: Remove Index code
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 17:05:59 -04:00
Anna Schumaker 93fb40360d core/filter: filter_search() returns a new GHashTable
I think this is cleaner than having an upper layer allocate the results
set manually.  This also lets me return NULL in the case that there were
no results.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 17:05:59 -04:00
Anna Schumaker fda3f761a0 core/filter: Call GHashTable functions directly
Implements #43: Filter code can use a GHashTable directly
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 17:05:54 -04:00
Anna Schumaker 63b46b6161 core/filter: Implement copy and intersect directly
I intend to remove the set class, so we still need a way to handle
filter results.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 17:05:54 -04:00
Anna Schumaker 29b2b6f6f4 core/filter: Store track pointers directly
I was storing database indexes, but this assumes that database indexes
are constant.  I intend to change this with database defragmentation.

Implements #46: Filter stores a track pointer
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 17:05:47 -04:00
Anna Schumaker 415bd9731a core/filter: Add filter_remove()
This is used to remove tracks from the filtering index.  We'll need this
once we switch over to storing pointers to keep the same track from
getting added multiple times.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 15:46:11 -04:00
Anna Schumaker 04a175d3e3 core/filter: Switch from using Index to a GHashTable
This is more straightforward than using my custom index code without
adding too much work.

Implements #43: Filter code can use a GHashTable directly
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 15:46:11 -04:00
Anna Schumaker ee4bbacf81 core/containers/set: Add set_alloc() and set_free() functions
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-29 15:46:11 -04:00
Anna Schumaker da3dd60ef3 Ocarina 6.4.16
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-07-29 15:41:05 -04:00
Anna Schumaker 951624c82f core/audio: Update Most and Least Played playlists
We need to do this when tracks are played to keep playlist counts
accurate.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-07-29 10:14:35 -04:00
Anna Schumaker 17910c72b2 tests: Add extra underscore to placeholder callback function
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-07-28 16:53:18 -04:00
Anna Schumaker 779969f28b core/idle: Let tests run without the thread pool
The thread pool is used to fetch album art in the background, but this
can slow down most tests that aren't interested in album art.  Adding a
(testing-only) function for running without the thread pool speeds
things up a bit.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-07-28 16:17:43 -04:00
Anna Schumaker bd8f3754e7 CHANGELOG updates
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-07-28 09:17:11 -04:00
Anna Schumaker 1d1e41916a core/playlists/system: Check that we read a valid playlist name
Otherwise we could crash when we try to access an out of bounds area of
the playlist array.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-07-28 08:55:53 -04:00
Anna Schumaker 3098282382 gui/artwork: Don't destroy objects twice
The file filter and preview widget are both cleaned up when the file
chooser dialog is destroyed.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-07-27 10:51:59 -04:00
Anna Schumaker 3b62125135 gui/artwork: Show hidden folders when selecting new images
Sometimes Ocarina gets the correct image for CD1, but not for CD2.
Allow traversing into hidden folders so users can select the correct
image out of the artwork cache.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-07-27 10:51:42 -04:00
Anna Schumaker 4de869d691 gui/artwork: Clear __artwork_cur_album when selecting album art
Otherwise the new image will never be updated due to the "did the album
change?" check in __artwork_set_pixbuf()

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-07-27 10:48:13 -04:00
Anna Schumaker 160295a6cc gui/artwork: Add a preview widget to the file chooser dialog
This lets users actually see images before selecting them for ablum art.

Implements #59: Add preview widget to album art chooser
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-07-27 10:45:59 -04:00
Anna Schumaker 343dec5d8d gui/artwork: Fix blurry album art
Using gdk_pixbuf_new_from_file_at_scale() can result in a blurry image
due to something in the gdk backend.  I want to have a sharper image, so
we need to perform the scale manually through Cairo.

Fixes issue #60: Album art is blurry
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-07-25 10:00:09 -04:00
Anna Schumaker 7dff5412bc gui/artwork: Reduce time spent polling for artwork
Since MusicBrainz limits us to one request each second, it doesn't
really make sense to poll for new artwork every half second.  Instead,
let's just put this into a new timeout function that gets called every
two seconds.  This should also let us stop polling once artwork has been
found.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-07-22 09:40:03 -04:00
Anna Schumaker 83724c5f6f gui/artwork: Split out album art functions into a new file
I think this is cleaner, and it should make it easier to maintain this
code.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-07-22 09:40:03 -04:00
Anna Schumaker 8893cdb58b tests: Unit test fixes
These should have been included as part of 6.4.15.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-07-22 09:40:02 -04:00
Anna Schumaker c5a563c112 Fix PKGBUILD dependency
We depend on the libmusicbrainz5 library, not libmusicbrainz.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-07-19 17:02:13 -04:00
Anna Schumaker 6ab5d99d71 Ocarina 6.4.16-rc
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2016-07-19 17:01:02 -04:00
Anna Schumaker e24ab97545 gui/playlist: Add size information to playlist sidebar
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 16:03:11 -04:00
Anna Schumaker 2c18f9e715 core/playlist: Add playlist_size() function
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 16:03:11 -04:00
Anna Schumaker 00dd72d46e core/playlist: Remove unused playlist_t enum
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 16:03:11 -04:00
Anna Schumaker 5e9b6bc975 core/playlist: Access playlists through a string
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 16:03:11 -04:00
Anna Schumaker 90e0b3ed78 core/playlist: Remove remaining dynamic playlist code
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 16:03:11 -04:00
Anna Schumaker 2ff7113668 core/playlists/system: Move the least played playlist into system.c
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 16:03:10 -04:00
Anna Schumaker 9d3cc2e5ab core/playlists/system: Move the most played playlist into system.c
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 16:03:09 -04:00
Anna Schumaker 5ab2e63734 core/playlists/system: Move the unplayed tracks playlist into system.c
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 16:03:06 -04:00
Anna Schumaker 1527ee0e6d core/playlist: Add playlist_update() function
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 16:03:06 -04:00
Anna Schumaker 5a0fc355e4 core/playlist: Remove __playlist_is_{dynamic, static}() functions
This helps prepare for moving dynamic playlists into the system playlist
code.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 16:03:06 -04:00
Anna Schumaker a465577c86 core/playlists/system: Move system playlists into a new file
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 16:03:06 -04:00
Anna Schumaker e3d4143565 core/playlist: Add new playlist_type struct
I intend to use this for creating playlists with different properties,
such as using the playlist interface to add library paths.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 16:03:06 -04:00
Anna Schumaker 140abe79bc core/playlist: Split out system playlist load and save functions
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 16:03:06 -04:00
Anna Schumaker 8026207037 core/playlist: Split out basic system playlist functions
I'm going to use this as a starting point for creating a new playlist
interface.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 16:03:06 -04:00
Anna Schumaker 0f5d4e6a34 core/playlist: Remove __playlist_fill_static()
And other support code for filling the obsolete playlist_q with tracks.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 16:03:06 -04:00
Anna Schumaker 151c446635 core/playlist: Use separate queues for favorite and hidden playlists
Implements issue #6: Store playlists as queues
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 16:03:04 -04:00
Anna Schumaker e5dc36ca6e core/playlist: Store a struct queue in the playlist_db
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 08:13:10 -04:00
Anna Schumaker 90fc9bff0a core/playlist: Convert playlist_db to a GHashTable
I want to manage this container on my own, rather than relying on the
generic index code.  This will eventually make it easier to store a
playlist as a queue, instead of a set.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 08:13:10 -04:00
Anna Schumaker c845530812 core/queue: Resort the queue when unsetting Q_ADD_FRONT
This flag is used for bulk inserts to cut down on the number of gui
operations during startup.  Let's just make it standard that unsetting
the flag causes the queue to resort itself.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 08:13:10 -04:00
Anna Schumaker 9a98a009e9 core/queue: Add queue_{save|load}_tracks() function
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 08:13:10 -04:00
Anna Schumaker 9ec2497ac5 core/queue: Add a queue_has() function
Used to check if a track is on a queue.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 08:13:10 -04:00
Anna Schumaker afb2653f65 core/queue: queue_remove_all() returns count of tracks removed
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 08:13:10 -04:00
Anna Schumaker 378ff72307 Remove core/containers/queue
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 08:13:10 -04:00
Anna Schumaker db01ed3208 core/queue: Use a GQueue directly
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 08:13:10 -04:00
Anna Schumaker 2b426cff48 core/queue: Switch over to using struct queue_iter
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 08:13:10 -04:00
Anna Schumaker 9835235acd core/queue: Implement a new queue_iter
I plan to remove the containers/queue implementation, so we need a new
iterator for queue access.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 08:13:10 -04:00
Anna Schumaker 6bb08ddbaa core/queue: Call g_queue_*() functions directly
This is a first step to removing core/containers/queue

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 08:13:10 -04:00
Anna Schumaker e8d7576704 core/containers/set: Return status of set_insert() and set_remove()
I use this status indicator in the playlist code fairly often, so let's
return it directly rather than needing to code around it.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 08:13:10 -04:00
Anna Schumaker 5a30a63904 core/string: Add string_match() function
This function is used to determine if both strings exist and have the
same contents.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 08:13:10 -04:00
Anna Schumaker b7cf2509f7 Ocarina 6.4.15
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-19 08:10:07 -04:00
Anna Schumaker 9928c3239f core/tags/album: Improve MusicBrainz query accuracy
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-15 09:37:54 -04:00
Anna Schumaker 4f1f76b764 gui/audio: Update album art when manually setting
If the user is fixing broken album art then we need to update the image
immediately, otherwise the user will be confused.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-14 13:36:33 -04:00
Anna Schumaker eb5c0185e7 core/tags/album: Album art fetching improvements
- Allocate CaaCoverArt as needed, rather than sharing one object
- Use MusicBrainz fuzzy search to match more albums
- Escape most special characters in filenames

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-14 11:38:05 -04:00
Anna Schumaker 617088c89b core/tags/album: Fix retrying MB5 query on error 503
Reusing the Mb5Query can have undesireable side effects, such as the
error code not getting reset between queries.  Fix this by allocating a
new Mb5Query before evey request.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-14 11:38:05 -04:00
Anna Schumaker c62a88ce09 gui/audio: Add support for manually setting album art
Implements #58: Manually set album art
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-14 11:37:53 -04:00
Anna Schumaker e30f7f8115 core/tags/album: Add support for importing album artwork
This is needed so the user can manually set album artwork in the cases
where either we don't fetch the right image or no image is found.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-12 14:36:40 -04:00
Anna Schumaker 80d921343d core/file: Add support for importing a file into the cache
This will be needed to allow the user to manually set album art from a
downloaded file.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-12 14:16:46 -04:00
Anna Schumaker 99b51f5257 core/tags/album: Make quotes around artist name configurable
This allows me to search for `"release" AND artist` but also `release
AND "artist"`

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-11 09:43:01 -04:00
Anna Schumaker df3436f68a core/tags/album: Search with quotes around artist instead of album name
I think this results in a slightly better search, although there are
still a few wrong images fetched.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-11 08:05:33 -04:00
Anna Schumaker c8ccf7b844 core/tags/album: Place multiple queries to find artwork
If searching for artist and album fails, then fall back on album and
year or even just album name.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-04 21:30:06 -04:00
Anna Schumaker b4218833c5 core/tags/album: Immediately retry query on 503 error
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-04 20:59:29 -04:00
Anna Schumaker 2ea5104d42 core/tags/album: Fetch album art in the same step as metadata
This will help prepare for running multiple queries to find a release.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-04 20:59:28 -04:00
Anna Schumaker 86b63298a5 gui/idle: Show the idle progress as a tooltip
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-07-04 20:07:50 -04:00
Anna Schumaker 3c9798a6b7 gui: Remove unused GtkAdjustment
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-30 11:19:59 -04:00
Anna Schumaker ecc7843ea5 gui: Clean up setting album art
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-30 10:28:17 -04:00
Anna Schumaker dd66b10dfa gui: Properly load a pixbuf
Using gdk_pixbuf_new_from_file_at_scale() will load a scaled pixbuf,
which looks better than loading a height x height square.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-30 10:28:17 -04:00
Anna Schumaker 9ef9b17b93 gui: Fix up main GtkGrid
Somewhere along the line the grid formatting got messed up, probably
after several merges while other bugs were fixed.  This patch reworks
the grid so everything is formatted correctly.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-30 10:27:57 -04:00
Anna Schumaker 1ac9e69128 gui: Minor spacing updates
This helps some widgets line up better, instead of looking slightly out
of place.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-30 09:19:15 -04:00
Anna Schumaker cbb9631877 gui: Add a function for determining the height of widgets
It's eaiser to do it in the builder code, rather than repeating this
code in several places.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-30 08:18:11 -04:00
Anna Schumaker 1e59968154 gui: Initialize track labels with whitespace
This gives labels the correct initial size, rather than forcing them to
resize after startup.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-30 08:08:56 -04:00
Anna Schumaker 5b7a985dc6 Ocarina 6.4.15-rc
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 11:13:55 -04:00
Anna Schumaker 54af68a57f gui/audio: Add album art support
Also add the "o_cover" widget to the glade file for setting album art.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker d0dddeacdb core/tags/album: Add album_artwork_path() function
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker 940d2d1dc2 core/tags/album: Query the Cover Art Archive for album art
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker 570efda299 core/tags/album: Query MusicBrainz for album release information
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker d79eb2c9b8 core/tags/album: Add album_artwork_exists() function
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker 0d9b7baff6 core/tags/album: Add a pointer to the struct artist
This is needed for album art fetching to get more accurate results for
our MusicBrainz query.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker 97c8f80393 core/idle: Add a way to reschedule idle tasks
This will be needed to reschedule MusicBrainz requests if the server is
busy.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker 6b52775e58 core/idle: Add support for running idle tasks in a new thread
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker 6a44f9e1a1 core/idle: Add idle_sync_t enum
I intend to use this to select between sync and async idle tasks.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker a6bfbbb1c6 core/file: Add file_remove() function
I plan to use this for upgrades to remove obsolete files.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker 0344a2088d core/file: Add cache_file_write() function
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker 9db9df619f core/file: Add cache_file_close() function
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker e137eb0108 core/file: Add cache_file_open() function
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker 3723dff87d core/file: Add cache_file_exists() function
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker 79ecaa11fb core/file: Add cache_file_write_path() function
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker 5dcf681e48 core/file: Add cache_file_path() function
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker b5e13af30c core/file: Add cache_file_init() function
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker 12949651f7 core/file: Add struct cache_file
I want to reuse as much file code as I can for cache files, but I need a
new struct since data and cache files track different properties.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker 9e0f017e61 core/file: Build in minimum version checks
I don't think it makes sense that callers of file_open() are expected to
check the file version after opening.  This should be something handled
by the file code so we can print a consistent error message.

Implements issue #5: Better file versioning
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker fe6f31b1e5 core/file: Rework REPORT_ERROR() function
I want to be able to print any message as an error message without
relying on errno.  Let's handle this by introducing REPORT_ERRNO() to
look up the right error message for a given errno.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker ee4f0d4c89 core/file: Writes go to a temporary file first
And then rename the temporary file when closed.  This protects users
data in case Ocarina gets killed.

Implements issue #31: Make file writes seem atomic
Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker c308ba7f8e core/file: Rewrite file opening
There are enough differences in the read and write paths that
__file_open_common() was still trying to account for.  Let's make a
simpler wrapper around g_fopen() and move differing code into
__file_open_read() and __file_open_write().

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker 224290d4eb core/file: file->f_name can be a pointer
I'm only setting this to a static string, so it doesn't make sense to
store this as a char array.  Let's use a string pointer instead.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker 427319b72e core/file: Move OCARINA_NAME configuration into version.h
I'm going to use this for saving album art to the right place.  I moved
it into an include file to avoid duplicating code.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
Anna Schumaker 98faba93d1 core/string: Add a string_length() function
This is a wrapper around strlen(), but checks for a NULL pointer first.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
2016-06-24 10:50:37 -04:00
194 changed files with 12447 additions and 9989 deletions

7
.gitignore vendored
View File

@ -13,3 +13,10 @@ bin/
*.gcno
core.*
.debug
CMakeCache.txt
CMakeFiles
Makefile
cmake_install.cmake
install_manifest.txt
CTestTestfile.cmake
Testing/

View File

@ -6,9 +6,9 @@ stages:
build:
stage: build
script:
- scons
- cmake -DCONFIG_DEBUG=ON && make
test:
stage: test
script:
- scons tests/core
- cmake -DCONFIG_TESTING_VERBOSE=ON -DCONFIG_TESTING_GUI=OFF && make tests

View File

@ -1,3 +1,98 @@
6.5.0:
- Add a switch to disable GUI tests
- Don't save and restore the "Last Played" tracks column width
- Save databases if they have been upgraded
- Don't duplicate tracks in the library after a defrag
- Remove "Add New ..." menubutton and replace with Add Library Path button
- Various UI tweaks
- Fix showing the first playlist in the list
- Fix memory leaks in core/
- Fix theme parsing errors
- Fix crash while reading 6.4.x playlists
6.5.0-rc:
- Convert to CMake/CTest
- Save current track in settings database
- Add "Queued Tracks" playlist
- Add support for changing current playlist
- Hide empty playlists
- Save current position in playlists
- Set file version from Ocarina minor version
- Add support for defragmenting tag databases on startup
- Add user created playlists
- Add support for changing volume
- Remove Repeat button
- Various UI improvements
- Add command line options
6.4.20:
- Enable idle processing when tracks are played to update dynamic playlists
6.4.20-rc:
- Use a single GtkTreeModelFilter instance for displaying tracks
- Use a single GuiQueueModel instance for displaying tracks
- Reduce use of database entry index accesses
- Reduce the size of the track tag
6.4.19:
- Fix memory corruption when adding new library playlists
- Use Enter key to accept new library paths
- Remove deleted tracks from artist playlists
- File chooser selects user's music directory
- Remove deleted tracks from artist playlists
6.4.19-rc:
- Allow filtering specific fields
- Replace filtering with a token matching system
- Clean up system playlist code
6.4.18:
- Load each artist playlist in a separate idle task
- UI spacing improvements
6.4.18-rc:
- Remove stack widget from UI
- Add Library Path playlists
- Add Artist playlists
6.4.17:
- Fix memory leak in __playlist_name()
- Remember search text when changing displayed playlist
6.4.17-rc:
- Filter can use GHashTables directly
- Filter can store track pointers instead of indexes
- Remove unused set and index classes
- Convert Collection into a playlist
- Convert History into a playlist
6.4.16:
- Let tests run without fetching album art
- Don't try to read data from nonexistant playlists
- Show hidden folders when selecting new images
- Add preview widget to album art chooser dialog
- Fix blurry album art
- Reduce time spent polling for album art
- Split album art code into a new file
- Fix PKGBUILD dependencies
6.4.16-rc:
- Implement generic playlist layer
- Implement separate queues for each playlist, including dynamic playlists
- Remove core/containers/queue
6.4.15:
- Add idle progress as a tooltip.
- Initialize track labels with the correct initial size
- Initialize album part with the correct size
- Gui spacing updates
6.4.15-rc:
- Write data files to a temporary file first, then rename
- Add support for writing files to the user's XDG_USER_CACHE_DIR
- Add idle tasks that run in a new thread
- Fetch album art
6.4.14:
- GtkPaned spacing improvements

84
CMakeLists.txt Normal file
View File

@ -0,0 +1,84 @@
cmake_minimum_required(VERSION 3.4)
project(Ocarina)
# Various options for users
option(CONFIG_DEBUG "Compile with debugging symbols" OFF)
option(CONFIG_TESTING_VERBOSE "Enable verbose output when running tests" OFF)
option(CONFIG_TESTING_GUI "Enable running GUI tests (requires an X server)" ON)
option(CONFIG_TESTING_ARTWORK "Enable album artwork fetching tests" ON)
# Configure settings
set(CONFIG_MAJOR 6)
set(CONFIG_MINOR 5)
set(CONFIG_MICRO 10)
set(CONFIG_RC ON)
set(CONFIG_VERSION "${CONFIG_MAJOR}.${CONFIG_MINOR}.${CONFIG_MICRO}")
if (CONFIG_RC)
set(CONFIG_VERSION "${CONFIG_VERSION}-rc")
endif()
set(DEBUG_OPTIONS -Wall -Werror -g -DCONFIG_DEBUG)
if (CONFIG_DEBUG)
add_definitions(${DEBUG_OPTIONS})
set(CONFIG_VERSION "${CONFIG_VERSION}-debug")
endif()
find_package(Git)
if (GIT_FOUND AND IS_DIRECTORY .git/)
configure_file(.git/HEAD .git/HEAD COPYONLY)
execute_process(COMMAND ${GIT_EXECUTABLE} describe --abbrev=0
COMMAND xargs ${GIT_EXECUTABLE} diff --quiet
RESULT_VARIABLE HAS_CHANGED)
if (HAS_CHANGED AND CONFIG_RC)
set(CONFIG_VERSION "${CONFIG_VERSION}+")
endif()
endif()
add_definitions(-DCONFIG_VERSION=\"${CONFIG_VERSION}\")
add_definitions(-DCONFIG_MAJOR=${CONFIG_MAJOR})
add_definitions(-DCONFIG_MINOR=${CONFIG_MINOR})
find_package(PkgConfig REQUIRED)
function(use_module name module)
pkg_check_modules(${name} REQUIRED ${module})
include_directories(${${name}_INCLUDE_DIRS})
add_definitions(${${name}_CFLAGS} ${${name}_CFLAGS_OTHERS})
link_libraries(${${name}_LIBRARIES} ${${name}_LDFLAGS})
endfunction()
use_module(GTK gtk+-3.0)
use_module(GMODULE gmodule-export-2.0)
use_module(GST gstreamer-1.0)
use_module(TAGLIB taglib_c)
use_module(CAA libcoverart)
use_module(MB5 libmusicbrainz5)
# Tell compiler where to find header and source files
include_directories(include)
file(GLOB_RECURSE core "core/*.c")
file(GLOB_RECURSE gui "gui/*.c")
# Configure executable
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY bin/)
add_executable(ocarina ${core} ${gui})
# Configure install targets
install(TARGETS ocarina DESTINATION /usr/bin/)
install(DIRECTORY share/ DESTINATION /usr/share/)
# Configure release target
set(CONFIG_RELEASE ocarina-${CONFIG_VERSION})
add_custom_target(release COMMAND git archive -v --prefix=${CONFIG_RELEASE}/ -o ${CONFIG_RELEASE}.tar.gz HEAD)
# Set up unit tests
set(CMAKE_TEST_COMMAND ctest --output-on-failure)
if (CONFIG_TESTING_VERBOSE)
set(CMAKE_TEST_COMMAND ctest -V)
endif()
if (IS_DIRECTORY tests/)
enable_testing()
add_custom_target(tests COMMAND ${CMAKE_TEST_COMMAND})
add_subdirectory(tests/)
endif()

1
CTestCustom.cmake Normal file
View File

@ -0,0 +1 @@
set(CTEST_CUSTOM_PRE_TEST "rm -rf $ENV{HOME}/.local/share/ocarina-test $ENV{HOME}/.cache/ocarina-test")

View File

@ -1,23 +1,24 @@
# Maintainer: Anna Schumaker <anna@ocarinaproject.net>
# Maintainer: Anna Schumaker <anna@nowheycreamery.com>
pkgname=ocarina
pkgver=6.4.14
pkgver=6.5.9
pkgrel=1
pkgdesc="A simple GTK+ and gstreamer based music player."
url="http://www.ocarinaproject.net/"
pkgdesc="A simple GTK+ and GStreamer based music player."
url="http://www.nowheycreamery.com/"
arch=('x86_64' 'i686' 'armv7h')
license=('GPL2')
depends=('gtkmm3>=3.16' 'gstreamer' 'gst-plugins-base' 'taglib')
depends=('gtk3>=3.22' 'gstreamer' 'gst-plugins-base' 'taglib' 'libmusicbrainz5' 'libcoverart')
optdepends=('gst-plugins-good' 'gst-plugins-bad' 'gst-plugins-ugly')
makedepends=('scons')
makedepends=('cmake')
conflicts=()
replaces=()
backup=()
source=("http://ocarinaproject.net/wp-content/ocarina/${pkgname}-${pkgver}.tar.gz")
source=("http://nowheycreamery.com/wp-content/ocarina/${pkgname}-${pkgver}.tar.gz")
sha1sums=('')
build() {
cd "${srcdir}/${pkgname}-${pkgver}"
scons $MAKEFLAGS
cmake .
make $MAKEFLAGS
}
package() {

20
README
View File

@ -1,20 +0,0 @@
Alternate clone url:
$ git clone git://git.ocarinaproject.net/anna/ocarina.git
Build:
$ scons
Clean:
$ scons -c
Install:
$ sudo scons install
Uninstall:
$ sudo scons -c install
Build tests:
$ scons tests
Clean tests:
$ scons -c tests

53
README.md Normal file
View File

@ -0,0 +1,53 @@
# Ocarina 6.5
Ocarina is a simple GTK+ and GStreamer based music player written to let you listen to your music without getting in the way.
### Git
Ocarina is tracked with Git, and can be cloned from the following sources:
* http://git.nowheycreamery.com/anna/ocarina.git
* git://git.nowheycreamery.com/anna/ocarina.git
##### Branches
* [master]
* The most recent release. This branch does not change frequently.
* [next]
* Changes that will be included in the next release. This branch is for testing and bugfixing.
### Building
Ocarina uses `cmake`to control the build process. After cloning the code, run `cmake .` to generate the Makefile. Compile using `make`
##### CMake Options
Ocarina supports the following options, which can be passed to `cmake` through `cmake -D<option>=<ON|OFF>`. For example, `cmake -DCONFIG_DEBUG=ON` would enable debugging.
* CONFIG_DEBUG
* Compile with debugging symbols enabled
* CONFIG_TESTING_VERBOSE
* Enable extra output when running unit tests
* CONFIG_TESTING_GUI
* Enable running GUI unit tests
* CONFIG_TESTING_ARTWORK
* Enable unit tests that fetch album artwork
### Running
Once compiled, Ocarina can be run by invoking `bin/ocarina` on the command line.
##### Runtime options
The following options can be passed to Ocarina during startup
* -h, --help
* Print help message and exit
* -n, --next
* Tell a running Ocarina instance to play the next track
* - P, --pause
* Tell a running Ocarina instance to pause playback
* -p, --play
* Tell a running Ocarina instance to start playback
* -N, --previous
* Tell a running Ocarina instance to play the previous track
* -t, --toggle
* Tell a paused Ocarina instance to begin playback, or tell a playing Ocarina instance to pause.
* -v, --version
* Print Ocarina version and exit
### Installing
Running `make install` will install Ocarina for use by all users. There is currently no "uninstall" option.
### Testing
Running `make tests` will compile and run the Ocarina unit tests. See the CMake Options section above for additional testing options.

View File

@ -1,59 +0,0 @@
#!/usr/bin/python
import os
CONFIG_VERSION = "6.4.14"
# Set up default environment
CONFIG_DEBUG = os.path.exists(".debug")
CONFIG_CCFLAGS = [ "-O2" ]
if CONFIG_DEBUG == True:
CONFIG_CCFLAGS = [ "-Wall", "-Werror", "-g", "-DCONFIG_DEBUG" ]
class OEnvironment(Environment):
def __init__(self, CCFLAGS = CONFIG_CCFLAGS):
Environment.__init__(self, CCFLAGS = CCFLAGS)
self.Append(CXXFLAGS = "-std=c++11");
self.Append(CPPPATH = os.path.abspath("include"))
self.Append(CXXCOMSTR = "C++ $TARGET")
self.Append(CCCOMSTR = "CC $TARGET")
self.Append(LINKCOMSTR = "Linking $TARGET")
if "DISPLAY" in os.environ.keys():
self.Append(ENV = { "DISPLAY" : os.environ["DISPLAY"] })
self.Debug = CONFIG_DEBUG
self.Version = CONFIG_VERSION
def UsePackage(self, name):
self.ParseConfig("pkg-config --cflags --libs %s" % name)
env = OEnvironment()
Export("env")
include = SConscript("include/Sconscript")
core = SConscript("core/Sconscript")
gui = SConscript("gui/Sconscript")
if os.path.isdir("tests"):
tests = SConscript("tests/Sconscript")
ocarina = env.Program("bin/ocarina", core + gui)
Default(ocarina)
Clean(ocarina, "bin/")
def ocarina_release(target, source, env):
o_vers = "ocarina-%s" % CONFIG_VERSION
os.popen("git archive --prefix=%s/ -o %s.tar.gz HEAD" % (o_vers, o_vers))
print os.popen("sha1sum %s.tar.gz" % o_vers).read()
Command("release", None, ocarina_release)
env.Install("/usr/bin", "bin/ocarina")
env.Install("/usr/share", "share/ocarina/")
env.Install("/usr/share/applications", "share/applications/ocarina.desktop")
install = Alias("install", [ "/usr/bin", "/usr/share", "/usr/share/applications" ])
Depends(install, ocarina)
Clean(install, "/usr/share/ocarina")

22
TODO
View File

@ -4,23 +4,6 @@ Future work:
to the wild. This section will be a list of features that I want, but
should be deferred to a future release so basic support can be coded.
- User created song groups:
Basic add and remove features can be implemented using the
Library and Banned Songs groups. This will give me a chance
to test saving groups on a small scale before letting users
create anything they want.
- Save a user's playlist as a group:
- Library defragment:
Ocarina 6.0 will leave holes in the library when tracks are
deleted, potentially leading to fragmentation and larger-than-
needed file sizes. A "defragment" utility can be created to
clean up unused slots.
To help with fixing groups, a mapping of (old values) ->
(new values) should be kept.
- Fix track durations:
Some tracks in my library are tagged with the wrong duration,
so fix them as they are played.
@ -29,10 +12,6 @@ Future work:
Make a pop-up window for editing the tags of a track. Be sure
to update the library information and the on-disk file.
- Album art:
(easy) Start with album art fetching script
(hard) Build in to Ocarina
- Copy a song group to a different directory:
This can be useful for keeping an external device (like an
Android phone) updated with the music you want to listen to.
@ -59,4 +38,3 @@ Future work:
- OSX
- Windows
- Android
- Command line tools for web interface development?

View File

@ -1,9 +0,0 @@
#!/usr/bin/python
Import("env")
res = Glob("*.c")
res += SConscript("tags/Sconscript")
res += SConscript("containers/Sconscript")
env.UsePackage("gstreamer-1.0")
Return("res")

View File

@ -2,90 +2,121 @@
* Copyright 2013 (c) Anna Schumaker.
*/
#include <core/audio.h>
#include <core/collection.h>
#include <core/history.h>
#include <core/idle.h>
#include <core/tempq.h>
#include <core/playlist.h>
#include <core/settings.h>
static struct file audio_file = FILE_INIT("cur_track", 0);
static struct track *audio_track = NULL;
static GstElement *audio_player = NULL;
static struct audio_ops *audio_ops = NULL;
static int audio_pause_count = -1;
static guint audio_bus = 0;
#define LOAD_PLAYING (1 << 0) /* Begin playback after loading */
#define LOAD_HISTORY (1 << 1) /* Add the track to the history */
#define LOAD_DEFAULT (LOAD_PLAYING | LOAD_HISTORY)
static const char *SETTINGS_TRACK = "core.audio.cur";
static const char *SETTINGS_VOLUME = "core.audio.volume";
static struct file audio_file = FILE_INIT_DATA("", "cur_track", 0);
static struct track *audio_track = NULL;
static int audio_pause_count = -1;
static GstElement *audio_pipeline = NULL;
static GstElement *audio_source = NULL;
static GstElement *audio_decoder = NULL;
static GstElement *audio_converter = NULL;
static GstElement *audio_volume = NULL;
static GstElement *audio_sink = NULL;
static guint audio_bus_id = 0;
static struct audio_callbacks *audio_cb = NULL;
static void __audio_save()
{
file_open(&audio_file, OPEN_WRITE);
file_writef(&audio_file, "%u\n", audio_track->tr_dbe.dbe_index);
file_close(&audio_file);
}
static bool __audio_change_state(GstState state)
{
GstStateChangeReturn ret = GST_STATE_CHANGE_FAILURE;
if (audio_cur_state() != state)
ret = gst_element_set_state(audio_player, state);
if (ret == GST_STATE_CHANGE_FAILURE)
if (audio_cur_state() == state)
return false;
audio_ops->on_state_change(state);
return true;
return gst_element_set_state(audio_pipeline, state) != GST_STATE_CHANGE_FAILURE;
}
/* Load a track, but don't add it to the history. */
static struct track *__audio_load_basic(struct track *track, GstState state)
static struct track *__audio_load(struct track *track, unsigned int flags)
{
struct track *prev = audio_track;
gchar *path, *uri;
gchar *path;
if (!track)
return NULL;
path = track_path(track);
uri = gst_filename_to_uri(path, NULL);
audio_track = track;
path = track_path(track);
if (audio_cur_state() != GST_STATE_NULL)
gst_element_set_state(audio_pipeline, GST_STATE_READY);
g_object_set(G_OBJECT(audio_source), "location", path, NULL);
gst_element_set_state(audio_pipeline, flags & LOAD_PLAYING ?
GST_STATE_PLAYING : GST_STATE_PAUSED);
gst_element_set_state(audio_player, GST_STATE_NULL);
g_object_set(G_OBJECT(audio_player), "uri", uri, NULL);
audio_ops->on_load(track);
__audio_change_state(state);
playlist_played(prev);
if (prev && TRACK_IS_EXTERNAL(prev))
track_free_external(prev);
tempq_updated(prev);
queue_updated(collection_get_queue(), prev);
queue_updated(collection_get_queue(), audio_track);
playlist_selected(track);
if (flags & LOAD_HISTORY && !TRACK_IS_EXTERNAL(track))
playlist_add(playlist_lookup(PL_SYSTEM, "History"), track);
if (audio_cb)
audio_cb->audio_cb_load(track);
__audio_save();
g_free(uri);
audio_save();
g_free(path);
return track;
}
static struct track *__audio_load(struct track *track, GstState state)
static void __audio_pad_added(GstElement *element, GstPad *pad, gpointer data)
{
if (__audio_load_basic(track, state))
history_add(track);
return track;
}
GstPad *sink = gst_element_get_static_pad(audio_decoder, "sink");
static struct track *__audio_next(GstState state)
{
struct track *track = tempq_next();
if (!track)
track = queue_next(collection_get_queue());
return __audio_load(track, state);
gst_element_link(element, audio_converter);
gst_pad_link(pad, sink);
gst_object_unref(sink);
}
static gboolean __audio_message(GstBus *bus, GstMessage *message, gpointer data)
{
GstObject *source = GST_OBJECT(GST_MESSAGE_SRC(message));
gchar *debug = NULL;
GError *error = NULL;
GstState old, state, next;
unsigned int load_flags = LOAD_DEFAULT;
switch (GST_MESSAGE_TYPE(message)) {
case GST_MESSAGE_ERROR:
audio_error(message);
gst_message_parse_error(message, &error, &debug);
g_printerr("ERROR from element %s: %s\n",
GST_OBJECT_NAME(source), error->message);
g_printerr("DEBUG details: %s\n", debug ? debug : "none");
g_error_free(error);
g_free(debug);
if (audio_cur_state() != GST_STATE_PLAYING)
load_flags = LOAD_HISTORY;
__audio_load(playlist_next(), load_flags);
break;
case GST_MESSAGE_EOS:
audio_eos();
track_played(audio_track);
if (audio_pause_count >= 0) {
audio_pause_after(audio_pause_count - 1);
if (audio_pause_count == -1)
load_flags = LOAD_HISTORY;
}
__audio_load(playlist_next(), load_flags);
break;
case GST_MESSAGE_STATE_CHANGED:
if (!audio_cb || source != GST_OBJECT(audio_pipeline))
break;
gst_message_parse_state_changed(message, &old, &state, &next);
if (state == GST_STATE_PLAYING || state == GST_STATE_PAUSED) {
if (next == GST_STATE_VOID_PENDING)
audio_cb->audio_cb_state_change(state);
}
default:
break;
}
@ -93,50 +124,95 @@ static gboolean __audio_message(GstBus *bus, GstMessage *message, gpointer data)
return true;
}
static void __audio_init_idle(void *data)
static bool __audio_init_idle(void *data)
{
unsigned int track;
if (file_open(&audio_file, OPEN_READ)) {
file_readf(&audio_file, "%u", &track);
if (settings_has(SETTINGS_TRACK)) {
track = settings_get(SETTINGS_TRACK);
__audio_load(track_get(track), LOAD_HISTORY);
} else if (file_open(&audio_file, OPEN_READ)) {
track = file_readu(&audio_file);
file_close(&audio_file);
__audio_load(track_get(track), GST_STATE_PAUSED);
file_remove(&audio_file);
__audio_load(track_get(track), LOAD_HISTORY);
}
return true;
}
void audio_init(int *argc, char ***argv, struct audio_ops *ops)
void audio_init(int *argc, char ***argv, struct audio_callbacks *callbacks)
{
unsigned int volume = 100;
GstBus *bus;
gst_init(argc, argv);
audio_player = gst_element_factory_make("playbin", "ocarina_player");
audio_ops = ops;
audio_cb = callbacks;
audio_pipeline = gst_pipeline_new("pipeline");
audio_source = gst_element_factory_make("filesrc", "source");
audio_decoder = gst_element_factory_make("decodebin", "decoder");
audio_converter = gst_element_factory_make("audioconvert", "converter");
audio_volume = gst_element_factory_make("volume", "volume");
audio_sink = gst_element_factory_make("autoaudiosink", "sink");
bus = gst_pipeline_get_bus(GST_PIPELINE(audio_pipeline));
audio_bus_id = gst_bus_add_watch(bus, __audio_message, NULL);
bus = gst_pipeline_get_bus(GST_PIPELINE(audio_player));
audio_bus = gst_bus_add_watch(bus, __audio_message, NULL);
gst_bin_add_many(GST_BIN(audio_pipeline), audio_source, audio_decoder,
audio_converter, audio_volume,
audio_sink, NULL);
gst_element_link(audio_source, audio_decoder);
gst_element_link_many(audio_converter, audio_volume, audio_sink, NULL);
g_signal_connect(audio_decoder, "pad-added", G_CALLBACK(__audio_pad_added), NULL);
gst_object_unref(bus);
idle_schedule(__audio_init_idle, NULL);
if (settings_has(SETTINGS_VOLUME))
volume = settings_get(SETTINGS_VOLUME);
audio_set_volume(volume);
idle_schedule(IDLE_SYNC, __audio_init_idle, NULL);
}
void audio_deinit()
{
gst_element_set_state(audio_player, GST_STATE_NULL);
gst_object_unref(GST_ELEMENT(audio_player));
g_source_remove(audio_bus);
gst_element_set_state(audio_pipeline, GST_STATE_NULL);
gst_object_unref(GST_ELEMENT(audio_pipeline));
g_source_remove(audio_bus_id);
audio_player = NULL;
audio_track = NULL;
audio_pipeline = NULL;
audio_source = NULL;
audio_decoder = NULL;
audio_converter = NULL;
audio_volume = NULL;
audio_sink = NULL;
audio_track = NULL;
gst_deinit();
}
void audio_save()
{
if (audio_track && !TRACK_IS_EXTERNAL(audio_track))
settings_set(SETTINGS_TRACK, track_index(audio_track));
}
bool audio_load(struct track *track)
{
if (track == audio_track)
return false;
return __audio_load(track, GST_STATE_PLAYING);
return __audio_load(track, LOAD_DEFAULT) != NULL;
}
bool audio_load_filepath(const gchar *filepath)
{
struct track *track;
if (!filepath)
return false;
track = track_lookup(filepath);
if (!track)
track = track_alloc_external(filepath);
return audio_load(track);
}
struct track *audio_cur_track()
@ -147,13 +223,32 @@ struct track *audio_cur_track()
GstState audio_cur_state()
{
GstState cur = GST_STATE_NULL;
if (audio_player)
gst_element_get_state(audio_player,
if (audio_pipeline)
gst_element_get_state(audio_pipeline,
&cur, NULL,
GST_CLOCK_TIME_NONE);
return cur;
}
void audio_set_volume(unsigned int volume)
{
gdouble vol;
if (volume > 100)
volume = 100;
vol = (gdouble)volume / 100;
settings_set(SETTINGS_VOLUME, volume);
g_object_set(G_OBJECT(audio_volume), "volume", vol, NULL);
}
unsigned int audio_get_volume()
{
gdouble volume;
g_object_get(G_OBJECT(audio_volume), "volume", &volume, NULL);
return volume * 100;
}
bool audio_play()
{
if (!audio_track)
@ -172,7 +267,7 @@ bool audio_seek(gint64 offset)
{
if (!audio_track)
return false;
return gst_element_seek_simple(audio_player,
return gst_element_seek_simple(audio_pipeline,
GST_FORMAT_TIME,
GST_SEEK_FLAG_FLUSH,
offset);
@ -181,17 +276,17 @@ bool audio_seek(gint64 offset)
gint64 audio_position()
{
gint64 position;
if (gst_element_query_position(audio_player,
if (gst_element_query_position(audio_pipeline,
GST_FORMAT_TIME,
&position))
return position;
return 0;
}
int64_t audio_duration()
gint64 audio_duration()
{
gint64 duration;
if (gst_element_query_duration(audio_player,
if (gst_element_query_duration(audio_pipeline,
GST_FORMAT_TIME,
&duration))
return duration;
@ -202,63 +297,48 @@ int64_t audio_duration()
struct track *audio_next()
{
return __audio_next(GST_STATE_PLAYING);
return __audio_load(playlist_next(), LOAD_DEFAULT);
}
struct track *audio_prev()
{
return __audio_load_basic(history_prev(), GST_STATE_PLAYING);
return __audio_load(playlist_prev(), LOAD_PLAYING);
}
struct track *audio_eos()
bool audio_pause_after(int n)
{
/* Mark current track as played */
if (audio_track)
track_played(audio_track);
/* Check pause count and pick the next track */
if (audio_pause_count >= 0) {
audio_pause_after(audio_pause_count - 1);
if (audio_pause_count == -1)
return __audio_next(GST_STATE_PAUSED);
}
return __audio_next(GST_STATE_PLAYING);
}
void audio_error(GstMessage *error)
{
gchar *path = NULL, *debug = NULL;
GError *err = NULL;
if (audio_track) {
collection_check_library(audio_track->tr_library);
path = track_path(audio_track);
}
if (error)
gst_message_parse_error(error, &err, &debug);
g_print("Error: %s (%s)\n", err->message, path);
if (debug)
g_print("Debug details: %s\n", debug);
__audio_next(audio_cur_state());
g_error_free(err);
g_free(debug);
g_free(path);
}
void audio_pause_after(int n)
{
if (n != audio_pause_count) {
if (n >= -1 && n != audio_pause_count) {
audio_pause_count = n;
audio_ops->on_config_pause(audio_pause_count);
if (audio_cb)
audio_cb->audio_cb_config_pause(audio_pause_count);
return true;
}
return false;
}
int audio_get_pause_count(void)
{
return audio_pause_count;
}
#ifdef CONFIG_TESTING
GstElement *test_audio_player()
void test_audio_eos()
{
return audio_player;
GstMessage *message = gst_message_new_eos(GST_OBJECT(audio_pipeline));
__audio_message(NULL, message, NULL);
gst_message_unref(message);
}
void test_audio_error(GError *error, gchar *debug)
{
GstMessage *message = gst_message_new_error(
GST_OBJECT(audio_pipeline), error, debug);
__audio_message(NULL, message, NULL);
gst_message_unref(message);
}
GstElement *test_audio_pipeline()
{
return audio_pipeline;
}
#endif /* CONFIG_TESTING */

View File

@ -1,283 +0,0 @@
/*
* Copyright 2013 (c) Anna Schumaker.
*/
#include <core/collection.h>
#include <core/idle.h>
#include <core/playlist.h>
#include <glib.h>
#include <stdlib.h>
#include <unistd.h>
static struct file c_file = FILE_INIT("library.q", 0);
static struct queue c_queue;
struct scan_data {
struct library *sd_lib;
gchar *sd_path;
};
static void __scan_dir(void *);
#ifdef CONFIG_TESTING
bool test_collection_error = false;
static inline int __g_access(const gchar *path)
{
if (test_collection_error)
return -1;
return g_access(path, F_OK);
}
#else
#define __g_access(path) g_access(path, F_OK)
#endif /* CONFIG_TESTING */
static void __scan_dir_later(struct library *library, const gchar *dir)
{
struct scan_data *data = g_malloc(sizeof(struct scan_data));
data->sd_lib = library;
data->sd_path = g_strdup(dir);
/* data is freed by __scan_dir() */
idle_schedule(__scan_dir, data);
}
static void __scan_path(struct scan_data *scan, const gchar *name)
{
gchar *path = g_strdup_printf("%s/%s", scan->sd_path, name);
struct track *track;
if (g_file_test(path, G_FILE_TEST_IS_DIR))
__scan_dir_later(scan->sd_lib, path);
else {
track = track_add(scan->sd_lib, path);
if (track)
queue_add(&c_queue, track);
}
g_free(path);
}
static void __scan_dir(void *data)
{
struct scan_data *scan = data;
const char *name;
GDir *dir;
dir = g_dir_open(scan->sd_path, 0, NULL);
if (dir == NULL)
goto out;
name = g_dir_read_name(dir);
while (name != NULL) {
__scan_path(scan, name);
name = g_dir_read_name(dir);
}
g_dir_close(dir);
track_db_commit();
out:
/* Allocated by __scan_dir_later() */
g_free(scan->sd_path);
g_free(scan);
}
static void __validate_library(void *data)
{
struct library *library = data;
struct db_entry *dbe, *next;
struct track *track;
int access;
gchar *path;
db_for_each(dbe, next, track_db_get()) {
track = TRACK(dbe);
if (track->tr_library != library)
continue;
path = track_path(track);
access = __g_access(path);
g_free(path);
if (access < 0) {
if (collection_check_library(library) < 0)
return;
queue_remove_all(&c_queue, track);
track_remove(track);
}
}
}
void __collection_init_idle(void *data)
{
struct db_entry *track, *next;
unsigned int i, n = 0;
int field, ascending;
db_for_each(track, next, track_db_get()) {
if (TRACK(track)->tr_library->li_enabled &&
!playlist_has(PL_HIDDEN, TRACK(track)))
queue_add(&c_queue, TRACK(track));
}
queue_unset_flag(&c_queue, Q_ADD_FRONT);
if (file_open(&c_file, OPEN_READ))
file_readf(&c_file, "%u %u", &c_queue.q_flags, &n);
for (i = 0; i < n; i++) {
file_readf(&c_file, "%u %d", &field, &ascending);
queue_sort(&c_queue, field + 1, (i == 0) ? true : false);
if (ascending == false)
queue_sort(&c_queue, field + 1, false);
}
file_close(&c_file);
if (!c_queue.q_sort) {
queue_sort(&c_queue, COMPARE_ARTIST, true);
queue_sort(&c_queue, COMPARE_YEAR, false);
queue_sort(&c_queue, COMPARE_TRACK, false);
}
queue_set_flag(&c_queue, Q_SAVE_SORT);
queue_set_flag(&c_queue, Q_SAVE_FLAGS);
collection_update_all();
}
/*
* External API begins here
*/
void collection_init(struct queue_ops *ops)
{
queue_init(&c_queue, Q_ENABLED | Q_REPEAT | Q_ADD_FRONT, ops);
idle_schedule(__collection_init_idle, NULL);
}
void collection_deinit()
{
queue_deinit(&c_queue);
}
void collection_save(struct queue *queue, enum queue_flags flag)
{
GSList *cur = c_queue.q_sort;
int field;
if (&c_queue != queue)
return;
if (!file_open(&c_file, OPEN_WRITE))
return;
file_writef(&c_file, "%u %u", c_queue.q_flags, g_slist_length(cur));
while (cur) {
field = GPOINTER_TO_INT(cur->data);
file_writef(&c_file, " %u %d", abs(field) - 1, field > 0);
cur = g_slist_next(cur);
}
file_writef(&c_file, "\n");
file_close(&c_file);
}
struct library *collection_add(const gchar *path)
{
struct library *library = NULL;
if (g_file_test(path, G_FILE_TEST_IS_DIR) == false)
return library;
library = library_find(path);
if (library)
collection_update(library);
return library;
}
void collection_remove(struct library *library)
{
if (library) {
collection_set_enabled(library, false);
track_remove_all(library);
library_remove(library);
}
}
void collection_update(struct library *library)
{
if (!library)
return;
idle_schedule(__validate_library, library);
__scan_dir_later(library, library->li_path);
}
void collection_update_all()
{
struct db_entry *library, *next;
db_for_each(library, next, library_db_get())
collection_update(LIBRARY(library));
}
bool collection_ban(struct track *track)
{
bool ret = playlist_add(PL_HIDDEN, track);
if (ret)
queue_remove_all(&c_queue, track);
return ret;
}
bool collection_unban(struct track *track)
{
bool ret = playlist_remove(PL_HIDDEN, track);
if (ret)
queue_add(&c_queue, track);
return ret;
}
void collection_set_enabled(struct library *library, bool enabled)
{
struct db_entry *dbe, *next;
struct track *track;
if (!library || (library->li_enabled == enabled))
return;
if (enabled && (collection_check_library(library) < 0))
return;
library_set_enabled(library, enabled);
queue_set_flag(&c_queue, Q_ADD_FRONT);
db_for_each(dbe, next, track_db_get()) {
track = TRACK(dbe);
if (track->tr_library == library) {
if (enabled) {
if (!playlist_has(PL_HIDDEN, track))
queue_add(&c_queue, track);
} else
queue_remove_all(&c_queue, track);
}
}
queue_unset_flag(&c_queue, Q_ADD_FRONT);
queue_resort(&c_queue);
}
int collection_check_library(struct library *library)
{
int ret = __g_access(library->li_path);
if (ret < 0) {
g_warning("Can't access library at %s/\n", library->li_path);
collection_set_enabled(library, false);
}
return ret;
}
struct queue *collection_get_queue()
{
return &c_queue;
}

View File

@ -1,3 +0,0 @@
#!/usr/bin/python
res = Glob("*.c")
Return("res")

View File

@ -1,94 +0,0 @@
/*
* Copyright 2014 (c) Anna Schumaker.
*/
#include <core/containers/index.h>
static struct index_entry *__index_alloc(gchar *key)
{
struct index_entry *ent = g_malloc(sizeof(struct index_entry));
dbe_init(&ent->ie_dbe, ent);
set_init(&ent->ie_set);
ent->ie_key = key;
return ent;
}
static struct db_entry *index_alloc(const gchar *key)
{
return &__index_alloc(g_strdup(key))->ie_dbe;
}
static void index_free(struct db_entry *dbe)
{
struct index_entry *ent = INDEX_ENTRY(dbe);
set_deinit(&ent->ie_set);
g_free(ent);
}
static gchar *index_key(struct db_entry *dbe)
{
return INDEX_ENTRY(dbe)->ie_key;
}
static void index_write(struct file *file, struct db_entry *dbe)
{
file_writef(file, "%s\n" , INDEX_ENTRY(dbe)->ie_key);
set_write(file, &INDEX_ENTRY(dbe)->ie_set);
}
static struct db_entry *index_read(struct file *file)
{
gchar *key = file_readl(file);
struct index_entry *ent;
ent = __index_alloc(key);
set_read(file, &ent->ie_set);
return &ent->ie_dbe;
}
static const struct db_ops index_ops = {
.dbe_alloc = index_alloc,
.dbe_free = index_free,
.dbe_key = index_key,
.dbe_read = index_read,
.dbe_write = index_write,
};
void index_init(struct database *index, const gchar *filepath, bool autosave)
{
db_init(index, filepath, autosave, &index_ops);
}
struct index_entry *index_insert(struct database *index, const gchar *key,
unsigned int value)
{
struct index_entry *it = INDEX_ENTRY(db_find(index, key));
set_insert(&it->ie_set, value);
db_autosave(index);
return it;
}
void index_remove(struct database *index, const gchar *key, unsigned int value)
{
struct index_entry *it = INDEX_ENTRY(db_get(index, key));
if (it) {
set_remove(&it->ie_set, value);
db_autosave(index);
}
}
bool index_has(struct database *index, const gchar *key, unsigned int value)
{
struct index_entry *it = INDEX_ENTRY(db_get(index, key));
if (!it)
return false;
return set_has(&it->ie_set, value);
}
#ifdef CONFIG_TESTING
const struct db_ops *test_index_ops() { return &index_ops; }
#endif /* CONFIG_TESTING */

View File

@ -1,29 +0,0 @@
/*
* Copyright 2015 (c) Anna Schumaker.
*/
#include <core/containers/queue.h>
guint _q_add_sorted(struct _queue *queue, gpointer data,
GCompareDataFunc func, gpointer user_data)
{
struct _q_iter it;
_q_for_each(queue, &it) {
if (func(_q_iter_val(&it), data, user_data) > 0) {
g_queue_insert_before(&queue->_queue, it.it_iter, data);
return it.it_pos;
}
}
return _q_add_tail(queue, data);
}
gpointer _q_remove_it(struct _queue *queue, struct _q_iter *it)
{
gpointer ret = _q_iter_val(it);
GList *link = it->it_iter;
_q_iter_prev(it);
g_queue_delete_link(&queue->_queue, link);
return ret;
}

View File

@ -1,43 +0,0 @@
/*
* Copyright 2015 (c) Anna Schumaker.
*/
#include <core/containers/set.h>
void set_copy(const struct set *lhs, struct set *rhs)
{
struct set_iter it_lhs;
set_for_each(lhs, &it_lhs)
set_insert(rhs, it_lhs.it_val);
}
void set_inline_intersect(const struct set *lhs, struct set *rhs)
{
struct set_iter it_rhs;
set_for_each(rhs, &it_rhs) {
if (!set_has(lhs, it_rhs.it_val))
g_hash_table_iter_remove(&it_rhs.it_iter);
}
}
void set_read(struct file *file, struct set *set)
{
unsigned int num, val, i;
file_readf(file, "%u ", &num);
for (i = 0; i < num; i++) {
file_readf(file, "%u ", &val);
set_insert(set, val);
}
}
void set_write(struct file *file, struct set *set)
{
struct set_iter it;
file_writef(file, "%u ", set_size(set));
set_for_each(set, &it)
file_writef(file, "%u ", it.it_val);
}

View File

@ -1,34 +1,34 @@
/*
* Copyright 2014 (c) Anna Schumaker.
*/
#include <core/audio.h>
#include <core/core.h>
#include <core/collection.h>
#include <core/filter.h>
#include <core/history.h>
#include <core/playlist.h>
#include <core/tags/tags.h>
#include <core/tempq.h>
void core_init(int *argc, char ***argv, struct core_init_data *init)
static bool core_defragment(void *data)
{
filter_init();
if (tags_defragment()) {
playlist_save();
audio_save();
}
return true;
}
void core_init(int *argc, char ***argv, struct playlist_callbacks *playlist_cb,
struct audio_callbacks *audio_cb, enum idle_sync_t idle_sync)
{
idle_init(idle_sync);
settings_init();
tags_init();
playlist_init(init->playlist_ops);
collection_init(init->collection_ops);
history_init(init->history_ops);
tempq_init(init->tempq_ops);
audio_init(argc, argv, init->audio_ops);
playlist_init(playlist_cb);
audio_init(argc, argv, audio_cb);
idle_schedule(IDLE_SYNC, core_defragment, NULL);
}
void core_deinit()
{
audio_deinit();
tempq_deinit();
history_deinit();
collection_deinit();
playlist_deinit();
tags_deinit();
filter_deinit();
settings_deinit();
idle_deinit();
}

View File

@ -1,7 +1,7 @@
/*
* Copyright 2013 (c) Anna Schumaker.
*/
#include <core/containers/database.h>
#include <core/database.h>
#include <core/idle.h>
#define DB_ENTRY_AT(db, index) \
@ -36,11 +36,9 @@ static struct db_entry *__dbe_next(const struct database *db, unsigned int index
static struct db_entry *__dbe_read(struct database *db, unsigned int index)
{
struct db_entry *dbe = NULL;
int valid;
file_readf(&db->db_file, "%d", &valid);
if (valid)
dbe = db->db_ops->dbe_read(&db->db_file);
if (file_readd(&db->db_file))
dbe = db->db_ops->dbe_read(&db->db_file, index);
g_ptr_array_index(db->db_entries, index) = dbe;
return dbe;
@ -55,8 +53,6 @@ static void __dbe_setup(struct database *db, unsigned int index)
dbe->dbe_key = db->db_ops->dbe_key(dbe);
g_hash_table_insert(db->db_keys, dbe->dbe_key, dbe);
db->db_size++;
if (db->db_ops->dbe_setup)
db->db_ops->dbe_setup(dbe);
}
}
@ -71,14 +67,14 @@ static void __dbe_write(struct database *db, struct db_entry *dbe)
}
void db_init(struct database *db, const char *filepath, bool autosave,
const struct db_ops *ops)
const struct db_ops *ops, unsigned int fmin)
{
db->db_ops = ops;
db->db_size = 0;
db->db_autosave = autosave;
db->db_entries = g_ptr_array_new();
db->db_keys = g_hash_table_new(g_str_hash, g_str_equal);
file_init(&db->db_file, filepath, 0);
file_init_data(&db->db_file, "", filepath, fmin);
}
void db_deinit(struct database *db)
@ -115,23 +111,23 @@ void db_autosave(struct database *db)
void db_load(struct database *db)
{
unsigned int size;
bool save;
if (file_open(&db->db_file, OPEN_READ) == false)
return;
file_readf(&db->db_file, "%u", &size);
size = file_readu(&db->db_file);
g_ptr_array_set_size(db->db_entries, size);
for (unsigned int i = 0; i < size; i++) {
if (__dbe_read(db, i))
__dbe_setup(db, i);
}
save = file_version(&db->db_file) != OCARINA_MINOR_VERSION;
file_close(&db->db_file);
}
void db_load_idle(struct database *db)
{
idle_schedule((void (*)(void *))db_load, db);
if (save)
db_autosave(db);
}
struct db_entry *db_insert(struct database *db, const gchar *key)
@ -139,7 +135,7 @@ struct db_entry *db_insert(struct database *db, const gchar *key)
struct db_entry *item = NULL;
if (key)
item = db->db_ops->dbe_alloc(key);
item = db->db_ops->dbe_alloc(key, db_actual_size(db));
if (item) {
g_ptr_array_add(db->db_entries, item);
@ -159,6 +155,43 @@ void db_remove(struct database *db, struct db_entry *item)
db_autosave(db);
}
bool db_defrag(struct database *db)
{
struct db_entry *dbe;
unsigned int i, cur;
if (db->db_size == db_actual_size(db))
return false;
for (cur = 0, i = 1; cur < db->db_size; cur++) {
if (DB_ENTRY_AT(db, cur))
continue;
while (i <= cur || !DB_ENTRY_AT(db, i))
i++;
dbe = DB_ENTRY_AT(db, i);
g_ptr_array_index(db->db_entries, i) = NULL;
g_ptr_array_index(db->db_entries, cur) = dbe;
dbe->dbe_index = cur;
}
g_ptr_array_set_size(db->db_entries, db->db_size);
db_autosave(db);
return true;
}
void db_rekey(struct database *db, struct db_entry *dbe)
{
if (dbe == NULL)
return;
g_hash_table_remove(db->db_keys, dbe->dbe_key);
g_free(dbe->dbe_key);
dbe->dbe_key = db->db_ops->dbe_key(dbe);
g_hash_table_insert(db->db_keys, dbe->dbe_key, dbe);
}
unsigned int db_actual_size(const struct database *db)
{
if (db->db_entries)
@ -178,7 +211,7 @@ struct db_entry *db_next(const struct database *db, struct db_entry *ent)
return NULL;
}
struct db_entry *db_at(struct database *db, unsigned int index)
struct db_entry *db_at(const struct database *db, unsigned int index)
{
if (index >= db_actual_size(db))
return NULL;

View File

@ -3,6 +3,7 @@
*/
#include <core/date.h>
#include <core/string.h>
#include <endian.h>
#include <time.h>
@ -25,7 +26,14 @@ void date_today(struct date *date)
void date_read(struct file *f, struct date *date)
{
file_readf(f, "%u %u %u", &date->d_year, &date->d_month, &date->d_day);
date->d_year = file_readu(f);
date->d_month = file_readu(f);
date->d_day = file_readu(f);
}
void date_read_stamp(struct file *f, struct date *date)
{
date->d_stamp = be32toh(file_readu(f));
}
void date_write(struct file *f, struct date *date)
@ -33,6 +41,11 @@ void date_write(struct file *f, struct date *date)
file_writef(f, "%u %u %u", date->d_year, date->d_month, date->d_day);
}
void date_write_stamp(struct file *f, struct date *date)
{
file_writef(f, "%u", htobe32(date->d_stamp));
}
gchar *date_string(const struct date *date)
{
struct tm tm = {

View File

@ -3,51 +3,105 @@
*/
#include <core/file.h>
#include <core/string.h>
#include <core/version.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#ifdef CONFIG_TESTING
const gchar *OCARINA_DIR = "ocarina-test";
#elif CONFIG_DEBUG
const gchar *OCARINA_DIR = "ocarina-debug";
#else
const gchar *OCARINA_DIR = "ocarina";
#endif
#define REPORT_ERROR(fname, error) \
g_printerr("%s (%s:%d): %s: %s\n", __func__, __FILE__, __LINE__, fname, error)
#define REPORT_ERRNO(fname) REPORT_ERROR(fname, strerror(errno))
#define REPORT_ERROR(fname) \
printf("%s (%s:%d): %s: %s\n", __func__, __FILE__, __LINE__, fname, strerror(errno))
static gchar *__file_path(gchar *name)
static void __file_init_common(struct file *file, const gchar *subdir,
const gchar *name, unsigned int min,
const gchar *(*user_dir)(void))
{
return g_strjoin("/", g_get_user_data_dir(), OCARINA_DIR, name, NULL);
file->f_file = NULL;
file->f_name = name;
file->f_subdir = subdir;
file->f_mode = CLOSED;
file->f_version = OCARINA_MINOR_VERSION;
file->f_prev = 0;
file->f_min = min;
file->f_user_dir = user_dir;
}
static bool __file_mkdir()
static bool __file_open(struct file *file, enum open_mode mode)
{
gchar *dir = __file_path(NULL);
gchar *cmode, *path;
if (mode == OPEN_READ || mode == OPEN_READ_BINARY) {
cmode = "r";
path = file_path(file);
} else {
cmode = "w";
path = file_write_path(file);
}
file->f_file = g_fopen(path, cmode);
if (!file->f_file)
REPORT_ERRNO(path);
g_free(path);
return file->f_file != NULL;
}
static bool __file_mkdir(struct file *file)
{
gchar *dir = g_build_filename(file->f_user_dir(), OCARINA_NAME,
file->f_subdir, NULL);
int ret = g_mkdir_with_parents(dir, 0755);
if (ret != 0)
REPORT_ERROR(dir);
REPORT_ERRNO(dir);
g_free(dir);
return ret == 0;
}
void file_init(struct file *file, const gchar *name, unsigned int version)
static bool __file_can_write(struct file *file)
{
file->f_mode = OPEN_READ;
file->f_version = version;
file->f_prev = 0;
file->f_file = NULL;
g_strlcpy(file->f_name, (name == NULL) ? "" : name, FILE_MAX_LEN);
gchar *path = file_path(file);
bool ret = true;
if (g_access(path, F_OK) == 0 && g_access(path, W_OK) < 0)
ret = false;
g_free(path);
return ret;
}
void file_init_data(struct file *file, const gchar *subdir,
const gchar *name, unsigned int min)
{
__file_init_common(file, subdir, name, min, g_get_user_data_dir);
}
void file_init_cache(struct file *file, const gchar *subdir, const gchar *name)
{
__file_init_common(file, subdir, name, 0, g_get_user_cache_dir);
}
gchar *file_path(struct file *file)
{
if (strlen(file->f_name) != 0)
return __file_path(file->f_name);
return g_strdup("");
if (string_length(file->f_name) == 0)
return g_strdup("");
return g_build_filename(file->f_user_dir(), OCARINA_NAME,
file->f_subdir, file->f_name, NULL);
}
gchar *file_write_path(struct file *file)
{
gchar *tmp, *res;
if (string_length(file->f_name) == 0)
return g_strdup("");
tmp = g_strdup_printf(".%s.tmp", file->f_name);
res = g_build_filename(file->f_user_dir(), OCARINA_NAME,
file->f_subdir, tmp, NULL);
g_free(tmp);
return res;
}
const unsigned int file_version(struct file *file)
@ -60,83 +114,98 @@ const unsigned int file_version(struct file *file)
bool file_exists(struct file *file)
{
gchar *path = file_path(file);
bool ret = g_file_test(path, G_FILE_TEST_EXISTS);
bool ret = g_file_test(path, G_FILE_TEST_EXISTS);
g_free(path);
return ret;
}
static bool __file_open_common(struct file *file, enum open_mode mode)
{
gchar *path = file_path(file);
file->f_file = g_fopen(path, (mode == OPEN_READ) ? "r" : "w");
g_free(path);
if (!file->f_file) {
REPORT_ERROR(file->f_name);
return false;
}
file->f_mode = mode;
return true;
}
static bool __file_open_read(struct file *file)
static bool __file_open_read(struct file *file, enum open_mode mode)
{
if (!file_exists(file))
return false;
if (!__file_open_common(file, OPEN_READ))
if (!__file_open(file, mode))
return false;
return file_readf(file, "%u\n", &file->f_prev) == 1;
file->f_mode = mode;
if (mode == OPEN_READ_BINARY)
return true;
file->f_prev = file_readu(file);
if (file->f_prev < file->f_min) {
REPORT_ERROR(file->f_name, "File too old to be upgraded.");
file_close(file);
exit(1);
}
if (file->f_prev > file->f_version) {
REPORT_ERROR(file->f_name, "File too new to be opened.");
file_close(file);
exit(1);
}
return true;
}
static bool __file_open_write(struct file *file)
static bool __file_open_write(struct file *file, enum open_mode mode)
{
if (!__file_mkdir())
if (!__file_mkdir(file))
return false;
if (!__file_open_common(file, OPEN_WRITE))
if (!__file_can_write(file))
return false;
if (!__file_open(file, OPEN_WRITE))
return false;
file->f_mode = mode;
if (mode == OPEN_WRITE_BINARY)
return true;
return file_writef(file, "%d\n", file->f_version) > 0;
}
bool file_open(struct file *file, enum open_mode mode)
{
if ((strlen(file->f_name) == 0) || (file->f_file != NULL))
if ((string_length(file->f_name) == 0) || (file->f_file != NULL))
return false;
if (mode == CLOSED)
return false;
if (mode == OPEN_READ)
return __file_open_read(file);
return __file_open_write(file);
if (mode == OPEN_READ || mode == OPEN_READ_BINARY)
return __file_open_read(file, mode);
return __file_open_write(file, mode);
}
void file_close(struct file *file)
{
if (file->f_file)
gchar *path = file_path(file);
gchar *tmp = file_write_path(file);
if (file->f_file) {
fclose(file->f_file);
if (file->f_mode == OPEN_WRITE || file->f_mode == OPEN_WRITE_BINARY)
g_rename(tmp, path);
}
file->f_file = NULL;
file->f_mode = CLOSED;
g_free(path);
g_free(tmp);
}
int file_readf(struct file *file, const char *fmt, ...)
gchar *file_readw(struct file *file)
{
va_list argp;
int ret;
va_start(argp, fmt);
ret = vfscanf(file->f_file, fmt, argp);
va_end(argp);
return ret;
gchar *s;
return fscanf(file->f_file, "%ms%*c", &s) ? s : g_strdup("");
}
gchar *file_readl(struct file *file)
{
gchar *res;
gchar *s = NULL;
size_t len = 0;
return getline(&s, &len, file->f_file) ? g_strchomp(s) : g_strdup("");
}
if (file_readf(file, "%m[^\n]\n", &res) == 0)
return g_strdup("");
g_strstrip(res);
return res;
unsigned int file_readu(struct file *file)
{
unsigned int u;
return fscanf(file->f_file, "%u%*c", &u) ? u : 0;
}
int file_writef(struct file *file, const char *fmt, ...)
@ -149,6 +218,62 @@ int file_writef(struct file *file, const char *fmt, ...)
va_end(argp);
if (ret < 0)
REPORT_ERROR(file->f_name);
REPORT_ERRNO(file->f_name);
return ret;
}
gchar *file_read(struct file *file)
{
int fd = fileno(file->f_file);
struct stat st;
gchar *buf;
if (fstat(fd, &st) < 0)
return NULL;
buf = g_malloc0(st.st_size + 1);
if (fread(buf, st.st_size, 1, file->f_file) == 1)
return buf;
g_free(buf);
return NULL;
}
int file_write(struct file *file, const void *data, size_t len)
{
if (fwrite(data, len, 1, file->f_file) == 1)
return len;
return -1;
}
bool file_import(struct file *file, const gchar *srcpath)
{
gchar *contents = NULL;
gsize length = 0;
if (!file->f_file || !srcpath)
return false;
if (!g_file_get_contents(srcpath, &contents, &length, NULL))
return false;
file_write(file, contents, length);
return true;
}
bool file_remove(struct file *file)
{
gchar *path, *dir;
int ret = -1;
if (!file->f_file) {
path = file_path(file);
ret = g_unlink(path);
dir = g_path_get_dirname(path);
if (string_length(file->f_subdir) > 0)
g_rmdir(dir);
g_free(path);
g_free(dir);
}
return ret == 0;
}

View File

@ -1,69 +0,0 @@
/*
* Copyright 2013 (c) Anna Schumaker.
*/
#include <core/containers/index.h>
#include <core/filter.h>
#include <core/string.h>
static struct database filter_index;
void filter_init() { index_init(&filter_index, "", false); }
void filter_deinit() { db_deinit(&filter_index); }
void filter_add(const gchar *text, unsigned int index)
{
const gchar *c = g_utf8_next_char(text);
glong begin, end;
gchar *substr;
for (begin = 0, end = 1; end <= g_utf8_strlen(text, -1); end++) {
substr = g_utf8_substring(text, begin, end);
index_insert(&filter_index, substr, index);
if (g_unichar_isspace(g_utf8_get_char(c))) {
c = g_utf8_next_char(c);
begin = ++end;
}
c = g_utf8_next_char(c);
g_free(substr);
}
}
void filter_search(const gchar *text, struct set *res)
{
gchar *lower = string_lowercase(text);
struct index_entry *found;
glong begin, end;
gchar *c, *substr;
set_clear(res);
c = lower;
for (begin = 0, end = 1; end <= g_utf8_strlen(lower, -1); end++) {
c = g_utf8_next_char(c);
if ((*c != '\0') && !g_unichar_isspace(g_utf8_get_char(c)))
continue;
substr = g_utf8_substring(lower, begin, end);
found = INDEX_ENTRY(db_get(&filter_index, substr));
g_free(substr);
if (!found) {
set_clear(res);
break;
}
if (begin == 0)
set_copy(&found->ie_set, res);
else
set_inline_intersect(&found->ie_set, res);
c = g_utf8_next_char(c);
begin = ++end;
}
g_free(lower);
}

View File

@ -1,34 +0,0 @@
/*
* Copyright 2015 (c) Anna Schumaker.
*/
#include <core/history.h>
static struct queue history_queue;
#define HISTORY_FLAGS Q_ENABLED | Q_REPEAT | Q_NO_SORT | Q_ADD_FRONT
void history_init(struct queue_ops *history_ops)
{
queue_init(&history_queue, HISTORY_FLAGS, history_ops);
}
void history_deinit()
{
queue_deinit(&history_queue);
}
void history_add(struct track *track)
{
queue_add(&history_queue, track);
_q_iter_set(&history_queue.q_tracks, &history_queue.q_cur, 0);
}
struct track *history_prev()
{
return queue_next(&history_queue);
}
struct queue *history_get_queue()
{
return &history_queue;
}

View File

@ -2,28 +2,86 @@
* Copyright 2013 (c) Anna Schumaker.
*/
#include <core/idle.h>
#include <glib.h>
struct idle_task {
void (*idle_func)(void *);
bool (*idle_func)(void *);
void *idle_data;
};
static GQueue idle_queue = G_QUEUE_INIT;
static float queued = 0.0;
static float serviced = 0.0;
static GThreadPool *idle_pool = NULL;
static GQueue idle_queue = G_QUEUE_INIT;
static enum idle_sync_t idle_mode = IDLE_SYNC;
static unsigned int queued = 0;
static unsigned int serviced = 0;
void idle_schedule(void (*func)(void *), void *data)
void __idle_free_task(struct idle_task *task)
{
struct idle_task *task = g_malloc(sizeof(struct idle_task));
g_free(task);
g_atomic_int_inc(&serviced);
}
bool __idle_run_task(struct idle_task *task)
{
bool finished = task->idle_func(task->idle_data);
if (finished)
__idle_free_task(task);
return finished;
}
void __idle_thread(gpointer task, gpointer data)
{
if (!__idle_run_task(task))
g_thread_pool_push(idle_pool, task, NULL);
}
void idle_init(enum idle_sync_t sync)
{
idle_mode = sync;
}
void idle_deinit()
{
struct idle_task *task;
while (!g_queue_is_empty(&idle_queue)) {
task = g_queue_pop_head(&idle_queue);
g_free(task);
}
if (idle_pool) {
g_thread_pool_free(idle_pool, true, true);
idle_pool = NULL;
}
queued = 0;
serviced = 0;
}
void idle_schedule(enum idle_sync_t sync, bool (*func)(void *), void *data)
{
struct idle_task *task;
if (sync == IDLE_ASYNC && idle_mode == IDLE_SYNC)
return;
task = g_malloc(sizeof(struct idle_task));
task->idle_func = func;
task->idle_data = data;
g_queue_push_tail(&idle_queue, task);
queued++;
if (sync == IDLE_SYNC)
g_queue_push_tail(&idle_queue, task);
else {
if (!idle_pool)
idle_pool = g_thread_pool_new(__idle_thread, NULL, 1,
false, NULL);
g_thread_pool_push(idle_pool, task, NULL);
}
g_atomic_int_inc(&queued);
}
bool idle_run_task()
@ -32,31 +90,22 @@ bool idle_run_task()
if (!g_queue_is_empty(&idle_queue)) {
task = g_queue_pop_head(&idle_queue);
task->idle_func(task->idle_data);
g_free(task);
serviced++;
if (!__idle_run_task(task))
g_queue_push_tail(&idle_queue, task);
}
if (g_queue_is_empty(&idle_queue)) {
queued = 0.0;
serviced = 0.0;
}
return !g_queue_is_empty(&idle_queue);
if (g_atomic_int_get(&queued) != g_atomic_int_get(&serviced))
return true;
queued = 0;
serviced = 0;
return false;
}
float idle_progress()
{
if (g_queue_is_empty(&idle_queue))
if (g_atomic_int_get(&serviced) == 0 &&
g_atomic_int_get(&queued) == 0)
return 1.0;
return serviced / queued;
}
void idle_cancel()
{
struct idle_task *task;
while (!g_queue_is_empty(&idle_queue)) {
task = g_queue_pop_head(&idle_queue);
g_free(task);
}
return (float)g_atomic_int_get(&serviced) / (float)queued;
}

View File

@ -1,126 +1,236 @@
/*
* Copyright 2013 (c) Anna Schumaker.
*/
#include <core/collection.h>
#include <core/string.h>
#include <core/playlist.h>
#include <core/settings.h>
#include <core/string.h>
static struct database playlist_db;
static struct queue playlist_q;
static enum playlist_t playlist_cur;
static const gchar *playlist_names[2] = { "Favorites", "Banned" };
static const gchar *SETTINGS_CUR_TYPE = "core.playlist.cur.type";
static const gchar *SETTINGS_CUR_ID = "core.playlist.cur.id";
static const gchar *SETTINGS_PREV_TYPE = "core.playlist.prev.type";
static const gchar *SETTINGS_PREV_ID = "core.playlist.prev.id";
static struct playlist *current = NULL;
static struct playlist *previous = NULL;
struct playlist_type *playlist_types[] = {
[PL_SYSTEM] = &pl_system,
[PL_ARTIST] = &pl_artist,
[PL_LIBRARY] = &pl_library,
[PL_USER] = &pl_user,
};
static inline struct index_entry *__playlist_lookup(enum playlist_t plist)
static struct playlist *__playlist_saved(const gchar *s_type, const gchar *s_id)
{
return INDEX_ENTRY(db_get(&playlist_db, playlist_names[plist]));
unsigned int type, id;
if (!settings_has(s_type) || !settings_has(s_id))
return NULL;
type = settings_get(s_type);
id = settings_get(s_id);
return playlist_types[type]->pl_get(id);
}
static inline bool __playlist_is_static(enum playlist_t plist)
void playlist_init(struct playlist_callbacks *cb)
{
return (plist == PL_FAVORITED) || (plist == PL_HIDDEN);
}
playlist_generic_set_callbacks(cb);
pl_system_init();
pl_artist_init();
pl_user_init();
pl_library_init();
static inline bool __playlist_is_dynamic(enum playlist_t plist)
{
return !__playlist_is_static(plist);
}
static void __playlist_fill_static(enum playlist_t plist)
{
struct index_entry *ent = __playlist_lookup(plist);
struct set_iter it;
if (ent) {
set_for_each(&ent->ie_set, &it)
queue_add(&playlist_q, track_get(it.it_val));
}
}
static void __playlist_fill_dynamic(enum playlist_t plist)
{
unsigned int count = 0, average = 0;
struct db_entry *track, *next;
if (plist == PL_MOST_PLAYED || plist == PL_LEAST_PLAYED)
average = track_db_average_plays();
db_for_each(track, next, track_db_get()) {
count = TRACK(track)->tr_count;
if (plist == PL_UNPLAYED && count == 0)
queue_add(&playlist_q, TRACK(track));
else if (plist == PL_MOST_PLAYED && count > average)
queue_add(&playlist_q, TRACK(track));
else if (plist == PL_LEAST_PLAYED && count <= average && count > 0)
queue_add(&playlist_q, TRACK(track));
}
}
void playlist_init(struct queue_ops *ops)
{
queue_init(&playlist_q, Q_ENABLED | Q_REPEAT, ops);
queue_sort(&playlist_q, COMPARE_ARTIST, true);
queue_sort(&playlist_q, COMPARE_YEAR, false);
queue_sort(&playlist_q, COMPARE_TRACK, false);
queue_set_flag(&playlist_q, Q_NO_SORT);
index_init(&playlist_db, "playlist.db", true);
db_load_idle(&playlist_db);
current = __playlist_saved(SETTINGS_CUR_TYPE, SETTINGS_CUR_ID);
previous = __playlist_saved(SETTINGS_PREV_TYPE, SETTINGS_PREV_ID);
if (!current)
current = playlist_lookup(PL_SYSTEM, "Collection");
}
void playlist_deinit()
{
queue_deinit(&playlist_q);
db_deinit(&playlist_db);
pl_system_deinit();
pl_artist_deinit();
pl_user_deinit();
pl_library_deinit();
}
bool playlist_add(enum playlist_t plist, struct track *track)
void playlist_save()
{
if (!track || !__playlist_is_static(plist) || playlist_has(plist, track))
unsigned int i;
for (i = 0; i < PL_MAX_TYPE; i++)
playlist_types[i]->pl_save();
}
void playlist_played(struct track *track)
{
unsigned int i;
if (track && !TRACK_IS_EXTERNAL(track)) {
for (i = 0; i < PL_MAX_TYPE; i++)
playlist_types[i]->pl_played(track);
}
}
void playlist_selected(struct track *track)
{
unsigned int i;
if (track && !TRACK_IS_EXTERNAL(track)) {
for (i = 0; i < PL_MAX_TYPE; i++)
playlist_types[i]->pl_selected(track);
if (playlist_size(current) == 0)
playlist_select(previous);
}
}
struct playlist *playlist_new(enum playlist_type_t type, const gchar *name)
{
if (type < PL_MAX_TYPE && playlist_types[type]->pl_new)
return playlist_types[type]->pl_new(name);
return NULL;
}
bool playlist_delete(struct playlist *playlist)
{
enum playlist_type_t type;
bool ret;
if (!playlist || !playlist->pl_ops->pl_delete)
return false;
index_insert(&playlist_db, playlist_names[plist],
track->tr_dbe.dbe_index);
if (playlist_cur == plist)
queue_add(&playlist_q, track);
type = playlist->pl_type;
ret = playlist->pl_ops->pl_delete(playlist);
if (ret)
playlist_types[type]->pl_save();
return ret;
}
struct playlist *playlist_lookup(enum playlist_type_t type, const gchar *name)
{
if (type >= PL_MAX_TYPE)
return NULL;
return playlist_types[type]->pl_lookup(name);
}
struct playlist *playlist_get(enum playlist_type_t type, unsigned int id)
{
if (type >= PL_MAX_TYPE)
return NULL;
return playlist_types[type]->pl_get(id);
}
struct playlist *playlist_current(void)
{
return current;
}
bool playlist_select(struct playlist *playlist)
{
if (!playlist || (playlist == current))
return false;
if (!playlist->pl_ops->pl_can_select)
return false;
if (!playlist->pl_ops->pl_can_select(playlist))
return false;
previous = current;
current = playlist;
settings_set(SETTINGS_CUR_TYPE, current->pl_type);
settings_set(SETTINGS_CUR_ID, current->pl_id);
if (previous) {
settings_set(SETTINGS_PREV_TYPE, previous->pl_type);
settings_set(SETTINGS_PREV_ID, previous->pl_id);
}
return true;
}
bool playlist_remove(enum playlist_t plist, struct track *track)
struct track *playlist_next(void)
{
if (!track || !playlist_has(plist, track))
struct track *track = playlist_generic_next(current);
if (track && current->pl_type < PL_MAX_TYPE)
playlist_types[current->pl_type]->pl_save();
return track;
}
struct track *playlist_prev(void)
{
return playlist_generic_next(playlist_lookup(PL_SYSTEM, "History"));
}
bool playlist_add(struct playlist *playlist, struct track *track)
{
bool ret;
if (!track || !playlist || !playlist->pl_ops->pl_add)
return false;
index_remove(&playlist_db, playlist_names[plist],
track->tr_dbe.dbe_index);
if (playlist_cur == plist)
queue_remove_all(&playlist_q, track);
return true;
ret = playlist->pl_ops->pl_add(playlist, track);
if (ret && playlist->pl_type < PL_MAX_TYPE)
playlist_types[playlist->pl_type]->pl_save();
if (playlist == playlist_lookup(PL_SYSTEM, "Queued Tracks"))
playlist_select(playlist);
return ret;
}
bool playlist_has(enum playlist_t plist, struct track *track)
bool playlist_remove(struct playlist *playlist, struct track *track)
{
if (!track || __playlist_is_dynamic(plist))
bool ret;
if (!track || !playlist || !playlist->pl_ops->pl_remove)
return false;
return index_has(&playlist_db, playlist_names[plist], track->tr_dbe.dbe_index);
ret = playlist->pl_ops->pl_remove(playlist, track);
if (ret && playlist->pl_type < PL_MAX_TYPE)
playlist_types[playlist->pl_type]->pl_save();
return ret;
}
void playlist_select(enum playlist_t plist)
void playlist_set_random(struct playlist *playlist, bool enabled)
{
queue_clear(&playlist_q);
queue_set_flag(&playlist_q, Q_ADD_FRONT);
if (__playlist_is_static(plist))
__playlist_fill_static(plist);
else
__playlist_fill_dynamic(plist);
queue_unset_flag(&playlist_q, Q_ADD_FRONT);
playlist_cur = plist;
queue_resort(&playlist_q);
if (playlist && playlist->pl_ops->pl_set_random) {
playlist->pl_ops->pl_set_random(playlist, enabled);
if (playlist->pl_type < PL_MAX_TYPE)
playlist_types[playlist->pl_type]->pl_save();
}
}
struct queue *playlist_get_queue()
bool playlist_sort(struct playlist *playlist, enum compare_t sort)
{
return &playlist_q;
if (!playlist || !playlist->pl_ops->pl_sort)
return false;
playlist->pl_ops->pl_sort(playlist, sort);
if (playlist->pl_type < PL_MAX_TYPE)
playlist_types[playlist->pl_type]->pl_save();
return g_slist_length(playlist->pl_sort) > 0;
}
bool playlist_rearrange(struct playlist *playlist, unsigned int old_pos,
unsigned int new_pos)
{
bool ret;
if (!playlist || !playlist->pl_ops->pl_rearrange)
return false;
ret = playlist->pl_ops->pl_rearrange(playlist, old_pos, new_pos);
if (ret && playlist->pl_type < PL_MAX_TYPE)
playlist_types[playlist->pl_type]->pl_save();
return ret;
}
void playlist_set_search(struct playlist *playlist, const gchar *text)
{
gchar **tokens = NULL;
if (!playlist)
return;
if (playlist->pl_search)
g_strfreev(playlist->pl_search);
if (strlen(text) > 0)
tokens = g_str_tokenize_and_fold(text, NULL, NULL);
playlist->pl_search = tokens;
}

157
core/playlists/artist.c Normal file
View File

@ -0,0 +1,157 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#include <core/idle.h>
#include <core/playlists/artist.h>
#include <core/string.h>
static struct file artist_file = FILE_INIT_DATA("", "playlist.artist", 0);
static struct playlist_ops pl_artist_ops = {
.pl_can_select = playlist_generic_can_select,
.pl_set_random = playlist_generic_set_random,
.pl_sort = playlist_generic_sort,
.pl_rearrange = playlist_generic_rearrange,
};
static struct playlist *__artist_pl_alloc(struct artist *artist)
{
return playlist_generic_alloc(artist->ar_name, PL_ARTIST,
artist_index(artist), &pl_artist_ops,
3, COMPARE_YEAR, COMPARE_ALBUM, COMPARE_TRACK);
}
static bool __artist_pl_add(void *data)
{
struct playlist *playlist = (struct playlist *)data;
struct artist *artist = artist_lookup(playlist->pl_name);
struct db_entry *dbe, *next;
db_for_each(dbe, next, track_db_get()) {
if (TRACK(dbe)->tr_album->al_artist == artist)
playlist_generic_add_front(playlist, TRACK(dbe));
}
playlist_generic_resort(playlist);
return true;
}
static struct playlist *__artist_pl_lookup(const gchar *name)
{
struct artist *artist = artist_lookup(name);
return artist ? artist->ar_playlist : NULL;
}
static bool __artist_pl_load(void *data)
{
struct playlist *playlist;
unsigned int i, n;
gchar *name;
if (!file_open(&artist_file, OPEN_READ))
return true;
n = file_readu(&artist_file);
for (i = 0; i < n; i++) {
name = file_readl(&artist_file);
playlist = __artist_pl_lookup(name);
if (playlist)
playlist_generic_load(playlist, &artist_file,
PL_SAVE_METADATA);
g_free(name);
}
file_close(&artist_file);
return true;
}
static void pl_artist_save(void)
{
struct db_entry *dbe, *next;
struct playlist *playlist;
if (!file_open(&artist_file, OPEN_WRITE))
return;
file_writef(&artist_file, "%u\n", artist_db_get()->db_size);
db_for_each(dbe, next, artist_db_get()) {
playlist = ARTIST(dbe)->ar_playlist;
file_writef(&artist_file, "%s\n", playlist->pl_name);
playlist_generic_save(playlist, &artist_file, PL_SAVE_METADATA);
}
file_close(&artist_file);
}
static struct playlist *pl_artist_lookup(const gchar *name)
{
return __artist_pl_lookup(name);
}
static struct playlist *pl_artist_get(unsigned int id)
{
struct artist *artist = artist_get(id);
return artist ? artist->ar_playlist : NULL;
}
static void pl_artist_played(struct track *track)
{
struct artist *artist = track->tr_album->al_artist;
playlist_generic_update(artist->ar_playlist, track);
}
struct playlist_type pl_artist = {
.pl_save = pl_artist_save,
.pl_lookup = pl_artist_lookup,
.pl_get = pl_artist_get,
.pl_played = pl_artist_played,
.pl_selected = pl_artist_played,
};
void pl_artist_init(void)
{
struct db_entry *dbe, *next;
struct playlist *playlist;
db_for_each(dbe, next, artist_db_get()) {
playlist = __artist_pl_alloc(ARTIST(dbe));
ARTIST(dbe)->ar_playlist = playlist;
idle_schedule(IDLE_SYNC, __artist_pl_add, playlist);
}
idle_schedule(IDLE_SYNC, __artist_pl_load, NULL);
}
void pl_artist_deinit()
{
struct db_entry *dbe, *next;
db_for_each(dbe, next, artist_db_get()) {
playlist_generic_free(ARTIST(dbe)->ar_playlist);
ARTIST(dbe)->ar_playlist = NULL;
}
}
void pl_artist_new_track(struct track *track)
{
struct artist *artist = track->tr_album->al_artist;
struct playlist *playlist = (struct playlist *)artist->ar_playlist;
if (!playlist) {
playlist = __artist_pl_alloc(artist);
artist->ar_playlist = playlist;
}
playlist_generic_add(playlist, track);
}
void pl_artist_delete_track(struct track *track)
{
struct artist *artist = track->tr_album->al_artist;
playlist_generic_remove(artist->ar_playlist, track);
}

313
core/playlists/generic.c Normal file
View File

@ -0,0 +1,313 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#include <core/idle.h>
#include <core/playlists/generic.h>
#include <stdlib.h>
static struct playlist_callbacks *callbacks = NULL;
static int __playlist_generic_find_sort(gconstpointer a, gconstpointer b)
{
return abs(GPOINTER_TO_INT(a)) - abs(GPOINTER_TO_INT(b));
}
static int __playlist_generic_less_than(gconstpointer a, gconstpointer b,
gpointer data)
{
struct track *lhs = (struct track *)a;
struct track *rhs = (struct track *)b;
GSList *cur = ((struct playlist *)data)->pl_sort;
int res, field;
while (cur) {
field = GPOINTER_TO_INT(cur->data);
res = track_compare(lhs, rhs, abs(field));
if (res != 0)
break;
cur = g_slist_next(cur);
};
return (field > 0) ? res : -res;
}
void playlist_generic_set_callbacks(struct playlist_callbacks *cb)
{
callbacks = cb;
}
static void __playlist_generic_vinit(struct playlist *playlist,
unsigned int nargs, va_list argp)
{
unsigned int i;
if (!playlist)
return;
g_queue_init(&playlist->pl_tracks);
playlist->pl_length = 0;
playlist->pl_random = false;
playlist->pl_current = NULL;
playlist->pl_sort = NULL;
playlist->pl_search = NULL;
for (i = 0; i < nargs; i++)
playlist_generic_sort(playlist, va_arg(argp, unsigned int));
}
void playlist_generic_init(struct playlist *playlist, unsigned int nargs, ...)
{
va_list argp;
va_start(argp, nargs);
__playlist_generic_vinit(playlist, nargs, argp);
va_end(argp);
}
void playlist_generic_deinit(struct playlist *playlist)
{
if (playlist) {
playlist_generic_clear(playlist);
playlist_clear_sort(playlist);
g_strfreev(playlist->pl_search);
playlist->pl_search = NULL;
playlist->pl_length = 0;
}
}
struct playlist *playlist_generic_alloc(gchar *name, enum playlist_type_t type,
unsigned int id, struct playlist_ops *ops,
unsigned int nargs, ...)
{
struct playlist *playlist = g_malloc(sizeof(struct playlist));
va_list argp;
playlist->pl_name = name;
playlist->pl_type = type;
playlist->pl_id = id;
playlist->pl_ops = ops;
va_start(argp, nargs);
__playlist_generic_vinit(playlist, nargs, argp);
if (callbacks)
callbacks->pl_cb_alloc(playlist);
va_end(argp);
return playlist;
}
void playlist_generic_free(struct playlist *playlist)
{
if (playlist) {
playlist_generic_deinit(playlist);
g_free(playlist);
}
}
void playlist_generic_save(struct playlist *playlist, struct file *file,
unsigned int flags)
{
playlist_iter it;
GSList *sort;
int field;
if (!playlist)
return;
if (flags & PL_SAVE_ITER)
file_writef(file, "%u ", playlist_current_index(playlist));
if (flags & PL_SAVE_FLAGS) {
sort = playlist->pl_sort;
file_writef(file, "%u ", playlist->pl_random ? PL_RANDOM : 0);
file_writef(file, "%u", g_slist_length(sort));
while (sort) {
field = GPOINTER_TO_INT(sort->data);
file_writef(file, " %u %d", abs(field) - 1, field > 0);
sort = g_slist_next(sort);
}
file_writef(file, "\n");
}
if (flags & PL_SAVE_TRACKS) {
file_writef(file, "%u", playlist_size(playlist));
playlist_for_each(playlist, it)
file_writef(file, " %u",
track_index(playlist_iter_track(it)));
file_writef(file, "\n");
}
}
void playlist_generic_load(struct playlist *playlist, struct file *file,
unsigned int flags)
{
unsigned int f, n, i, t, it = 0;
int field, ascending;
if (!playlist)
return;
if (flags & PL_SAVE_ITER)
it = file_readu(file);
if (flags & PL_SAVE_FLAGS) {
f = file_readu(file);
n = file_readu(file);
playlist_clear_sort(playlist);
for (i = 0; i < n; i++) {
field = file_readu(file) + 1;
ascending = file_readd(file);
if (!ascending)
field = -field;
playlist->pl_sort = g_slist_append(playlist->pl_sort,
GINT_TO_POINTER(field));
}
playlist_generic_resort(playlist);
}
if (flags & PL_SAVE_TRACKS) {
n = file_readu(file);
for (i = 0; i < n; i++) {
t = file_readu(file);
playlist_generic_add(playlist, track_get(t));
}
}
playlist_generic_set_random(playlist, f == PL_RANDOM);
playlist_current_set(playlist, it);
}
bool playlist_generic_can_select(struct playlist *playlist)
{
return playlist_size(playlist) > 0;
}
void playlist_generic_clear(struct playlist *playlist)
{
unsigned int n;
if (!playlist)
return;
n = playlist_size(playlist);
g_queue_clear(&playlist->pl_tracks);
playlist->pl_length = 0;
playlist->pl_current = NULL;
if (callbacks)
callbacks->pl_cb_removed(playlist, NULL, n);
}
bool playlist_generic_add(struct playlist *playlist, struct track *track)
{
if (!playlist || !track || playlist_has(playlist, track))
return false;
playlist->pl_length += track->tr_length;
if (playlist->pl_sort) {
g_queue_insert_sorted(&playlist->pl_tracks, track,
__playlist_generic_less_than, playlist);
} else
g_queue_push_tail(&playlist->pl_tracks, track);
if (callbacks)
callbacks->pl_cb_added(playlist, track);
return true;
}
bool playlist_generic_add_front(struct playlist *playlist, struct track *track)
{
if (!playlist || !track)
return false;
playlist->pl_length += track->tr_length;
g_queue_push_head(&playlist->pl_tracks, track);
if (callbacks)
callbacks->pl_cb_added(playlist, track);
return true;
}
bool playlist_generic_remove(struct playlist *playlist, struct track *track)
{
unsigned int count;
if (!playlist || !track)
return false;
while (playlist_current_track(playlist) == track)
playlist_current_previous(playlist);
count = g_queue_remove_all(&playlist->pl_tracks, track);
playlist->pl_length -= (count * track->tr_length);
if (callbacks)
callbacks->pl_cb_removed(playlist, track, count);
return count > 0;
}
void playlist_generic_update(struct playlist *playlist, struct track *track)
{
if (playlist && callbacks)
callbacks->pl_cb_updated(playlist, track);
}
void playlist_generic_set_random(struct playlist *playlist, bool enabled)
{
playlist->pl_random = enabled;
}
void playlist_generic_sort(struct playlist *playlist, enum compare_t field)
{
gpointer sort = GINT_TO_POINTER(field);
GSList *found = g_slist_find_custom(playlist->pl_sort, sort,
__playlist_generic_find_sort);
if (found)
found->data = GINT_TO_POINTER(-field);
else
playlist->pl_sort = g_slist_append(playlist->pl_sort, sort);
playlist_generic_resort(playlist);
}
void playlist_generic_resort(struct playlist *playlist)
{
if (!playlist || !playlist->pl_sort)
return;
g_queue_sort(&playlist->pl_tracks, __playlist_generic_less_than, playlist);
playlist_generic_update(playlist, NULL);
}
bool playlist_generic_rearrange(struct playlist *playlist, unsigned int old_pos,
unsigned int new_pos)
{
GList *nth;
if (old_pos == new_pos || old_pos >= playlist_size(playlist) ||
new_pos > playlist_size(playlist))
return false;
playlist_clear_sort(playlist);
nth = g_queue_pop_nth_link(&playlist->pl_tracks, old_pos);
g_queue_push_nth_link(&playlist->pl_tracks, new_pos, nth);
playlist_generic_update(playlist, NULL);
return true;
}
struct track *playlist_generic_next(struct playlist *playlist)
{
unsigned int pos, size = playlist_size(playlist);
if (size == 0)
return NULL;
else if (playlist->pl_random) {
pos = playlist_current_index(playlist);
pos += g_random_int_range(1, size);
playlist_current_set(playlist, pos % size);
} else if (!playlist_current_next(playlist))
playlist_current_set(playlist, 0);
return playlist_current_track(playlist);
}

277
core/playlists/library.c Normal file
View File

@ -0,0 +1,277 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#include <core/idle.h>
#include <core/playlists/artist.h>
#include <core/playlists/library.h>
#include <core/playlists/system.h>
#include <core/playlists/user.h>
#include <unistd.h>
struct scan_data {
struct library *sd_library;
gchar *sd_path;
};
static bool __lib_pl_scan_dir(void *);
static struct file lib_file = FILE_INIT_DATA("", "playlist.library", 0);
static struct playlist_ops pl_library_ops;
static struct playlist *__lib_pl_alloc(struct library *library)
{
return playlist_generic_alloc(library->li_path, PL_LIBRARY,
library_index(library), &pl_library_ops,
4, COMPARE_ARTIST, COMPARE_YEAR,
COMPARE_ALBUM, COMPARE_TRACK);
}
static bool __lib_pl_add(void *data)
{
struct playlist *playlist = (struct playlist *)data;
struct library *library = library_lookup(playlist->pl_name);
struct db_entry *dbe, *next;
db_for_each(dbe, next, track_db_get()) {
if (TRACK(dbe)->tr_library == library)
playlist_generic_add_front(playlist, TRACK(dbe));
}
playlist_generic_resort(playlist);
return true;
}
static struct playlist *__lib_pl_lookup(const gchar *name)
{
struct library *library = library_lookup(name);
return library ? library->li_playlist : NULL;
}
static bool __lib_pl_load(void *data)
{
struct playlist *playlist;
unsigned int i, n;
gchar *name;
if (!file_open(&lib_file, OPEN_READ))
return true;
n = file_readu(&lib_file);
for (i = 0; i < n; i++) {
name = file_readl(&lib_file);
playlist = __lib_pl_lookup(name);
if (playlist)
playlist_generic_load(playlist, &lib_file,
PL_SAVE_METADATA);
g_free(name);
}
file_close(&lib_file);
return true;
}
static void __lib_pl_scan_dir_idle(struct library *library, const gchar *path)
{
struct scan_data *scan = g_malloc(sizeof(struct scan_data));
scan->sd_library = library;
scan->sd_path = g_strdup(path);
/* scan data is freed by __lib_pl_scan_dir() */
idle_schedule(IDLE_SYNC, __lib_pl_scan_dir, scan);
}
static void __lib_pl_read_path(struct scan_data *scan, const gchar *name)
{
gchar *path = g_build_filename(scan->sd_path, name, NULL);
struct playlist *playlist = scan->sd_library->li_playlist;
struct track *track;
if (g_file_test(path, G_FILE_TEST_IS_DIR))
__lib_pl_scan_dir_idle(scan->sd_library, path);
else {
track = track_add(scan->sd_library, path);
if (track) {
playlist_generic_add(playlist, track);
pl_system_new_track(track);
pl_artist_new_track(track);
}
}
g_free(path);
}
static bool __lib_pl_scan_dir(void *data)
{
struct scan_data *scan = (struct scan_data *)data;
const gchar *name;
GDir *dir;
dir = g_dir_open(scan->sd_path, 0, NULL);
if (!dir)
goto out;
name = g_dir_read_name(dir);
while (name != NULL) {
__lib_pl_read_path(scan, name);
name = g_dir_read_name(dir);
}
g_dir_close(dir);
track_db_commit();
out:
/* Allocated by __lib_pl_scan_dir_idle() */
g_free(scan->sd_path);
g_free(scan);
return true;
}
static bool __lib_pl_update(void *data)
{
struct playlist *playlist = (struct playlist *)data;
struct library *library = library_lookup(playlist->pl_name);
struct db_entry *dbe, *next;
gchar *path;
db_for_each(dbe, next, track_db_get()) {
if (TRACK(dbe)->tr_library != library)
continue;
path = track_path(TRACK(dbe));
if (g_access(path, F_OK) < 0) {
pl_system_delete_track(TRACK(dbe));
pl_artist_delete_track(TRACK(dbe));
playlist_generic_remove(playlist, TRACK(dbe));
track_remove(TRACK(dbe));
}
g_free(path);
}
track_db_commit();
__lib_pl_scan_dir_idle(library, library->li_path);
return true;
}
static bool pl_library_delete(struct playlist *playlist)
{
struct library *library = library_lookup(playlist->pl_name);
playlist_iter it;
if (!library)
return false;
playlist_for_each(playlist, it) {
pl_system_delete_track(playlist_iter_track(it));
pl_artist_delete_track(playlist_iter_track(it));
pl_user_delete_track(playlist_iter_track(it));
}
playlist_generic_free(playlist);
track_remove_all(library);
library_remove(library);
return true;
}
static struct playlist_ops pl_library_ops = {
.pl_can_select = playlist_generic_can_select,
.pl_delete = pl_library_delete,
.pl_set_random = playlist_generic_set_random,
.pl_sort = playlist_generic_sort,
.pl_rearrange = playlist_generic_rearrange,
};
static void pl_library_save(void)
{
struct db_entry *dbe, *next;
struct playlist *playlist;
if (!file_open(&lib_file, OPEN_WRITE))
return;
file_writef(&lib_file, "%u\n", library_db_get()->db_size);
db_for_each(dbe, next, library_db_get()) {
playlist = LIBRARY(dbe)->li_playlist;
file_writef(&lib_file, "%s\n", playlist->pl_name);
playlist_generic_save(playlist, &lib_file, PL_SAVE_METADATA);
}
file_close(&lib_file);
}
static struct playlist *pl_library_lookup(const gchar *name)
{
return __lib_pl_lookup(name);
}
static struct playlist *pl_library_get(unsigned int id)
{
struct library *library = LIBRARY(db_at(library_db_get(), id));
return library ? library->li_playlist : NULL;
}
static struct playlist *pl_library_new(const gchar *name)
{
struct library *library;
if (__lib_pl_lookup(name) || !g_file_test(name, G_FILE_TEST_IS_DIR))
return NULL;
library = library_find(name);
library->li_playlist = __lib_pl_alloc(library);
__lib_pl_scan_dir_idle(library, name);
return library->li_playlist;
}
static void pl_library_played(struct track *track)
{
struct library *library = track->tr_library;
playlist_generic_update(library->li_playlist, track);
}
struct playlist_type pl_library = {
.pl_save = pl_library_save,
.pl_lookup = pl_library_lookup,
.pl_get = pl_library_get,
.pl_new = pl_library_new,
.pl_played = pl_library_played,
.pl_selected = pl_library_played,
};
void pl_library_init(void)
{
struct db_entry *dbe, *next;
struct playlist *playlist;
db_for_each(dbe, next, library_db_get()) {
playlist = __lib_pl_alloc(LIBRARY(dbe));
LIBRARY(dbe)->li_playlist = playlist;
idle_schedule(IDLE_SYNC, __lib_pl_add, playlist);
idle_schedule(IDLE_SYNC, __lib_pl_update, playlist);
}
idle_schedule(IDLE_SYNC, __lib_pl_load, NULL);
}
void pl_library_deinit()
{
struct db_entry *dbe, *next;
db_for_each(dbe, next, library_db_get()) {
playlist_generic_free(LIBRARY(dbe)->li_playlist);
LIBRARY(dbe)->li_playlist = NULL;
}
}
void pl_library_update(struct playlist *playlist)
{
idle_schedule(IDLE_SYNC, __lib_pl_update, playlist);
}

454
core/playlists/system.c Normal file
View File

@ -0,0 +1,454 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#include <core/idle.h>
#include <core/playlists/system.h>
#include <core/string.h>
static struct playlist *pl_system_lookup(const gchar *);
static struct playlist *pl_system_get(unsigned int);
static void pl_system_save();
static struct file sys_file = FILE_INIT_DATA("", "playlist.db", 0);
static struct file sys_deck_f = FILE_INIT_DATA("", "deck", 1);
static struct file sys_collection_f = FILE_INIT_DATA("", "library.q", 0);
static struct file sys_pl_file = FILE_INIT_DATA("", "playlist.system", 0);
/*
* Generic system playlist operations.
*/
static bool sys_pl_delete_clear(struct playlist *playlist)
{
playlist_generic_clear(playlist);
pl_system_save();
return false;
}
static bool sys_pl_update_check(struct playlist *playlist, struct track *track)
{
switch (playlist->pl_id) {
case SYS_PL_UNPLAYED:
return track->tr_count == 0;
case SYS_PL_LEAST_PLAYED:
return track->tr_count <= track_db_average_plays() &&
track->tr_count > 0;
case SYS_PL_MOST_PLAYED:
return track->tr_count > track_db_average_plays();
}
return true;
}
static bool sys_pl_update_func(void *data)
{
struct playlist *playlist = (struct playlist *)data;
struct db_entry *dbe, *next;
db_for_each(dbe, next, track_db_get()) {
struct track *track = TRACK(dbe);
if (!sys_pl_update_check(playlist, track))
playlist_generic_remove(playlist, track);
else if (!playlist_has(pl_system_get(SYS_PL_HIDDEN), track) &&
!playlist_has(playlist, track))
playlist_generic_add_front(playlist, track);
}
playlist_generic_resort(playlist);
return true;
}
static void sys_pl_update(struct playlist *playlist)
{
switch (playlist->pl_id) {
case SYS_PL_COLLECTION:
case SYS_PL_UNPLAYED:
case SYS_PL_LEAST_PLAYED:
case SYS_PL_MOST_PLAYED:
idle_schedule(IDLE_SYNC, sys_pl_update_func, playlist);
default:
break;
}
}
/*
* Favorite tracks playlist operations.
*/
static struct playlist_ops favorites_ops = {
.pl_add = playlist_generic_add,
.pl_can_select = playlist_generic_can_select,
.pl_delete = sys_pl_delete_clear,
.pl_remove = playlist_generic_remove,
.pl_set_random = playlist_generic_set_random,
.pl_sort = playlist_generic_sort,
.pl_rearrange = playlist_generic_rearrange,
};
/*
* Hidden tracks playlist operations.
*/
static bool sys_pl_hidden_add(struct playlist *playlist, struct track *track)
{
bool ret = playlist_generic_add(pl_system_get(SYS_PL_HIDDEN), track);
playlist_generic_remove(pl_system_get(SYS_PL_COLLECTION), track);
playlist_generic_remove(pl_system_get(SYS_PL_UNPLAYED), track);
playlist_generic_remove(pl_system_get(SYS_PL_MOST_PLAYED), track);
playlist_generic_remove(pl_system_get(SYS_PL_LEAST_PLAYED), track);
return ret;
}
static bool sys_pl_hidden_remove(struct playlist *playlist, struct track *track)
{
bool ret = playlist_generic_remove(playlist, track);
unsigned int average = track_db_average_plays();
unsigned int add_id = SYS_PL_LEAST_PLAYED;
add_id = (track->tr_count == 0) ? SYS_PL_UNPLAYED : add_id;
add_id = (track->tr_count > average) ? SYS_PL_MOST_PLAYED : add_id;
playlist_generic_add(pl_system_get(SYS_PL_COLLECTION), track);
playlist_generic_add(pl_system_get(add_id), track);
return ret;
}
static bool sys_pl_hidden_clear(struct playlist *playlist)
{
struct track *track;
while (playlist_size(playlist) > 0) {
track = playlist_at(playlist, 0);
sys_pl_hidden_remove(playlist, track);
}
pl_system_save();
return false;
}
static struct playlist_ops hidden_ops = {
.pl_add = sys_pl_hidden_add,
.pl_can_select = playlist_generic_can_select,
.pl_delete = sys_pl_hidden_clear,
.pl_remove = sys_pl_hidden_remove,
.pl_set_random = playlist_generic_set_random,
.pl_sort = playlist_generic_sort,
.pl_rearrange = playlist_generic_rearrange,
};
/*
* Queued tracks playlist operations.
*/
static bool sys_pl_queued_load()
{
struct playlist *playlist = pl_system_get(SYS_PL_QUEUED);
unsigned int num, i, flags = 0;
if (!file_open(&sys_deck_f, OPEN_READ))
return true;
num = file_readu(&sys_deck_f);
for (i = 0; i < num; i++) {
flags = file_readu(&sys_deck_f);
flags &= PL_RANDOM;
if (i == 0)
playlist_generic_set_random(playlist,
flags == PL_RANDOM);
playlist_generic_load(playlist, &sys_deck_f, PL_SAVE_TRACKS);
}
file_close(&sys_deck_f);
file_remove(&sys_deck_f);
return true;
}
static bool sys_pl_queued_delete(struct playlist *playlist)
{
playlist_generic_set_random(playlist, false);
playlist_clear_sort(playlist);
return sys_pl_delete_clear(playlist);
}
static bool sys_pl_queued_remove(struct playlist *playlist, struct track *track)
{
bool ret = playlist_generic_remove(playlist, track);
if (playlist_size(playlist) == 0)
sys_pl_queued_delete(playlist);
return ret;
}
static struct playlist_ops queued_ops = {
.pl_add = playlist_generic_add,
.pl_can_select = playlist_generic_can_select,
.pl_delete = sys_pl_queued_delete,
.pl_remove = sys_pl_queued_remove,
.pl_set_random = playlist_generic_set_random,
.pl_sort = playlist_generic_sort,
.pl_rearrange = playlist_generic_rearrange,
};
/*
* Collection playlist operations.
*/
static bool sys_pl_collection_load()
{
struct playlist *playlist = pl_system_get(SYS_PL_COLLECTION);
if (file_open(&sys_collection_f, OPEN_READ)) {
playlist_generic_load(playlist, &sys_collection_f, PL_SAVE_FLAGS);
file_close(&sys_collection_f);
file_remove(&sys_collection_f);
}
return true;
}
static struct playlist_ops collection_ops = {
.pl_can_select = playlist_generic_can_select,
.pl_remove = sys_pl_hidden_add,
.pl_set_random = playlist_generic_set_random,
.pl_sort = playlist_generic_sort,
.pl_rearrange = playlist_generic_rearrange,
};
/*
* History playlist operations.
*/
static bool sys_pl_history_add(struct playlist *playlist, struct track *track)
{
playlist_generic_add_front(playlist, track);
playlist_current_set(playlist, 0);
return true;
}
static struct playlist_ops history_ops = {
.pl_add = sys_pl_history_add,
};
/*
* Unplayed, Most Played, and Least Played tracks playlist operations.
*/
static struct playlist_ops dynamic_ops = {
.pl_can_select = playlist_generic_can_select,
.pl_set_random = playlist_generic_set_random,
.pl_sort = playlist_generic_sort,
.pl_rearrange = playlist_generic_rearrange,
};
#define SYS_PLAYLIST(id, name, ops) \
[id] = DEFINE_PLAYLIST(PL_SYSTEM, name, id, ops)
static struct playlist sys_playlists[SYS_PL_NUM_PLAYLISTS] = {
SYS_PLAYLIST(SYS_PL_FAVORITES, "Favorites", &favorites_ops),
SYS_PLAYLIST(SYS_PL_HIDDEN, "Hidden", &hidden_ops),
SYS_PLAYLIST(SYS_PL_QUEUED, "Queued Tracks", &queued_ops),
SYS_PLAYLIST(SYS_PL_COLLECTION, "Collection", &collection_ops),
SYS_PLAYLIST(SYS_PL_HISTORY, "History", &history_ops),
SYS_PLAYLIST(SYS_PL_UNPLAYED, "Unplayed", &dynamic_ops),
SYS_PLAYLIST(SYS_PL_MOST_PLAYED, "Most Played", &dynamic_ops),
SYS_PLAYLIST(SYS_PL_LEAST_PLAYED, "Least Played", &dynamic_ops),
};
static bool __sys_pl_update_save()
{
pl_system_save();
return true;
}
static bool __sys_pl_load()
{
struct playlist *playlist;
unsigned int i, n;
gchar *name;
if (!file_open(&sys_file, OPEN_READ))
return true;
n = file_readu(&sys_file);
for (i = 0; i < n; i++) {
file_readu(&sys_file);
name = file_readl(&sys_file);
if (string_match(name, "Banned")) {
g_free(name);
name = g_strdup("Hidden");
}
playlist = pl_system_lookup(name);
g_free(name);
if (playlist)
playlist_generic_load(playlist, &sys_file, PL_SAVE_TRACKS);
}
file_close(&sys_file);
file_remove(&sys_file);
return true;
}
static bool __sys_pl_load_new()
{
struct playlist *playlist;
unsigned int i, n, load;
gchar *name;
if (!file_open(&sys_pl_file, OPEN_READ)) {
__sys_pl_load();
sys_pl_collection_load();
sys_pl_queued_load();
__sys_pl_update_save();
return true;
}
n = file_readu(&sys_pl_file);
for (i = 0; i < n; i++) {
load = PL_SAVE_METADATA;
name = file_readl(&sys_pl_file);
if (string_match(name, "Banned")) {
g_free(name);
name = g_strdup("Hidden");
}
playlist = pl_system_lookup(name);
g_free(name);
switch (i) {
case SYS_PL_FAVORITES:
case SYS_PL_HIDDEN:
case SYS_PL_QUEUED:
load = PL_SAVE_ALL;
}
playlist_generic_load(playlist, &sys_pl_file, load);
}
file_close(&sys_pl_file);
return true;
}
static void pl_system_save(void)
{
struct playlist *playlist;
unsigned int i, save;
if (!file_open(&sys_pl_file, OPEN_WRITE))
return;
file_writef(&sys_pl_file, "%u\n", SYS_PL_NUM_PLAYLISTS);
for (i = 0; i < SYS_PL_NUM_PLAYLISTS; i++) {
save = PL_SAVE_METADATA;
playlist = pl_system_get(i);
switch (i) {
case SYS_PL_FAVORITES:
case SYS_PL_HIDDEN:
case SYS_PL_QUEUED:
save = PL_SAVE_ALL;
}
file_writef(&sys_pl_file, "%s\n", playlist->pl_name);
playlist_generic_save(playlist, &sys_pl_file, save);
}
file_close(&sys_pl_file);
}
static struct playlist *pl_system_lookup(const gchar *name)
{
unsigned int i;
for (i = 0; i < SYS_PL_NUM_PLAYLISTS; i++) {
if (string_match(name, pl_system_get(i)->pl_name))
return pl_system_get(i);
}
return NULL;
}
static struct playlist *pl_system_get(unsigned int id)
{
return (id < SYS_PL_NUM_PLAYLISTS) ? &sys_playlists[id] : NULL;
}
static void pl_system_played(struct track *track)
{
unsigned int i;
for (i = 0; i < SYS_PL_NUM_PLAYLISTS; i++)
playlist_generic_update(pl_system_get(i), track);
sys_pl_update(pl_system_lookup("Unplayed"));
sys_pl_update(pl_system_lookup("Most Played"));
sys_pl_update(pl_system_lookup("Least Played"));
}
static void pl_system_selected(struct track *track)
{
unsigned int i;
sys_pl_queued_remove(pl_system_get(SYS_PL_QUEUED), track);
for (i = 0; i < SYS_PL_NUM_PLAYLISTS; i++)
playlist_generic_update(pl_system_get(i), track);
}
struct playlist_type pl_system = {
.pl_save = pl_system_save,
.pl_lookup = pl_system_lookup,
.pl_get = pl_system_get,
.pl_played = pl_system_played,
.pl_selected = pl_system_selected,
};
void pl_system_init(void)
{
struct playlist *playlist;
unsigned int i;
idle_schedule(IDLE_SYNC, __sys_pl_load_new, NULL);
for (i = 0; i < SYS_PL_NUM_PLAYLISTS; i++) {
playlist = pl_system_get(i);
switch (i) {
case SYS_PL_QUEUED:
case SYS_PL_HISTORY:
playlist_generic_init(playlist, 0);
break;
case SYS_PL_COLLECTION:
case SYS_PL_UNPLAYED:
case SYS_PL_MOST_PLAYED:
case SYS_PL_LEAST_PLAYED:
sys_pl_update(playlist);
case SYS_PL_FAVORITES:
case SYS_PL_HIDDEN:
playlist_generic_init(playlist, 4, COMPARE_ARTIST,
COMPARE_YEAR, COMPARE_ALBUM, COMPARE_TRACK);
break;
}
}
}
void pl_system_deinit()
{
for (unsigned int i = 0; i < SYS_PL_NUM_PLAYLISTS; i++)
playlist_generic_deinit(pl_system_get(i));
}
void pl_system_new_track(struct track *track)
{
playlist_generic_add(pl_system_lookup("Collection"), track);
playlist_generic_add(pl_system_lookup("Unplayed"), track);
}
void pl_system_delete_track(struct track *track)
{
for (unsigned int i = 0; i < SYS_PL_NUM_PLAYLISTS; i++)
playlist_generic_remove(pl_system_get(i), track);
}

178
core/playlists/user.c Normal file
View File

@ -0,0 +1,178 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#include <core/playlists/user.h>
static struct database user_db;
static struct playlist_ops user_ops;
static struct user_playlist *__user_db_alloc(gchar *name, unsigned int index)
{
struct user_playlist *playlist = g_malloc(sizeof(struct user_playlist));
dbe_init(&playlist->pl_dbe, playlist);
playlist->pl_playlist.pl_name = name;
playlist->pl_playlist.pl_type = PL_USER;
playlist->pl_playlist.pl_id = index;
playlist->pl_playlist.pl_ops = &user_ops;
playlist_generic_init(&playlist->pl_playlist, 0);
return playlist;
}
static struct db_entry *user_db_alloc(const gchar *name, unsigned int index)
{
return &__user_db_alloc(g_strdup(name), index)->pl_dbe;
}
static void user_db_free(struct db_entry *dbe)
{
playlist_generic_deinit(&USER_PLAYLIST(dbe)->pl_playlist);
g_free(USER_PLAYLIST(dbe)->pl_playlist.pl_name);
g_free(USER_PLAYLIST(dbe));
}
static gchar *user_db_key(struct db_entry *dbe)
{
return g_strdup(USER_PLAYLIST(dbe)->pl_playlist.pl_name);
}
static struct db_entry *user_db_read(struct file *file, unsigned int index)
{
gchar *name = file_readl(file);
struct user_playlist *playlist = __user_db_alloc(name, index);
playlist_generic_load(&playlist->pl_playlist, file, PL_SAVE_ALL);
return &playlist->pl_dbe;
}
static void user_db_write(struct file *file, struct db_entry *dbe)
{
struct playlist *playlist = &USER_PLAYLIST(dbe)->pl_playlist;
file_writef(file, "%s\n", playlist->pl_name);
playlist_generic_save(playlist, file, PL_SAVE_ALL);
}
static const struct db_ops user_db_ops = {
.dbe_alloc = user_db_alloc,
.dbe_free = user_db_free,
.dbe_key = user_db_key,
.dbe_read = user_db_read,
.dbe_write = user_db_write,
};
static bool pl_user_delete(struct playlist *playlist)
{
struct db_entry *dbe = db_get(&user_db, playlist->pl_name);
if (dbe) {
db_remove(&user_db, dbe);
db_defrag(&user_db);
}
return dbe != NULL;
}
static struct playlist_ops user_ops = {
.pl_add = playlist_generic_add,
.pl_can_select = playlist_generic_can_select,
.pl_delete = pl_user_delete,
.pl_remove = playlist_generic_remove,
.pl_set_random = playlist_generic_set_random,
.pl_sort = playlist_generic_sort,
.pl_rearrange = playlist_generic_rearrange,
};
static void pl_user_save(void)
{
db_save(&user_db);
}
static struct playlist *pl_user_lookup(const gchar *name)
{
struct db_entry *dbe = db_get(&user_db, name);
return dbe ? &USER_PLAYLIST(dbe)->pl_playlist : NULL;
}
static struct playlist *pl_user_get(unsigned int id)
{
struct db_entry *dbe = db_at(&user_db, id);
return dbe ? &USER_PLAYLIST(dbe)->pl_playlist : NULL;
}
static struct playlist *pl_user_new(const gchar *name)
{
struct db_entry *dbe;
if (db_get(&user_db, name))
return NULL;
dbe = db_insert(&user_db, name);
return dbe ? &USER_PLAYLIST(dbe)->pl_playlist : NULL;
}
static void pl_user_played(struct track *track)
{
struct db_entry *dbe, *next;
db_for_each(dbe, next, &user_db)
playlist_generic_update(&USER_PLAYLIST(dbe)->pl_playlist, track);
}
struct playlist_type pl_user = {
.pl_save = pl_user_save,
.pl_lookup = pl_user_lookup,
.pl_get = pl_user_get,
.pl_new = pl_user_new,
.pl_played = pl_user_played,
.pl_selected = pl_user_played,
};
void pl_user_init(void)
{
db_init(&user_db, "playlist.user", true, &user_db_ops, 0);
db_load(&user_db);
}
void pl_user_deinit()
{
db_deinit(&user_db);
}
struct database *pl_user_db_get()
{
return &user_db;
}
void pl_user_delete_track(struct track *track)
{
struct db_entry *dbe, *next;
struct playlist *playlist;
db_for_each(dbe, next, &user_db) {
playlist = &USER_PLAYLIST(dbe)->pl_playlist;
playlist_generic_remove(playlist, track);
}
}
bool pl_user_rename(struct playlist *playlist, const gchar *name)
{
struct db_entry *dbe;
if (!playlist || db_get(&user_db, name))
return false;
dbe = db_get(&user_db, playlist->pl_name);
if (!dbe)
return false;
g_free(playlist->pl_name);
playlist->pl_name = g_strdup(name);
db_rekey(&user_db, dbe);
pl_user_save();
return true;
}

View File

@ -1,271 +0,0 @@
/*
* Copyright 2013 (c) Anna Schumaker.
*/
#include <core/queue.h>
#include <core/string.h>
#include <stdlib.h>
static int track_less_than(const void *a, const void *b, void *data)
{
struct track *lhs = (struct track *)a;
struct track *rhs = (struct track *)b;
GSList *cur = (GSList *)data;
int res, field;
while (cur) {
field = GPOINTER_TO_INT(cur->data);
if (field > 0)
res = track_compare(lhs, rhs, field);
else
res = track_compare(rhs, lhs, abs(field));
if (res == 0) {
cur = g_slist_next(cur);
continue;
}
break;
};
return res;
}
static inline void *__queue_init(struct queue *queue)
{
if (queue->q_ops)
return queue->q_ops->qop_init(queue);
return NULL;
}
static inline void __queue_deinit(struct queue *queue)
{
if (queue->q_ops)
queue->q_ops->qop_deinit(queue);
}
static inline unsigned int __queue_added(struct queue *queue,
struct track *track,
unsigned int pos)
{
queue->q_length += track->tr_length;
if (queue->q_ops)
queue->q_ops->qop_added(queue, pos);
return pos;
}
static inline bool __queue_erase(struct queue *queue, struct _q_iter *it)
{
struct track *track = (struct track *)_q_iter_val(it);
if (queue->q_ops)
return queue->q_ops->qop_erase(queue, track);
return true;
}
static inline void __queue_remove(struct queue *queue, struct _q_iter *it)
{
unsigned int pos = it->it_pos;
struct track *track = (struct track *)_q_remove_it(&queue->q_tracks, it);
queue->q_length -= track->tr_length;
if (queue->q_cur.it_pos == pos)
_q_iter_prev(&queue->q_cur);
if (queue->q_ops)
queue->q_ops->qop_removed(queue, pos);
}
static inline void __queue_clear(struct queue *queue, unsigned int n)
{
queue->q_length = 0;
if (queue->q_ops)
queue->q_ops->qop_cleared(queue, n);
}
static inline void __queue_updated(struct queue *queue, unsigned int pos)
{
if (queue->q_ops)
queue->q_ops->qop_updated(queue, pos);
}
static inline struct track *__queue_selected(struct queue *queue, unsigned int pos)
{
struct track *track = (struct track *)_q_iter_val(&queue->q_cur);
if (queue_has_flag(queue, Q_REPEAT) == false)
__queue_remove(queue, &queue->q_cur);
else
__queue_updated(queue, pos);
return track;
}
static inline void __queue_save(struct queue *queue, enum queue_flags flag)
{
if (queue->q_ops && queue_has_flag(queue, flag))
queue->q_ops->qop_save(queue, flag);
}
void queue_init(struct queue *queue, unsigned int flags,
const struct queue_ops *ops)
{
queue->q_flags = flags;
queue->q_length = 0;
queue->q_sort = NULL;
queue->q_ops = ops;
queue->q_cur.it_pos = -1;
queue->q_cur.it_iter = NULL;
_q_init(&queue->q_tracks);
queue->q_private = __queue_init(queue);
}
void queue_deinit(struct queue *queue)
{
queue_clear(queue);
__queue_deinit(queue);
g_slist_free(queue->q_sort);
queue->q_sort = NULL;
}
void queue_set_flag(struct queue *queue, enum queue_flags flag)
{
queue->q_flags |= flag;
__queue_save(queue, Q_SAVE_FLAGS);
}
void queue_unset_flag(struct queue *queue, enum queue_flags flag)
{
queue->q_flags &= ~flag;
__queue_save(queue, Q_SAVE_FLAGS);
}
unsigned int queue_add(struct queue *queue, struct track *track)
{
unsigned int pos;
if (queue_has_flag(queue, Q_ADD_FRONT))
pos = _q_add_head(&queue->q_tracks, track);
else if (queue->q_sort)
pos = _q_add_sorted(&queue->q_tracks, track,
track_less_than, queue->q_sort);
else
pos = _q_add_tail(&queue->q_tracks, track);
return __queue_added(queue, track, pos);
}
void queue_erase(struct queue *queue, unsigned int index)
{
struct _q_iter it;
_q_iter_set(&queue->q_tracks, &it, index);
if (__queue_erase(queue, &it))
__queue_remove(queue, &it);
}
void queue_remove(struct queue *queue, unsigned int index)
{
struct _q_iter it;
_q_iter_set(&queue->q_tracks, &it, index);
__queue_remove(queue, &it);
}
void queue_remove_all(struct queue *queue, struct track *track)
{
struct _q_iter it;
while (queue_at(queue, 0) == track)
queue_remove(queue, 0);
_q_for_each(&queue->q_tracks, &it) {
if (_q_iter_val(&it) == track)
__queue_remove(queue, &it);
}
}
void queue_clear(struct queue *queue)
{
unsigned int n = queue_size(queue);
_q_clear(&queue->q_tracks);
__queue_clear(queue, n);
}
void queue_updated(struct queue *queue, struct track *track)
{
struct _q_iter it;
_q_for_each(&queue->q_tracks, &it) {
if (_q_iter_val(&it) == track)
__queue_updated(queue, it.it_pos);
}
}
struct track *queue_selected(struct queue *queue, unsigned int index)
{
if (index > queue_size(queue))
return NULL;
if (queue->q_cur.it_pos != index)
_q_iter_set(&queue->q_tracks, &queue->q_cur, index);
return __queue_selected(queue, index);
}
struct track *queue_next(struct queue *queue)
{
unsigned int pos, size = queue_size(queue);
if (!queue_has_flag(queue, Q_ENABLED))
return NULL;
else if (size == 0)
return NULL;
if (size == 1)
_q_iter_set(&queue->q_tracks, &queue->q_cur, 0);
else if (queue_has_flag(queue, Q_RANDOM)) {
pos = g_random_int_range(1, (size < 15) ? size : size / 3);
pos += queue->q_cur.it_pos;
_q_iter_set(&queue->q_tracks, &queue->q_cur, pos % size);
} else {
_q_iter_next(&queue->q_cur);
if (!queue->q_cur.it_iter)
_q_iter_set(&queue->q_tracks, &queue->q_cur, 0);
}
return __queue_selected(queue, queue->q_cur.it_pos);
}
void queue_resort(struct queue *queue)
{
_q_sort(&queue->q_tracks, track_less_than, queue->q_sort);
for (unsigned int i = 0; i < queue_size(queue); i++)
__queue_updated(queue, i);
}
void queue_sort(struct queue *queue, enum compare_t sort, bool reset)
{
GSList *cur = NULL;
int field;
if (queue_has_flag(queue, Q_NO_SORT))
return;
if (reset) {
g_slist_free(queue->q_sort);
queue->q_sort = NULL;
}
cur = queue->q_sort;
while (cur) {
field = GPOINTER_TO_INT(cur->data);
if (abs(field) == sort) {
cur->data = GINT_TO_POINTER(-field);
goto out_sort;
}
cur = g_slist_next(cur);
}
queue->q_sort = g_slist_append(queue->q_sort, GINT_TO_POINTER(sort));
out_sort:
queue_resort(queue);
__queue_save(queue, Q_SAVE_SORT);
}

View File

@ -2,10 +2,10 @@
* Copyright 2015 (c) Anna Schumaker.
*/
#include <core/file.h>
#include <gui/settings.h>
#include <core/settings.h>
static GHashTable *gui_settings = NULL;
static struct file gui_settings_file = FILE_INIT("settings", 0);
static struct file gui_settings_file = FILE_INIT_DATA("", "settings", 0);
static void __settings_save_item(gpointer key, gpointer value, gpointer data)
@ -29,14 +29,16 @@ static void __settings_read()
unsigned int num, i, value;
gchar *key;
file_readf(&gui_settings_file, "%u\n", &num);
num = file_readu(&gui_settings_file);
for (i = 0; i < num; i++) {
file_readf(&gui_settings_file, "%m[^ ] %u\n", &key, &value);
key = file_readw(&gui_settings_file);
value = file_readu(&gui_settings_file);
g_hash_table_insert(gui_settings, key, GUINT_TO_POINTER(value));
}
}
void gui_settings_init()
void settings_init()
{
gui_settings = g_hash_table_new_full(g_str_hash, g_str_equal,
g_free, NULL);
@ -46,38 +48,38 @@ void gui_settings_init()
file_close(&gui_settings_file);
}
void gui_settings_deinit()
void settings_deinit()
{
g_hash_table_destroy(gui_settings);
gui_settings = NULL;
}
void gui_settings_set(const gchar *key, unsigned int value)
void settings_set(const gchar *key, unsigned int value)
{
if (gui_settings) {
if (gui_settings && key) {
g_hash_table_replace(gui_settings, g_strdup(key),
GUINT_TO_POINTER(value));
__settings_save();
}
}
unsigned int gui_settings_get(const gchar *key)
unsigned int settings_get(const gchar *key)
{
if (gui_settings)
if (gui_settings && key)
return GPOINTER_TO_UINT(g_hash_table_lookup(gui_settings, key));
return 0;
}
bool gui_settings_has(const gchar *key)
bool settings_has(const gchar *key)
{
if (gui_settings)
if (gui_settings && key)
return g_hash_table_contains(gui_settings, key);
return false;
}
#ifdef CONFIG_TESTING
GHashTable *test_get_gui_settings()
GHashTable *test_get_settings()
{
return gui_settings;
}

View File

@ -46,51 +46,60 @@ gchar *string_tm2str(struct tm *tm)
return buf;
}
static gunichar __string_get_char(const gchar *str, const gchar *cur,
const gchar *res)
int string_compare_tokens(gchar **lhs, gchar **rhs)
{
gunichar c = g_utf8_get_char(cur);
gchar *prev = g_utf8_find_prev_char(str, res);
unsigned int i, cmp;
if (g_unichar_ismark(c))
return '\0';
if (g_unichar_ispunct(c))
return '\0';
if (g_unichar_isspace(c)) {
if (!prev || (*prev == ' '))
return '\0';
return ' ';
if (!lhs[0] && rhs[0])
return 1;
if (lhs[0] && !rhs[0])
return -1;
for (i = 0; lhs[i]; i++) {
if (!rhs[i])
break;
cmp = g_utf8_collate(lhs[i], rhs[i]);
if (cmp != 0)
return cmp;
}
return g_unichar_tolower(c);
if (lhs[i])
return 1;
if (rhs[i])
return -1;
return 0;
}
gchar *string_lowercase(const gchar *str)
bool string_match_token(const gchar *prefix, gchar **tokens)
{
gchar *res = g_utf8_normalize(str, -1, G_NORMALIZE_DEFAULT);
gchar *i, *j = res;
gunichar c;
unsigned int i;
for (i = res; *i != '\0'; i = g_utf8_next_char(i)) {
c = __string_get_char(res, i, j);
if (c) {
*j = c;
j = g_utf8_next_char(j);
for (i = 0; tokens[i]; i++) {
if (g_str_has_prefix(tokens[i], prefix))
return true;
}
return false;
}
bool string_is_subdir(const gchar *a, const gchar *b)
{
gchar **parent = b ? g_strsplit(b, "/", -1) : NULL;
gchar **child = a ? g_strsplit(a, "/", -1) : NULL;
bool subdir = true;
int i;
if (!parent || !child)
return false;
for (i = 0; parent[i]; i++) {
if (!child[i] || g_utf8_collate(parent[i], child[i]) != 0) {
subdir = false;
break;
}
}
*j = '\0';
g_strchomp(res);
return g_realloc(res, strlen(res) + 1);
}
int string_compare(const gchar *lhs, const gchar *rhs)
{
if (strlen(lhs) == 0) {
if (strlen(rhs) == 0)
return 0;
return 1;
}
if (strlen(rhs) == 0)
return -1;
return g_utf8_collate(lhs, rhs);
g_strfreev(parent);
g_strfreev(child);
return subdir;
}

View File

@ -1,6 +0,0 @@
#!/usr/bin/python
Import("env")
env.UsePackage("taglib_c")
res = Glob("*.c")
Return("res")

View File

@ -1,85 +1,307 @@
/*
* Copyright 2014 (c) Anna Schumaker.
*/
#include <core/idle.h>
#include <core/string.h>
#include <core/version.h>
#include <core/tags/album.h>
#include <core/tags/genre.h>
#include <coverart/caa_c.h>
#include <musicbrainz5/mb5_c.h>
#ifdef CONFIG_TESTING
#define OCARINA_AGENT "ocarina-test"
#else
#define OCARINA_AGENT OCARINA_NAME
#endif
#define ALBUM_DB_MIN 0 /* Ocarina 6.0 */
static struct database album_db;
static bool album_db_upgraded = false;
static gchar *__album_key(const gchar *name, unsigned int year)
struct album_cache_file {
struct file ac_file;
gchar *ac_subdir;
gchar *ac_name;
};
struct album_cache_file *__album_alloc_file(struct album *al)
{
return g_strdup_printf("%u/%s", year, name);
struct album_cache_file *ret = g_malloc(sizeof(struct album_cache_file));
gchar *name = g_uri_escape_string(al->al_name, " ", true);
ret->ac_subdir = g_strdup_printf("%d", al->al_year);
ret->ac_name = g_strdup_printf("%s.jpg", name);
file_init_cache(&ret->ac_file, ret->ac_subdir, ret->ac_name);
g_free(name);
return ret;
}
static struct album *__album_alloc(gchar *name, unsigned int year)
static inline void __album_free_file(struct album_cache_file *acf)
{
g_free(acf->ac_subdir);
g_free(acf->ac_name);
g_free(acf);
}
static bool __album_fetch_cover(struct album *album, gchar *releaseid)
{
struct album_cache_file *file;
CaaImageData image;
CaaCoverArt *caa;
gchar error[256];
caa = caa_coverart_new(OCARINA_AGENT);
if (!caa)
return false;
image = caa_coverart_fetch_front(caa, releaseid);
if (!image) {
caa_coverart_get_lasterrormessage(caa, error, sizeof(error));
g_printf("Cover Art Archive: %s\n", error);
goto out;
}
file = __album_alloc_file(album);
if (file_open(&file->ac_file, OPEN_WRITE_BINARY)) {
file_write(&file->ac_file, caa_imagedata_data(image),
caa_imagedata_size(image));
file_close(&file->ac_file);
}
__album_free_file(file);
caa_imagedata_delete(image);
out:
caa_coverart_delete(caa);
return album_artwork_exists(album);
}
static bool __album_foreach_fetch(struct album *album, Mb5Metadata metadata)
{
Mb5ReleaseList list = mb5_metadata_get_releaselist(metadata);
gchar releaseid[40];
Mb5Release release;
unsigned int i;
for (i = 0; i < mb5_release_list_size(list); i++) {
release = mb5_release_list_item(list, i);
if (!release)
break;
mb5_release_get_id(release, releaseid, sizeof(releaseid));
if (__album_fetch_cover(album, releaseid))
return true;
}
return false;
}
static bool __album_run_query(struct album *album, gchar *term1,
gchar *term2, gchar *term3)
{
gchar *param, *query = "query";
Mb5Metadata data = NULL;
unsigned int code;
gchar error[256];
bool ret = false;
Mb5Query *mb5;
param = g_strjoin(" AND ", term1, term2, term3, NULL);
do {
mb5 = mb5_query_new(OCARINA_AGENT, NULL, 0);
if (!mb5)
break;
data = mb5_query_query(mb5, "release", "", "", 1, &query, &param);
code = mb5_query_get_lasthttpcode(mb5);
if (mb5_query_get_lastresult(mb5) != 0) {
mb5_query_get_lasterrormessage(mb5, error, sizeof(error));
g_printf("MusicBrainz: %s\n", error);
}
mb5_query_delete(mb5);
} while (code == 503);
if (data) {
ret = __album_foreach_fetch(album, data);
mb5_metadata_delete(data);
}
g_free(param);
return ret;
}
static bool __album_query_artist(struct album *album, struct artist *al_artist,
gchar *lower)
{
gchar *release, *artist, *year;
bool found = false;
if (!al_artist || !string_length(al_artist->ar_name) ||
strcmp(al_artist->ar_tokens[0], "various") == 0)
return false;
release = g_strdup_printf("release:\"%s\"~", lower);
artist = g_strdup_printf("artist:\"%s\"~", al_artist->ar_name);
year = g_strdup_printf("date:%d*", album->al_year);
if (album->al_year > 0)
found = __album_run_query(album, release, artist, year);
if (!found)
found = __album_run_query(album, release, artist, NULL);
if (!found && album->al_year > 0)
found = __album_run_query(album, lower, artist, year);
if (!found)
found = __album_run_query(album, lower, artist, NULL);
g_free(release);
g_free(artist);
g_free(year);
return found;
}
static bool __album_fetch_artwork(struct album *album)
{
gchar *lower;
if (album_artwork_exists(album))
return true;
if (string_length(album->al_name) == 0)
return true;
lower = g_strjoinv(" ", album->al_tokens);
if (!__album_query_artist(album, album->al_artist, lower))
__album_run_query(album, lower, NULL, NULL);
g_free(lower);
return true;
}
static gchar *__album_key(struct artist *artist, struct genre *genre,
const gchar *name, unsigned int year)
{
if (!artist || !genre)
return g_strdup_printf("%u/%s", year, name);
return g_strdup_printf("%u/%u/%u/%s", artist_index(artist),
genre_index(genre), year, name);
}
static struct album *__album_alloc(struct artist *artist, struct genre *genre,
gchar *name, unsigned int year)
{
struct album *album = g_malloc(sizeof(struct album));
dbe_init(&album->al_dbe, album);
album->al_year = year;
album->al_name = name;
album->al_lower = string_lowercase(album->al_name);
album->al_year = year;
album->al_name = name;
album->al_tokens = g_str_tokenize_and_fold(name, NULL, &album->al_alts);
album->al_artist = artist;
album->al_genre = genre;
if (!album_artwork_exists(album) && artist && genre)
idle_schedule(IDLE_ASYNC, IDLE_FUNC(__album_fetch_artwork), album);
return album;
}
static struct db_entry *album_alloc(const gchar *key)
static struct db_entry *__album_alloc_v0(const gchar *key)
{
unsigned int year;
gchar *name;
if (sscanf(key, "%u/%m[^\n]", &year, &name) == 1)
name = g_strdup("");
return &__album_alloc(name, year)->al_dbe;
return &__album_alloc(NULL, NULL, name, year)->al_dbe;
}
static struct db_entry *album_alloc(const gchar *key, unsigned int index)
{
unsigned int artist_id, genre_id, year, n;
gchar *name;
n = sscanf(key, "%u/%u/%u/%m[^\n]", &artist_id, &genre_id, &year, &name);
if (n == 1)
return __album_alloc_v0(key);
else if (n == 3)
name = g_strdup("");
return &__album_alloc(artist_get(artist_id), genre_get(genre_id),
name, year)->al_dbe;
}
static void album_free(struct db_entry *dbe)
{
g_free(ALBUM(dbe)->al_name);
g_free(ALBUM(dbe)->al_lower);
g_strfreev(ALBUM(dbe)->al_tokens);
g_strfreev(ALBUM(dbe)->al_alts);
g_free(ALBUM(dbe));
}
static gchar *album_key(struct db_entry *dbe)
{
return __album_key(ALBUM(dbe)->al_name, ALBUM(dbe)->al_year);
return __album_key(ALBUM(dbe)->al_artist, ALBUM(dbe)->al_genre,
ALBUM(dbe)->al_name, ALBUM(dbe)->al_year);
}
static struct db_entry *album_read(struct file *file)
static struct album *__album_parse_v0(gchar *line)
{
unsigned int year;
gchar *name;
if (sscanf(line, "%u %m[^\n]", &year, &name) == 1)
name = g_strdup("");
return __album_alloc(NULL, NULL, name, year);
}
static struct db_entry *album_read(struct file *file, unsigned int index)
{
unsigned int year, artist_id, genre_id, n;
struct album *album;
gchar *line, *name;
line = file_readl(file);
if (sscanf(line, "%u %m[^\n]", &year, &name) == 1)
if (file_version(file) == 0) {
album = __album_parse_v0(line);
album_db_upgraded = true;
goto out;
}
n = sscanf(line, "%u %u %u %m[^\n]", &artist_id, &genre_id, &year, &name);
if (n == 3)
name = g_strdup("");
album = __album_alloc(artist_get(artist_id),
genre_get(genre_id), name, year);
out:
g_free(line);
return &__album_alloc(name, year)->al_dbe;
return &album->al_dbe;
}
static void album_write(struct file *file, struct db_entry *dbe)
{
file_writef(file, "%u %s", ALBUM(dbe)->al_year, ALBUM(dbe)->al_name);
struct album *album = ALBUM(dbe);
struct artist *artist = album->al_artist;
struct genre *genre = album->al_genre;
file_writef(file, "%u %u %u %s", artist ? artist_index(artist) : 0,
genre ? genre_index(genre) : 0,
album->al_year, album->al_name);
}
static const struct db_ops album_ops = {
album_alloc,
album_free,
album_key,
album_read,
NULL,
album_write,
.dbe_alloc = album_alloc,
.dbe_free = album_free,
.dbe_key = album_key,
.dbe_read = album_read,
.dbe_write = album_write,
};
void album_db_init()
{
db_init(&album_db, "album.db", true, &album_ops);
db_load_idle(&album_db);
db_init(&album_db, "album.db", true, &album_ops, ALBUM_DB_MIN);
db_load(&album_db);
}
void album_db_deinit()
@ -87,9 +309,30 @@ void album_db_deinit()
db_deinit(&album_db);
}
struct album *album_find(const gchar *name, unsigned int year)
bool album_db_defrag()
{
gchar *key = __album_key(name, year);
return db_defrag(&album_db);
}
bool album_db_upgrade_done()
{
struct db_entry *dbe, *next;
if (album_db_upgraded == false)
return false;
db_for_each(dbe, next, &album_db) {
if (!ALBUM(dbe)->al_artist && !ALBUM(dbe)->al_genre)
db_remove(&album_db, dbe);
}
return true;
}
struct album *album_find(struct artist *artist, struct genre *genre,
const gchar *name, unsigned int year)
{
gchar *key = __album_key(artist, genre, name, year);
struct album *album = ALBUM(db_find(&album_db, key));
g_free(key);
return album;
@ -102,7 +345,7 @@ struct album *album_get(const unsigned int index)
int album_compare(struct album *lhs, struct album *rhs)
{
return string_compare(lhs->al_lower, rhs->al_lower);
return string_compare_tokens(lhs->al_tokens, rhs->al_tokens);
}
int album_compare_year(struct album *lhs, struct album *rhs)
@ -112,6 +355,52 @@ int album_compare_year(struct album *lhs, struct album *rhs)
return lhs->al_year - rhs->al_year;
}
bool album_match_token(struct album *album, const gchar *string)
{
return string_match_token(string, album->al_tokens) ||
string_match_token(string, album->al_alts);
}
bool album_artwork_exists(struct album *album)
{
struct album_cache_file *file;
bool ret;
file = __album_alloc_file(album);
ret = file_exists(&file->ac_file);
__album_free_file(file);
return ret;
}
gchar *album_artwork_path(struct album *album)
{
struct album_cache_file *file;
gchar *ret = NULL;
file = __album_alloc_file(album);
if (file_exists(&file->ac_file))
ret = file_path(&file->ac_file);
__album_free_file(file);
return ret;
}
bool album_artwork_import(struct album *album, gchar *path)
{
struct album_cache_file *file;
bool ret = false;
file = __album_alloc_file(album);
if (path && file_open(&file->ac_file, OPEN_WRITE_BINARY)) {
ret = file_import(&file->ac_file, path);
file_close(&file->ac_file);
}
__album_free_file(file);
return ret;
}
#ifdef CONFIG_TESTING
const struct db_ops *test_album_ops() { return &album_ops; }
#endif /* CONFIG_TESTING */

View File

@ -12,21 +12,23 @@ static struct artist *__artist_alloc(gchar *name)
struct artist *artist = g_malloc(sizeof(struct artist));
dbe_init(&artist->ar_dbe, artist);
artist->ar_name = name;
artist->ar_lower = string_lowercase(name);
artist->ar_name = name;
artist->ar_tokens = g_str_tokenize_and_fold(name, NULL, &artist->ar_alts);
artist->ar_playlist = NULL;
return artist;
}
static struct db_entry *artist_alloc(const gchar *name)
static struct db_entry *artist_alloc(const gchar *name, unsigned int index)
{
return &__artist_alloc(g_strdup(name))->ar_dbe;
}
static void artist_free(struct db_entry *dbe)
{
g_free(ARTIST(dbe)->ar_lower);
g_strfreev(ARTIST(dbe)->ar_tokens);
g_strfreev(ARTIST(dbe)->ar_alts);
g_free(ARTIST(dbe));
}
@ -35,7 +37,7 @@ static gchar *artist_key(struct db_entry *dbe)
return ARTIST(dbe)->ar_name;
}
struct db_entry *artist_read(struct file *file)
struct db_entry *artist_read(struct file *file, unsigned int index)
{
return &__artist_alloc(file_readl(file))->ar_dbe;
}
@ -47,19 +49,18 @@ static void artist_write(struct file *file, struct db_entry *dbe)
static const struct db_ops artist_ops = {
artist_alloc,
artist_free,
artist_key,
artist_read,
NULL,
artist_write,
.dbe_alloc = artist_alloc,
.dbe_free = artist_free,
.dbe_key = artist_key,
.dbe_read = artist_read,
.dbe_write = artist_write,
};
void artist_db_init()
{
db_init(&artist_db, "artist.db", true, &artist_ops);
db_load_idle(&artist_db);
db_init(&artist_db, "artist.db", true, &artist_ops, 0);
db_load(&artist_db);
}
void artist_db_deinit()
@ -67,11 +68,21 @@ void artist_db_deinit()
db_deinit(&artist_db);
}
const struct database *artist_db_get()
{
return &artist_db;
}
struct artist *artist_find(const gchar *name)
{
return ARTIST(db_find(&artist_db, name));
}
struct artist *artist_lookup(const gchar *name)
{
return ARTIST(db_get(&artist_db, name));
}
struct artist *artist_get(const unsigned int index)
{
return ARTIST(db_at(&artist_db, index));
@ -79,9 +90,14 @@ struct artist *artist_get(const unsigned int index)
int artist_compare(struct artist *lhs, struct artist *rhs)
{
return string_compare(lhs->ar_lower, rhs->ar_lower);
return string_compare_tokens(lhs->ar_tokens, rhs->ar_tokens);
}
bool artist_match_token(struct artist *artist, const gchar *string)
{
return string_match_token(string, artist->ar_tokens) ||
string_match_token(string, artist->ar_alts);
}
#ifdef CONFIG_TESTING
const struct db_ops *test_artist_ops() { return &artist_ops; }
#endif /* CONFIG_TESTING */

View File

@ -12,20 +12,21 @@ static struct genre *__genre_alloc(gchar *name)
struct genre *genre = g_malloc(sizeof(struct genre));
dbe_init(&genre->ge_dbe, genre);
genre->ge_name = name;
genre->ge_lower = string_lowercase(name);
genre->ge_name = name;
genre->ge_tokens = g_str_tokenize_and_fold(name, NULL, &genre->ge_alts);
return genre;
}
static struct db_entry *genre_alloc(const gchar *name)
static struct db_entry *genre_alloc(const gchar *name, unsigned int index)
{
return &__genre_alloc(g_strdup(name))->ge_dbe;
}
static void genre_free(struct db_entry *dbe)
{
g_free(GENRE(dbe)->ge_lower);
g_strfreev(GENRE(dbe)->ge_tokens);
g_strfreev(GENRE(dbe)->ge_alts);
g_free(GENRE(dbe));
}
@ -34,7 +35,7 @@ static gchar *genre_key(struct db_entry *dbe)
return GENRE(dbe)->ge_name;
}
static struct db_entry *genre_read(struct file *file)
static struct db_entry *genre_read(struct file *file, unsigned int index)
{
return &__genre_alloc(file_readl(file))->ge_dbe;
}
@ -46,19 +47,18 @@ static void genre_write(struct file *file, struct db_entry *dbe)
static const struct db_ops genre_ops = {
genre_alloc,
genre_free,
genre_key,
genre_read,
NULL,
genre_write,
.dbe_alloc = genre_alloc,
.dbe_free = genre_free,
.dbe_key = genre_key,
.dbe_read = genre_read,
.dbe_write = genre_write,
};
void genre_db_init()
{
db_init(&genre_db, "genre.db", true, &genre_ops);
db_load_idle(&genre_db);
db_init(&genre_db, "genre.db", true, &genre_ops, 0);
db_load(&genre_db);
}
void genre_db_deinit()
@ -78,7 +78,13 @@ struct genre *genre_get(const unsigned int index)
int genre_compare(struct genre *lhs, struct genre *rhs)
{
return string_compare(lhs->ge_lower, rhs->ge_lower);
return string_compare_tokens(lhs->ge_tokens, rhs->ge_tokens);
}
bool genre_match_token(struct genre *genre, const gchar *string)
{
return string_match_token(string, genre->ge_tokens) ||
string_match_token(string, genre->ge_alts);
}
#ifdef CONFIG_TESTING

View File

@ -1,26 +1,26 @@
/*
* Copyright 2014 (c) Anna Schumaker.
*/
#include <core/string.h>
#include <core/tags/library.h>
#define LIBRARY_DB_MIN 0 /* Ocarina 6.0 */
static struct database library_db;
static struct library *__library_alloc(gchar *path, bool enabled)
static struct library *__library_alloc(gchar *path)
{
struct library *library = g_malloc(sizeof(struct library));
dbe_init(&library->li_dbe, library);
library->li_size = 0;
library->li_enabled = enabled;
library->li_path = path;
library->li_path = path;
library->li_playlist = NULL;
return library;
}
static struct db_entry *library_alloc(const gchar *path)
static struct db_entry *library_alloc(const gchar *path, unsigned int index)
{
return &__library_alloc(g_strdup(path), true)->li_dbe;
return &__library_alloc(g_strdup(path))->li_dbe;
}
static void library_free(struct db_entry *dbe)
@ -33,36 +33,37 @@ static gchar *library_key(struct db_entry *dbe)
return LIBRARY(dbe)->li_path;
}
static struct db_entry *library_read(struct file *file)
static struct db_entry *library_read(struct file *file, unsigned int index)
{
int enabled;
gchar *path;
file_readf(file, "%d %m[^\n]", &enabled, &path);
return &__library_alloc(path, enabled)->li_dbe;
/* Old "enabled" flag */
if (file_version(file) == 0)
file_readd(file);
path = file_readl(file);
return &__library_alloc(path)->li_dbe;
}
static void library_write(struct file *file, struct db_entry *dbe)
{
file_writef(file, "%d %s", LIBRARY(dbe)->li_enabled,
LIBRARY(dbe)->li_path);
file_writef(file, "%s", LIBRARY(dbe)->li_path);
}
static const struct db_ops library_ops = {
library_alloc,
library_free,
library_key,
library_read,
NULL,
library_write,
.dbe_alloc = library_alloc,
.dbe_free = library_free,
.dbe_key = library_key,
.dbe_read = library_read,
.dbe_write = library_write,
};
void library_db_init()
{
db_init(&library_db, "library.db", true, &library_ops);
db_load_idle(&library_db);
db_init(&library_db, "library.db", true, &library_ops, LIBRARY_DB_MIN);
db_load(&library_db);
}
void library_db_deinit()
@ -70,6 +71,11 @@ void library_db_deinit()
db_deinit(&library_db);
}
bool library_db_defrag()
{
return db_defrag(&library_db);
}
const struct database *library_db_get()
{
return &library_db;
@ -77,7 +83,22 @@ const struct database *library_db_get()
struct library *library_find(const gchar *path)
{
return LIBRARY(db_find(&library_db, path));
struct library *library = library_lookup(path);
if (library)
return library;
return LIBRARY(db_insert(&library_db, path));
}
struct library *library_lookup(const gchar *path)
{
struct db_entry *dbe, *next;
db_for_each(dbe, next, &library_db) {
if (string_is_subdir(path, LIBRARY(dbe)->li_path))
return LIBRARY(dbe);
}
return NULL;
}
struct library *library_get(const unsigned int index)
@ -91,12 +112,6 @@ void library_remove(struct library *library)
db_remove(&library_db, &library->li_dbe);
}
void library_set_enabled(struct library *library, bool enabled)
{
library->li_enabled = enabled;
db_save(&library_db);
}
gchar *library_file(struct library *library, const gchar *path)
{
return g_strdup_printf("%s/%s", library->li_path, path);

View File

@ -1,6 +1,7 @@
/*
* Copyright 2014 (c) Anna Schumaker.
*/
#include <core/idle.h>
#include <core/tags/album.h>
#include <core/tags/artist.h>
#include <core/tags/genre.h>
@ -8,13 +9,22 @@
#include <core/tags/tags.h>
#include <core/tags/track.h>
static bool tags_upgrade_done(void *data)
{
if (album_db_upgrade_done())
track_db_commit();
return true;
}
void tags_init()
{
album_db_init();
artist_db_init();
genre_db_init();
album_db_init();
library_db_init();
track_db_init();
idle_schedule(IDLE_SYNC, tags_upgrade_done, NULL);
}
void tags_deinit()
@ -22,6 +32,19 @@ void tags_deinit()
track_db_deinit();
library_db_deinit();
genre_db_deinit();
artist_db_deinit();
album_db_deinit();
artist_db_deinit();
}
bool tags_defragment(void *data)
{
bool album = album_db_defrag();
bool library = library_db_defrag();
bool track = track_db_defrag();
if (library)
track_db_rekey();
if (album || library || track)
track_db_commit();
return track;
}

View File

@ -1,14 +1,13 @@
/*
* Copyright 2014 (c) Anna Schumaker.
*/
#include <core/filter.h>
#include <core/string.h>
#include <core/tags/track.h>
#include <glib.h>
#include <taglib/tag_c.h>
#define TRACK_DB_MIN 0 /* Ocarina 6.0 */
static struct database track_db;
static unsigned int unplayed_count = 0;
static unsigned int play_count = 0;
@ -18,7 +17,7 @@ static gchar *__track_key(struct library *library, gchar *path)
gchar *res;
if (library)
res = g_strdup_printf("%u/%s", library->li_dbe.dbe_index, path);
res = g_strdup_printf("%u/%s", library_index(library), path);
else
res = g_strdup("");
@ -42,48 +41,70 @@ static struct track *__track_alloc()
return track;
}
struct db_entry *track_alloc(const gchar *key)
static struct track *__track_alloc_filepath(const gchar *filepath)
{
TagLib_File *file = taglib_file_new(filepath);
const TagLib_AudioProperties *audio;
struct library *library;
struct track *track = NULL;
unsigned int lib_id;
TagLib_File *file;
TagLib_Tag *tag;
char *fullpath, *path;
struct artist *artist;
struct genre *genre;
TagLib_Tag *tag;
sscanf(key, "%u/%m[^\n]", &lib_id, &path);
library = library_get(lib_id);
fullpath = library_file(library, path);
file = taglib_file_new(fullpath);
if (!file || !taglib_file_is_valid(file)) {
printf("WARNING: Could not read tags for: %s\n", fullpath);
goto out;
g_printerr("WARNING: Could not read tags for: %s\n", filepath);
return NULL;
}
track = __track_alloc();
tag = taglib_file_tag(file);
audio = taglib_file_audioproperties(file);
track = __track_alloc();
tag = taglib_file_tag(file);
audio = taglib_file_audioproperties(file);
artist = artist_find(taglib_tag_artist(tag));
genre = genre_find(taglib_tag_genre(tag));
track->tr_album = album_find(taglib_tag_album(tag), taglib_tag_year(tag));
track->tr_artist = artist_find(taglib_tag_artist(tag));
track->tr_genre = genre_find(taglib_tag_genre(tag));
track->tr_library = library;
track->tr_album = album_find(artist, genre, taglib_tag_album(tag),
taglib_tag_year(tag));
unplayed_count++;
track->tr_count = 0;
track->tr_length = taglib_audioproperties_length(audio);
track->tr_track = taglib_tag_track(tag);
date_set(&track->tr_date, 0, 0, 0);
track->tr_path = g_strdup(key);
track->tr_title = g_strdup(taglib_tag_title(tag));
track->tr_lower = string_lowercase(track->tr_title);
track->tr_title = g_strdup(taglib_tag_title(tag));
track->tr_tokens = g_str_tokenize_and_fold(track->tr_title, NULL,
&track->tr_alts);
taglib_tag_free_strings();
taglib_file_free(file);
out:
return track;
}
static void __track_free(struct track *track)
{
g_strfreev(track->tr_tokens);
g_strfreev(track->tr_alts);
g_free(track->tr_title);
g_free(track);
}
struct db_entry *track_alloc(const gchar *key, unsigned int index)
{
struct library *library;
char *fullpath, *path;
struct track *track;
unsigned int lib_id;
sscanf(key, "%u/%m[^\n]", &lib_id, &path);
library = library_get(lib_id);
fullpath = library_file(library, path);
track = __track_alloc_filepath(fullpath);
if (track) {
track->tr_library = library;
track->tr_path = g_strdup(key);
unplayed_count++;
}
g_free(path);
g_free(fullpath);
return track ? &track->tr_dbe : NULL;
@ -96,22 +117,8 @@ static void track_free(struct db_entry *dbe)
play_count -= track->tr_count;
if (track->tr_count == 0)
unplayed_count--;
if (track->tr_library)
track->tr_library->li_size--;
g_free(track->tr_title);
g_free(track->tr_lower);
g_free(track);
}
static void track_setup(struct db_entry *dbe)
{
struct track *track = TRACK(dbe);
filter_add(track->tr_lower, dbe->dbe_index);
filter_add(track->tr_artist->ar_lower, dbe->dbe_index);
filter_add(track->tr_album->al_lower, dbe->dbe_index);
track->tr_library->li_size++;
__track_free(track);
}
static gchar *track_key(struct db_entry *dbe)
@ -119,29 +126,48 @@ static gchar *track_key(struct db_entry *dbe)
return TRACK(dbe)->tr_path;
}
static struct db_entry *track_read(struct file *file)
static void track_read_v0(struct file *file, struct track *track)
{
struct artist *artist = artist_get(file_readu(file));
struct album *album = album_get( file_readu(file));
struct genre *genre = genre_get( file_readu(file));
track->tr_track = file_readhu(file);
date_read(file, &track->tr_date);
if (album->al_artist != artist || album->al_genre != genre)
album = album_find(artist, genre, album->al_name, album->al_year);
track->tr_album = album;
}
static struct db_entry *track_read(struct file *file, unsigned int index)
{
unsigned int library_id, artist_id, album_id, genre_id;
struct track *track = __track_alloc();
file_readf(file, "%u %u %u %u %u", &library_id, &artist_id, &album_id,
&genre_id, &track->tr_track);
date_read(file, &track->tr_date);
file_readf(file, "%u %u", &track->tr_count, &track->tr_length);
track->tr_library = library_get(file_readu(file));
if (file_version(file) == 0)
track_read_v0(file, track);
else {
track->tr_album = album_get(file_readu(file));
track->tr_track = file_readhu(file);
date_read_stamp(file, &track->tr_date);
}
track->tr_count = file_readhu(file);
track->tr_length = file_readhu(file);
play_count += track->tr_count;
if (track->tr_count == 0)
unplayed_count++;
track->tr_album = album_get(album_id);
track->tr_artist = artist_get(artist_id);
track->tr_genre = genre_get(genre_id);
track->tr_library = library_get(library_id);
track->tr_title = file_readl(file);
track->tr_lower = string_lowercase(track->tr_title);
track->tr_title = file_readl(file);
track->tr_tokens = g_str_tokenize_and_fold(track->tr_title, NULL,
&track->tr_alts);
track->tr_path = __track_key(track->tr_library, file_readl(file));
return &track->tr_dbe;
}
static void track_write(struct file *file, struct db_entry *dbe)
@ -149,13 +175,11 @@ static void track_write(struct file *file, struct db_entry *dbe)
struct track *track = TRACK(dbe);
gchar *path = __track_path(track);
file_writef(file, "%u %u %u %u %u ", track->tr_library->li_dbe.dbe_index,
track->tr_artist->ar_dbe.dbe_index,
track->tr_album->al_dbe.dbe_index,
track->tr_genre->ge_dbe.dbe_index,
track->tr_track);
date_write(file, &track->tr_date);
file_writef(file, " %u %u %s\n%s\n", track->tr_count,
file_writef(file, "%u %u %hu ", library_index(track->tr_library),
album_index(track->tr_album),
track->tr_track);
date_write_stamp(file, &track->tr_date);
file_writef(file, " %hu %hu %s\n%s", track->tr_count,
track->tr_length,
track->tr_title,
path);
@ -165,19 +189,18 @@ static void track_write(struct file *file, struct db_entry *dbe)
static const struct db_ops track_ops = {
track_alloc,
track_free,
track_key,
track_read,
track_setup,
track_write,
.dbe_alloc = track_alloc,
.dbe_free = track_free,
.dbe_key = track_key,
.dbe_read = track_read,
.dbe_write = track_write,
};
void track_db_init()
{
db_init(&track_db, "track.db", false, &track_ops);
db_load_idle(&track_db);
db_init(&track_db, "track.db", false, &track_ops, TRACK_DB_MIN);
db_load(&track_db);
}
void track_db_deinit()
@ -190,6 +213,30 @@ void track_db_commit()
db_save(&track_db);
}
bool track_db_defrag()
{
return db_defrag(&track_db);
}
void track_db_rekey()
{
struct db_entry *dbe, *next;
struct track *track;
unsigned int lib;
gchar *path;
db_for_each(dbe, next, &track_db) {
track = TRACK(dbe);
sscanf(track->tr_path, "%u/%m[^\n]", &lib, &path);
if (lib != library_index(track->tr_library)) {
track->tr_path = __track_key(track->tr_library, path);
db_rekey(&track_db, dbe);
} else
g_free(path);
}
}
const struct database *track_db_get()
{
return &track_db;
@ -212,6 +259,24 @@ unsigned int track_db_average_plays()
return play_count / (track_db.db_size - unplayed_count);
}
struct track *track_alloc_external(const gchar *filepath)
{
struct track *track = __track_alloc_filepath(filepath);
if (track) {
track->tr_library = NULL;
track->tr_path = g_strdup(filepath);
}
return track;
}
void track_free_external(struct track *track)
{
if (TRACK_IS_EXTERNAL(track)) {
g_free(track->tr_path);
__track_free(track);
}
}
struct track *track_add(struct library *library, const gchar *filepath)
{
unsigned int offset = strlen(library->li_path) + 1;
@ -246,23 +311,42 @@ struct track *track_get(const unsigned int index)
return TRACK(db_at(&track_db, index));
}
struct track *track_lookup(const gchar *filepath)
{
struct library *library = library_lookup(filepath);
unsigned int offset;
struct track *track;
gchar *key;
if (!library)
return NULL;
offset = strlen(library->li_path) + 1;
key = __track_key(library, g_strdup(filepath + offset));
track = TRACK(db_get(&track_db, key));
g_free(key);
return track;
}
int track_compare(struct track *lhs, struct track *rhs, enum compare_t compare)
{
switch (compare) {
case COMPARE_ARTIST:
return artist_compare(lhs->tr_artist, rhs->tr_artist);
return artist_compare(lhs->tr_album->al_artist,
rhs->tr_album->al_artist);
case COMPARE_ALBUM:
return album_compare(lhs->tr_album, rhs->tr_album);
case COMPARE_COUNT:
return lhs->tr_count - rhs->tr_count;
case COMPARE_GENRE:
return genre_compare(lhs->tr_genre, rhs->tr_genre);
return genre_compare(lhs->tr_album->al_genre,
rhs->tr_album->al_genre);
case COMPARE_LENGTH:
return lhs->tr_length - rhs->tr_length;
case COMPARE_PLAYED:
return date_compare(&lhs->tr_date, &rhs->tr_date);
case COMPARE_TITLE:
return string_compare(lhs->tr_lower, rhs->tr_lower);
return string_compare_tokens(lhs->tr_tokens, rhs->tr_tokens);
case COMPARE_TRACK:
return lhs->tr_track - rhs->tr_track;
case COMPARE_YEAR:
@ -272,6 +356,12 @@ int track_compare(struct track *lhs, struct track *rhs, enum compare_t compare)
return 0; /* We should never get here. */
}
bool track_match_token(struct track *track, const gchar *token)
{
return string_match_token(token, track->tr_tokens) ||
string_match_token(token, track->tr_alts);
}
gchar *track_path(struct track *track)
{
gchar *path, *res;
@ -282,11 +372,13 @@ gchar *track_path(struct track *track)
g_free(path);
return res;
}
return g_strdup("");
return g_strdup(track->tr_path);
}
void track_played(struct track *track)
{
if (TRACK_IS_EXTERNAL(track))
return;
if (track->tr_count == 0)
unplayed_count--;
track->tr_count++;

View File

@ -1,167 +0,0 @@
/*
* Copyright 2013 (c) Anna Schumaker.
*/
#include <core/collection.h>
#include <core/file.h>
#include <core/history.h>
#include <core/idle.h>
#include <core/tempq.h>
#include <glib.h>
static struct file tempq_file = FILE_INIT("deck", 1);
static struct queue_ops *tempq_ops = NULL;
static GSList *tempq_list;
static struct file tempq_file;
static struct queue *__tempq_alloc(unsigned int flags)
{
struct queue *queue = g_malloc(sizeof(struct queue));
flags = flags | Q_SAVE_FLAGS | Q_SAVE_SORT;
tempq_list = g_slist_append(tempq_list, queue);
queue_init(queue, flags, tempq_ops);
return queue;
}
static void __tempq_free(struct queue *queue)
{
tempq_list = g_slist_remove(tempq_list, queue);
queue_deinit(queue);
g_free(queue);
}
static void __tempq_read_queue()
{
unsigned int flags, count, i, track;
struct queue *queue;
file_readf(&tempq_file, "%u %u", &flags, &count);
queue = __tempq_alloc(flags);
for (i = 0; i < count; i++) {
file_readf(&tempq_file, "%u", &track);
queue_add(queue, track_get(track));
}
}
static void __tempq_write_queue(struct queue *queue)
{
struct _q_iter it;
struct track *track;
file_writef(&tempq_file, "%u %u", queue->q_flags, queue_size(queue));
_q_for_each(&queue->q_tracks, &it) {
track = (struct track *)_q_iter_val(&it);
file_writef(&tempq_file, " %u", track->tr_dbe.dbe_index);
}
file_writef(&tempq_file, "\n");
}
static void __tempq_init_idle(void *data)
{
unsigned int num, i;
if (!file_open(&tempq_file, OPEN_READ))
return;
if (file_version(&tempq_file) < 1)
goto out;
file_readf(&tempq_file, "%u", &num);
for (i = 0; i < num; i++)
__tempq_read_queue();
out:
file_close(&tempq_file);
}
void tempq_init(struct queue_ops *ops)
{
tempq_ops = ops;
idle_schedule(__tempq_init_idle, NULL);
}
void tempq_deinit()
{
while (tempq_list)
__tempq_free((struct queue *)tempq_list->data);
}
void tempq_save(struct queue *queue, enum queue_flags flag)
{
GSList *cur;
if (!file_open(&tempq_file, OPEN_WRITE))
return;
file_writef(&tempq_file, "%zu\n", g_slist_length(tempq_list));
for (cur = tempq_list; cur; cur = g_slist_next(cur))
__tempq_write_queue((struct queue *)cur->data);
file_close(&tempq_file);
}
struct queue *tempq_alloc(unsigned int flags)
{
struct queue *queue = __tempq_alloc(Q_ENABLED | flags);
tempq_save(queue, Q_ENABLED);
return queue;
}
void tempq_free(struct queue *queue)
{
__tempq_free(queue);
tempq_save(NULL, Q_ENABLED);
}
struct queue *tempq_get(unsigned int index)
{
return (struct queue *)g_slist_nth_data(tempq_list, index);
}
unsigned int tempq_index(struct queue *queue)
{
return g_slist_index(tempq_list, queue);
}
void tempq_move(struct queue *queue, unsigned int index)
{
GSList *cur = g_slist_find(tempq_list, queue);
GSList *nth = g_slist_nth(tempq_list, index);
if (cur && nth && (cur != nth)) {
tempq_list = g_slist_remove(tempq_list, queue);
tempq_list = g_slist_insert_before(tempq_list, nth, queue);
tempq_save(queue, Q_ENABLED);
}
}
struct track *tempq_next()
{
struct track *track = NULL;
struct queue *queue = NULL;
GSList *cur;
for (cur = tempq_list; cur; cur = g_slist_next(cur)) {
queue = (struct queue *)cur->data;
if (queue_has_flag(queue, Q_ENABLED)) {
track = queue_next(queue);
break;
}
}
if (queue && queue_size(queue) == 0)
tempq_free(queue);
return track;
}
unsigned int tempq_count()
{
return g_slist_length(tempq_list);
}
void tempq_updated(struct track *track)
{
GSList *cur;
for (cur = tempq_list; cur; cur = g_slist_next(cur))
queue_updated((struct queue *)cur->data, track);
}

View File

@ -1,8 +0,0 @@
#!/usr/bin/python
Import("env")
env.UsePackage("gtk+-3.0")
env.UsePackage("gmodule-export-2.0");
res = Glob("*.c")
Return("res")

147
gui/artwork.c Normal file
View File

@ -0,0 +1,147 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#include <core/audio.h>
#include <gui/artwork.h>
#include <gui/window.h>
#include <glib/gi18n.h>
#define ARTWORK_PREVIEW_SIZE 150
static struct album *artwork_current = NULL;
static unsigned int artwork_timeout = 0;
static cairo_surface_t *__gui_artwork_scale(cairo_surface_t *orig, int new_h)
{
int old_h = cairo_image_surface_get_height(orig);
int old_w = cairo_image_surface_get_width(orig);
int new_w = (old_w * new_h) / old_h;
int scale = gtk_widget_get_scale_factor(GTK_WIDGET(gui_artwork()));
cairo_content_t content = cairo_surface_get_content(orig);
cairo_surface_t *scaled = cairo_surface_create_similar(orig, content,
new_w, new_h);
cairo_t *cairo = cairo_create(scaled);
cairo_scale(cairo, (double)(scale * new_w) / old_w,
(double)(scale * new_h) / old_h);
cairo_set_source_surface(cairo, orig, 0, 0);
cairo_paint(cairo);
cairo_destroy(cairo);
return scaled;
}
static cairo_surface_t *__gui_artwork_get(gchar *path, int new_h)
{
GdkPixbuf *pixbuf = path ? gdk_pixbuf_new_from_file(path, NULL) : NULL;
cairo_surface_t *surface = NULL;
cairo_surface_t *scaled = NULL;
if (!pixbuf)
return NULL;
surface = gdk_cairo_surface_create_from_pixbuf(pixbuf, 0, NULL);
if (surface) {
scaled = __gui_artwork_scale(surface, new_h);
cairo_surface_destroy(surface);
}
g_object_unref(G_OBJECT(pixbuf));
return scaled;
}
static bool __gui_artwork_set_path(GtkImage *image, gchar *path, int new_h)
{
cairo_surface_t *surface = __gui_artwork_get(path, new_h);
bool status = surface != NULL;
if (surface) {
gtk_image_set_from_surface(image, surface);
cairo_surface_destroy(surface);
} else
gtk_image_set_from_icon_name(image, "image-missing",
GTK_ICON_SIZE_DIALOG);
return status;
}
static gboolean __gui_artwork_timeout(gpointer data)
{
struct track *track = audio_cur_track();
int height = gui_builder_widget_height("artwork");
gchar *path;
bool status;
if (!track || track->tr_album == artwork_current)
return G_SOURCE_REMOVE;
path = album_artwork_path(track->tr_album);
status = __gui_artwork_set_path(gui_artwork(), path, height);
gtk_widget_set_sensitive(GTK_WIDGET(gui_artwork()), status);
artwork_current = status ? track->tr_album : NULL;
artwork_timeout = status ? artwork_timeout : 0;
g_free(path);
return status ? G_SOURCE_CONTINUE : G_SOURCE_REMOVE;
}
void __gui_artwork_update_preview(GtkFileChooser *chooser, gpointer data)
{
GtkWidget *preview = gtk_file_chooser_get_preview_widget(chooser);
gchar *path = gtk_file_chooser_get_preview_filename(chooser);
__gui_artwork_set_path(GTK_IMAGE(preview), path, ARTWORK_PREVIEW_SIZE);
g_free(path);
}
void __gui_artwork_select_cover(GtkButton *button)
{
struct track *track = audio_cur_track();
GtkWidget *preview, *dialog;
GtkFileFilter *filter;
gchar *path;
if (!track)
return;
filter = gtk_file_filter_new();
preview = gtk_image_new_from_icon_name("", GTK_ICON_SIZE_DIALOG);
dialog = gtk_file_chooser_dialog_new("Choose an image", gui_window(),
GTK_FILE_CHOOSER_ACTION_OPEN,
_("_Cancel"), GTK_RESPONSE_CANCEL,
_("_Open"), GTK_RESPONSE_ACCEPT,
NULL);
gtk_file_filter_add_mime_type(filter, "image/*");
gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(dialog), filter);
gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(dialog), true);
gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(dialog), preview);
gtk_file_chooser_set_use_preview_label(GTK_FILE_CHOOSER(dialog), false);
g_signal_connect(dialog, "update-preview",
(GCallback)__gui_artwork_update_preview, NULL);
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
gui_artwork_import(track, path);
g_free(path);
}
gtk_widget_destroy(dialog);
}
void gui_artwork_set_cover(void)
{
if (__gui_artwork_timeout(NULL) != G_SOURCE_CONTINUE)
return;
artwork_timeout = g_timeout_add(2000, __gui_artwork_timeout, NULL);
}
void gui_artwork_import(struct track *track, gchar *path)
{
album_artwork_import(track->tr_album, path);
if (track == audio_cur_track()) {
artwork_current = NULL;
gui_artwork_set_cover();
}
}

View File

@ -1,126 +1,205 @@
/*
* Copyright 2014 (c) Anna Schumaker.
*/
#include <core/audio.h>
#include <core/collection.h>
#include <core/playlist.h>
#include <core/string.h>
#include <gui/artwork.h>
#include <gui/audio.h>
#include <gui/builder.h>
#include <gui/view.h>
#include <gui/idle.h>
#include <gui/playlist.h>
#include <gui/treeview.h>
#include <gui/window.h>
static guint audio_timeout = 0;
static guint popover_timeout = 0;
static inline void __audio_set_label(const gchar *label, const gchar *size,
const gchar *text)
static inline void __gui_audio_set_label_markup(GtkLabel *label,
const gchar *size,
const gchar *text)
{
gchar *markup = g_markup_printf_escaped("<span size='%s'>%s</span>",
size, text);
gtk_label_set_markup(GTK_LABEL(gui_builder_widget(label)), markup);
const gchar *fmt = "<span size='%s'>%s</span>";
gchar *markup = g_markup_printf_escaped(fmt, size, text);
gtk_label_set_markup(label, markup);
g_free(markup);
}
static inline void __audio_set_time_label(const gchar *label, unsigned int time)
static void __gui_audio_load(struct track *track)
{
gchar *str = string_sec2str(time);
__audio_set_label(label, "large", str);
g_free(str);
gchar *duration = string_sec2str(track->tr_length);
__gui_audio_set_label_markup(gui_title_tag(), "xx-large",
track->tr_title);
__gui_audio_set_label_markup(gui_album_tag(), "x-large",
track->tr_album->al_name);
__gui_audio_set_label_markup(gui_artist_tag(), "x-large",
track->tr_album->al_artist->ar_name);
__gui_audio_set_label_markup(gui_duration(), "large", duration);
__gui_audio_set_label_markup(gui_position(), "large", "0:00");
gtk_adjustment_set_upper(gui_seek(), track->tr_length);
gui_pl_system_track_loaded(track);
gui_treeview_scroll();
gui_artwork_set_cover();
gui_idle_enable();
g_free(duration);
}
static void __audio_load(struct track *track)
static void __gui_audio_set_pause_text(int n, GstState state)
{
__audio_set_label("o_title", "xx-large", track->tr_title);
__audio_set_label("o_artist", "x-large", track->tr_artist->ar_name);
__audio_set_label("o_album", "x-large", track->tr_album->al_name);
__audio_set_time_label("o_duration", track->tr_length);
bool sensitive = true;
gchar *text;
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gui_builder_widget("o_hide")),
playlist_has(PL_HIDDEN, track));
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gui_builder_widget("o_favorite")),
playlist_has(PL_FAVORITED, track));
gui_view_scroll();
if (n == -1) {
sensitive = false;
if (state == GST_STATE_PLAYING)
text = g_strdup("Keep playing");
else
text = g_strdup("Paused");
} else if (n == 0)
text = g_strdup("Pause after this track");
else if (n == 1)
text = g_strdup("Pause after next track");
else
text = g_strdup_printf("Pause after %d tracks", n);
gtk_widget_set_sensitive(GTK_WIDGET(gui_pause_down()), sensitive);
gtk_entry_set_text(gui_pause_entry(), text);
g_free(text);
}
static void __audio_change_state(GstState state)
static void __gui_audio_change_state(GstState state)
{
if (state == GST_STATE_PLAYING) {
gtk_widget_hide(gui_builder_widget("o_play"));
gtk_widget_show(gui_builder_widget("o_pause"));
} else {
gtk_widget_show(gui_builder_widget("o_play"));
gtk_widget_hide(gui_builder_widget("o_pause"));
bool playing = (state == GST_STATE_PLAYING);
gtk_widget_set_visible(GTK_WIDGET(gui_play_button()), !playing);
gtk_widget_set_visible(GTK_WIDGET(gui_pause_button()), playing);
__gui_audio_set_pause_text(audio_get_pause_count(), state);
}
static void __gui_audio_config_pause(int n)
{
__gui_audio_set_pause_text(n, audio_cur_state());
}
struct audio_callbacks audio_cb = {
.audio_cb_load = __gui_audio_load,
.audio_cb_state_change = __gui_audio_change_state,
.audio_cb_config_pause = __gui_audio_config_pause,
};
void __gui_audio_pause(GtkButton *button, gpointer data)
{
audio_pause();
if (audio_get_pause_count() > -1) {
gtk_popover_popup(gui_pause_popover());
popover_timeout = g_timeout_add_seconds(10,
gui_audio_popover_timeout, NULL);
}
}
static void __audio_config_pause(int n)
void __gui_audio_pause_change_text(GtkEntry *entry, gpointer data)
{
GtkComboBox *combo = GTK_COMBO_BOX(gui_builder_widget("o_pause_after"));
gtk_combo_box_set_active(combo, n + 1);
const gchar *text = gtk_entry_get_text(entry);
int n = audio_get_pause_count();
unsigned int i;
if (g_str_match_string("Keep", text, true))
n = -1;
else if (g_str_match_string("This", text, true))
n = 0;
else if (g_str_match_string("Next", text, true))
n = 1;
else {
for (i = 0; text[i] != '\0'; i++) {
if (!g_ascii_isdigit(text[i]))
continue;
if (i > 0 && text[i-1] == '-')
i -= 1;
n = g_strtod(text + i, NULL);
break;
}
}
if (!audio_pause_after(n))
__gui_audio_set_pause_text(audio_get_pause_count(), audio_cur_state());
}
void __audio_pause_changed(GtkComboBox *combo, gpointer data)
void __gui_audio_pause_inc(GtkButton *button, gpointer data)
{
int val = gtk_combo_box_get_active(combo) - 1;
audio_pause_after(val);
audio_pause_after(audio_get_pause_count() + 1);
}
void __audio_seek(GtkRange *range, GtkScrollType type, double value, gpointer data)
void __gui_audio_pause_dec(GtkButton *button, gpointer data)
{
audio_pause_after(audio_get_pause_count() - 1);
}
void __gui_audio_pause_popover_popdown(GtkButton *button, gpointer data)
{
gtk_popover_popdown(gui_pause_popover());
#ifdef CONFIG_TESTING
gtk_widget_hide(GTK_WIDGET(gui_pause_popover()));
#endif /* CONFIG_TESTING */
g_source_remove(popover_timeout);
popover_timeout = 0;
}
void __gui_audio_pause_popover_clear(GtkButton *button, gpointer data)
{
audio_pause_after(-1);
__gui_audio_pause_popover_popdown(button, data);
}
void __gui_audio_seek(GtkRange *range, GtkScrollType type,
double value, gpointer data)
{
audio_seek(value * GST_SECOND);
}
void __audio_favorite(GtkToggleButton *toggle, gpointer data)
void __gui_audio_volume_changed(GtkScaleButton *button, gdouble value,
gpointer data)
{
if (gtk_toggle_button_get_active(toggle))
playlist_add(PL_FAVORITED, audio_cur_track());
else
playlist_remove(PL_FAVORITED, audio_cur_track());
audio_set_volume((unsigned int)value);
}
void __audio_hide(GtkToggleButton *toggle, gpointer data)
gboolean __gui_audio_can_accel(GtkWidget *widget, guint signal_id)
{
if (gtk_toggle_button_get_active(toggle)) {
if (collection_ban(audio_cur_track()))
audio_next();
} else
collection_unban(audio_cur_track());
}
static int __audio_timeout(gpointer data)
{
GtkAdjustment *progress = data;
gtk_adjustment_set_upper(progress, audio_duration() / GST_SECOND);
gtk_adjustment_set_value(progress, audio_position() / GST_SECOND);
__audio_set_time_label("o_position", audio_position() / GST_SECOND);
return G_SOURCE_CONTINUE;
}
gboolean __audio_can_accel(GtkWidget *widget, guint signal_id)
{
GtkWindow *window = GTK_WINDOW(gui_builder_widget("o_window"));
g_signal_stop_emission_by_name(widget, "can-activate-accel");
return !GTK_IS_ENTRY(gtk_window_get_focus(window)) &&
return !GTK_IS_ENTRY(gtk_window_get_focus(gui_window())) &&
gtk_widget_is_visible(widget) &&
gtk_widget_is_sensitive(widget);
}
struct audio_ops audio_ops = {
__audio_load,
__audio_change_state,
__audio_config_pause,
};
void gui_audio_init()
{
g_timeout_add(500, __audio_timeout, gui_builder_object("o_progress"));
gtk_scale_button_set_value(gui_volume_button(), audio_get_volume());
gtk_button_set_relief(GTK_BUTTON(gui_volume_button()), GTK_RELIEF_NORMAL);
audio_timeout = g_timeout_add(500, gui_audio_timeout, NULL);
}
#ifdef CONFIG_TESTING
void test_gui_audio_timeout()
void gui_audio_deinit()
{
__audio_timeout(gui_builder_object("o_progress"));
g_source_remove(audio_timeout);
if (popover_timeout > 0)
g_source_remove(popover_timeout);
}
int gui_audio_timeout(gpointer data)
{
gchar *position = string_sec2str(audio_position() / GST_SECOND);
gtk_adjustment_set_upper(gui_seek(), audio_duration() / GST_SECOND);
gtk_adjustment_set_value(gui_seek(), audio_position() / GST_SECOND);
__gui_audio_set_label_markup(gui_position(), "large", position);
g_free(position);
return G_SOURCE_CONTINUE;
}
int gui_audio_popover_timeout(gpointer data)
{
__gui_audio_pause_popover_popdown(NULL, data);
return G_SOURCE_REMOVE;
}
#endif /* CONFIG_TESTING */

View File

@ -29,6 +29,11 @@ GtkWidget *gui_builder_widget(const char *name)
return GTK_WIDGET(gui_builder_object(name));
}
int gui_builder_widget_height(const char *name)
{
return gtk_widget_get_allocated_height(gui_builder_widget(name));
}
#ifdef CONFIG_TESTING
GtkBuilder *test_get_gui_builder()
{

View File

@ -1,248 +0,0 @@
/*
* Copyright 2015 (c) Anna Schumaker.
*/
#include <core/collection.h>
#include <core/idle.h>
#include <core/playlist.h>
#include <core/tempq.h>
#include <gui/builder.h>
#include <gui/collection.h>
#include <gui/idle.h>
#include <gui/sidebar.h>
enum collection_sidebar_columns {
C_SB_IMAGE,
C_SB_PATH,
C_SB_LIBRARY,
};
static GtkTreeModel *c_model;
static void __collection_set_header(GtkTreeIter *iter, const gchar *image,
const gchar *text)
{
gtk_tree_store_set(GTK_TREE_STORE(c_model), iter, C_SB_IMAGE, image,
C_SB_PATH, text, -1);
}
static void __collection_set_library(GtkTreeIter *iter, struct library *library)
{
gtk_tree_store_set(GTK_TREE_STORE(c_model), iter,
C_SB_IMAGE, "folder",
C_SB_PATH, library->li_path,
C_SB_LIBRARY, library, -1);
}
static struct library *__collection_get_library(GtkTreeIter *iter)
{
struct library *library;
gtk_tree_model_get(c_model, iter, C_SB_LIBRARY, &library, -1);
return library;
}
void __collection_activated(GtkTreeView *treeview, GtkTreePath *path,
GtkTreeViewColumn *column, gpointer data)
{
struct library *library = NULL;
GtkTreeIter iter;
if (gtk_tree_model_get_iter(c_model, &iter, path))
library = __collection_get_library(&iter);
if (!library)
return;
collection_update(library);
gui_idle_enable();
}
void __collection_toggled(GtkCheckMenuItem *check, gpointer data)
{
GtkTreeView *treeview = GTK_TREE_VIEW(gui_builder_widget("o_collection_view"));
GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
struct library *library = NULL;
GtkTreePath *path;
GtkTreeIter iter;
GList *rows;
rows = gtk_tree_selection_get_selected_rows(selection, &c_model);
if (!rows)
return;
path = rows->data;
if (gtk_tree_model_get_iter(c_model, &iter, path))
library = __collection_get_library(&iter);
if (!library)
return;
collection_set_enabled(library, gtk_check_menu_item_get_active(check));
__collection_set_library(&iter, library);
}
bool __collection_keypress(GtkTreeView *treeview, GdkEventKey *event,
gpointer data)
{
GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
struct library *library = NULL;
GtkTreePath *path;
GtkTreeIter iter;
GList *rows;
if (event->keyval != GDK_KEY_Delete)
return false;
rows = gtk_tree_selection_get_selected_rows(selection, &c_model);
path = rows->data;
if (gtk_tree_model_get_iter(c_model, &iter, path))
library = __collection_get_library(&iter);
if (!library)
goto out;
collection_remove(library);
gtk_tree_store_remove(GTK_TREE_STORE(c_model), &iter);
out:
g_list_free_full(rows, (GDestroyNotify)gtk_tree_path_free);
return true;
}
bool __collection_buttonpress(GtkWidget *widget, GdkEventButton *event, gpointer data)
{
GtkCheckMenuItem *check = GTK_CHECK_MENU_ITEM(gui_builder_widget("o_collection_enabled"));
GtkTreeView *treeview = GTK_TREE_VIEW(gui_builder_widget("o_collection_view"));
GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
GtkMenu *menu = GTK_MENU(gui_builder_widget("o_collection_rc"));
struct library *library = NULL;
GtkTreePath *path;
GtkTreeIter iter;
if (event->button != 3)
return false;
if (gtk_tree_view_get_path_at_pos(treeview, event->x, event->y,
&path, NULL, NULL, NULL))
gtk_tree_selection_select_path(selection, path);
if (gtk_tree_model_get_iter(c_model, &iter, path))
library = __collection_get_library(&iter);
if (library) {
gtk_check_menu_item_set_active(check, library->li_enabled);
gtk_menu_popup(menu, NULL, NULL, NULL, NULL,
event->button, event->time);
}
gtk_tree_path_free(path);
return true;
}
void __collection_add(GtkButton *button, GtkFileChooser *chooser)
{
gchar *filename = gtk_file_chooser_get_filename(chooser);
GtkTreeIter iter, last;
gtk_tree_model_get_iter_first(c_model, &iter);
gtk_tree_model_iter_nth_child(c_model, &last, &iter, 0);
while (__collection_get_library(&last) != NULL)
gtk_tree_model_iter_next(c_model, &last);
gtk_tree_store_insert_before(GTK_TREE_STORE(c_model), &iter, NULL, &last);
__collection_set_library(&iter, collection_add(filename));
gui_idle_enable();
g_free(filename);
}
void __collection_selection_changed(GtkTreeSelection *selection,
GtkFileChooser *chooser)
{
GtkStack *stack = GTK_STACK(gui_builder_widget("o_stack"));
struct library *library;
GtkTreeIter iter;
if (gtk_tree_selection_get_selected(selection, &c_model, &iter)) {
gtk_stack_set_visible_child_name(stack, "chooser");
library = __collection_get_library(&iter);
gui_sidebar_selected(SB_COLLECTION, NULL);
if (library)
gtk_file_chooser_set_current_folder(chooser,
library->li_path);
}
}
static void *__collection_init(struct queue *queue)
{
return gui_queue_alloc(queue, "Collection", GQ_CAN_RANDOM);
}
static void __collection_added(struct queue *queue, unsigned int pos)
{
gui_queue_added(queue, pos);
gui_sidebar_set_size(gui_queue(queue));
}
static bool __collection_erase(struct queue *queue, struct track *track)
{
return playlist_add(PL_HIDDEN, track);
}
static void __collection_removed(struct queue *queue, unsigned int pos)
{
gui_queue_removed(queue, pos);
gui_sidebar_set_size(gui_queue(queue));
}
void __gui_collection_init_idle()
{
struct db_entry *library, *next;
GtkTreeIter parent, iter, last;
gtk_tree_model_get_iter_first(c_model, &parent);
while (!gtk_tree_model_iter_has_child(c_model, &parent))
gtk_tree_model_iter_next(c_model, &parent);
gtk_tree_model_iter_nth_child(c_model, &last, &parent, 0);
/* Add library paths. */
db_for_each(library, next, library_db_get()) {
gtk_tree_store_insert_before(GTK_TREE_STORE(c_model), &iter, &parent, &last);
__collection_set_library(&iter, LIBRARY(library));
}
}
void gui_collection_init()
{
GtkTreeIter parent, iter;
GtkTreeView *treeview;
c_model = GTK_TREE_MODEL(gui_builder_object("o_collection_store"));
/* Add "Collection" header. */
gtk_tree_store_insert(GTK_TREE_STORE(c_model), &parent, NULL, -1);
__collection_set_header(&parent, "system-file-manager", "<big>Collection</big>");
/* Add "Add new Library" entry. */
gtk_tree_store_insert(GTK_TREE_STORE(c_model), &iter, &parent, -1);
__collection_set_header(&iter, "folder-new",
"<span style='italic'>&lt;Add new path&gt;</span>");
treeview = GTK_TREE_VIEW(gui_builder_widget("o_collection_view"));
gtk_tree_view_expand_all(treeview);
gtk_tree_selection_set_select_function(
gtk_tree_view_get_selection(treeview),
gui_sidebar_on_select, NULL, NULL);
idle_schedule(__gui_collection_init_idle, NULL);
gui_sidebar_set_size(gui_queue(collection_get_queue()));
gui_idle_enable();
}
struct queue_ops collection_ops = {
.qop_init = __collection_init,
.qop_deinit = gui_queue_free,
.qop_added = __collection_added,
.qop_erase = __collection_erase,
.qop_removed = __collection_removed,
.qop_cleared = gui_queue_cleared,
.qop_save = collection_save,
.qop_updated = gui_queue_updated,
};

142
gui/filter.c Normal file
View File

@ -0,0 +1,142 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#include <core/audio.h>
#include <core/string.h>
#include <gui/filter.h>
#include <gui/model.h>
static GtkTreeModelFilter *filter_model = NULL;
static inline GtkTreePath *__gui_filter_convert_path(GtkTreePath *path)
{
return gtk_tree_model_filter_convert_path_to_child_path(filter_model,
path);
}
static inline gboolean __gui_filter_match_token(struct track *track,
const gchar *token,
unsigned int how)
{
switch (how) {
case GUI_FILTER_ALBUM:
return album_match_token(track->tr_album, token);
case GUI_FILTER_ARTIST:
return artist_match_token(track->tr_album->al_artist, token);
case GUI_FILTER_GENRE:
return genre_match_token(track->tr_album->al_genre, token);
case GUI_FILTER_TITLE:
return track_match_token(track, token);
case GUI_FILTER_DEFAULT:
return track_match_token(track, token) ||
album_match_token(track->tr_album, token) ||
artist_match_token(track->tr_album->al_artist, token) ||
genre_match_token(track->tr_album->al_genre, token);
default:
return false;
}
}
static gboolean __gui_filter_visible_func(GtkTreeModel *model,
GtkTreeIter *iter,
gpointer data)
{
unsigned int i, how = gtk_combo_box_get_active(gui_filter_how());
gchar **search = gui_model_get_playlist()->pl_search;
struct track *track;
if (!search)
return TRUE;
track = gui_model_iter_get_track(iter);
for (i = 0; search[i]; i++) {
if (!__gui_filter_match_token(track, search[i], how))
return FALSE;
}
return TRUE;
}
void __gui_filter_search_changed(GtkSearchEntry *search, gpointer data)
{
playlist_set_search(gui_model_get_playlist(),
gtk_entry_get_text(GTK_ENTRY(search)));
gtk_tree_model_filter_refilter(gui_filter_get());
}
void __gui_filter_how_changed(int n)
{
__gui_filter_search_changed(gui_filter_search(), NULL);
}
void gui_filter_init()
{
GtkTreeModel *model = GTK_TREE_MODEL(gui_model_get());
GtkTreeModel *filter = gtk_tree_model_filter_new(model, NULL);
filter_model = GTK_TREE_MODEL_FILTER(filter);
gtk_tree_model_filter_set_visible_func(filter_model,
__gui_filter_visible_func,
NULL, NULL);
}
void gui_filter_deinit()
{
g_object_unref(G_OBJECT(filter_model));
}
void gui_filter_set_playlist(struct playlist *playlist)
{
gchar **search = playlist ? (gchar **)playlist->pl_search : NULL;
gchar *text = search ? g_strjoinv(" ", search) : g_strdup("");
gui_model_set_playlist(playlist);
gtk_entry_set_text(GTK_ENTRY(gui_filter_search()), text);
g_free(text);
}
GtkTreeModelFilter *gui_filter_get()
{
return filter_model;
}
struct track *gui_filter_path_get_track(GtkTreePath *path)
{
GtkTreePath *real = __gui_filter_convert_path(path);
struct track *track = real ? gui_model_path_get_track(real) : NULL;
gtk_tree_path_free(real);
return track;
}
void gui_filter_path_load_track(GtkTreePath *path)
{
struct track *track = gui_filter_path_get_track(path);
unsigned int index = gtk_tree_path_get_indices(path)[0];
audio_load(track);
playlist_current_set(gui_model_get_playlist(), index);
}
unsigned int gui_filter_path_get_index(GtkTreePath *path)
{
GtkTreePath *real = __gui_filter_convert_path(path);
unsigned int ret = gtk_tree_path_get_indices(real)[0];
gtk_tree_path_free(real);
return ret;
}
GtkTreePath *gui_filter_path_from_index(unsigned int index)
{
GtkTreePath *real, *path;
real = gtk_tree_path_new_from_indices(index, -1);
path = gtk_tree_model_filter_convert_child_path_to_path(filter_model,
real);
gtk_tree_path_free(real);
return path;
}
void gui_filter_refilter(struct playlist *playlist)
{
if (!playlist || playlist == gui_model_get_playlist())
gtk_tree_model_filter_refilter(gui_filter_get());
}

View File

@ -1,38 +0,0 @@
/*
* Copyright 2014 (c) Anna Schumaker.
*/
#include <gui/queue.h>
#include <gui/sidebar.h>
static void *__history_init(struct queue *queue)
{
return gui_queue_alloc(queue, "History", 0);
}
static void __history_added(struct queue *queue, unsigned int pos)
{
gui_queue_added(queue, pos);
gui_sidebar_set_size(gui_queue(queue));
}
static bool __history_erase(struct queue *queue, struct track *track)
{
return false;
}
static void __history_removed(struct queue *queue, unsigned int pos)
{
gui_queue_removed(queue, pos);
gui_sidebar_set_size(gui_queue(queue));
}
struct queue_ops history_ops = {
.qop_init = __history_init,
.qop_deinit = gui_queue_free,
.qop_added = __history_added,
.qop_erase = __history_erase,
.qop_removed = __history_removed,
.qop_cleared = gui_queue_cleared,
.qop_updated = gui_queue_updated,
};

View File

@ -1,37 +1,31 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#include <core/idle.h>
#include <gui/builder.h>
#include <gtk/gtk.h>
#include <gui/idle.h>
static guint idle_id = 0;
static gboolean __on_idle(gpointer data)
{
GtkProgressBar *progress = GTK_PROGRESS_BAR(data);
bool more = idle_run_task();
gchar *text = g_strdup_printf("%f%%", idle_progress() * 100);
if (idle_run_task()) {
gtk_progress_bar_set_fraction(progress, idle_progress());
return gtk_widget_is_visible(gui_builder_widget("o_window"));
} else {
gtk_widget_hide(GTK_WIDGET(progress));
idle_id = 0;
return G_SOURCE_REMOVE;
}
gtk_widget_set_visible(GTK_WIDGET(gui_progress_bar()), more);
gtk_progress_bar_set_fraction(gui_progress_bar(), idle_progress());
gtk_widget_set_tooltip_text(GTK_WIDGET(gui_progress_bar()), text);
g_free(text);
idle_id = more ? idle_id : 0;
return more ? G_SOURCE_CONTINUE : G_SOURCE_REMOVE;
}
void gui_idle_enable()
{
GtkWidget *progress = gui_builder_widget("o_idle_progress");
gtk_widget_show(progress);
idle_id = g_idle_add(__on_idle, progress);
idle_id = g_idle_add(__on_idle, NULL);
}
void gui_idle_disable()
{
if (idle_id)
g_source_remove(idle_id);
idle_cancel();
}

View File

@ -2,64 +2,57 @@
* Copyright 2016 (c) Anna Schumaker.
*/
#include <core/audio.h>
#include <core/queue.h>
#include <core/string.h>
#include <gui/builder.h>
#include <gui/model.h>
static const GTypeInfo queue_type_info;
static const GInterfaceInfo queue_tree_model;
static GObjectClass *parent_class = NULL;
static gchar *font_current = "bold";
static gchar *font_regular = "";
static gboolean __gui_model_iter_nth_child(GtkTreeModel *, GtkTreeIter *,
GtkTreeIter *, gint);
static GType queue_columns[Q_MODEL_N_COLUMNS] = {
[Q_MODEL_TRACK_NR] = G_TYPE_UINT,
[Q_MODEL_TITLE] = G_TYPE_STRING,
[Q_MODEL_LENGTH] = G_TYPE_STRING,
[Q_MODEL_ARTIST] = G_TYPE_STRING,
[Q_MODEL_ALBUM] = G_TYPE_STRING,
[Q_MODEL_YEAR] = G_TYPE_UINT,
[Q_MODEL_GENRE] = G_TYPE_STRING,
[Q_MODEL_COUNT] = G_TYPE_UINT,
[Q_MODEL_LAST_PLAY] = G_TYPE_STRING,
[Q_MODEL_FILE_PATH] = G_TYPE_STRING,
[Q_MODEL_FONT] = G_TYPE_STRING,
static struct playlist *cur_playlist = NULL;
static GObjectClass *parent_class = NULL;
static GuiModel *gui_model = NULL;
static GType gui_model_type = 0;
static GType gui_model_columns[GUI_MODEL_N_COLUMNS] = {
[GUI_MODEL_TRACK_NR] = G_TYPE_UINT,
[GUI_MODEL_TITLE] = G_TYPE_STRING,
[GUI_MODEL_LENGTH] = G_TYPE_STRING,
[GUI_MODEL_ARTIST] = G_TYPE_STRING,
[GUI_MODEL_ALBUM] = G_TYPE_STRING,
[GUI_MODEL_YEAR] = G_TYPE_UINT,
[GUI_MODEL_GENRE] = G_TYPE_STRING,
[GUI_MODEL_COUNT] = G_TYPE_UINT,
[GUI_MODEL_LAST_PLAY] = G_TYPE_STRING,
[GUI_MODEL_FILE_PATH] = G_TYPE_STRING,
[GUI_MODEL_FONT] = G_TYPE_STRING,
};
static gboolean __queue_model_iter_nth(GuiQueueModel *model,
GtkTreeIter *iter,
gint n)
{
if (n >= queue_size(model->gqm_queue))
return FALSE;
const GtkTargetEntry gui_model_drag_targets[] = {
{ GUI_DRAG_DATA, GTK_TARGET_SAME_APP, 0 },
};
_q_iter_set(&model->gqm_queue->q_tracks, &model->gqm_iter, n);
iter->stamp = model->gqm_stamp;
iter->user_data = &model->gqm_iter;
iter->user_data2 = _q_iter_val(&model->gqm_iter);
return TRUE;
}
const unsigned int gui_model_n_targets = G_N_ELEMENTS(gui_model_drag_targets);
static GtkTreeModelFlags _queue_model_get_flags(GtkTreeModel *model)
static GtkTreeModelFlags __gui_model_get_flags(GtkTreeModel *model)
{
return GTK_TREE_MODEL_LIST_ONLY;
}
static gint _queue_model_get_n_columns(GtkTreeModel *model)
static gint __gui_model_get_n_columns(GtkTreeModel *model)
{
return Q_MODEL_N_COLUMNS;
return GUI_MODEL_N_COLUMNS;
}
static GType _queue_model_get_column_type(GtkTreeModel *model, gint index)
static GType __gui_model_get_column_type(GtkTreeModel *model, gint index)
{
g_return_val_if_fail(index >= 0 && index < Q_MODEL_N_COLUMNS,
G_TYPE_INVALID);
return queue_columns[index];
g_return_val_if_fail(index >= 0, G_TYPE_INVALID);
g_return_val_if_fail(index < GUI_MODEL_N_COLUMNS, G_TYPE_INVALID);
return gui_model_columns[index];
}
static gboolean _queue_model_get_iter(GtkTreeModel *model, GtkTreeIter *iter,
GtkTreePath *path)
static gboolean __gui_model_get_iter(GtkTreeModel *model, GtkTreeIter *iter,
GtkTreePath *path)
{
gint *indices, depth;
@ -67,257 +60,324 @@ static gboolean _queue_model_get_iter(GtkTreeModel *model, GtkTreeIter *iter,
indices = gtk_tree_path_get_indices_with_depth(path, &depth);
g_assert(depth == 1);
return __queue_model_iter_nth(GUI_QUEUE_MODEL(model), iter, indices[0]);
return __gui_model_iter_nth_child(model, iter, NULL, indices[0]);
}
static GtkTreePath *_queue_model_get_path(GtkTreeModel *model, GtkTreeIter *iter)
static GtkTreePath *__gui_model_get_path(GtkTreeModel *model, GtkTreeIter *iter)
{
struct _q_iter *q_it;
GtkTreePath *path;
unsigned int pos;
g_return_val_if_fail(iter != NULL, FALSE);
g_return_val_if_fail(iter->user_data, FALSE);
g_return_val_if_fail(iter->user_data2, FALSE);
q_it = iter->user_data;
g_return_val_if_fail(iter != NULL, FALSE);
g_return_val_if_fail(iter->user_data, FALSE);
path = gtk_tree_path_new();
gtk_tree_path_append_index(path, q_it->it_pos);
pos = playlist_iter_index(cur_playlist, iter->user_data);
gtk_tree_path_append_index(path, pos);
return path;
}
static void _queue_model_get_value(GtkTreeModel *model, GtkTreeIter *iter,
gint column, GValue *value)
static void __gui_model_get_value(GtkTreeModel *model, GtkTreeIter *iter,
gint column, GValue *value)
{
struct track *track = iter->user_data2;
struct track *track = playlist_iter_track(iter->user_data);
gchar *str;
g_return_if_fail(iter != NULL);
g_return_if_fail(iter->user_data2 != NULL);
g_return_if_fail(column < Q_MODEL_N_COLUMNS);
g_return_if_fail(iter->user_data != NULL);
g_return_if_fail(column < GUI_MODEL_N_COLUMNS);
g_value_init(value, queue_columns[column]);
g_value_init(value, gui_model_columns[column]);
switch (column) {
case Q_MODEL_TRACK_NR:
case GUI_MODEL_TRACK_NR:
g_value_set_uint(value, track->tr_track);
break;
case Q_MODEL_TITLE:
case GUI_MODEL_TITLE:
g_value_set_static_string(value, track->tr_title);
break;
case Q_MODEL_LENGTH:
case GUI_MODEL_LENGTH:
g_value_take_string(value, string_sec2str(track->tr_length));
break;
case Q_MODEL_ARTIST:
g_value_set_static_string(value, track->tr_artist->ar_name);
case GUI_MODEL_ARTIST:
g_value_set_static_string(value, track->tr_album->al_artist->ar_name);
break;
case Q_MODEL_ALBUM:
case GUI_MODEL_ALBUM:
g_value_set_static_string(value, track->tr_album->al_name);
break;
case Q_MODEL_YEAR:
case GUI_MODEL_YEAR:
g_value_set_uint(value, track->tr_album->al_year);
break;
case Q_MODEL_GENRE:
g_value_set_static_string(value, track->tr_genre->ge_name);
case GUI_MODEL_GENRE:
g_value_set_static_string(value, track->tr_album->al_genre->ge_name);
break;
case Q_MODEL_COUNT:
case GUI_MODEL_COUNT:
g_value_set_uint(value, track->tr_count);
break;
case Q_MODEL_LAST_PLAY:
case GUI_MODEL_LAST_PLAY:
g_value_take_string(value, track_last_play(track));
break;
case Q_MODEL_FILE_PATH:
case GUI_MODEL_FILE_PATH:
str = track_path(track);
g_value_take_string(value, g_markup_escape_text(str, -1));
g_free(str);
break;
case Q_MODEL_FONT:
str = (track == audio_cur_track()) ? font_current : font_regular;
g_value_set_static_string(value, str);
case GUI_MODEL_FONT:
str = (track == audio_cur_track()) ? "bold" : "";
g_value_take_string(value, g_strdup(str));
break;
}
}
static gboolean _queue_model_iter_next(GtkTreeModel *model, GtkTreeIter *iter)
static gboolean __gui_model_iter_next(GtkTreeModel *model, GtkTreeIter *iter)
{
GuiQueueModel *gqm = GUI_QUEUE_MODEL(model);
g_return_val_if_fail(iter != NULL, FALSE);
g_return_val_if_fail(iter->user_data, FALSE);
g_return_val_if_fail(iter != NULL, FALSE);
g_return_val_if_fail(iter->user_data, FALSE);
g_return_val_if_fail(iter->user_data2, FALSE);
_q_iter_next(&gqm->gqm_iter);
if (gqm->gqm_iter.it_iter == NULL)
return FALSE;
iter->stamp = gqm->gqm_stamp;
iter->user_data = &gqm->gqm_iter;
iter->user_data2 = _q_iter_val(&gqm->gqm_iter);
return TRUE;
iter->user_data = playlist_iter_next(iter->user_data);
return iter->user_data != NULL;
}
static gboolean _queue_model_iter_children(GtkTreeModel *model,
GtkTreeIter *iter,
GtkTreeIter *parent)
static gboolean __gui_model_iter_children(GtkTreeModel *model, GtkTreeIter *iter,
GtkTreeIter *parent)
{
if (parent)
return FALSE;
return __queue_model_iter_nth(GUI_QUEUE_MODEL(model), iter, 0);
return __gui_model_iter_nth_child(model, iter, parent, 0);
}
static gboolean _queue_model_iter_has_child(GtkTreeModel *model,
GtkTreeIter *iter)
static gboolean __gui_model_iter_has_child(GtkTreeModel *model, GtkTreeIter *iter)
{
return FALSE;
}
static gint _queue_model_iter_n_children(GtkTreeModel *model,
GtkTreeIter *iter)
static gint __gui_model_iter_n_children(GtkTreeModel *model, GtkTreeIter *iter)
{
if (iter != NULL)
if (iter != NULL || !cur_playlist)
return 0;
return queue_size(GUI_QUEUE_MODEL(model)->gqm_queue);
return playlist_size(cur_playlist);
}
static gboolean _queue_model_iter_nth_child(GtkTreeModel *model,
GtkTreeIter *iter,
GtkTreeIter *parent,
gint n)
static gboolean __gui_model_iter_nth_child(GtkTreeModel *model,
GtkTreeIter *iter,
GtkTreeIter *parent,
gint n)
{
if (parent)
if (parent || !cur_playlist || n >= playlist_size(cur_playlist))
return FALSE;
return __queue_model_iter_nth(GUI_QUEUE_MODEL(model), iter, n);
iter->stamp = gui_model->gm_stamp;
iter->user_data = playlist_iter_get(cur_playlist, n);
return iter->user_data != NULL;
}
static gboolean _queue_model_iter_parent(GtkTreeModel *model,
GtkTreeIter *iter,
GtkTreeIter *child)
static gboolean __gui_model_iter_parent(GtkTreeModel *model, GtkTreeIter *iter,
GtkTreeIter *child)
{
return FALSE;
}
static void _queue_model_init(GuiQueueModel *model)
static gboolean __gui_model_drag_data_get(GtkTreeDragSource *drag_source,
GtkTreePath *path,
GtkSelectionData *selection_data)
{
model->gqm_stamp = g_random_int();
struct gui_model_drag_data *data = g_malloc(sizeof(*data));
data->drag_row = gtk_tree_path_get_indices(path)[0];
data->drag_track = gui_model_path_get_track(path);
gtk_selection_data_set(selection_data, gdk_atom_intern(GUI_DRAG_DATA, false),
8 /* bytes */, (void *)data, sizeof(*data));
g_free(data);
return true;
}
static void _queue_model_finalize(GObject *object)
static gboolean __gui_model_drag_data_delete(GtkTreeDragSource *drag_source,
GtkTreePath *path)
{
return true;
}
static void __gui_model_init(GuiModel *model)
{
model->gm_stamp = g_random_int();
}
static void __gui_model_finalize(GObject *object)
{
parent_class->finalize(object);
}
static void _queue_model_class_init(GuiQueueModelClass *class)
static void __gui_model_class_init(GuiModelClass *class)
{
GObjectClass *object_class;
GObjectClass *object_class = (GObjectClass *)class;
object_class->finalize = __gui_model_finalize;
parent_class = g_type_class_peek_parent(class);
object_class = (GObjectClass *)class;
object_class->finalize = _queue_model_finalize;
}
static void _queue_tree_model_init(GtkTreeModelIface *iface)
static void __gui_tree_model_init(GtkTreeModelIface *iface)
{
iface->get_flags = _queue_model_get_flags;
iface->get_n_columns = _queue_model_get_n_columns;
iface->get_column_type = _queue_model_get_column_type;
iface->get_iter = _queue_model_get_iter;
iface->get_path = _queue_model_get_path;
iface->get_value = _queue_model_get_value;
iface->iter_next = _queue_model_iter_next;
iface->iter_children = _queue_model_iter_children;
iface->iter_has_child = _queue_model_iter_has_child;
iface->iter_n_children = _queue_model_iter_n_children;
iface->iter_nth_child = _queue_model_iter_nth_child;
iface->iter_parent = _queue_model_iter_parent;
iface->get_flags = __gui_model_get_flags;
iface->get_n_columns = __gui_model_get_n_columns;
iface->get_column_type = __gui_model_get_column_type;
iface->get_iter = __gui_model_get_iter;
iface->get_path = __gui_model_get_path;
iface->get_value = __gui_model_get_value;
iface->iter_next = __gui_model_iter_next;
iface->iter_children = __gui_model_iter_children;
iface->iter_has_child = __gui_model_iter_has_child;
iface->iter_n_children = __gui_model_iter_n_children;
iface->iter_nth_child = __gui_model_iter_nth_child;
iface->iter_parent = __gui_model_iter_parent;
}
GuiQueueModel *gui_queue_model_new(struct queue *queue)
static void __gui_drag_source_init(GtkTreeDragSourceIface *iface)
{
GuiQueueModel *model = g_object_new(GUI_QUEUE_MODEL_TYPE, NULL);
g_assert(model != NULL);
model->gqm_queue = queue;
return model;
iface->drag_data_get = __gui_model_drag_data_get;
iface->drag_data_delete = __gui_model_drag_data_delete;
}
GType gui_queue_model_get_type()
{
static GType queue_type = 0;
if (queue_type == 0) {
queue_type = g_type_register_static(G_TYPE_OBJECT,
"GuiQueueModel",
&queue_type_info,
(GTypeFlags)0);
g_type_add_interface_static(queue_type,
GTK_TYPE_TREE_MODEL,
&queue_tree_model);
}
return queue_type;
}
void gui_queue_model_add(GuiQueueModel *model, unsigned int row)
{
GtkTreePath *path = gtk_tree_path_new_from_indices(row, -1);
GtkTreeIter iter;
_queue_model_get_iter(GTK_TREE_MODEL(model), &iter, path);
gtk_tree_model_row_inserted(GTK_TREE_MODEL(model), path, &iter);
gtk_tree_path_free(path);
}
void gui_queue_model_remove(GuiQueueModel *model, unsigned int row)
{
GtkTreePath *path = gtk_tree_path_new_from_indices(row, -1);
gtk_tree_model_row_deleted(GTK_TREE_MODEL(model), path);
gtk_tree_path_free(path);
}
void gui_queue_model_clear(GuiQueueModel *model, unsigned int n)
{
unsigned int i;
for (i = 1; i <= n; i++)
gui_queue_model_remove(model, n - i);
}
void gui_queue_model_update(GuiQueueModel *model, unsigned int row)
{
GtkTreePath *path = gtk_tree_path_new_from_indices(row, -1);
GtkTreeIter iter;
_queue_model_get_iter(GTK_TREE_MODEL(model), &iter, path);
gtk_tree_model_row_changed(GTK_TREE_MODEL(model), path, &iter);
gtk_tree_path_free(path);
}
struct track * gui_queue_model_path_get_track(GuiQueueModel *model,
GtkTreePath *path)
{
GtkTreeIter iter;
_queue_model_get_iter(GTK_TREE_MODEL(model), &iter, path);
return gui_queue_model_iter_get_track(model, &iter);
}
static const GTypeInfo queue_type_info = {
.class_size = sizeof(GuiQueueModelClass),
static const GTypeInfo gui_model_type_info = {
.class_size = sizeof(GuiModelClass),
.base_init = NULL,
.base_finalize = NULL,
.class_init = (GClassInitFunc)_queue_model_class_init,
.class_init = (GClassInitFunc)__gui_model_class_init,
.class_finalize = NULL,
.class_data = NULL,
.instance_size = sizeof(GuiQueueModel),
.instance_size = sizeof(GuiModel),
.n_preallocs = 0,
.instance_init = (GInstanceInitFunc)_queue_model_init,
.instance_init = (GInstanceInitFunc)__gui_model_init,
};
static const GInterfaceInfo queue_tree_model = {
.interface_init = (GInterfaceInitFunc)_queue_tree_model_init,
static const GInterfaceInfo gui_tree_model = {
.interface_init = (GInterfaceInitFunc)__gui_tree_model_init,
.interface_finalize = NULL,
.interface_data = NULL,
};
static const GInterfaceInfo gui_drag_source = {
.interface_init = (GInterfaceInitFunc)__gui_drag_source_init,
.interface_finalize = NULL,
.interface_data = NULL,
};
void gui_model_init(void)
{
gui_model_type = g_type_register_static(G_TYPE_OBJECT, "GuiModel",
&gui_model_type_info,
(GTypeFlags)0);
g_type_add_interface_static(gui_model_type, GTK_TYPE_TREE_MODEL,
&gui_tree_model);
g_type_add_interface_static(gui_model_type, GTK_TYPE_TREE_DRAG_SOURCE,
&gui_drag_source);
gui_model = g_object_new(gui_model_type, NULL);
g_assert(gui_model != NULL);
}
void gui_model_deinit(void)
{
g_object_unref(gui_model);
gui_model_type = 0;
gui_model = NULL;
cur_playlist = NULL;
}
static void __gui_model_set_runtime(void)
{
gchar *len = NULL;
if (cur_playlist)
len = string_sec2str_long(cur_playlist->pl_length);
gtk_label_set_text(gui_model_runtime(), len);
g_free(len);
}
static gboolean __gui_model_foreach_changed(GtkTreeModel *model, GtkTreePath *path,
GtkTreeIter *iter, gpointer data)
{
if (!data || data == gui_model_iter_get_track(iter))
gtk_tree_model_row_changed(model, path, iter);
return FALSE;
}
GuiModel *gui_model_get(void)
{
return gui_model;
}
GType gui_model_get_type()
{
return gui_model_type;
}
void gui_model_add(struct playlist *playlist, struct track *track)
{
GtkTreePath *path;
GtkTreeIter iter;
if (cur_playlist != playlist)
return;
path = gtk_tree_path_new_from_indices(0, -1);
__gui_model_get_iter(GTK_TREE_MODEL(gui_model), &iter, path);
gtk_tree_model_row_inserted(GTK_TREE_MODEL(gui_model), path, &iter);
gtk_tree_path_free(path);
__gui_model_set_runtime();
}
void gui_model_remove(struct playlist *playlist, struct track *track,
unsigned int n)
{
GtkTreePath *path;
unsigned int i;
if (cur_playlist != playlist)
return;
path = gtk_tree_path_new_from_indices(n - 1, -1);
for (i = 0; i < n; i++) {
gtk_tree_model_row_deleted(GTK_TREE_MODEL(gui_model), path);
gtk_tree_path_prev(path);
}
gtk_tree_path_free(path);
__gui_model_set_runtime();
}
void gui_model_update(struct playlist *playlist, struct track *track)
{
if (cur_playlist == playlist)
gtk_tree_model_foreach(GTK_TREE_MODEL(gui_model),
__gui_model_foreach_changed, track);
__gui_model_set_runtime();
}
void gui_model_set_playlist(struct playlist *playlist)
{
if (cur_playlist)
gui_model_remove(cur_playlist, NULL, playlist_size(cur_playlist));
cur_playlist = playlist;
__gui_model_set_runtime();
if (playlist && playlist_size(playlist) > 0)
gui_model_add(playlist, 0);
}
struct playlist *gui_model_get_playlist(void)
{
return cur_playlist;
}
struct track * gui_model_path_get_track(GtkTreePath *path)
{
GtkTreeIter iter;
__gui_model_get_iter(GTK_TREE_MODEL(gui_model), &iter, path);
return gui_model_iter_get_track(&iter);
}

View File

@ -4,32 +4,34 @@
#include <core/core.h>
#include <gui/audio.h>
#include <gui/builder.h>
#include <gui/collection.h>
#include <gui/history.h>
#include <gui/filter.h>
#include <gui/idle.h>
#include <gui/model.h>
#include <gui/playlist.h>
#include <gui/settings.h>
#include <gui/sidebar.h>
#include <gui/tempq.h>
#include <gui/view.h>
#include <gui/treeview.h>
#include <gui/window.h>
#define OCARINA_FLAGS (G_APPLICATION_FLAGS_NONE)
#define OCARINA_FLAGS (G_APPLICATION_HANDLES_COMMAND_LINE)
#ifndef CONFIG_DEBUG
const static gchar *OCARINA_NAME = "org.gtk.ocarina";
#else
const static gchar *OCARINA_NAME = "org.gtk.ocarina-debug";
#endif
struct core_init_data init_data = {
&collection_ops,
&history_ops,
&playlist_ops,
&tempq_ops,
&audio_ops,
static const GOptionEntry ocarina_options[] = {
{ "next", 'n', 0, G_OPTION_ARG_NONE, NULL, "Play next track", NULL },
{ "pause", 'P', 0, G_OPTION_ARG_NONE, NULL, "Pause playback", NULL },
{ "play", 'p', 0, G_OPTION_ARG_NONE, NULL, "Start playback", NULL },
{ "previous", 'N', 0, G_OPTION_ARG_NONE, NULL, "Play previous track", NULL },
{ "sync", 's', 0, G_OPTION_ARG_NONE, NULL, "Don't run background tasks", NULL },
{ "toggle", 't', 0, G_OPTION_ARG_NONE, NULL, "Toggle playback state", NULL },
{ "version", 'v', 0, G_OPTION_ARG_NONE, NULL, "Print version and exit", NULL },
{ NULL },
};
#ifndef CONFIG_DEBUG
const static gchar *OCARINA_APP = "org.gtk.ocarina";
#else
const static gchar *OCARINA_APP = "org.gtk.ocarina-debug";
#endif
static enum idle_sync_t idle_sync = IDLE_ASYNC;
static int startup_argc;
static char **startup_argv;
@ -46,27 +48,72 @@ static gchar *find_file_path(const gchar *file)
static void __ocarina_activate(GApplication *application, gpointer data)
{
gtk_application_add_window(GTK_APPLICATION(application),
GTK_WINDOW(gui_builder_widget("o_window")));
gtk_application_add_window(GTK_APPLICATION(application), gui_window());
}
static int __ocarina_local_options(GApplication *application,
GVariantDict *options, gpointer data)
{
if (g_variant_dict_contains(options, "sync"))
idle_sync = IDLE_SYNC;
if (!g_variant_dict_contains(options, "version"))
return -1;
g_printf("Ocarina %s\n", get_version());
g_printf("GTK+ %u.%u.%u\n", GTK_MAJOR_VERSION, GTK_MINOR_VERSION, GTK_MICRO_VERSION);
g_printf("%s\n", gst_version_string());
g_printf("%s, %s\n", __DATE__, __TIME__);
return 0;
}
static int __ocarina_command_line(GApplication *application,
GApplicationCommandLine *command,
gpointer data)
{
GVariantDict *options;
gchar **args;
g_application_activate(application);
args = g_application_command_line_get_arguments(command, NULL);
if (args && args[1]) {
audio_load_filepath(args[1]);
g_strfreev(args);
}
options = g_application_command_line_get_options_dict(command);
if (g_variant_dict_contains(options, "next"))
audio_next();
else if (g_variant_dict_contains(options, "pause"))
audio_pause();
else if (g_variant_dict_contains(options, "play"))
audio_play();
else if (g_variant_dict_contains(options, "previous"))
audio_prev();
else if (g_variant_dict_contains(options, "toggle")) {
if (audio_cur_state() == GST_STATE_PLAYING)
audio_pause();
else
audio_play();
}
return 0;
}
static void __ocarina_startup(GApplication *application, gpointer data)
{
gchar *ui = find_file_path("ocarina6.glade");
gchar *ui = find_file_path("ocarina.ui");
gchar *icon = find_file_path("ocarina.png");
gui_builder_init(ui);
core_init(&startup_argc, &startup_argv, &init_data);
gui_settings_init();
gui_view_init();
core_init(&startup_argc, &startup_argv, &playlist_cb, &audio_cb, idle_sync);
gui_window_init(icon);
gui_model_init();
gui_filter_init();
gui_treeview_init();
gui_sidebar_init();
gui_collection_init();
gui_playlist_init();
gui_audio_init();
gui_idle_enable();
gui_sidebar_select_first();
g_free(ui);
g_free(icon);
@ -75,22 +122,30 @@ static void __ocarina_startup(GApplication *application, gpointer data)
static void __ocarina_shutdown(GApplication *application, gpointer data)
{
gui_idle_disable();
gui_audio_deinit();
core_deinit();
gui_treeview_deinit();
gui_filter_deinit();
gui_model_deinit();
gui_window_deinit();
gui_settings_deinit();
gui_builder_deinit();
}
int main(int argc, char **argv)
{
GtkApplication *ocarina = gtk_application_new(OCARINA_NAME, OCARINA_FLAGS);
GtkApplication *ocarina = gtk_application_new(OCARINA_APP, OCARINA_FLAGS);
startup_argc = argc;
startup_argv = argv;
g_signal_connect(G_APPLICATION(ocarina), "activate", (GCallback)__ocarina_activate, NULL);
g_signal_connect(G_APPLICATION(ocarina), "startup", (GCallback)__ocarina_startup, NULL);
g_signal_connect(G_APPLICATION(ocarina), "shutdown", (GCallback)__ocarina_shutdown, NULL);
g_application_add_main_option_entries(G_APPLICATION(ocarina), ocarina_options);
g_application_add_option_group(G_APPLICATION(ocarina), gst_init_get_option_group());
g_signal_connect(G_APPLICATION(ocarina), "activate", (GCallback)__ocarina_activate, NULL);
g_signal_connect(G_APPLICATION(ocarina), "handle-local-options",
(GCallback)__ocarina_local_options, NULL);
g_signal_connect(G_APPLICATION(ocarina), "command-line", (GCallback)__ocarina_command_line, NULL);
g_signal_connect(G_APPLICATION(ocarina), "startup", (GCallback)__ocarina_startup, NULL);
g_signal_connect(G_APPLICATION(ocarina), "shutdown", (GCallback)__ocarina_shutdown, NULL);
return g_application_run(G_APPLICATION(ocarina), argc, argv);
}

View File

@ -1,107 +1,259 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#include <core/collection.h>
#include <gui/builder.h>
#include <core/idle.h>
#include <core/string.h>
#include <gui/filter.h>
#include <gui/model.h>
#include <gui/playlist.h>
#include <gui/treeview.h>
#include <gui/sidebar.h>
enum playlist_sidebar_columns {
P_SB_IMAGE,
P_SB_NAME,
P_SB_PLAYLIST,
static void (*update_size[PL_MAX_TYPE])(struct playlist *) = {
[PL_SYSTEM] = gui_pl_system_update,
[PL_ARTIST] = gui_pl_artist_update,
[PL_LIBRARY] = gui_pl_library_update,
[PL_USER] = gui_pl_user_update,
};
static GtkTreeStore *p_store;
static enum playlist_t p_cur;
static void (*select_playlist[PL_MAX_TYPE])(struct playlist *) = {
[PL_SYSTEM] = gui_pl_system_select,
[PL_ARTIST] = gui_pl_artist_select,
[PL_LIBRARY] = gui_pl_library_select,
[PL_USER] = gui_pl_user_select,
};
static void __playlist_set(GtkTreeIter *iter, const gchar *name,
const gchar *image, enum playlist_t plist)
static inline void __gui_playlist_update_size(struct playlist *playlist)
{
gtk_tree_store_set(p_store, iter, P_SB_NAME, name,
P_SB_IMAGE, image,
P_SB_PLAYLIST, plist, -1);
update_size[playlist->pl_type](playlist);
}
static void __playlist_add(GtkTreeIter *parent, const gchar *name,
const gchar *image, enum playlist_t plist)
static void __gui_playlist_alloc(struct playlist *playlist)
{
GtkTreeIter iter;
gtk_tree_store_insert(p_store, &iter, parent, -1);
__playlist_set(&iter, name, image, plist);
if (playlist->pl_type == PL_ARTIST)
gui_pl_artist_add(playlist);
}
void __playlist_selection_changed(GtkTreeSelection *selection, gpointer data)
static void __gui_playlist_added(struct playlist *playlist, struct track *track)
{
GtkStack *stack = GTK_STACK(gui_builder_widget("o_stack"));
GtkTreeModel *model = GTK_TREE_MODEL(p_store);
GtkTreeIter iter;
gui_model_add(playlist, track);
gui_filter_refilter(playlist);
__gui_playlist_update_size(playlist);
}
if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
gtk_tree_model_get(model, &iter, P_SB_PLAYLIST, &p_cur, -1);
static void __gui_playlist_removed(struct playlist *playlist, struct track *track,
unsigned int n)
{
gui_model_remove(playlist, track, n);
__gui_playlist_update_size(playlist);
}
gtk_stack_set_visible_child_name(stack, "queues");
gui_sidebar_selected(SB_PLAYLIST,
gui_queue(playlist_get_queue()));
playlist_select(p_cur);
struct playlist_callbacks playlist_cb = {
.pl_cb_alloc = __gui_playlist_alloc,
.pl_cb_added = __gui_playlist_added,
.pl_cb_removed = __gui_playlist_removed,
.pl_cb_updated = gui_model_update,
};
static void __gui_playlist_add_selected_to(struct playlist *playlist)
{
GList *cur, *list = NULL;
struct track *track;
if (!playlist)
return;
list = gui_treeview_list_selected_tracks();
cur = g_list_first(list);
while (cur) {
track = (struct track *)cur->data;
playlist_add(playlist, track);
cur = g_list_next(cur);
}
g_list_free(list);
}
void __gui_playlist_add_favorites(GtkMenuItem *item, gpointer data)
{
__gui_playlist_add_selected_to(playlist_lookup(PL_SYSTEM, "Favorites"));
}
void __gui_playlist_add_hidden(GtkMenuItem *item, gpointer data)
{
__gui_playlist_add_selected_to(playlist_lookup(PL_SYSTEM, "Hidden"));
}
void __gui_playlist_add_user(GtkMenuItem *item, gpointer data)
{
__gui_playlist_add_selected_to(gui_pl_user_add_dialog());
}
void __gui_playlist_add_other(GtkMenuItem *item, gpointer data)
{
__gui_playlist_add_selected_to(data);
}
void __gui_playlist_add_queued(GtkMenuItem *item, gpointer data)
{
__gui_playlist_add_selected_to(playlist_lookup(PL_SYSTEM, "Queued Tracks"));
}
void __gui_playlist_delete(GtkMenuItem *item, gpointer data)
{
struct playlist *playlist = gui_model_get_playlist();
GList *cur, *list = NULL;
struct track *track;
if (!playlist)
return;
list = gui_treeview_list_selected_tracks();
cur = g_list_first(list);
while (cur) {
track = (struct track *)cur->data;
playlist_remove(playlist, track);
cur = g_list_next(cur);
}
g_list_free(list);
}
void __gui_playlist_keypress(GtkTreeView *treeview, GdkEventKey *event,
gpointer data)
{
switch (event->keyval) {
case GDK_KEY_f:
__gui_playlist_add_favorites(NULL, NULL);
break;
case GDK_KEY_h:
__gui_playlist_add_hidden(NULL, NULL);
break;
case GDK_KEY_p:
__gui_playlist_add_user(NULL, NULL);
break;
case GDK_KEY_q:
__gui_playlist_add_queued(NULL, NULL);
break;
case GDK_KEY_Delete:
__gui_playlist_delete(NULL, NULL);
default:
break;
}
}
static void *__playlist_init(struct queue *queue)
static GtkWidget *__gui_playlist_build_submenu(void)
{
return gui_queue_alloc(queue, "Playlist", 0);
struct playlist *playlist;
GList *list = gui_pl_user_list();
GList *cur = g_list_first(list);
GtkWidget *submenu, *item;
if (!list)
return NULL;
submenu = gtk_menu_new();
while (cur) {
playlist = (struct playlist *)cur->data;
item = gtk_menu_item_new_with_label(playlist->pl_name);
gtk_menu_shell_append(GTK_MENU_SHELL(submenu), item);
g_signal_connect(item, "activate",
G_CALLBACK(__gui_playlist_add_other),
playlist);
cur = g_list_next(cur);
}
gtk_widget_show_all(submenu);
g_list_free(list);
return submenu;
}
static bool __playlist_erase(struct queue *queue, struct track *track)
bool __gui_playlist_button_press(GtkTreeView *treeview, GdkEventButton *event,
gpointer data)
{
/* collection_unban() and playlist_remove() handle queue changes */
if (gui_playlist_cur() == PL_HIDDEN)
collection_unban(track);
else
playlist_remove(gui_playlist_cur(), track);
return false;
GtkWidget *submenu;
if (event->button != GDK_BUTTON_SECONDARY)
return false;
submenu = __gui_playlist_build_submenu();
gtk_menu_item_set_submenu(gui_rc_add_to_other(), submenu);
gtk_widget_set_visible(GTK_WIDGET(gui_rc_add_to_other()),
submenu != NULL);
gui_treeview_select_path_at_pos(event->x, event->y);
gtk_menu_popup_at_pointer(gui_rc_menu(), (GdkEvent *)event);
return true;
}
void __gui_playlist_row_activated(GtkTreeView *treeview, GtkTreePath *path,
GtkTreeViewColumn *col, gpointer data)
{
struct playlist *prev = playlist_current();
gui_sidebar_filter_path_select(path);
__gui_playlist_update_size(prev);
__gui_playlist_update_size(playlist_current());
}
void __gui_playlist_row_collapsed(GtkTreeView *treeview, GtkTreeIter *iter,
GtkTreePath *path, gpointer data)
{
gui_sidebar_filter_row_expanded(iter, false);
}
void __gui_playlist_row_expanded(GtkTreeView *treeview, GtkTreeIter *iter,
GtkTreePath *path, gpointer data)
{
gui_sidebar_filter_row_expanded(iter, true);
}
void __gui_playlist_drag_data_received(GtkTreeView *treeview, GdkDragContext *context,
gint x, gint y, GtkSelectionData *data,
guint info, guint time, gpointer user_data)
{
struct playlist *playlist;
GtkTreeIter iter;
if (gui_sidebar_iter_from_xy(x, y, &iter))
playlist = gui_sidebar_iter_playlist(&iter);
if (!playlist)
playlist = gui_pl_user_add_dialog();
if (!playlist)
goto out;
if (playlist == playlist_lookup(PL_SYSTEM, "Collection") &&
gui_model_get_playlist() == playlist_lookup(PL_SYSTEM, "Hidden"))
__gui_playlist_delete(NULL, NULL);
else if (playlist != playlist_lookup(PL_SYSTEM, "History"))
__gui_playlist_add_selected_to(playlist);
out:
g_signal_stop_emission_by_name(treeview, "drag_data_received");
gtk_drag_finish(context, true, true, time);
}
bool __gui_playlist_init_idle()
{
struct playlist *playlist = playlist_current();
GtkTreeModel *filter = GTK_TREE_MODEL(gui_sidebar_filter());
GtkTreeIter iter;
gtk_tree_model_get_iter_first(filter, &iter);
do {
gui_sidebar_filter_set_expand(&iter);
} while (gtk_tree_model_iter_next(filter, &iter));
select_playlist[playlist->pl_type](playlist);
return true;
}
void gui_playlist_init()
{
GtkTreeView *treeview;
GtkTreeIter parent;
gui_pl_system_init();
gui_pl_artist_init();
gui_pl_user_init();
gui_pl_library_init();
p_store = GTK_TREE_STORE(gui_builder_object("o_playlist_store"));
/* Add "Playlist" header. */
gtk_tree_store_insert(p_store, &parent, NULL, -1);
gtk_tree_store_insert(p_store, &parent, NULL, -1);
__playlist_set(&parent, "<span size='large'>Playlists</span>",
"emblem-documents", 0);
/* Add playlists. */
__playlist_add(&parent, "Favorites", "emblem-favorite", PL_FAVORITED);
__playlist_add(&parent, "Hidden", "window-close",PL_HIDDEN);
__playlist_add(&parent, "Most Played", "go-up", PL_MOST_PLAYED);
__playlist_add(&parent, "Least Played", "go-down", PL_LEAST_PLAYED);
__playlist_add(&parent, "Unplayed", "audio-x-generic", PL_UNPLAYED);
treeview = GTK_TREE_VIEW(gui_builder_widget("o_playlist_view"));
gtk_tree_view_expand_all(treeview);
gtk_tree_selection_set_select_function(
gtk_tree_view_get_selection(treeview),
gui_sidebar_on_select, NULL, NULL);
gtk_tree_store_insert(p_store, &parent, NULL, -1);
idle_schedule(IDLE_SYNC, __gui_playlist_init_idle, NULL);
}
enum playlist_t gui_playlist_cur()
{
return p_cur;
}
struct queue_ops playlist_ops = {
.qop_init = __playlist_init,
.qop_deinit = gui_queue_free,
.qop_added = gui_queue_added,
.qop_erase = __playlist_erase,
.qop_removed = gui_queue_removed,
.qop_cleared = gui_queue_cleared,
.qop_updated = gui_queue_updated,
};

70
gui/playlists/artist.c Normal file
View File

@ -0,0 +1,70 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#include <core/idle.h>
#include <core/playlist.h>
#include <gui/sidebar.h>
static bool __gui_pl_artist_header(GtkTreeIter *iter)
{
if (gui_sidebar_iter_first(iter))
return gui_sidebar_iter_find(iter, "Collection", PL_SYSTEM);
return false;
}
static bool __gui_pl_artist_init_idle()
{
struct db_entry *artist, *next;
struct playlist *playlist;
GtkTreeIter iter;
if (!__gui_pl_artist_header(&iter))
return false;
db_for_each(artist, next, artist_db_get()) {
playlist = ARTIST(artist)->ar_playlist;
gui_sidebar_iter_sort_child(&iter, playlist, "system-users");
}
return true;
}
void gui_pl_artist_init()
{
idle_schedule(IDLE_SYNC, __gui_pl_artist_init_idle, NULL);
}
bool gui_pl_artist_add(struct playlist *playlist)
{
GtkTreeIter iter;
if (!__gui_pl_artist_header(&iter))
return false;
gui_sidebar_iter_sort_child(&iter, playlist, "system-users");
return true;
}
void gui_pl_artist_update(struct playlist *playlist)
{
GtkTreeIter iter, child;
if (!__gui_pl_artist_header(&iter))
return;
if (!gui_sidebar_iter_down(&iter, &child))
return;
if (gui_sidebar_iter_find(&child, playlist->pl_name, playlist->pl_type))
gui_sidebar_iter_update(&child);
}
void gui_pl_artist_select(struct playlist *playlist)
{
GtkTreeIter iter, child;
if (!__gui_pl_artist_header(&iter))
return;
if (!gui_sidebar_iter_down(&iter, &child))
return;
if (gui_sidebar_iter_find(&child, playlist->pl_name, playlist->pl_type))
gui_sidebar_iter_select(&child);
}

117
gui/playlists/library.c Normal file
View File

@ -0,0 +1,117 @@
/*
* Copyright 2015 (c) Anna Schumaker.
*/
#include <core/idle.h>
#include <core/playlist.h>
#include <gui/idle.h>
#include <gui/playlists/library.h>
#include <gui/sidebar.h>
#include <gui/window.h>
#include <glib/gi18n.h>
void __gui_pl_library_choose(GtkButton *button, gpointer data)
{
GtkFileFilter *filter;
const gchar *music;
GtkWidget *dialog;
gchar *path;
gint res;
filter = gtk_file_filter_new();
gtk_file_filter_add_mime_type(filter, "inode/directory");
dialog = gtk_file_chooser_dialog_new("Add Music", gui_window(),
GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
_("_Cancel"), GTK_RESPONSE_CANCEL,
_("_Open"), GTK_RESPONSE_ACCEPT,
NULL);
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(dialog), filter);
music = g_get_user_special_dir(G_USER_DIRECTORY_MUSIC);
if (music)
gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),
music);
res = gtk_dialog_run(GTK_DIALOG(dialog));
if (res == GTK_RESPONSE_ACCEPT) {
path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
gui_pl_library_add(path);
g_free(path);
}
gtk_widget_destroy(dialog);
}
static bool __gui_pl_library_header(GtkTreeIter *iter)
{
if (gui_sidebar_iter_first(iter))
return gui_sidebar_iter_find(iter, "Library", PL_MAX_TYPE);
return false;
}
static bool __gui_pl_library_init_idle()
{
struct db_entry *library, *next;
struct playlist *playlist;
GtkTreeIter iter;
if (!__gui_pl_library_header(&iter))
return false;
db_for_each(library, next, library_db_get()) {
playlist = LIBRARY(library)->li_playlist;
gui_sidebar_iter_sort_child(&iter, playlist, "folder");
}
#ifndef CONFIG_TESTING
if (library_db_get()->db_size == 0)
__gui_pl_library_choose(NULL, NULL);
#endif /* CONFIG_TESTING */
return true;
}
void gui_pl_library_init()
{
idle_schedule(IDLE_SYNC, __gui_pl_library_init_idle, NULL);
}
struct playlist *gui_pl_library_add(const gchar *filename)
{
struct playlist *playlist;
GtkTreeIter iter;
if (!__gui_pl_library_header(&iter))
return false;
playlist = playlist_new(PL_LIBRARY, filename);
if (playlist) {
gui_sidebar_iter_sort_child(&iter, playlist, "folder");
gui_idle_enable();
}
return playlist;
}
void gui_pl_library_update(struct playlist *playlist)
{
GtkTreeIter iter, child;
if (!__gui_pl_library_header(&iter))
return;
if (!gui_sidebar_iter_down(&iter, &child))
return;
if (gui_sidebar_iter_find(&child, playlist->pl_name, playlist->pl_type))
gui_sidebar_iter_update(&child);
}
void gui_pl_library_select(struct playlist *playlist)
{
GtkTreeIter iter, child;
if (!__gui_pl_library_header(&iter))
return;
if (!gui_sidebar_iter_down(&iter, &child))
return;
if (gui_sidebar_iter_find(&child, playlist->pl_name, playlist->pl_type))
gui_sidebar_iter_select(&child);
}

139
gui/playlists/system.c Normal file
View File

@ -0,0 +1,139 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#include <core/audio.h>
#include <core/idle.h>
#include <core/playlist.h>
#include <core/string.h>
#include <gui/playlists/system.h>
#include <gui/sidebar.h>
static struct playlist *favorites;
static struct playlist *hidden;
static bool __gui_pl_system_is_playlist(struct playlist *playlist)
{
return string_match(playlist->pl_name, "Favorites") ||
string_match(playlist->pl_name, "Hidden");
}
static bool __gui_pl_system_is_dynamic(struct playlist *playlist)
{
return string_match(playlist->pl_name, "Most Played") ||
string_match(playlist->pl_name, "Least Played") ||
string_match(playlist->pl_name, "Unplayed");
}
static bool __gui_pl_system_find_descend_header(GtkTreeIter *iter,
const gchar *name)
{
GtkTreeIter header;
if (!gui_sidebar_iter_first(&header))
return false;
if (!gui_sidebar_iter_find(&header, name, PL_MAX_TYPE))
return false;
return gui_sidebar_iter_down(&header, iter);
}
void __gui_pl_system_favorite_toggled(GtkToggleButton *toggle, gpointer data)
{
if (gtk_toggle_button_get_active(toggle))
playlist_add(favorites, audio_cur_track());
else
playlist_remove(favorites, audio_cur_track());
}
void __gui_pl_system_hide_toggled(GtkToggleButton *toggle, gpointer data)
{
if (gtk_toggle_button_get_active(toggle)) {
if (playlist_add(hidden, audio_cur_track()))
audio_next();
} else
playlist_remove(hidden, audio_cur_track());
}
static bool __gui_pl_system_init_idle()
{
GtkTreeIter iter;
/* Add toplevel playlists. */
gui_sidebar_iter_first(&iter);
gui_sidebar_iter_add(&iter, playlist_lookup(PL_SYSTEM, "Queued Tracks"),
"audio-x-generic");
gui_sidebar_iter_add(&iter, playlist_lookup(PL_SYSTEM, "Collection"),
"media-optical");
gui_sidebar_iter_add(&iter, playlist_lookup(PL_SYSTEM, "History"),
"document-open-recent");
/* Add user-modifiable playlists. */
gui_sidebar_iter_find(&iter, "Playlists", PL_MAX_TYPE);
gui_sidebar_iter_append_child(&iter, playlist_lookup(PL_SYSTEM, "Favorites"),
"emblem-favorite");
gui_sidebar_iter_append_child(&iter, playlist_lookup(PL_SYSTEM, "Hidden"),
"window-close");
/* Add dynamic playlists. */
gui_sidebar_iter_find(&iter, "Dynamic", PL_MAX_TYPE);
gui_sidebar_iter_append_child(&iter, playlist_lookup(PL_SYSTEM, "Most Played"),
"go-up");
gui_sidebar_iter_append_child(&iter, playlist_lookup(PL_SYSTEM, "Least Played"),
"go-down");
gui_sidebar_iter_append_child(&iter, playlist_lookup(PL_SYSTEM, "Unplayed"),
"audio-x-generic");
return true;
}
void gui_pl_system_init()
{
favorites = playlist_lookup(PL_SYSTEM, "Favorites");
hidden = playlist_lookup(PL_SYSTEM, "Hidden");
idle_schedule(IDLE_SYNC, __gui_pl_system_init_idle, NULL);
}
void gui_pl_system_update(struct playlist *playlist)
{
GtkTreeIter iter;
if (__gui_pl_system_is_playlist(playlist)) {
if (!__gui_pl_system_find_descend_header(&iter, "Playlists"))
return;
} else if (__gui_pl_system_is_dynamic(playlist)) {
if (!__gui_pl_system_find_descend_header(&iter, "Dynamic"))
return;
} else {
if (!gui_sidebar_iter_first(&iter))
return;
}
if (gui_sidebar_iter_find(&iter, playlist->pl_name, playlist->pl_type))
gui_sidebar_iter_update(&iter);
}
void gui_pl_system_select(struct playlist *playlist)
{
GtkTreeIter iter;
if (__gui_pl_system_is_playlist(playlist)) {
if (!__gui_pl_system_find_descend_header(&iter, "Playlists"))
return;
} else if (__gui_pl_system_is_dynamic(playlist)) {
if (!__gui_pl_system_find_descend_header(&iter, "Dynamic"))
return;
} else {
if (!gui_sidebar_iter_first(&iter))
return;
}
if (gui_sidebar_iter_find(&iter, playlist->pl_name, playlist->pl_type))
gui_sidebar_iter_select(&iter);
}
void gui_pl_system_track_loaded(struct track *track)
{
gtk_toggle_button_set_active(gui_favorite_button(),
playlist_has(favorites, track));
gtk_toggle_button_set_active(gui_hide_button(),
playlist_has(hidden, track));
}

153
gui/playlists/user.c Normal file
View File

@ -0,0 +1,153 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#include <core/idle.h>
#include <core/playlist.h>
#include <gui/sidebar.h>
#include <gui/window.h>
#include <glib/gi18n.h>
static bool __gui_pl_user_header(GtkTreeIter *iter)
{
if (gui_sidebar_iter_first(iter))
return gui_sidebar_iter_find(iter, "Playlists", PL_MAX_TYPE);
return false;
}
static gint __gui_pl_user_compare(gconstpointer a, gconstpointer b)
{
struct playlist *pl_a = (struct playlist *)a;
struct playlist *pl_b = (struct playlist *)b;
return g_utf8_collate(pl_a->pl_name, pl_b->pl_name);
}
static bool __gui_pl_user_init_idle()
{
struct db_entry *user, *next;
struct playlist *playlist;
GtkTreeIter iter;
if (!__gui_pl_user_header(&iter))
return false;
db_for_each(user, next, pl_user_db_get()) {
playlist = &USER_PLAYLIST(user)->pl_playlist;
gui_sidebar_iter_sort_child(&iter, playlist, "text-x-generic");
}
return true;
}
void __gui_pl_user_editing_started(GtkCellRenderer *renderer,
GtkCellEditable *editable,
gchar *path, gpointer data)
{
struct playlist *playlist;
GtkTreeIter iter;
if (!gui_sidebar_iter_from_string(path, &iter))
return;
playlist = gui_sidebar_iter_playlist(&iter);
if (GTK_IS_ENTRY(editable))
gtk_entry_set_text(GTK_ENTRY(editable), playlist->pl_name);
}
void __gui_pl_user_edited(GtkCellRendererText *renderer, gchar *path,
gchar *new_name, gpointer data)
{
struct playlist *playlist;
GtkTreeIter iter;
if (!gui_sidebar_iter_from_string(path, &iter))
return;
playlist = gui_sidebar_iter_playlist(&iter);
pl_user_rename(playlist, new_name);
gui_sidebar_iter_update_playlist(&iter, playlist);
gui_sidebar_iter_set_editable(&iter, false);
}
void gui_pl_user_init()
{
idle_schedule(IDLE_SYNC, __gui_pl_user_init_idle, NULL);
}
struct playlist *gui_pl_user_add(const gchar *name)
{
struct playlist *playlist;
GtkTreeIter iter;
if (!__gui_pl_user_header(&iter))
return NULL;
playlist = playlist_new(PL_USER, name);
if (playlist)
gui_sidebar_iter_sort_child(&iter, playlist, "text-x-generic");
return playlist;
}
struct playlist *gui_pl_user_add_dialog(void)
{
unsigned int flags = GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL;
GtkWidget *entry, *dialog, *content;
struct playlist *playlist = NULL;
entry = gtk_entry_new();
dialog = gtk_dialog_new_with_buttons("New Playlist Name?",
gui_window(), flags,
_("_Cancel"), GTK_RESPONSE_CANCEL,
_("_OK"), GTK_RESPONSE_ACCEPT,
NULL);
content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
gtk_entry_set_activates_default(GTK_ENTRY(entry), true);
gtk_container_add(GTK_CONTAINER(content), entry);
gtk_widget_show_all(dialog);
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT)
playlist = gui_pl_user_add(gtk_entry_get_text(GTK_ENTRY(entry)));
gtk_widget_destroy(dialog);
return playlist;
}
void gui_pl_user_update(struct playlist *playlist)
{
GtkTreeIter iter, child;
if (!__gui_pl_user_header(&iter))
return;
if (!gui_sidebar_iter_down(&iter, &child))
return;
if (gui_sidebar_iter_find(&child, playlist->pl_name, playlist->pl_type))
gui_sidebar_iter_update(&child);
}
void gui_pl_user_select(struct playlist *playlist)
{
GtkTreeIter iter, child;
if (!__gui_pl_user_header(&iter))
return;
if (!gui_sidebar_iter_down(&iter, &child))
return;
if (gui_sidebar_iter_find(&child, playlist->pl_name, playlist->pl_type))
gui_sidebar_iter_select(&child);
}
GList *gui_pl_user_list(void)
{
struct db_entry *user, *next;
struct playlist *playlist;
GList *list = NULL;
db_for_each(user, next, pl_user_db_get()) {
playlist = &USER_PLAYLIST(user)->pl_playlist;
list = g_list_insert_sorted(list, playlist,
__gui_pl_user_compare);
}
return list;
}

View File

@ -1,186 +0,0 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#include <core/filter.h>
#include <core/string.h>
#include <gui/builder.h>
#include <gui/model.h>
#include <gui/queue.h>
#include <gui/view.h>
static struct gui_queue *gq_queue = NULL;
static void __queue_set_runtime(struct gui_queue *queue)
{
gchar *len;
if (queue != gq_queue)
return;
len = string_sec2str_long(queue->gq_queue->q_length);
gtk_label_set_text(GTK_LABEL(gui_builder_widget("o_runtime")), len);
g_free(len);
}
static void __queue_toggle_flag(bool active, GtkWidget *widget,
enum queue_flags flag)
{
if (gq_queue == NULL)
return;
/*
* Some GTK themes have trouble with toggle buttons,
* so let's help users by changing image sensitivity.
*/
if (active)
queue_set_flag(gq_queue->gq_queue, flag);
else
queue_unset_flag(gq_queue->gq_queue, flag);
gtk_widget_set_sensitive(widget, active);
}
void __queue_random(GtkToggleButton *button, gpointer data)
{
__queue_toggle_flag(gtk_toggle_button_get_active(button),
gtk_button_get_image(GTK_BUTTON(button)),
Q_RANDOM);
}
void __queue_repeat(GtkToggleButton *button, gpointer data)
{
__queue_toggle_flag(gtk_toggle_button_get_active(button),
gtk_button_get_image(GTK_BUTTON(button)),
Q_REPEAT);
}
void __queue_disabled(GtkSwitch *enabled, GParamSpec *pspec, gpointer data)
{
__queue_toggle_flag(gtk_switch_get_active(enabled),
gui_builder_widget("o_treeview"),
Q_ENABLED);
}
static gboolean __queue_visible_func(GtkTreeModel *model, GtkTreeIter *iter,
gpointer data)
{
struct track *track;
if (!gq_queue || (strlen(gtk_entry_get_text(GTK_ENTRY(data))) == 0))
return TRUE;
track = gui_queue_model_iter_get_track(gq_queue->gq_model, iter);
return set_has(&gq_queue->gq_visible, track->tr_dbe.dbe_index);
}
void __queue_filter(GtkSearchEntry *entry, gpointer data)
{
const gchar *text = gtk_entry_get_text(GTK_ENTRY(entry));
if (!gq_queue)
return;
if (strlen(text) > 0)
filter_search(text, &gq_queue->gq_visible);
gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER(gq_queue->gq_filter));
gui_view_scroll();
}
struct gui_queue *gui_queue_alloc(struct queue *queue, const gchar *text,
unsigned int flags)
{
struct gui_queue *gq = g_malloc(sizeof(struct gui_queue));
gq->gq_flags = flags;
gq->gq_text = g_strdup(text);
gq->gq_model = gui_queue_model_new(queue);
gq->gq_filter = gtk_tree_model_filter_new(
GTK_TREE_MODEL(gq->gq_model), NULL);
gq->gq_queue = queue;
set_init(&gq->gq_visible);
gtk_tree_model_filter_set_visible_func(
GTK_TREE_MODEL_FILTER(gq->gq_filter),
__queue_visible_func,
gui_builder_object("o_search"), NULL);
return gq;
}
void gui_queue_free(struct queue *queue)
{
struct gui_queue *gq = gui_queue(queue);
set_deinit(&gq->gq_visible);
queue->q_private = NULL;
if (gq_queue == gq)
gui_view_set_model(NULL);
g_object_unref(gq->gq_model);
g_free(gq->gq_text);
g_free(gq);
}
void gui_queue_show(struct gui_queue *queue)
{
GtkTreeView *view = GTK_TREE_VIEW(gui_builder_widget("o_treeview"));
GtkButton *random = GTK_BUTTON(gui_builder_widget("o_random"));
GtkButton *repeat = GTK_BUTTON(gui_builder_widget("o_repeat"));
GtkSwitch *enabled = GTK_SWITCH(gui_builder_widget("o_enable"));
GtkEntry *search = GTK_ENTRY(gui_builder_widget("o_search"));
GtkLabel *runtime = GTK_LABEL(gui_builder_widget("o_runtime"));
bool has_random = false, has_repeat = false, is_enabled = false;;
gq_queue = queue;
gtk_widget_set_sensitive(GTK_WIDGET(random), gui_queue_can_random(queue));
gtk_widget_set_sensitive(GTK_WIDGET(repeat), gui_queue_can_repeat(queue));
gtk_widget_set_sensitive(GTK_WIDGET(enabled), gui_queue_can_disable(queue));
if (queue) {
has_random = queue_has_flag(queue->gq_queue, Q_RANDOM);
has_repeat = queue_has_flag(queue->gq_queue, Q_REPEAT);
is_enabled = queue_has_flag(queue->gq_queue, Q_ENABLED);
gui_view_set_model(GTK_TREE_MODEL_FILTER(queue->gq_filter));
__queue_set_runtime(queue);
} else
gtk_label_set_text(runtime, "");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(random), has_random);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(repeat), has_repeat);
gtk_switch_set_active(GTK_SWITCH(enabled), is_enabled);
/*
* Some GTK themes have trouble with toggle buttons,
* so let's help users know what the current state is.
*/
gtk_widget_set_sensitive(gtk_button_get_image(random), has_random);
gtk_widget_set_sensitive(gtk_button_get_image(repeat), has_repeat);
gtk_widget_set_sensitive(GTK_WIDGET(view), is_enabled);
gtk_widget_set_sensitive(GTK_WIDGET(search), queue != NULL);
gtk_entry_set_text(search, "");
}
void gui_queue_added(struct queue *queue, unsigned int row)
{
gui_queue_model_add(gui_queue(queue)->gq_model, row);
__queue_set_runtime(gui_queue(queue));
}
void gui_queue_removed(struct queue *queue, unsigned int row)
{
gui_queue_model_remove(gui_queue(queue)->gq_model, row);
__queue_set_runtime(gui_queue(queue));
}
void gui_queue_cleared(struct queue *queue, unsigned int n)
{
gui_queue_model_clear(gui_queue(queue)->gq_model, n);
__queue_set_runtime(gui_queue(queue));
}
void gui_queue_updated(struct queue *queue, unsigned int row)
{
gui_queue_model_update(gui_queue(queue)->gq_model, row);
__queue_set_runtime(gui_queue(queue));
}

View File

@ -1,216 +1,508 @@
/*
* Copyright 2015 (c) Anna Schumaker.
*/
#include <core/collection.h>
#include <core/history.h>
#include <core/queue.h>
#include <core/tempq.h>
#include <gui/builder.h>
#include <gui/queue.h>
#include <gui/settings.h>
#include <core/settings.h>
#include <core/string.h>
#include <gui/model.h>
#include <gui/sidebar.h>
const gchar *SIDEBAR_SETTING = "gui.sidebar.pos";
const gchar *COLLECTION_FMT = "<big>Collection</big>\n%d track%s";
const gchar *HISTORY_FMT = "<big>History</big>\n%d track%s";
static GtkListStore *sb_store;
#include <gui/treeview.h>
enum sidebar_columns {
SB_IMAGE,
SB_IMAGE_SZ,
SB_TEXT,
SB_QUEUE,
SB_NAME,
SB_TYPE,
SB_EDITABLE,
};
static void __sidebar_set_size(GtkTreeIter *iter, const gchar *name,
unsigned int size)
{
const gchar *fmt = "<big>%s</big>\n%d track%s";
gchar *text;
const gchar *SIDEBAR_SETTING = "gui.sidebar.pos";
text = g_strdup_printf(fmt, name, size, (size == 1) ? "" : "s");
gtk_list_store_set(sb_store, iter, SB_TEXT, text, -1);
static gchar *__gui_sidebar_size_str(struct playlist *playlist)
{
const gchar *fmt = "%s\n%d track%s";
unsigned int size;
if (!playlist)
return NULL;
size = playlist_size(playlist);
if (playlist_current() == playlist)
fmt = "<b>%s\n%d track%s</b>";
return g_markup_printf_escaped(fmt, playlist->pl_name, size,
(size != 1) ? "s" : "");
}
static void __gui_sidebar_set(GtkTreeIter *iter, const gchar *name,
const gchar *image, enum playlist_type_t type)
{
gtk_tree_store_set(gui_sidebar_store(), iter, SB_NAME, name,
SB_IMAGE, image,
SB_TYPE, type,
SB_EDITABLE, false, -1);
}
static void __gui_sidebar_set_playlist(GtkTreeIter *iter,
struct playlist *playlist,
const gchar *image)
{
gchar *text = __gui_sidebar_size_str(playlist);
__gui_sidebar_set(iter, text, image, playlist->pl_type);
g_free(text);
}
static void __sidebar_set_queue(GtkTreeIter *iter, struct gui_queue *queue)
static void __gui_sidebar_add_header(GtkTreeIter *iter, const gchar *name,
const gchar *image)
{
gtk_list_store_set(sb_store, iter, SB_QUEUE, queue, -1);
gchar *formatted = g_strdup_printf("<big>%s</big>", name);
gtk_tree_store_insert(gui_sidebar_store(), iter, NULL, -1);
__gui_sidebar_set(iter, NULL, NULL, PL_MAX_TYPE);
gtk_tree_store_insert(gui_sidebar_store(), iter, NULL, -1);
__gui_sidebar_set(iter, formatted, image, PL_MAX_TYPE);
g_free(formatted);
}
static struct gui_queue *__sidebar_get_queue(GtkTreeIter *iter)
static int __gui_sidebar_compare(GtkTreeIter *iter, const gchar *name,
enum playlist_type_t type)
{
struct gui_queue *queue;
gchar *cur;
int ret;
gtk_tree_model_get(GTK_TREE_MODEL(sb_store), iter, SB_QUEUE, &queue, -1);
return queue;
if (gui_sidebar_iter_type(iter) != type)
return gui_sidebar_iter_type(iter) - type;
cur = gui_sidebar_iter_name(iter);
ret = g_utf8_collate(cur, name);
g_free(cur);
return ret;
}
static bool __sidebar_find_queue(struct gui_queue *queue, GtkTreeIter *iter)
static inline void __gui_sidebar_filter_iter_convert(GtkTreeIter *iter,
GtkTreeIter *child)
{
gtk_tree_model_get_iter_first(GTK_TREE_MODEL(sb_store), iter);
do {
if (__sidebar_get_queue(iter) == queue)
return true;
} while (gtk_tree_model_iter_next(GTK_TREE_MODEL(sb_store), iter));
return false;
gtk_tree_model_filter_convert_iter_to_child_iter(gui_sidebar_filter(),
child, iter);
}
void __sidebar_resize(GtkPaned *pane, GParamSpec *pspec, gpointer data)
static gchar *__gui_sidebar_filter_iter_name(GtkTreeIter *iter)
{
gui_settings_set(SIDEBAR_SETTING, gtk_paned_get_position(pane));
GtkTreeIter child;
__gui_sidebar_filter_iter_convert(iter, &child);
return gui_sidebar_iter_name(&child);
}
bool __sidebar_keypress(GtkTreeView *treeview, GdkEventKey *event, gpointer data)
static gboolean __gui_sidebar_visible_func(GtkTreeModel *model,
GtkTreeIter *iter,
gpointer data)
{
GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
GtkTreeModel *model = GTK_TREE_MODEL(sb_store);
struct gui_queue *queue;
GtkTreePath *path;
enum playlist_type_t type = gui_sidebar_iter_type(iter);
if (type == PL_SYSTEM || type == PL_ARTIST)
return playlist_size(gui_sidebar_iter_playlist(iter)) > 0;
return TRUE;
}
static gboolean __gui_sidebar_can_select(GtkTreeSelection *selection,
GtkTreeModel *model, GtkTreePath *path,
gboolean selected, gpointer data)
{
GtkTreeIter iter, child;
gtk_tree_model_get_iter(model, &iter, path);
__gui_sidebar_filter_iter_convert(&iter, &child);
return gui_sidebar_iter_type(&child) != PL_MAX_TYPE;
}
void __gui_sidebar_selection_changed(GtkTreeSelection *selection, gpointer data)
{
bool active = false, sensitive = false;
struct playlist *playlist = NULL;
GtkTreeIter iter;
GList *rows;
if (event->keyval != GDK_KEY_Delete)
if (gui_sidebar_iter_current(&iter)) {
playlist = gui_sidebar_iter_playlist(&iter);
active = playlist->pl_random;
sensitive = (playlist->pl_ops->pl_set_random != NULL);
}
gui_treeview_set_playlist(playlist);
gtk_toggle_button_set_active(gui_random_button(), active);
gtk_widget_set_sensitive(GTK_WIDGET(gui_random_button()), sensitive);
}
static void __gui_sidebar_do_rename(GtkTreePath *path)
{
GtkTreeView *treeview = gui_sidebar_treeview();
GtkTreeModel *model = gtk_tree_view_get_model(treeview);
GtkTreeViewColumn *column = gtk_tree_view_get_column(treeview, SB_NAME);
GtkTreeIter iter, child;
if (!gtk_tree_model_get_iter(model, &iter, path))
return;
__gui_sidebar_filter_iter_convert(&iter, &child);
gui_sidebar_iter_set_editable(&child, true);
gtk_tree_view_set_cursor(treeview, path, column, true);
}
static GtkTreePath *__gui_sidebar_current_path(void)
{
GtkTreeView *treeview = gui_sidebar_treeview();
GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
GtkTreeModel *model = gtk_tree_view_get_model(treeview);
GtkTreeIter iter;
if (!gtk_tree_selection_get_selected(selection, &model, &iter))
return NULL;
return gtk_tree_model_get_path(model, &iter);
}
bool __gui_sidebar_rename(GtkMenuItem *item, gpointer data)
{
GtkTreePath *path = __gui_sidebar_current_path();
if (path) {
__gui_sidebar_do_rename(path);
gtk_tree_path_free(path);
}
return path != NULL;
}
bool __gui_sidebar_select(GtkMenuItem *item, gpointer data)
{
GtkTreeView *treeview = gui_sidebar_treeview();
GtkTreePath *path = __gui_sidebar_current_path();
if (path) {
gtk_tree_view_row_activated(treeview, path, NULL);
gtk_tree_path_free(path);
}
return path != NULL;
}
bool __gui_sidebar_delete(GtkMenuItem *item, gpointer data)
{
GtkTreeIter iter;
if (!gui_sidebar_iter_current(&iter))
return false;
rows = gtk_tree_selection_get_selected_rows(selection, &model);
path = rows->data;
if (gtk_tree_model_get_iter(model, &iter, path))
queue = __sidebar_get_queue(&iter);
if (tempq_index(queue->gq_queue) == -1)
goto out;
tempq_free(queue->gq_queue);
out:
g_list_free_full(rows, (GDestroyNotify)gtk_tree_path_free);
if (playlist_delete(gui_model_get_playlist()))
gtk_tree_store_remove(gui_sidebar_store(), &iter);
return true;
}
void __sidebar_selection_changed(GtkTreeSelection *selection, gpointer data)
bool __gui_sidebar_keypress(GtkTreeView *treeview, GdkEventKey *event,
gpointer data)
{
GtkStack *stack = GTK_STACK(gui_builder_widget("o_stack"));
GtkTreeModel *model = GTK_TREE_MODEL(sb_store);
GtkTreeIter iter;
if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
gtk_stack_set_visible_child_name(stack, "queues");
gui_sidebar_selected(SB_SIDEBAR, __sidebar_get_queue(&iter));
gtk_tree_path_free(path);
switch (event->keyval) {
case GDK_KEY_BackSpace:
return __gui_sidebar_rename(NULL, NULL);
case GDK_KEY_Return:
return __gui_sidebar_select(NULL, NULL);
case GDK_KEY_Delete:
return __gui_sidebar_delete(NULL, NULL);
default:
return false;
}
}
void __sidebar_deselect(const gchar *widget)
bool __gui_sidebar_button_press(GtkTreeView *treeview, GdkEventButton *event,
gpointer data)
{
GtkTreeView *treeview = GTK_TREE_VIEW(gui_builder_widget(widget));
GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
enum playlist_type_t type = PL_MAX_TYPE;
GtkTreePath *path;
GtkTreeIter iter;
bool ret = true;
gtk_tree_selection_unselect_all(selection);
if (!gtk_tree_view_get_path_at_pos(treeview, event->x, event->y,
&path, NULL, NULL, NULL))
return false;
if (event->button == GDK_BUTTON_SECONDARY) {
gtk_tree_view_set_cursor(treeview, path, NULL, false);
if (gui_sidebar_iter_current(&iter))
type = gui_sidebar_iter_type(&iter);
gtk_widget_set_visible(gui_builder_widget("rc_sidebar_rename"),
type == PL_USER);
gtk_menu_popup_at_pointer(gui_sidebar_menu(), (GdkEvent *)event);
} else if (event->type == GDK_2BUTTON_PRESS &&
event->button == GDK_BUTTON_MIDDLE) {
__gui_sidebar_do_rename(path);
} else
ret = false;
gtk_tree_path_free(path);
return ret;
}
void __gui_sidebar_resized(GtkPaned *pane, GParamSpec *pspec, gpointer data)
{
settings_set(SIDEBAR_SETTING, gtk_paned_get_position(pane));
}
void __gui_sidebar_random_toggled(GtkToggleButton *button, gpointer data)
{
struct playlist *playlist = gui_model_get_playlist();
bool active = gtk_toggle_button_get_active(button);
if (playlist)
playlist_set_random(playlist, active);
}
void gui_sidebar_init()
{
GtkPaned *pane = GTK_PANED(gui_builder_widget("o_sidebar"));
int pos = settings_get(SIDEBAR_SETTING);
GtkTreeSelection *selection;
GtkTreeIter iter;
int pos;
/* Set up entries in the liststore. */
sb_store = GTK_LIST_STORE(gui_builder_object("o_sidebar_store"));
gtk_tree_view_enable_model_drag_dest(gui_sidebar_treeview(),
gui_model_drag_targets, gui_model_n_targets,
GDK_ACTION_MOVE);
gtk_tree_model_get_iter_first(GTK_TREE_MODEL(sb_store), &iter);
__sidebar_set_queue(&iter, gui_queue(collection_get_queue()));
if (!gui_sidebar_iter_first(&iter)) {
__gui_sidebar_add_header(&iter, "Playlists", "emblem-documents");
__gui_sidebar_add_header(&iter, "Dynamic", "emblem-generic");
__gui_sidebar_add_header(&iter, "Library", "emblem-system");
gtk_tree_model_iter_next(GTK_TREE_MODEL(sb_store), &iter);
__sidebar_set_queue(&iter, gui_queue(history_get_queue()));
selection = gtk_tree_view_get_selection(gui_sidebar_treeview());
gtk_tree_selection_set_select_function(selection,
__gui_sidebar_can_select,
NULL, NULL);
gtk_tree_model_filter_set_visible_func(gui_sidebar_filter(),
__gui_sidebar_visible_func,
NULL, NULL);
}
/* Set sidebar width. */
pos = gui_settings_get(SIDEBAR_SETTING);
if (pos > 0)
gtk_paned_set_position(pane, pos);
gtk_paned_set_position(gui_sidebar(), pos);
}
void gui_sidebar_select_first()
gboolean gui_sidebar_iter_current(GtkTreeIter *iter)
{
GtkTreeView *treeview = GTK_TREE_VIEW(gui_builder_widget("o_sidebar_view"));
GtkTreeView *treeview = gui_sidebar_treeview();
GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
struct gui_queue *gq;
GtkTreePath *path;
GtkTreeIter iter;
GtkTreeModel *model = GTK_TREE_MODEL(gui_sidebar_filter());
GtkTreeIter it;
gtk_tree_model_get_iter_first(GTK_TREE_MODEL(sb_store), &iter);
if (!gtk_tree_selection_get_selected(selection, &model, &it))
return false;
__gui_sidebar_filter_iter_convert(&it, iter);
return true;
}
gboolean gui_sidebar_iter_first(GtkTreeIter *iter)
{
return gtk_tree_model_get_iter_first(gui_sidebar_model(), iter);
}
gboolean gui_sidebar_iter_next(GtkTreeIter *iter)
{
return gtk_tree_model_iter_next(gui_sidebar_model(), iter);
}
gboolean gui_sidebar_iter_down(GtkTreeIter *iter, GtkTreeIter *child)
{
return gtk_tree_model_iter_children(gui_sidebar_model(), child, iter);
}
gchar *gui_sidebar_iter_name(GtkTreeIter *iter)
{
gchar *text = NULL, *parsed = NULL, *name, **split;
gtk_tree_model_get(gui_sidebar_model(), iter, SB_NAME, &text, -1);
if (!text)
return g_strdup("");
pango_parse_markup(text, -1, 0, NULL, &parsed, NULL, NULL);
if (!parsed)
return g_strdup("");
split = g_strsplit(parsed, "\n", 2);
name = g_strdup(split[0]);
g_strfreev(split);
g_free(parsed);
g_free(text);
return name;
}
enum playlist_type_t gui_sidebar_iter_type(GtkTreeIter *iter)
{
enum playlist_type_t type;
gtk_tree_model_get(gui_sidebar_model(), iter, SB_TYPE, &type, -1);
return type;
}
bool gui_sidebar_iter_editable(GtkTreeIter *iter)
{
gboolean editable;
gtk_tree_model_get(gui_sidebar_model(), iter, SB_EDITABLE, &editable, -1);
return editable == TRUE;
}
struct playlist *gui_sidebar_iter_playlist(GtkTreeIter *iter)
{
enum playlist_type_t type = gui_sidebar_iter_type(iter);
gchar *name = gui_sidebar_iter_name(iter);
struct playlist *playlist = playlist_lookup(type, name);
g_free(name);
return playlist;
}
void gui_sidebar_iter_add(GtkTreeIter *iter, struct playlist *playlist,
const gchar *image)
{
GtkTreeIter new;
gtk_tree_store_insert_before(gui_sidebar_store(), &new, NULL, iter);
__gui_sidebar_set_playlist(&new, playlist, image);
}
void gui_sidebar_iter_sort_child(GtkTreeIter *iter, struct playlist *playlist,
const gchar *image)
{
GtkTreeIter child, new;
if (!gui_sidebar_iter_down(iter, &child))
goto out_append;
do {
gq = __sidebar_get_queue(&iter);
} while (!queue_has_flag(gq->gq_queue, Q_ENABLED) &&
gtk_tree_model_iter_next(GTK_TREE_MODEL(sb_store), &iter));
if (__gui_sidebar_compare(&child, playlist->pl_name,
playlist->pl_type) >= 0) {
gtk_tree_store_insert_before(gui_sidebar_store(),
&new, iter, &child);
__gui_sidebar_set_playlist(&new, playlist, image);
return;
}
} while (gui_sidebar_iter_next(&child));
path = gtk_tree_model_get_path(GTK_TREE_MODEL(sb_store), &iter);
gtk_tree_selection_select_path(selection, path);
out_append:
gui_sidebar_iter_append_child(iter, playlist, image);
}
void gui_sidebar_iter_append_child(GtkTreeIter *iter, struct playlist *playlist,
const gchar *image)
{
GtkTreeIter new;
gtk_tree_store_insert_before(gui_sidebar_store(), &new, iter, NULL);
__gui_sidebar_set_playlist(&new, playlist, image);
}
void gui_sidebar_iter_update_playlist(GtkTreeIter *iter,
struct playlist *playlist)
{
gchar *text;
if (!playlist)
return;
text = __gui_sidebar_size_str(playlist);
gtk_tree_store_set(gui_sidebar_store(), iter, SB_NAME, text, -1);
g_free(text);
}
void gui_sidebar_iter_update(GtkTreeIter *iter)
{
gui_sidebar_iter_update_playlist(iter, gui_sidebar_iter_playlist(iter));
}
void gui_sidebar_iter_select(GtkTreeIter *iter)
{
GtkTreeSelection *selection;
GtkTreeIter filter;
gtk_tree_model_filter_convert_child_iter_to_iter(gui_sidebar_filter(),
&filter, iter);
selection = gtk_tree_view_get_selection(gui_sidebar_treeview());
gtk_tree_selection_select_iter(selection, &filter);
}
bool gui_sidebar_iter_set_editable(GtkTreeIter *iter, bool editable)
{
enum playlist_type_t type = gui_sidebar_iter_type(iter);
if (type != PL_USER)
return false;
gtk_tree_store_set(gui_sidebar_store(), iter, SB_EDITABLE, editable, -1);
return true;
}
void gui_sidebar_filter_path_select(GtkTreePath *path)
{
GtkTreeModel *model = GTK_TREE_MODEL(gui_sidebar_filter());
GtkTreeIter iter, child;
gtk_tree_model_get_iter(model, &iter, path);
__gui_sidebar_filter_iter_convert(&iter, &child);
if (playlist_select(gui_sidebar_iter_playlist(&child)))
gui_sidebar_iter_update(&child);
}
void gui_sidebar_filter_set_expand(GtkTreeIter *iter)
{
gchar *name = __gui_sidebar_filter_iter_name(iter);
gchar *setting = g_strdup_printf("gui.sidebar.expand.%s", name);
GtkTreePath *path;
if (settings_get(setting) == true) {
path = gtk_tree_model_get_path(
GTK_TREE_MODEL(gui_sidebar_filter()), iter);
gtk_tree_view_expand_row(gui_sidebar_treeview(), path, false);
gtk_tree_path_free(path);
}
g_free(setting);
g_free(name);
}
void gui_sidebar_filter_row_expanded(GtkTreeIter *iter, bool expanded)
{
gchar *name = __gui_sidebar_filter_iter_name(iter);
gchar *setting = g_strdup_printf("gui.sidebar.expand.%s", name);
settings_set(setting, expanded);
g_free(setting);
g_free(name);
}
gboolean gui_sidebar_iter_find(GtkTreeIter *iter, const gchar *name,
enum playlist_type_t type)
{
do {
if (__gui_sidebar_compare(iter, name, type) == 0)
return TRUE;
} while (gui_sidebar_iter_next(iter));
return FALSE;
}
gboolean gui_sidebar_iter_from_string(const gchar *path, GtkTreeIter *child)
{
GtkTreeModel *model = GTK_TREE_MODEL(gui_sidebar_filter());
GtkTreeIter iter;
if (!gtk_tree_model_get_iter_from_string(model, &iter, path))
return FALSE;
__gui_sidebar_filter_iter_convert(&iter, child);
return TRUE;
}
gboolean gui_sidebar_iter_from_xy(gint x, gint y, GtkTreeIter *child)
{
GtkTreeModel *model = GTK_TREE_MODEL(gui_sidebar_filter());
GtkTreePath *path;
GtkTreeIter iter;
if (!gtk_tree_view_get_path_at_pos(gui_sidebar_treeview(), x, y,
&path, NULL, NULL, NULL))
return false;
gtk_tree_model_get_iter(model, &iter, path);
__gui_sidebar_filter_iter_convert(&iter, child);
gtk_tree_path_free(path);
}
gboolean gui_sidebar_on_select(GtkTreeSelection *selection,
GtkTreeModel *model, GtkTreePath *path,
gboolean selected, gpointer data)
{
return gtk_tree_path_get_depth(path) != 1;
}
void gui_sidebar_selected(enum sidebar_selection_t selected,
struct gui_queue *queue)
{
if (selected != SB_COLLECTION)
__sidebar_deselect("o_collection_view");
if (selected != SB_PLAYLIST)
__sidebar_deselect("o_playlist_view");
if (selected != SB_SIDEBAR)
__sidebar_deselect("o_sidebar_view");
gui_queue_show(queue);
}
void gui_sidebar_add(struct gui_queue *queue)
{
const gchar *text = "<big>Queued Tracks</big>\n0 tracks";
GtkTreeIter iter, sibling;
if (!GTK_IS_TREE_MODEL(sb_store))
return;
gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(sb_store),
&sibling, NULL,
tempq_index(queue->gq_queue));
gtk_list_store_insert_before(sb_store, &iter, &sibling);
gtk_list_store_set(sb_store, &iter, SB_IMAGE, "audio-x-generic",
SB_IMAGE_SZ, GTK_ICON_SIZE_BUTTON,
SB_TEXT, text,
SB_QUEUE, queue, -1);
__sidebar_set_size(&iter, queue->gq_text, queue_size(queue->gq_queue));
}
void gui_sidebar_remove(struct gui_queue *queue)
{
GtkTreeIter iter;
if (__sidebar_find_queue(queue, &iter))
gtk_list_store_remove(sb_store, &iter);
}
void gui_sidebar_set_size(struct gui_queue *queue)
{
GtkTreeIter iter;
if (!GTK_IS_TREE_MODEL(sb_store))
return;
if (__sidebar_find_queue(queue, &iter))
__sidebar_set_size(&iter, queue->gq_text,
queue_size(queue->gq_queue));
return true;
}

View File

@ -1,59 +0,0 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#include <core/tempq.h>
#include <gui/queue.h>
#include <gui/sidebar.h>
#define TEMPQ_FLAGS (GQ_CAN_RANDOM | GQ_CAN_REPEAT | GQ_CAN_DISABLE)
static void *__tempq_init(struct queue *queue)
{
struct gui_queue *gq;
gq = gui_queue_alloc(queue, "Queued Tracks", TEMPQ_FLAGS);
gui_sidebar_add(gq);
return gq;
}
static void __tempq_deinit(struct queue *queue)
{
gui_sidebar_remove(gui_queue(queue));
gui_queue_free(queue);
}
static void __tempq_added(struct queue *queue, unsigned int pos)
{
gui_queue_added(queue, pos);
gui_sidebar_set_size(gui_queue(queue));
tempq_save(queue, Q_ENABLED);
}
static bool __tempq_erase(struct queue *queue, struct track *track)
{
return true;
}
static void __tempq_removed(struct queue *queue, unsigned int pos)
{
gui_queue_removed(queue, pos);
gui_sidebar_set_size(gui_queue(queue));
tempq_save(queue, Q_ENABLED);
}
static void __tempq_cleared(struct queue *queue, unsigned int n)
{
gui_queue_cleared(queue, n);
gui_sidebar_set_size(gui_queue(queue));
}
struct queue_ops tempq_ops = {
.qop_init = __tempq_init,
.qop_deinit = __tempq_deinit,
.qop_added = __tempq_added,
.qop_erase = __tempq_erase,
.qop_removed = __tempq_removed,
.qop_cleared = __tempq_cleared,
.qop_save = tempq_save,
.qop_updated = gui_queue_updated,
};

289
gui/treeview.c Normal file
View File

@ -0,0 +1,289 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#include <core/settings.h>
#include <gui/filter.h>
#include <gui/model.h>
#include <gui/treeview.h>
#include <stdlib.h>
struct col_map_entry {
enum compare_t compare;
const gchar *setting;
};
static const struct col_map_entry GUI_COL_MAP[GUI_MODEL_N_COLUMNS] = {
[GUI_MODEL_TRACK_NR] = { COMPARE_TRACK, "gui.queue.track" },
[GUI_MODEL_TITLE] = { COMPARE_TITLE, "gui.queue.title" },
[GUI_MODEL_LENGTH] = { COMPARE_LENGTH, "gui.queue.length" },
[GUI_MODEL_ARTIST] = { COMPARE_ARTIST, "gui.queue.artist" },
[GUI_MODEL_ALBUM] = { COMPARE_ALBUM, "gui.queue.album" },
[GUI_MODEL_YEAR] = { COMPARE_YEAR, "gui.queue.year" },
[GUI_MODEL_GENRE] = { COMPARE_GENRE, "gui.queue.genre" },
[GUI_MODEL_COUNT] = { COMPARE_COUNT, "gui.queue.count" },
[GUI_MODEL_LAST_PLAY] = { COMPARE_PLAYED, NULL },
};
static unsigned int sort_count = 0;
static gchar *sort_text = NULL;
static bool can_scroll = true;
static int __gui_treeview_colum_match_sort(enum compare_t compare)
{
struct playlist *playlist = gui_model_get_playlist();
GSList *cur = playlist ? playlist->pl_sort : NULL;
while (cur) {
int field = GPOINTER_TO_INT(cur->data);
if (abs(field) == compare)
return field;
cur = g_slist_next(cur);
}
return 0;
}
static void __gui_treeview_set_sort_indicators()
{
GtkTreeViewColumn *col;
unsigned int i, order;
int field;
for (i = 0; i < GUI_MODEL_N_COLUMNS; i++) {
col = gtk_tree_view_get_column(gui_treeview(), i);
if (!col)
continue;
field = __gui_treeview_colum_match_sort(GUI_COL_MAP[i].compare);
order = (field > 0) ? GTK_SORT_ASCENDING : GTK_SORT_DESCENDING;
gtk_tree_view_column_set_sort_indicator(col, field != 0);
gtk_tree_view_column_set_sort_order(col, order);
}
}
static void __gui_treeview_clear_sorting()
{
sort_count = 0;
if (sort_text) {
g_free(sort_text);
sort_text = NULL;
gtk_label_set_text(gui_sorting(), "");
}
}
static void __gui_treeview_set_sorting(gchar *text)
{
gchar *formatted;
__gui_treeview_clear_sorting();
if (!text)
return;
sort_text = g_strdup(text);
formatted = g_strdup_printf("Sorting: {%s}", text);
gtk_label_set_text(gui_sorting(), formatted);
g_free(formatted);
}
static int __gui_treeview_dec_sort(gpointer data)
{
if (sort_count > 0)
sort_count--;
if (sort_count == 0)
__gui_treeview_clear_sorting();
return FALSE;
}
static gchar *__gui_treeview_sort_text_append(GtkTreeViewColumn *col)
{
const gchar *title = gtk_tree_view_column_get_title(col);
gchar *text, **split;
unsigned int i;
if (!sort_text)
return g_strdup(title);
if (gtk_tree_view_column_get_sort_order(col) == GTK_SORT_ASCENDING)
return g_strdup_printf("%s, %s", sort_text, title);
/* Find the column and prefix it with a minus sign */
split = g_strsplit(sort_text, ", ", 0);
for (i = 0; split[i] != NULL; i++) {
if (g_strcmp0(split[i], title) == 0)
break;
}
g_free(split[i]);
split[i] = g_strdup_printf("-%s", title);
text = g_strjoinv(", ", split);
g_strfreev(split);
return text;
}
static void __gui_treeview_column_clicked(GtkTreeViewColumn *col,
gpointer data)
{
struct playlist *playlist = gui_model_get_playlist();
enum compare_t compare = GPOINTER_TO_UINT(data);
gchar *text;
if (!playlist)
return;
if (sort_count == 0)
playlist_clear_sort(playlist);
if (!playlist_sort(playlist, compare))
return;
__gui_treeview_set_sort_indicators();
text = __gui_treeview_sort_text_append(col);
__gui_treeview_set_sorting(text);
g_free(text);
sort_count++;
g_timeout_add_seconds(3, __gui_treeview_dec_sort, NULL);
}
void __gui_treeview_column_resized(GtkTreeViewColumn *col, GParamSpec *pspec,
gpointer data)
{
settings_set(GUI_COL_MAP[GPOINTER_TO_UINT(data)].setting,
gtk_tree_view_column_get_width(col));
}
void __gui_treeview_row_activated(GtkTreeView *treeview, GtkTreePath *path,
GtkTreeViewColumn *col, gpointer data)
{
can_scroll = false;
gui_filter_path_load_track(path);
can_scroll = true;
}
void __gui_treeview_drag_data_received(GtkTreeView *treeview, GdkDragContext *context,
gint x, gint y, GtkSelectionData *data,
guint info, guint time, gpointer user_data)
{
struct gui_model_drag_data *drag_data;
unsigned int to, from;
GtkTreePath *path;
drag_data = (void *)gtk_selection_data_get_data(data);
if (gtk_tree_view_get_path_at_pos(gui_treeview(), x, y,
&path, NULL, NULL, NULL))
gtk_tree_path_prev(path);
else if (!gtk_tree_view_get_visible_range(gui_treeview(), NULL, &path))
return;
from = drag_data->drag_row;
to = gui_filter_path_get_index(path);
if (playlist_rearrange(gui_model_get_playlist(), from, to)) {
gtk_tree_selection_unselect_all(gui_treeview_selection());
gtk_tree_selection_select_path(gui_treeview_selection(), path);
__gui_treeview_set_sort_indicators();
}
g_signal_stop_emission_by_name(treeview, "drag_data_received");
gtk_drag_finish(context, true, true, time);
gtk_tree_path_free(path);
}
bool __gui_treeview_drag_drop(GtkTreeView *treeview, GdkDragContext *context,
gint x, gint y, guint time, gpointer user_data)
{
gtk_drag_get_data(GTK_WIDGET(treeview), context,
gdk_atom_intern(GUI_DRAG_DATA, false), time);
return true;
}
void gui_treeview_init()
{
GtkTreeViewColumn *col;
int i, pos;
gtk_tree_view_set_model(gui_treeview(),
GTK_TREE_MODEL(gui_filter_get()));
gtk_tree_view_enable_model_drag_source(gui_treeview(), GDK_BUTTON1_MASK,
gui_model_drag_targets, gui_model_n_targets,
GDK_ACTION_MOVE);
gtk_tree_view_enable_model_drag_dest(gui_treeview(),
gui_model_drag_targets, gui_model_n_targets,
GDK_ACTION_MOVE);
for (i = 0; i < GUI_MODEL_N_COLUMNS; i++) {
col = gtk_tree_view_get_column(gui_treeview(), i);
if (col) {
g_signal_connect(col, "clicked",
G_CALLBACK(__gui_treeview_column_clicked),
GUINT_TO_POINTER(GUI_COL_MAP[i].compare));
g_signal_connect(col, "notify::width",
G_CALLBACK(__gui_treeview_column_resized),
GUINT_TO_POINTER(i));
pos = settings_get(GUI_COL_MAP[i].setting);
if (pos > 0)
gtk_tree_view_column_set_fixed_width(col, pos);
}
}
}
void gui_treeview_deinit()
{
__gui_treeview_clear_sorting();
}
void gui_treeview_set_playlist(struct playlist *playlist)
{
gui_filter_set_playlist(playlist);
__gui_treeview_clear_sorting();
__gui_treeview_set_sort_indicators();
gui_treeview_scroll();
}
void gui_treeview_scroll()
{
int pos = playlist_current_index(gui_model_get_playlist());
GtkTreePath *path;
if (!can_scroll || pos < 0)
return;
path = gui_filter_path_from_index(pos);
if (!path)
return;
gtk_tree_view_set_cursor(gui_treeview(), path, NULL, false);
gtk_tree_view_scroll_to_cell(gui_treeview(), path, NULL, true, 0.5, 0.5);
gtk_tree_path_free(path);
}
void gui_treeview_select_path_at_pos(unsigned int x, unsigned int y)
{
GtkTreePath *path;
if (gtk_tree_view_get_path_at_pos(gui_treeview(), x, y,
&path, NULL, NULL, NULL))
{
gtk_tree_selection_select_path(gui_treeview_selection(), path);
gtk_tree_path_free(path);
}
}
GList *gui_treeview_list_selected_tracks(void)
{
GtkTreeSelection *selection = gui_treeview_selection();
GList *rows = gtk_tree_selection_get_selected_rows(selection, NULL);
GList *cur = g_list_first(rows);
GList *list = NULL;
while (cur) {
list = g_list_append(list, gui_filter_path_get_track(cur->data));
cur = g_list_next(cur);
}
g_list_free_full(rows, (GDestroyNotify) gtk_tree_path_free);
gtk_tree_selection_unselect_all(selection);
return list;
}

View File

@ -1,370 +0,0 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#include <core/audio.h>
#include <core/collection.h>
#include <core/playlist.h>
#include <core/tempq.h>
#include <gui/builder.h>
#include <gui/model.h>
#include <gui/queue.h>
#include <gui/settings.h>
#include <gui/view.h>
#include <stdlib.h>
static const gchar *QUEUE_SETTINGS[Q_MODEL_N_COLUMNS] = {
[Q_MODEL_TRACK_NR] = "gui.queue.track",
[Q_MODEL_TITLE] = "gui.queue.title",
[Q_MODEL_LENGTH] = "gui.queue.length",
[Q_MODEL_ARTIST] = "gui.queue.artist",
[Q_MODEL_ALBUM] = "gui.queue.album",
[Q_MODEL_YEAR] = "gui.queue.year",
[Q_MODEL_GENRE] = "gui.queue.genre",
[Q_MODEL_COUNT] = "gui.queue.count",
[Q_MODEL_LAST_PLAY] = "gui.queue.played",
[Q_MODEL_FILE_PATH] = "gui.queue.filepath",
[Q_MODEL_FONT] = "gui.queue.font",
};
static const enum compare_t QUEUE_SORT[Q_MODEL_N_COLUMNS] = {
[Q_MODEL_TRACK_NR] = COMPARE_TRACK,
[Q_MODEL_TITLE] = COMPARE_TITLE,
[Q_MODEL_LENGTH] = COMPARE_LENGTH,
[Q_MODEL_ARTIST] = COMPARE_ARTIST,
[Q_MODEL_ALBUM] = COMPARE_ALBUM,
[Q_MODEL_YEAR] = COMPARE_YEAR,
[Q_MODEL_GENRE] = COMPARE_GENRE,
[Q_MODEL_COUNT] = COMPARE_COUNT,
[Q_MODEL_LAST_PLAY] = COMPARE_PLAYED,
};
static GtkTreeView *view_treeview = NULL;
static GtkTreeModelFilter *view_filter = NULL;
static unsigned int view_sort_count = 0;
static bool view_no_scroll = false;
static inline GuiQueueModel *__view_filter_get_model()
{
if (view_filter == NULL)
return NULL;
return GUI_QUEUE_MODEL(gtk_tree_model_filter_get_model(view_filter));
}
static inline struct queue *__view_filter_get_queue()
{
if (view_filter == NULL)
return NULL;
return __view_filter_get_model()->gqm_queue;
}
static inline GtkTreePath *__view_filter_convert_path(GtkTreePath *orig)
{
return gtk_tree_model_filter_convert_path_to_child_path(view_filter, orig);
}
static struct track *__view_filter_get_track(GtkTreePath *orig)
{
GuiQueueModel *model = __view_filter_get_model();
GtkTreePath *real = __view_filter_convert_path(orig);
struct track *track = gui_queue_model_path_get_track(model, real);
gtk_tree_path_free(real);
return track;
}
static unsigned int __view_filter_get_index(GtkTreePath *orig)
{
GtkTreePath *real = __view_filter_convert_path(orig);
unsigned int ret = gtk_tree_path_get_indices(real)[0];
gtk_tree_path_free(real);
return ret;
}
static unsigned int __view_get_column_index(GtkTreeViewColumn *col)
{
unsigned int i;
for (i = 0; i < Q_MODEL_N_COLUMNS; i++) {
if (col == gtk_tree_view_get_column(view_treeview, i))
return i;
}
return Q_MODEL_N_COLUMNS;
}
static inline void __view_display_sorting(gchar *text)
{
gtk_label_set_text(GTK_LABEL(gui_builder_widget("o_sorting")), text);
}
static int __view_dec_sort(gpointer data)
{
if (view_sort_count > 0)
view_sort_count--;
if (view_sort_count == 0)
__view_display_sorting("");
return FALSE;
}
static void __view_set_column_sort_indicator(GtkTreeViewColumn *col,
unsigned int index)
{
struct queue *queue = __view_filter_get_queue();
GSList *cur = queue ? queue->q_sort : NULL;
unsigned int order = GTK_SORT_ASCENDING;
bool show = false;
int field;
while (cur) {
order = GTK_SORT_ASCENDING;
field = GPOINTER_TO_INT(cur->data);
if (abs(field) == QUEUE_SORT[index]) {
show = true;
if (field < 0)
order = GTK_SORT_DESCENDING;
break;
}
cur = g_slist_next(cur);
}
gtk_tree_view_column_set_sort_indicator(col, show);
gtk_tree_view_column_set_sort_order(col, order);
}
static void __view_set_sort_indicators()
{
GtkTreeViewColumn *col;
for (unsigned int i = 0; i < Q_MODEL_N_COLUMNS; i++) {
col = gtk_tree_view_get_column(view_treeview, i);
if (col)
__view_set_column_sort_indicator(col, i);
}
}
void __view_row_activated(GtkTreeView *treeview, GtkTreePath *path,
GtkTreeViewColumn *col, gpointer data)
{
view_no_scroll = true;
audio_load(__view_filter_get_track(path));
queue_selected(__view_filter_get_model()->gqm_queue,
gtk_tree_path_get_indices(path)[0]);
view_no_scroll = false;
}
void __view_column_resized(GtkTreeViewColumn *col, GParamSpec *pspec,
gpointer data)
{
unsigned int index = __view_get_column_index(col);
gui_settings_set(QUEUE_SETTINGS[index],
gtk_tree_view_column_get_width(col));
}
void __view_column_clicked(GtkTreeViewColumn *col, gpointer data)
{
struct queue *queue = __view_filter_get_queue();
unsigned int index = __view_get_column_index(col);
gchar *text;
if (!queue || queue_has_flag(queue, Q_NO_SORT))
return;
queue_sort(queue, QUEUE_SORT[index], view_sort_count == 0);
if (view_sort_count == 0) {
text = g_strdup_printf("Sorting within %s",
gtk_tree_view_column_get_title(col));
__view_display_sorting(text);
g_free(text);
}
__view_set_sort_indicators();
view_sort_count++;
g_timeout_add_seconds(3, __view_dec_sort, NULL);
}
static void __view_add_to_queue(GtkTreeModel *model, GtkTreePath *path,
GtkTreeIter *iter, gpointer data)
{
queue_add(data, __view_filter_get_track(path));
}
static void __view_add_to_playlist(GtkTreeModel *model, GtkTreePath *path,
GtkTreeIter *iter, gpointer data)
{
playlist_add(GPOINTER_TO_UINT(data), __view_filter_get_track(path));
}
static void __view_delete_selection(GtkTreeSelection *selection)
{
struct queue *queue = __view_filter_get_queue();
GList *rows = gtk_tree_selection_get_selected_rows(selection, NULL);
GList *cur = g_list_reverse(rows);
while (cur) {
queue_erase(queue, __view_filter_get_index(cur->data));
cur = g_list_next(cur);
}
g_list_free_full(rows, (GDestroyNotify) gtk_tree_path_free);
}
static void __view_process_selection(GtkTreeView *treeview, unsigned int keyval)
{
GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
unsigned int flags = 0;
struct queue *queue;
switch (keyval) {
case GDK_KEY_KP_0 ... GDK_KEY_KP_9:
keyval = (keyval - GDK_KEY_KP_0) + GDK_KEY_0;
case GDK_KEY_0 ... GDK_KEY_9:
queue = tempq_get(keyval - GDK_KEY_0);
if (queue)
gtk_tree_selection_selected_foreach(selection,
__view_add_to_queue, queue);
break;
case GDK_KEY_f:
gtk_tree_selection_selected_foreach(selection,
__view_add_to_playlist,
GUINT_TO_POINTER(PL_FAVORITED));
break;
case GDK_KEY_r:
flags = Q_RANDOM;
case GDK_KEY_q:
queue = tempq_alloc(flags);
gtk_tree_selection_selected_foreach(selection,
__view_add_to_queue, queue);
break;
case GDK_KEY_Delete:
__view_delete_selection(selection);
break;
}
}
void __view_keypress(GtkTreeView *treeview, GdkEventKey *event, gpointer data)
{
__view_process_selection(treeview, event->keyval);
}
void __view_rc_new_queue(GtkMenuItem *item, gpointer data)
{
__view_process_selection(view_treeview, GDK_KEY_q);
}
void __view_rc_add_to_queue(GtkMenuItem *item, gpointer data)
{
unsigned int i;
gchar *name;
for (i = 0; i < 10; i++) {
name = g_strdup_printf("o_queue_%d", i);
if (GTK_WIDGET(item) == gui_builder_widget(name))
__view_process_selection(view_treeview, GDK_KEY_0 + i);
g_free(name);
}
}
void __view_rc_add_favorites(GtkMenuItem *item, gpointer data)
{
__view_process_selection(view_treeview, GDK_KEY_f);
}
void __view_rc_add_hidden(GtkMenuItem *item, gpointer data)
{
GtkTreeSelection *selection = gtk_tree_view_get_selection(view_treeview);
GList *rows = gtk_tree_selection_get_selected_rows(selection, NULL);
GList *cur = g_list_reverse(rows);
while (cur) {
collection_ban(__view_filter_get_track(cur->data));
cur = g_list_next(cur);
}
g_list_free_full(rows, (GDestroyNotify) gtk_tree_path_free);
}
bool __view_button_press(GtkTreeView *treeview, GdkEventButton *event,
gpointer data)
{
GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
GtkMenu *menu = GTK_MENU(gui_builder_widget("o_menu"));
unsigned int i, size = tempq_count();
GtkTreePath *path;
gchar *name;
if (event->button != 3)
return false;
/* Select path if it isn't already selected */
if (gtk_tree_view_get_path_at_pos(treeview, event->x, event->y,
&path, NULL, NULL, NULL))
{
gtk_tree_selection_select_path(selection, path);
gtk_tree_path_free(path);
}
/* Determine which menu items can be shown */
gtk_widget_set_visible(gui_builder_widget("o_new_queue"), size < 10);
gtk_widget_set_visible(gui_builder_widget("o_add_to_queue"), size > 0);
for (i = 0; i < 10; i++) {
name = g_strdup_printf("o_queue_%d", i);
gtk_widget_set_visible(gui_builder_widget(name), i < size);
g_free(name);
}
gtk_menu_popup(menu, NULL, NULL, NULL, NULL, event->button, event->time);
return true;
}
void gui_view_init()
{
GtkTreeViewColumn *col;
int i, pos;
view_treeview = GTK_TREE_VIEW(gui_builder_widget("o_treeview"));
for (i = 0; i < Q_MODEL_N_COLUMNS; i++) {
col = gtk_tree_view_get_column(view_treeview, i);
pos = gui_settings_get(QUEUE_SETTINGS[i]);
if (col && pos > 0)
gtk_tree_view_column_set_fixed_width(col, pos);
}
}
void gui_view_set_model(GtkTreeModelFilter *filter)
{
view_filter = filter;
gtk_tree_view_set_model(view_treeview, GTK_TREE_MODEL(filter));
view_sort_count = 0;
__view_display_sorting("");
__view_set_sort_indicators();
gui_view_scroll();
}
void gui_view_scroll()
{
struct queue *queue = __view_filter_get_queue();
GtkTreePath *real, *path;
if (!queue || (int)queue->q_cur.it_pos < 0 || view_no_scroll)
return;
real = gtk_tree_path_new_from_indices(queue->q_cur.it_pos, -1);
path = gtk_tree_model_filter_convert_child_path_to_path(view_filter, real);
if (!path)
goto out;
gtk_tree_view_set_cursor(view_treeview, path, NULL, false);
gtk_tree_view_scroll_to_cell(view_treeview, path, NULL, TRUE, 0.5, 0.5);
gtk_tree_path_free(path);
out:
gtk_tree_path_free(real);
}

View File

@ -1,31 +1,47 @@
/*
* Copyright 2014 (c) Anna Schumaker.
*/
#include <core/settings.h>
#include <core/version.h>
#include <gui/builder.h>
#include <gui/settings.h>
#include <gui/window.h>
static const gchar *SETTINGS_WIDTH = "gui.window.width";
static const gchar *SETTINGS_HEIGHT = "gui.window.height";
static const gchar *SETTINGS_X = "gui.window.x";
static const gchar *SETTINGS_Y = "gui.window.y";
static int saved_width = 0;
static int saved_height = 0;
static int saved_x = 0;
static int saved_y = 0;
gboolean __window_configure(GtkWindow *window, GdkEventConfigure *event,
gpointer data)
{
int width = gui_settings_get(SETTINGS_WIDTH);
int height = gui_settings_get(SETTINGS_HEIGHT);
int width = settings_get(SETTINGS_WIDTH);
int height = settings_get(SETTINGS_HEIGHT);
int x = settings_get(SETTINGS_X);
int y = settings_get(SETTINGS_Y);
if (event->width != width) {
saved_width = width;
gui_settings_set(SETTINGS_WIDTH, event->width);
settings_set(SETTINGS_WIDTH, event->width);
}
if (event->height != height) {
saved_height = height;
gui_settings_set(SETTINGS_HEIGHT, event->height);
settings_set(SETTINGS_HEIGHT, event->height);
}
if (event->x != x) {
saved_x = x;
settings_set(SETTINGS_X, event->x);
}
if (event->y != y) {
saved_y = y;
settings_set(SETTINGS_Y, event->y);
}
return FALSE;
@ -36,8 +52,10 @@ gboolean __window_state(GtkWindow *window, GdkEventWindowState *event,
{
if (event->changed_mask & GDK_WINDOW_STATE_MAXIMIZED &&
event->new_window_state & GDK_WINDOW_STATE_MAXIMIZED) {
gui_settings_set(SETTINGS_WIDTH, saved_width);
gui_settings_set(SETTINGS_HEIGHT, saved_height);
settings_set(SETTINGS_WIDTH, saved_width);
settings_set(SETTINGS_HEIGHT, saved_height);
settings_set(SETTINGS_X, saved_x);
settings_set(SETTINGS_Y, saved_y);
}
return FALSE;
@ -46,21 +64,25 @@ gboolean __window_state(GtkWindow *window, GdkEventWindowState *event,
void gui_window_init(const gchar *icon)
{
GtkWindow *window = GTK_WINDOW(gui_builder_widget("o_window"));
gchar *title = g_strdup_printf("Ocarina %s", get_version());
gtk_window_set_title(window, title);
gtk_window_set_icon_from_file(window, icon, NULL);
gtk_window_set_title(gui_window(), title);
gtk_window_set_icon_from_file(gui_window(), icon, NULL);
saved_width = gui_settings_get(SETTINGS_WIDTH);
saved_height = gui_settings_get(SETTINGS_HEIGHT);
if (saved_width > 0 && saved_height > 0)
gtk_window_resize(window, saved_width, saved_height);
saved_width = settings_get(SETTINGS_WIDTH);
saved_height = settings_get(SETTINGS_HEIGHT);
saved_x = settings_get(SETTINGS_X);
saved_y = settings_get(SETTINGS_Y);
if (saved_width > 0 || saved_height > 0)
gtk_window_resize(gui_window(), saved_width, saved_height);
if (saved_x > 0 || saved_y > 0)
gtk_window_move(gui_window(), saved_x, saved_y);
g_free(title);
}
void gui_window_deinit()
{
gtk_widget_destroy(gui_builder_widget("o_window"));
gtk_widget_destroy(GTK_WIDGET(gui_window()));
}

View File

@ -1,18 +0,0 @@
#!/usr/bin/python
import subprocess
Import("env")
version = str(env.Version)
if env.Debug == True:
version += "-debug"
try:
devnull = open("/dev/null", "w")
diff = subprocess.check_output("git diff $(git describe --abbrev=0)",
stderr=devnull, shell=True)
if diff != "":
version += "+"
except:
pass
env.Append( CCFLAGS = [ "-DCONFIG_VERSION='\"%s\"'" % version ] )

View File

@ -11,27 +11,31 @@
#include <gst/gst.h>
struct audio_ops {
struct audio_callbacks {
/* Called when a track is loaded. */
void (*on_load)(struct track *);
void (*audio_cb_load)(struct track *);
/* Called when playback state changes. */
void (*on_state_change)(GstState);
void (*audio_cb_state_change)(GstState);
/* Called when the automatic pause state changes. */
void (*on_config_pause)(int);
void (*audio_cb_config_pause)(int);
};
/* Called to initialize the audio manager. */
void audio_init(int *, char ***, struct audio_ops *);
void audio_init(int *, char ***, struct audio_callbacks *);
/* Called to deinitialize the audio manager. */
void audio_deinit();
/* Called to force-save the current track. */
void audio_save();
/* Called to load a track for playback. */
/* Called to load either a track or file for playback. */
bool audio_load(struct track *);
bool audio_load_filepath(const gchar *);
/* Called to get the current track. */
struct track *audio_cur_track();
@ -40,6 +44,13 @@ struct track *audio_cur_track();
GstState audio_cur_state();
/* Called to set the playback volume. */
void audio_set_volume(unsigned int);
/* Called to get the playback volume. */
unsigned int audio_get_volume();
/* Called to begin playback. */
bool audio_play();
@ -62,16 +73,16 @@ struct track *audio_next();
/* Called to load the previous track. */
struct track *audio_prev();
/* Called when playback has reached the end-of-stream position. */
struct track *audio_eos();
/* Called when gstreamer has received an error. */
void audio_error(GstMessage *);
/* Called to configure automatic pausing. */
void audio_pause_after(int);
/*
* Called to configure automatic pausing.
* Returns true if the value has been changed.
*/
bool audio_pause_after(int);
int audio_get_pause_count(void);
#ifdef CONFIG_TESTING
GstElement *test_audio_player();
void test_audio_eos();
void test_audio_error(GError *, gchar *);
GstElement *test_audio_pipeline();
#endif /* CONFIG_TESTING */
#endif /* OCARINA_CORE_AUDIO_H */

View File

@ -1,62 +0,0 @@
/*
* Copyright 2013 (c) Anna Schumaker.
*
* The collection manager is in charge of creating, updating, and removing
* Library and Track tags. This code also manages a special queue used by
* the GUI to display all tracks in the collection.
*
* The entire collection queue does not need to be saved, so instead the
* collection manager only stores flags and the current sort order:
*
* flags sort.length ... sort[N].field sort[N].ascending ...
*/
#ifndef OCARINA_CORE_COLLECTION_H
#define OCARINA_CORE_COLLECTION_H
#include <core/queue.h>
/* Called to initialize the collection manager. */
void collection_init(struct queue_ops *);
/* Called to deinitialize the collection manager. */
void collection_deinit();
/* Called to save the collection queue. */
void collection_save(struct queue *, enum queue_flags);
/* Called to add a new library directory to the collection manager. */
struct library *collection_add(const gchar *);
/* Called to remove a library directory from the collection manager. */
void collection_remove(struct library *);
/* Called to update a library directory. */
void collection_update(struct library *);
/* Called to update all library paths. */
void collection_update_all();
/* Called to ban a track from the collection. */
bool collection_ban(struct track *);
/* Called to unban a track from the collection. */
bool collection_unban(struct track *);
/* Called to enable or disable a library directory. */
void collection_set_enabled(struct library *, bool);
/* Called to check if the library path is good. */
int collection_check_library(struct library *);
/* Called to access the collection queue. */
struct queue *collection_get_queue();
#ifdef CONFIG_TESTING
extern bool test_collection_error;
#endif /* CONFIG_TESTING */
#endif /* OCARINA_CORE_COLLECTION_H */

View File

@ -1,38 +0,0 @@
/*
* Copyright 2014 (c) Anna Schumaker.
*
* The struct index_entry is used to associate a database key
* with a set of integers, creating an inverted index.
*/
#ifndef OCARINA_CORE_CONTAINERS_INDEX_H
#define OCARINA_CORE_CONTAINERS_INDEX_H
#include <core/containers/database.h>
#include <core/containers/set.h>
struct index_entry {
gchar *ie_key;
struct set ie_set;
struct db_entry ie_dbe;
};
#define INDEX_ENTRY(dbe) ((struct index_entry *)DBE_DATA(dbe))
/* Initialize a database for use as an index. */
void index_init(struct database *, const gchar *, bool);
/* Add a value to an index item with the specified key. */
struct index_entry *index_insert(struct database *, const gchar *, unsigned int);
/* Remove a value from an index item with the specified key. */
void index_remove(struct database *, const gchar *, unsigned int);
/* Called to check if the index has the specified (key, value) pair. */
bool index_has(struct database *, const gchar *, unsigned int);
#ifdef CONFIG_TESTING
const struct db_ops *test_index_ops();
#endif /* CONFIG_TESTING */
#endif /* OCARINA_CORE_CONTAINERS_INDEX_H */

View File

@ -1,108 +0,0 @@
/*
* Copyright 2015 (c) Anna Schumaker.
*/
#ifndef OCARINA_CORE_CONTAINERS_QUEUE_H
#define OCARINA_CORE_CONTAINERS_QUEUE_H
#include <glib.h>
struct _queue {
GQueue _queue;
};
struct _q_iter {
guint it_pos;
GList *it_iter;
};
/* Called to initialize a queue iterator. */
static inline void _q_iter_init(const struct _queue *queue, struct _q_iter *it)
{
it->it_iter = g_list_first(queue->_queue.head);
it->it_pos = 0;
}
/* Called to advance a queue iterator by one step. */
static inline void _q_iter_next(struct _q_iter *it)
{
it->it_iter = g_list_next(it->it_iter);
it->it_pos++;
}
/* Called to rewind a queue iterator by one step. */
static inline void _q_iter_prev(struct _q_iter *it)
{
it->it_iter = g_list_previous(it->it_iter);
it->it_pos--;
}
/* Called to set a queue iterator to a specific position. */
static inline void _q_iter_set(struct _queue *queue, struct _q_iter *it,
unsigned int pos)
{
it->it_iter = g_queue_peek_nth_link(&queue->_queue, pos);
it->it_pos = pos;
}
/* Called to access the value of a queue iterator. */
static inline gpointer _q_iter_val(struct _q_iter *it)
{
return (it->it_iter) ? it->it_iter->data : NULL;
}
#define _q_for_each(queue, it) \
for (_q_iter_init(queue, it); (it)->it_iter; _q_iter_next(it))
#define _Q_INIT() \
{ \
._queue = G_QUEUE_INIT, \
}
/* Called to initialize a queue. */
static inline void _q_init(struct _queue *queue)
{
g_queue_init(&queue->_queue);
}
/* Called to find the size of a queue. */
static inline guint _q_size(struct _queue *queue)
{
return g_queue_get_length(&queue->_queue);
}
/* Called to add an item to the head of a queue. */
static inline guint _q_add_head(struct _queue *queue, gpointer data)
{
g_queue_push_head(&queue->_queue, data);
return 0;
}
/* Called to add an item to the tail of a queue. */
static inline guint _q_add_tail(struct _queue *queue, gpointer data)
{
g_queue_push_tail(&queue->_queue, data);
return _q_size(queue) - 1;
}
/* Called to add an item to a sorted queue. */
guint _q_add_sorted(struct _queue *, gpointer, GCompareDataFunc, gpointer);
/* Called to remove all items from a queue */
static inline void _q_clear(struct _queue *queue)
{
g_queue_clear(&queue->_queue);
}
/* Called to remove an item by iterator. */
gpointer _q_remove_it(struct _queue *, struct _q_iter *);
/* Called to sort the queue. */
static inline void _q_sort(struct _queue *queue, GCompareDataFunc func,
gpointer user_data)
{
g_queue_sort(&queue->_queue, func, user_data);
}
#endif /* OCARINA_CORE_CONTAINERS_QUEUE_H */

View File

@ -1,91 +0,0 @@
/*
* Copyright 2015 (c) Anna Schumaker.
*/
#ifndef OCARINA_CORE_CONTAINERS_SET_H
#define OCARINA_CORE_CONTAINERS_SET_H
#include <core/file.h>
#include <glib.h>
#include <stdbool.h>
struct set {
GHashTable *s_set;
};
struct set_iter {
unsigned int it_val;
GHashTableIter it_iter;
};
#define SET_INIT() \
{ \
.s_set = g_hash_table_new(g_direct_hash, g_direct_equal), \
}
static inline void set_init(struct set *set)
{
set->s_set = g_hash_table_new(g_direct_hash, g_direct_equal);
}
static inline void set_deinit(struct set *set)
{
g_hash_table_destroy(set->s_set);
}
static inline void set_insert(struct set *set, unsigned int value)
{
g_hash_table_add(set->s_set, GUINT_TO_POINTER(value));
}
static inline void set_remove(struct set *set, unsigned int value)
{
g_hash_table_remove(set->s_set, GUINT_TO_POINTER(value));
}
static inline void set_clear(struct set *set)
{
g_hash_table_remove_all(set->s_set);
}
static inline bool set_has(const struct set *set, unsigned int value)
{
return g_hash_table_contains(set->s_set, GUINT_TO_POINTER(value));
}
static inline unsigned int set_size(struct set *set)
{
return g_hash_table_size(set->s_set);
}
/* Copy values from set1 into set2. */
void set_copy(const struct set *, struct set *);
/* Remove values from set2 that are not also in set1. */
void set_inline_intersect(const struct set *, struct set *);
/* Read values from file. */
void set_read(struct file *, struct set *);
/* Write values to file. */
void set_write(struct file *, struct set *);
static inline bool set_iter_next(struct set_iter *it)
{
gpointer key;
bool ret = g_hash_table_iter_next(&it->it_iter, &key, NULL);
it->it_val = GPOINTER_TO_INT(key);
return ret;
}
static inline void set_iter_init(const struct set *set, struct set_iter *it)
{
g_hash_table_iter_init(&it->it_iter, set->s_set);
}
#define set_for_each(set, it) \
for (set_iter_init(set, it); set_iter_next(it); )
#endif /* OCARINA_CORE_CONTAINERS_SET_H */

View File

@ -3,20 +3,16 @@
*/
#ifndef OCARINA_CORE_CORE_H
#define OCARINA_CORE_CORE_H
#include <core/queue.h>
struct core_init_data {
struct queue_ops *collection_ops;
struct queue_ops *history_ops;
struct queue_ops *playlist_ops;
struct queue_ops *tempq_ops;
struct audio_ops *audio_ops;
};
#include <core/audio.h>
#include <core/idle.h>
#include <core/playlist.h>
#include <core/settings.h>
#include <core/tags/tags.h>
#include <stdbool.h>
/* Called to initialize all core Ocarina components. */
void core_init(int *, char ***, struct core_init_data *);
void core_init(int *, char ***, struct playlist_callbacks *,
struct audio_callbacks *, enum idle_sync_t);
/* Called to deinitialize all core Ocarina componentns. */
void core_deinit();

View File

@ -48,7 +48,7 @@ static inline void *DBE_DATA(struct db_entry *dbe)
struct db_ops {
/* Allocate a new struct db_entry from a given key. */
struct db_entry *(*dbe_alloc)(const gchar *);
struct db_entry *(*dbe_alloc)(const gchar *, unsigned int);
/* Free a struct db_entry. */
void (*dbe_free)(struct db_entry *);
@ -57,10 +57,7 @@ struct db_ops {
gchar *(*dbe_key)(struct db_entry *);
/* Read a single struct db_entry from disk. */
struct db_entry *(*dbe_read)(struct file *);
/* Set up a struct db_entry after adding to the database. */
void (*dbe_setup)(struct db_entry *);
struct db_entry *(*dbe_read)(struct file *, unsigned int);
/* Write a single struct db_entry to disk. */
void (*dbe_write)(struct file *, struct db_entry *);
@ -77,11 +74,11 @@ struct database {
const struct db_ops *db_ops; /* The database's operations vector. */
};
#define DB_INIT(fname, autosave, ops) \
#define DB_INIT(fname, autosave, ops, fmin) \
{ \
.db_size = 0, \
.db_autosave = autosave, \
.db_file = FILE_INIT(fname, 0), \
.db_file = FILE_INIT_DATA("", fname, fmin), \
.db_entries = g_ptr_array_new(), \
.db_keys = g_hash_table_new(g_str_hash, g_str_equal), \
.db_ops = ops, \
@ -92,7 +89,8 @@ struct database {
* Initialize a database using filepath as a location on disk to store data
* and autosave as a hint for if this database should be automatically saved.
*/
void db_init(struct database *, const char *, bool, const struct db_ops *);
void db_init(struct database *, const char *, bool, const struct db_ops *,
unsigned int);
/* Called to prevent memory leaks by freeing all remaining database entries. */
void db_deinit(struct database *);
@ -106,9 +104,6 @@ void db_autosave(struct database *);
/* Called to read the database from disk. */
void db_load(struct database *);
/* Called to read the database from disk, but only when idle. */
void db_load_idle(struct database *);
/* Returns the size of the backing std::vector. */
unsigned int db_actual_size(const struct database *);
@ -122,9 +117,18 @@ struct db_entry *db_insert(struct database *, const gchar *);
/* Called to remove an item from the database. */
void db_remove(struct database *, struct db_entry *);
/*
* Called to shrink the database by removing any NULL pointers without
* changing the order of items in the database.
* Returns true if the database has been modified.
*/
bool db_defrag(struct database *);
/* Called to change the key of a database entry. */
void db_rekey(struct database *, struct db_entry *);
/* Returns the database item at the requested index. */
struct db_entry *db_at(struct database *, unsigned int);
struct db_entry *db_at(const struct database *, unsigned int);
/* Returns the database item with the specified key. */
struct db_entry *db_get(struct database *, const gchar *);

View File

@ -5,12 +5,18 @@
#define OCARINA_CORE_DATE_H
#include <core/file.h>
#include <stdint.h>
struct date {
unsigned int d_year;
unsigned int d_month;
unsigned int d_day;
union {
struct {
uint16_t d_year;
uint8_t d_month;
uint8_t d_day;
};
uint32_t d_stamp;
};
};
@ -22,9 +28,11 @@ void date_today(struct date *);
/* Read the date from file. */
void date_read(struct file *, struct date *);
void date_read_stamp(struct file *, struct date *);
/* Write the date to file. */
void date_write(struct file *, struct date *);
void date_write_stamp(struct file *, struct date *);
/*
* Convert the date into a string.

View File

@ -23,44 +23,61 @@
#ifndef OCARINA_CORE_FILE_H
#define OCARINA_CORE_FILE_H
#include <core/version.h>
#include <glib.h>
#include <glib/gstdio.h>
#include <stdbool.h>
#define FILE_MAX_LEN 16
enum open_mode {
OPEN_READ,
OPEN_WRITE,
CLOSED, /* File is not open. */
OPEN_READ, /* File is open for reading text. */
OPEN_READ_BINARY, /* File is open for reading binary data. */
OPEN_WRITE, /* File is open for writing text. */
OPEN_WRITE_BINARY, /* File is open for writing binary data. */
};
struct file {
enum open_mode f_mode; /* The file's current open mode. */
unsigned int f_version; /* The file's current data version. */
unsigned int f_prev; /* The file's on-disk data version. */
FILE *f_file; /* The file's IO stream. */
gchar f_name[FILE_MAX_LEN]; /* The file's basename. */
FILE *f_file; /* The file's IO stream. */
const gchar *f_name; /* The file's basename. */
const gchar *f_subdir; /* The file's subdirectory. */
enum open_mode f_mode; /* The file's current open mode. */
unsigned int f_version; /* The file's current data version. */
unsigned int f_prev; /* The file's on-disk data version. */
unsigned int f_min; /* The file's minimum data version. */
const gchar *(*f_user_dir)(void); /* The file's user directory. */
};
#define FILE_INIT(fname, version) \
{ \
.f_mode = OPEN_READ, \
.f_version = version, \
.f_prev = 0, .f_file = NULL, \
.f_name = fname, \
#define FILE_INIT_DATA(fdir, fname, min) \
{ \
.f_file = NULL, \
.f_name = fname, \
.f_subdir = fdir, \
.f_mode = CLOSED, \
.f_version = OCARINA_MINOR_VERSION, \
.f_prev = 0, \
.f_min = min, \
.f_user_dir = g_get_user_data_dir, \
}
/* Initialize a new file object. */
void file_init(struct file *, const gchar *, unsigned int);
/* Initialize a file object. */
void file_init_data(struct file *, const gchar *, const gchar *, unsigned int);
void file_init_cache(struct file *, const gchar *, const gchar *);
/*
* Returns the full path of the file or an empty string if filename is not set.
* This function allocates a new string that MUST be freed with g_free().
* NOTE: This function allocates a new string that MUST be freed with g_free().
*/
gchar *file_path(struct file *);
/*
* Returns the path to the temporary file used for writes.
* NOTE: This function allocates a new string that MUST be freed with g_free().
*/
gchar *file_write_path(struct file *);
/* Returns the version number of the file. */
const unsigned int file_version(struct file *);
@ -71,32 +88,42 @@ bool file_exists(struct file *);
* Call to open a file for either reading or writing. Callers
* are expected to call file_close() when IO is completed.
*
* When opening a file for reading (OPEN_READ):
* - Check if the file exists.
* - Read in file->_prev_version from the start of the file.
* When opening a file for reading (OPEN_READ / OPEN_READ_BINARY):
* - Check if the file exists
* - If open for reading text (OPEN_READ):
* - Read in file->_prev_version from the start of the file.
*
* When opening a file for writing (OPEN_WRITE):
* When opening a file for writing (OPEN_WRITE / OPEN_WRITE_BINARY):
* - Create missing directories as needed.
* - Write file->_version to the start of the file.
* - Open a temporary file to protect data if Ocarina crashes.
* - If open for writing text (OPEN_WRITE):
* - Write file->_version to the start of the file (data files only).
*
* Returns true if the open was successful and false otherwise.
*/
bool file_open(struct file *, enum open_mode);
/* Close an open file, setting file->f_file to NULL. */
/*
* Closes an open file, setting file->f_file to NULL and file->f_mode
* to CLOSED. If the file was opened for writing, then rename the
* temporary file to file_path().
*/
void file_close(struct file *);
/*
* Read an entire line from the file and return it to the caller.
* This function allocates a new string that MUST be freed with g_free().
* Called to read an unsigned int, signed int, single word, or entire
* line from the file.
* NOTE: file_readw() and file_readl() both return a new string that
* MUST be freed with g_free()
*/
gchar *file_readw(struct file *);
gchar *file_readl(struct file *);
/*
* Read from a file with an fscanf(3) style format string.
* Returns the number of items matched.
*/
int file_readf(struct file *, const char *, ...);
unsigned int file_readu(struct file *);
static inline int file_readd(struct file *file)
{ return (int)file_readu(file); }
static inline unsigned short int file_readhu(struct file *file)
{ return (unsigned short int)file_readu(file); }
/*
* Write to a file with an fprintf(3) style format string.
@ -104,4 +131,22 @@ int file_readf(struct file *, const char *, ...);
*/
int file_writef(struct file *, const char *, ...);
/*
* Reads the contents of a file as binary data.
* NOTE: This function returns a new string which MUST be freed with g_free().
*/
gchar *file_read(struct file *);
/*
* Write binary data a cache file, similar to fwrite(3).
* Returns the number of bytes successfully written.
*/
int file_write(struct file *, const void *, size_t);
/* Import a file into the cache. */
bool file_import(struct file *, const gchar *);
/* Removes a closed file from disk. */
bool file_remove(struct file *);
#endif /* OCARINA_CORE_FILE_H */

View File

@ -1,24 +0,0 @@
/*
* Copyright 2013 (c) Anna Schumaker.
*
* The filter layer is used to search for a subset of
* songs based on an input string.
*/
#ifndef OCARINA_CORE_FILTER_H
#define OCARINA_CORE_FILTER_H
#include <core/containers/set.h>
/* Called to initialize the filter index. */
void filter_init();
/* Called to clean up the filter index. */
void filter_deinit();
/* Add the input string to the index. */
void filter_add(const gchar *, unsigned int);
/* Search for the input string in the index. */
void filter_search(const gchar *, struct set *);
#endif /* OCARINA_CORE_FILTER_H */

View File

@ -1,26 +0,0 @@
/*
* Copyright 2015 (c) Anna Schumaker.
*/
#ifndef OCARINA_CORE_HISTORY_H
#define OCARINA_CORE_HISTORY_H
#include <core/queue.h>
/* Called to initialize the history queue. */
void history_init(struct queue_ops *);
/* Called to deinitialize the history queue. */
void history_deinit();
/* Called to add a track to the history queue. */
void history_add(struct track *);
/* Called to pick a track from the history. */
struct track *history_prev();
/* Called to access the queue of recent tracks. */
struct queue *history_get_queue();
#endif /* OCARINA_CORE_HISTORY_H */

View File

@ -15,8 +15,22 @@
#define OCARINA_CORE_IDLE_H
#include <stdbool.h>
enum idle_sync_t {
IDLE_SYNC, /* Run task in the main thread. */
IDLE_ASYNC, /* Run task in a separate thread. */
};
#define IDLE_FUNC(x) ((bool (*)(void *))x)
/* Called to initialize the idle queue. */
void idle_init(enum idle_sync_t);
/* Called to deinitialize the idle queue. */
void idle_deinit();
/* Called to schedule a function to run later. */
void idle_schedule(void (*)(void *), void *);
void idle_schedule(enum idle_sync_t, bool (*)(void *), void *);
/*
* Called to run the next task on the idle queue.
@ -27,7 +41,4 @@ bool idle_run_task();
/* Called to find the percentage of idle tasks that have been run. */
float idle_progress();
/* Called to cancel all idle tasks on the queue. */
void idle_cancel();
#endif /* OCARINA_CORE_IDLE_H */

View File

@ -7,40 +7,75 @@
*/
#ifndef OCARINA_CORE_PLAYLIST_H
#define OCARINA_CORE_PLAYLIST_H
#include <core/playlists/artist.h>
#include <core/playlists/generic.h>
#include <core/playlists/library.h>
#include <core/playlists/system.h>
#include <core/playlists/user.h>
#include <core/containers/index.h>
#include <core/queue.h>
enum playlist_t {
PL_FAVORITED, /* Songs that the user likes. */
PL_HIDDEN, /* Songs that the user has hidden. */
PL_UNPLAYED, /* Songs that have not been played yet. */
PL_MOST_PLAYED, /* Songs with an above average play count. */
PL_LEAST_PLAYED, /* Songs with a below average play count. */
};
/* Called to initialize the playlist manager. */
void playlist_init(struct queue_ops *);
void playlist_init(struct playlist_callbacks *);
/* Called to deinitialize the playlist manager. */
void playlist_deinit();
/* Called to force-save all playlists. */
void playlist_save();
/* Called to notify all playlists that a track has been played. */
void playlist_played(struct track *);
/* Called to notify all playlists that a track has been selected. */
void playlist_selected(struct track *);
/* Called to create a new playlist. */
struct playlist *playlist_new(enum playlist_type_t, const gchar *);
/* Called to delete a playlist. */
bool playlist_delete(struct playlist *);
/* Called to look up playlists either by name or id. */
struct playlist *playlist_lookup(enum playlist_type_t, const gchar *);
struct playlist *playlist_get(enum playlist_type_t, unsigned int);
/* Called to access the current playlist. */
struct playlist *playlist_current(void);
/* Called to select the current playlist. */
bool playlist_select(struct playlist *);
/* Called to get the next track from the default playlist. */
struct track *playlist_next(void);
/* Called to get a previously played track. */
struct track *playlist_prev(void);
/* Called to add a track to a playlist. */
bool playlist_add(enum playlist_t, struct track *);
bool playlist_add(struct playlist *, struct track *);
/* Called to remove a track from a playlist. */
bool playlist_remove(enum playlist_t, struct track *);
bool playlist_remove(struct playlist *, struct track *);
/* Called to check if a specific track is in the playlist. */
bool playlist_has(enum playlist_t, struct track *);
/* Called to fill the queue with a specific playlist. */
void playlist_select(enum playlist_t);
bool playlist_has(struct playlist *, struct track *);
/* Called to access the playlist queue. */
struct queue *playlist_get_queue();
/* Called to set the playlist's random flag. */
void playlist_set_random(struct playlist *, bool);
/* Called to change the sort order of the playlist. */
bool playlist_sort(struct playlist *, enum compare_t);
/* Called to manually rearrange the order of the playlist. */
bool playlist_rearrange(struct playlist *, unsigned int, unsigned int);
/* Called to set the playlist's search text */
void playlist_set_search(struct playlist *, const gchar *);
#endif /* OCARINA_CORE_PLAYLIST_H */

View File

@ -0,0 +1,24 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#ifndef OCARINA_CORE_PLAYLISTS_ARTIST_H
#define OCARINA_CORE_PLAYLISTS_ARTIST_H
#include <core/playlists/generic.h>
/* Artist playlist type. */
extern struct playlist_type pl_artist;
/* Called to initialize artist playlists. */
void pl_artist_init(void);
/* Called to deinitialize library playlists. */
void pl_artist_deinit();
/* Called to tell system playlists about a new track. */
void pl_artist_new_track(struct track *);
/* Called to tell artist playlists that a track is getting deleted. */
void pl_artist_delete_track(struct track *);
#endif /* OCARINA_CORE_PLAYLISTS_ARTIST_H */

View File

@ -0,0 +1,91 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#ifndef OCARINA_CORE_PLAYLISTS_GENERIC_H
#define OCARINA_CORE_PLAYLISTS_GENERIC_H
#include <core/playlists/iterator.h>
#include <core/playlists/playlist.h>
enum playlist_save_flags {
PL_SAVE_FLAGS = (1 << 0), /* Save playlist random and sort data. */
PL_SAVE_ITER = (1 << 1), /* Save playlist iterator position. */
PL_SAVE_TRACKS = (1 << 2), /* Save playlist tracks. */
};
#define PL_SAVE_METADATA (PL_SAVE_FLAGS | PL_SAVE_ITER)
#define PL_SAVE_ALL (PL_SAVE_TRACKS | PL_SAVE_METADATA)
struct playlist_callbacks {
/* Called to notify that a new playlist has been allocated. */
void (*pl_cb_alloc)(struct playlist *);
/* Called to notify that a track has been added. */
void (*pl_cb_added)(struct playlist *, struct track *);
/*
* Called to notify that N instances of a track have been removed.
* Track may be NULL to indicate that several different tracks were
* removed at once.
*/
void (*pl_cb_removed)(struct playlist *, struct track *, unsigned int n);
/*
* Called to notify that a track has been updated.
* If the track is NULL, then the entire playlist should be updated.
*/
void (*pl_cb_updated)(struct playlist *, struct track *);
};
/* Called to set playlist callbacks. */
void playlist_generic_set_callbacks(struct playlist_callbacks *);
/* Generic playlist init functions. */
void playlist_generic_init(struct playlist *, unsigned int, ...);
/* Generic playlist deinit function. */
void playlist_generic_deinit(struct playlist *);
/* Generic playlist alloc function. */
struct playlist *playlist_generic_alloc(gchar *, enum playlist_type_t,
unsigned int, struct playlist_ops *,
unsigned int, ...);
/* Generic playlist free function. */
void playlist_generic_free(struct playlist *);
/* Generic playlist save function. */
void playlist_generic_save(struct playlist *, struct file *, unsigned int);
/* Generic playlist load function. */
void playlist_generic_load(struct playlist *, struct file *, unsigned int);
/* Generic playlist can-select function. */
bool playlist_generic_can_select(struct playlist *);
/* Generic playlist clear operation. */
void playlist_generic_clear(struct playlist *);
/* Generic playlist add track operations. */
bool playlist_generic_add(struct playlist *, struct track *);
bool playlist_generic_add_front(struct playlist *, struct track *);
/* Generic playlist remove track operation. */
bool playlist_generic_remove(struct playlist *, struct track *);
/* Generic playlist update track operation. */
void playlist_generic_update(struct playlist *, struct track *);
/* Generic playlist set_random operation. */
void playlist_generic_set_random(struct playlist *, bool);
/* Generic playlist sorting operations. */
void playlist_generic_sort(struct playlist *, enum compare_t);
void playlist_generic_resort(struct playlist *);
/* Generic playlist rearranging operation. */
bool playlist_generic_rearrange(struct playlist *, unsigned int, unsigned int);
/* Generic playlist next track operation. */
struct track *playlist_generic_next(struct playlist *);
#endif /* OCARINA_CORE_PLAYLISTS_GENERIC_H */

View File

@ -0,0 +1,83 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*
* NOTE: The playlist_iter type is defined in include/core/playlists/playlist.h
*/
#ifndef OCARINA_CORE_PLAYLISTS_ITERATOR_H
#define OCARINA_CORE_PLAYLISTS_ITERATOR_H
#include <core/playlists/playlist.h>
/* Called to set the playlist iterator to a specific position. */
static inline playlist_iter playlist_iter_get(struct playlist *playlist,
unsigned int n)
{
return playlist ? g_queue_peek_nth_link(&playlist->pl_tracks, n) : NULL;
}
/* Called to advance the requested playlist iterator. */
static inline playlist_iter playlist_iter_next(playlist_iter iter)
{
return g_list_next(iter);
}
/* Called to get a pointer to the track at the requested iterator. */
static inline struct track *playlist_iter_track(playlist_iter iter)
{
return iter ? iter->data : NULL;
}
/* Called to find the playlist index of the requested iterator. */
static inline int playlist_iter_index(struct playlist *playlist,
playlist_iter iter)
{
return (playlist && iter) ? g_queue_link_index(&playlist->pl_tracks, iter) : -1;
}
/* Called to iterate over the entire playlist. */
#define playlist_for_each(playlist, it) \
for (it = playlist_iter_get(playlist, 0); it; it = playlist_iter_next(it))
/* Called to set the index of the current track. */
static inline bool playlist_current_set(struct playlist *playlist,
unsigned int n)
{
if (playlist)
playlist->pl_current = playlist_iter_get(playlist, n);
return playlist && playlist->pl_current;
}
/* Called to advance the current track. */
static inline bool playlist_current_next(struct playlist *playlist)
{
if (playlist)
playlist->pl_current = playlist_iter_next(playlist->pl_current);
return playlist && playlist->pl_current;
}
/* Called to rewind the current track. */
static inline bool playlist_current_previous(struct playlist *playlist)
{
if (playlist)
playlist->pl_current = g_list_previous(playlist->pl_current);
return playlist && playlist->pl_current;
}
/* Called to get a pointer to the current track. */
static inline struct track *playlist_current_track(struct playlist *playlist)
{
return playlist ? playlist_iter_track(playlist->pl_current) : NULL;
}
/* Called to get the playlist index of the current track. */
static inline int playlist_current_index(struct playlist *playlist)
{
return playlist ? playlist_iter_index(playlist, playlist->pl_current) : -1;
}
/* Called to find the nth track on a playlist. */
static inline struct track *playlist_at(struct playlist *playlist, unsigned int n)
{
return playlist_iter_track(playlist_iter_get(playlist, n));
}
#endif /* OCARINA_CORE_PLAYLISTS_ITERATOR_H */

View File

@ -0,0 +1,21 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#ifndef OCARINA_CORE_PLAYLISTS_LIBRARY_H
#define OCARINA_CORE_PLAYLISTS_LIBRARY_H
#include <core/playlists/generic.h>
/* Library playlist type. */
extern struct playlist_type pl_library;
/* Called to initialize library playlists. */
void pl_library_init(void);
/* Called to deinitialize system playlists. */
void pl_library_deinit();
/* Called to update a library path. */
void pl_library_update(struct playlist *);
#endif /* OCARINA_CORE_PLAYLISTS_LIBRARY_H */

View File

@ -0,0 +1,112 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#ifndef OCARINA_CORE_PLAYLISTS_PLAYLIST_H
#define OCARINA_CORE_PLAYLISTS_PLAYLIST_H
#include <core/tags/track.h>
#include <stdbool.h>
typedef GList * playlist_iter;
struct playlist;
enum playlist_type_t {
PL_SYSTEM,
PL_ARTIST,
PL_LIBRARY,
PL_USER,
PL_MAX_TYPE,
};
#define PL_RANDOM (1 << 1)
struct playlist_ops {
/* Called to add a track to a playlist. */
bool (*pl_add)(struct playlist *, struct track *);
/* Called to check if a playlist can be selected. */
bool (*pl_can_select)(struct playlist *);
/* Called to delete a playlist. */
bool (*pl_delete)(struct playlist *);
/* Called to remove a track from the playlist. */
bool (*pl_remove)(struct playlist *, struct track *);
/* Called to set a playlist flag. */
void (*pl_set_random)(struct playlist *, bool);
/* Called to sort the playlist. */
void (*pl_sort)(struct playlist *, enum compare_t);
/* Called to rearrange the playlist. */
bool (*pl_rearrange)(struct playlist *, unsigned int, unsigned int);
};
struct playlist {
enum playlist_type_t pl_type; /* This playlist's type. */
gchar *pl_name; /* This playlist's name. */
unsigned int pl_id; /* This playlist's identifier. */
GQueue pl_tracks; /* This playlist's queue of tracks. */
unsigned int pl_length; /* This playlist's length, in seconds. */
bool pl_random; /* This playlist's random setting. */
playlist_iter pl_current; /* This playlist's current track. */
GSList *pl_sort; /* This playlist's sort order. */
gchar **pl_search; /* This playlist's search text. */
const struct playlist_ops *pl_ops; /* This playlist's supported operations. */
};
#define DEFINE_PLAYLIST(type, name, id, ops) { \
.pl_type = type, \
.pl_name = name, \
.pl_id = id, \
.pl_ops = ops, \
}
struct playlist_type {
/* Called to save all playlists of the given type. */
void (*pl_save)(void);
/* Called to look up playlists. */
struct playlist *(*pl_lookup)(const gchar *);
struct playlist *(*pl_get)(unsigned int);
/* Called to create a new playlist. */
struct playlist *(*pl_new)(const gchar *);
/* Called to notify that a track has been played. */
void (*pl_played)(struct track *);
/* Called to notify that a track has been selected. */
void (*pl_selected)(struct track *);
};
/* Called to check if the playlist contains a specific track. */
static inline bool playlist_has(struct playlist *playlist, struct track *track)
{
return playlist ? g_queue_find(&playlist->pl_tracks, track) != NULL : false;
}
/* Called to find the size of a playlist. */
static inline unsigned int playlist_size(struct playlist *playlist)
{
return playlist ? g_queue_get_length(&playlist->pl_tracks) : 0;
}
/* Called to clear the sort order of the playlist. */
static inline void playlist_clear_sort(struct playlist *playlist)
{
if (playlist) {
g_slist_free(playlist->pl_sort);
playlist->pl_sort = NULL;
}
}
#endif /* OCARINA_CORE_PLAYLISTS_PLAYLIST_H */

View File

@ -0,0 +1,38 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#ifndef OCARINA_CORE_PLAYLISTS_SYSTEM_H
#define OCARINA_CORE_PLAYLISTS_SYSTEM_H
#include <core/playlists/generic.h>
enum sys_playlist_t {
SYS_PL_FAVORITES, /* Songs that the user likes. */
SYS_PL_HIDDEN, /* Songs that the user has hidden. */
SYS_PL_QUEUED, /* Songs that the user has queued up. */
SYS_PL_COLLECTION, /* Songs that have not been hidden. */
SYS_PL_HISTORY, /* Songs that have just been played. */
SYS_PL_UNPLAYED, /* Songs that have not been played yet. */
SYS_PL_MOST_PLAYED, /* Songs with an above average play count. */
SYS_PL_LEAST_PLAYED, /* Songs with a below average play count. */
SYS_PL_NUM_PLAYLISTS, /* Number of system playlists. */
};
/* System playlist type. */
extern struct playlist_type pl_system;
/* Called to initialize system playlists. */
void pl_system_init(void);
/* Called to deinitialize system playlists. */
void pl_system_deinit();
/* Called to tell system playlists about a new track. */
void pl_system_new_track(struct track *);
/* Called to tell system playlists that a track is getting deleted. */
void pl_system_delete_track(struct track *);
#endif /* OCARINA_CORE_PLAYLISTS_SYSTEM_H */

View File

@ -0,0 +1,33 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#ifndef OCARINA_CORE_PLAYLISTS_USER_H
#define OCARINA_CORE_PLAYLISTS_USER_H
#include <core/playlists/generic.h>
struct user_playlist {
struct playlist pl_playlist;
struct db_entry pl_dbe;
};
#define USER_PLAYLIST(dbe) ((struct user_playlist *)DBE_DATA(dbe))
/* User playlist type. */
extern struct playlist_type pl_user;
/* Called to initialize user playlists. */
void pl_user_init(void);
/* Called to deinitialize user playlists. */
void pl_user_deinit();
/* Called to tell user playlists that a track is getting deleted. */
void pl_user_delete_track(struct track *);
/* Called to rename a user playlist. */
bool pl_user_rename(struct playlist *, const gchar *);
struct database *pl_user_db_get();
#endif /* OCARINA_CORE_PLAYLISTS_USER_H */

View File

@ -1,134 +0,0 @@
/*
* Copyright 2013 (c) Anna Schumaker.
*
* Queues are lists of tracks that the user has requested to play next.
* Users of queues are expected to implement their own save and load functions,
* and to provide a filled out queue_ops structure during initialization.
*/
#ifndef OCARINA_CORE_QUEUE_H
#define OCARINA_CORE_QUEUE_H
#include <core/containers/queue.h>
#include <core/file.h>
#include <core/tags/track.h>
struct queue;
enum queue_flags {
Q_ENABLED = (1 << 0), /* Queue is enabled. */
Q_RANDOM = (1 << 1), /* Queue will pick songs randomly. */
Q_REPEAT = (1 << 2), /* Queue will not remove songs when picked. */
Q_NO_SORT = (1 << 3), /* Queue will not be sorted. */
Q_SAVE_FLAGS = (1 << 4), /* Queue will be saved when flags change. */
Q_SAVE_SORT = (1 << 5), /* Queue will be saved when sorted. */
Q_ADD_FRONT = (1 << 6), /* Queue will add new tracks at the front. */
};
struct queue_ops {
/* Called to tell a higher layer that a queue has been initialized. */
void *(*qop_init)(struct queue *);
/* Called to tell a higher layer that a queue is deinitializing. */
void (*qop_deinit)(struct queue *);
/* Called to tell a higher layer that a track has been added. */
void (*qop_added)(struct queue *, unsigned int);
/* Called to ask a higher layer if a track can be erased. */
bool (*qop_erase)(struct queue *, struct track *);
/* Called to tell a higher layer that a track has been removed. */
void (*qop_removed)(struct queue *, unsigned int);
/* Called to tell a higher layer that the queue has been cleared. */
void (*qop_cleared)(struct queue *, unsigned int);
/* Called to have a higher layer save the queue. */
void (*qop_save)(struct queue *, enum queue_flags);
/* Called to tell a higher layer that a track has been updated. */
void (*qop_updated)(struct queue *, unsigned int);
};
struct queue {
unsigned int q_flags; /* The queue's set of flags. */
unsigned int q_length; /* The queue's total runtime (in seconds). */
struct _queue q_tracks; /* The queue's list of tracks. */
struct _q_iter q_cur; /* The queue's last-played position. */
GSList *q_sort; /* The queue's sort order. */
void *q_private; /* The queue's private data. */
const struct queue_ops *q_ops; /* The queue's operations vector. */
};
/* Called to initialize a queue. */
void queue_init(struct queue *, unsigned int, const struct queue_ops *);
/* Called to deinitialize a queue. */
void queue_deinit(struct queue *);
/* Called to set a queue flag. */
void queue_set_flag(struct queue *, enum queue_flags);
/* Called to clear a queue flag. */
void queue_unset_flag(struct queue *, enum queue_flags);
/* Called to check if the queue has a specific flag set. */
static inline bool queue_has_flag(struct queue *queue, enum queue_flags flag)
{
return (queue->q_flags & flag) == (unsigned int)flag;
}
/* Called to find the size of the queue. */
static inline unsigned int queue_size(struct queue *queue)
{
return _q_size(&queue->q_tracks);
}
/* Called to access the queued track at a given index. */
static inline struct track *queue_at(struct queue *queue, unsigned int index)
{
return (struct track *)g_queue_peek_nth(&queue->q_tracks._queue, index);
}
/* Called to add a track to the queue. */
unsigned int queue_add(struct queue *, struct track *);
/*
* Called to erase a track from the queue by index.
* This can be prevented if qop_erase() returns "false".
*/
void queue_erase(struct queue *, unsigned int);
/* Called to remove a track from the queue by index. */
void queue_remove(struct queue *, unsigned int);
/* Called to remove all instances of the track from the queue. */
void queue_remove_all(struct queue *, struct track *);
/* Called to remove all tracks from the queue. */
void queue_clear(struct queue *);
/* Called to tell the queue that a track has been updated. */
void queue_updated(struct queue *, struct track *);
/* Called to tell the queue that a specific index has been selected. */
struct track *queue_selected(struct queue *, unsigned int);
/* Called to pick the next track from the queue. */
struct track *queue_next(struct queue *);
/* Called to sort the queue without changing sort order. */
void queue_resort(struct queue *);
/* Called to change the sort order and resort the queue. */
void queue_sort(struct queue *, enum compare_t, bool);
#endif /* OCARINA_CORE_QUEUE_H */

View File

@ -3,29 +3,29 @@
*
* The settings layer is used to store values configured by the user.
*/
#ifndef OCARINA_GUI_SETTINGS_H
#define OCARINA_GUI_SETTINGS_H
#ifndef OCARINA_SETTINGS_H
#define OCARINA_SETTINGS_H
#include <glib.h>
#include <stdbool.h>
/* Called to initialize GUI settings. */
void gui_settings_init();
void settings_init();
/* Called to deinitialize GUI settings. */
void gui_settings_deinit();
void settings_deinit();
/* Called to configure a specific setting by key. */
void gui_settings_set(const gchar *, unsigned int);
void settings_set(const gchar *, unsigned int);
/* Called to access a specific setting by key. */
unsigned int gui_settings_get(const gchar *);
unsigned int settings_get(const gchar *);
/* Called to check if a specific settings exists. */
bool gui_settings_has(const gchar *);
bool settings_has(const gchar *);
#ifdef CONFIG_TESTING
GHashTable *test_get_gui_settings();
GHashTable *test_get_settings();
#endif /* CONFIG_TESTING */
#endif /* OCARINA_GUI_SETTINGS_H */
#endif /* OCARINA_SETTINGS_H */

View File

@ -9,6 +9,7 @@
#include <glib.h>
#include <string.h>
#include <stdbool.h>
/* Convert number of seconds into a string with format mm:ss. */
@ -20,9 +21,6 @@ gchar *string_sec2str_long(unsigned int);
/* Convert a struct tm to a locale-dependant date string. */
gchar *string_tm2str(struct tm *);
/* Convert the input string to lowercase, dropping special characters. */
gchar *string_lowercase(const gchar *);
/*
* Compare two strings.
*
@ -30,6 +28,24 @@ gchar *string_lowercase(const gchar *);
* if ret = 0: lhs == rhs.
* if ret > 0: lhs > rhs, or lhs is empty.
*/
int string_compare(const gchar *, const gchar *);
int string_compare_tokens(gchar **, gchar **);
/* Returns True if the two strings match. */
static inline bool string_match(const gchar *a, const gchar *b)
{
return (a && b) ? g_utf8_collate(a, b) == 0 : false;
}
/* Returns True if one of the tokens begins with the specified prefix. */
bool string_match_token(const gchar *, gchar **);
/* Returns True if string a is a subdirectory of string b. */
bool string_is_subdir(const gchar *, const gchar *);
/* Return the length of the string, with NULL checks */
static inline int string_length(const gchar *str)
{
return str ? strlen(str) : 0;
}
#endif /* OCARINA_CORE_STRING_H */

View File

@ -4,22 +4,26 @@
* The album tag is used to store the name and year of albums
* added to the tag database.
*
* When writing an album tag to disk, write out the album_year field
* followed by the album_name on the same line:
* When writing an album tag to disk, write out the album_artist, album_genre,
* and album_year fields followed by the album_name on the same line:
*
* ... 1998 Hyrule Symphony
* ... 2006 Twilight Princess
* ... 2011 Skyward Sword
* ...0 0 1998 Hyrule Symphony
* ...0 0 2006 Twilight Princess
* ...0 0 2011 Skyward Sword
*/
#ifndef OCARINA_CORE_TAGS_ALBUM_H
#define OCARINA_CORE_TAGS_ALBUM_H
#include <core/containers/database.h>
#include <core/database.h>
#include <core/tags/artist.h>
struct album {
unsigned int al_year; /* This album's year. */
gchar *al_name; /* This album's name. */
gchar *al_lower; /* This album's name (lowercased). */
unsigned int al_year; /* This album's year. */
gchar *al_name; /* This album's name. */
gchar **al_tokens; /* This album's tokenized strings. */
gchar **al_alts; /* This album's alternate ascii tokens. */
struct artist *al_artist;
struct genre *al_genre;
struct db_entry al_dbe;
};
@ -32,8 +36,15 @@ void album_db_init();
/* Called to clean up the album database. */
void album_db_deinit();
/* Called to find an album tag by name and year. */
struct album *album_find(const gchar *, unsigned int);
/* Called to defragment the album database. */
bool album_db_defrag();
/* Called to clean up the album database after an upgrade. */
bool album_db_upgrade_done();
/* Called to find an album tag by artist, name and year. */
struct album *album_find(struct artist *, struct genre *,
const gchar *, unsigned int);
/* Called to get an album tag with a specific index. */
struct album *album_get(const unsigned int);
@ -44,6 +55,27 @@ int album_compare(struct album *, struct album *);
/* Called to compare two album tags by year. */
int album_compare_year(struct album *, struct album *);
/* Called to check if an artist has a token that matches the given string. */
bool album_match_token(struct album *album, const gchar *);
/* Called to find the database index of the album tag. */
static inline unsigned int album_index(struct album *album)
{
return album->al_dbe.dbe_index;
}
/* Called to check if album artwork has been downloaded. */
bool album_artwork_exists(struct album *);
/*
* Called to find the path to an album's artwork.
* This function returns a new string that MUST be freed with g_free().
*/
gchar *album_artwork_path(struct album *);
/* Called to manually set artwork for a given album. */
bool album_artwork_import(struct album *, gchar *);
#ifdef CONFIG_TESTING
const struct db_ops *test_album_ops();
#endif /* CONFIG_TESTING */

View File

@ -13,11 +13,13 @@
#ifndef OCARINA_CORE_TAGS_ARTIST_H
#define OCARINA_CORE_TAGS_ARTIST_H
#include <core/containers/database.h>
#include <core/database.h>
struct artist {
gchar *ar_name; /* This artist's name. */
gchar *ar_lower; /* This artist's name (lowercased). */
gchar *ar_name; /* This artist's name. */
gchar **ar_tokens; /* This artist's tokenized strings. */
gchar **ar_alts; /* This artist's alternate ascii tokens. */
void *ar_playlist; /* This artist's associated playlist. */
struct db_entry ar_dbe;
};
@ -30,8 +32,16 @@ void artist_db_init();
/* Called to clean up the artist database. */
void artist_db_deinit();
/* Called to find an artist tag by name. */
/* Called to access the artist database. */
const struct database *artist_db_get();
/*
* Called to find an artist tag by name. The difference is that artist_find()
* will allocate a new artiststruct if the requested one doesn't exist yet,
* but library_lookup() will return NULL in this situation.
*/
struct artist *artist_find(const gchar *);
struct artist *artist_lookup(const gchar *);
/* Called to get an artist tag with a specific index. */
struct artist *artist_get(const unsigned int);
@ -39,6 +49,15 @@ struct artist *artist_get(const unsigned int);
/* Called to compare two artist tags. */
int artist_compare(struct artist *, struct artist *);
/* Called to check if an artist has a token that matches the given string. */
bool artist_match_token(struct artist *artist, const gchar *);
/* Called to find the database index of the artist tag. */
static inline unsigned int artist_index(struct artist *artist)
{
return artist->ar_dbe.dbe_index;
}
#ifdef CONFIG_TESTING
const struct db_ops *test_artist_ops();
#endif /* CONFIG_TESTING */

View File

@ -13,11 +13,12 @@
#ifndef OCARINA_CORE_TAGS_GENRE_H
#define OCARINA_CORE_TAGS_GENRE_H
#include <core/containers/database.h>
#include <core/database.h>
struct genre {
gchar *ge_name; /* This genre's name. */
gchar *ge_lower; /* This genre's name (lowercased). */
gchar *ge_name; /* This genre's name. */
gchar **ge_tokens; /* This genre's tokenized strings. */
gchar **ge_alts; /* This genre's alternate ascii tokens. */
struct db_entry ge_dbe;
};
@ -39,6 +40,15 @@ struct genre *genre_get(const unsigned int);
/* Called to compare two genre tags. */
int genre_compare(struct genre *, struct genre *);
/* Called to check if a genre has a token that matches the given string. */
bool genre_match_token(struct genre *, const gchar *);
/* Called to find the database index of the genre tag. */
static inline unsigned int genre_index(struct genre *genre)
{
return genre->ge_dbe.dbe_index;
}
#ifdef CONFIG_TESTING
const struct db_ops *test_genre_ops();
#endif /* CONFIG_TESTING */

View File

@ -13,12 +13,11 @@
#ifndef OCARINA_CORE_TAGS_LIBRARY_H
#define OCARINA_CORE_TAGS_LIBRARY_H
#include <core/containers/database.h>
#include <core/database.h>
struct library {
unsigned int li_size; /* This library's track count. */
bool li_enabled;/* True if this library is enabled. */
gchar *li_path; /* This library's root path. */
gchar *li_path; /* This library's root path. */
void *li_playlist; /* This library's associated playlist. */
struct db_entry li_dbe;
};
@ -31,11 +30,21 @@ void library_db_init();
/* Called to clean up the library database. */
void library_db_deinit();
/* Called to defragment the library database. */
bool library_db_defrag();
/* Called to access the library database. */
const struct database *library_db_get();
/* Called to find a library tag by library path. */
/*
* Called to find a library tag by path. The difference is that
* library_find() will allocate a new library struct if the requested one
* doesn't exist yet, but library_lookup() will return NULL in this situation.
*
* Note that path may be a subdirectory of the returned library.
*/
struct library *library_find(const gchar *);
struct library *library_lookup(const gchar *);
/* Called to get a library tag with a specific index. */
struct library *library_get(const unsigned int);
@ -43,8 +52,11 @@ struct library *library_get(const unsigned int);
/* Called to remove a specific library tag. */
void library_remove(struct library *);
/* Called to configure if the library tag is enabled. */
void library_set_enabled(struct library *, bool);
/* Called to find the database index of the library tag. */
static inline unsigned int library_index(struct library *library)
{
return library->li_dbe.dbe_index;
}
/*
* Called to find the full path of files under the library directory.

View File

@ -11,4 +11,7 @@ void tags_init();
/* Called to clean up all databases. */
void tags_deinit();
/* Called to defragment all databases. */
bool tags_defragment();
#endif /* OCARINA_CORE_TAGS_TAGS_H */

View File

@ -7,20 +7,19 @@
* When writing a Track tag to disk, write as many fields as
* possible on one line before spilling over to a second:
*
* library number count
* | artist | year | length
* | | album| | month | | title
* | | | genre | | day| | | path
* | | | | | | | | | | | |
* ... 0 1 2 1 12 2015 10 15 13 232 Ocarina Medley |
* Hyrule Symphony/12 - Ocarina Medley.mp3 <---
* ... 0 1 2 1 13 2015 10 15 10 288 Legend of Zelda Medley
* library datestamp
* | album | count title
* | | number | | length | path
* | | | | | | | |
* ... 0 2 12 3741780495 13 232 Ocarina Medley |
* Hyrule Symphony/12 - Ocarina Medley.mp3 <---
* ... 0 2 13 3741780495 10 288 Legend of Zelda Medley
* Hyrule Symphony/13 - Legend of Zelda Medly.mp3
*/
#ifndef OCARINA_CORE_TAGS_TRACK_H
#define OCARINA_CORE_TAGS_TRACK_H
#include <core/containers/database.h>
#include <core/database.h>
#include <core/date.h>
#include <core/tags/album.h>
#include <core/tags/artist.h>
@ -43,23 +42,23 @@ enum compare_t {
struct track {
struct album *tr_album; /* This track's associated album. */
struct artist *tr_artist; /* This track's associated artist. */
struct genre *tr_genre; /* This track's associated genre. */
struct library *tr_library; /* This track's associated library. */
unsigned int tr_count; /* This track's play count. */
unsigned int tr_length; /* This track's length, in seconds. */
unsigned int tr_track; /* This track's track number. */
unsigned short int tr_count; /* This track's play count. */
unsigned short int tr_length; /* This track's length, in seconds. */
unsigned short int tr_track; /* This track's track number. */
struct date tr_date; /* This track's last-played date. */
gchar *tr_path; /* This track's path, relative to the library. */
gchar *tr_title; /* This track's title. */
gchar *tr_lower; /* This track's title (lowercased). */
struct date tr_date; /* This track's last-played date. */
gchar *tr_path; /* This track's path, relative to the library. */
gchar *tr_title; /* This track's title. */
gchar **tr_tokens; /* This track's tokenized strings. */
gchar **tr_alts; /* This track's alternate ascii tokens. */
struct db_entry tr_dbe;
};
#define TRACK(dbe) ((struct track *)DBE_DATA(dbe))
#define TRACK_IS_EXTERNAL(track) (track->tr_library == NULL)
/* Called to initialize the track database. */
@ -71,6 +70,12 @@ void track_db_deinit();
/* Called to commit the track database. */
void track_db_commit();
/* Called to defragment the track database. */
bool track_db_defrag();
/* Called to update track database keys. */
void track_db_rekey();
/* Called to access the track database. */
const struct database *track_db_get();
@ -83,6 +88,12 @@ unsigned int track_db_count_plays();
/* Called to find the average play count of tracks in the database. */
unsigned int track_db_average_plays();
/* Called to allocate a track without adding it to the database. */
struct track *track_alloc_external(const gchar *);
/* Called to free an external track. */
void track_free_external(struct track *);
/* Called to add a track tag to the database. */
struct track *track_add(struct library *, const gchar *);
@ -95,9 +106,21 @@ void track_remove_all(struct library *);
/* Called to get a track tag with a specific index. */
struct track *track_get(const unsigned int);
/* Called to look up a track tag using only a filepath. */
struct track *track_lookup(const gchar *);
/* Called to compare two track tags */
int track_compare(struct track *, struct track *, enum compare_t);
/* Called to check if a track has a token that matches the given string */
bool track_match_token(struct track *, const gchar *);
/* Called to find the database index of the track tag. */
static inline unsigned int track_index(struct track *track)
{
return track->tr_dbe.dbe_index;
}
/*
* Called to find the full path of the track tag.
* This function returns a new string that MUST be freed with g_free().

View File

@ -1,51 +0,0 @@
/*
* Copyright 2013 (c) Anna Schumaker.
*
* The temporary queue manager controls access to any temporary
* queues created by the user. When saving to disk:
*
* length(tempq_list)
* flags num_tracks track_0 track_1 ... track_N
* flags num_tracks track_0 track_1 ... track_N
*/
#ifndef OCARINA_CORE_DECK_H
#define OCARINA_CORE_DECK_H
#include <core/queue.h>
/* Called to initialize the temporary queue manager. */
void tempq_init(struct queue_ops *);
/* Called to deinitialize the temporary queue manager. */
void tempq_deinit();
/* Called to save the temporary queue list. */
void tempq_save(struct queue *, enum queue_flags);
/* Called to allocate a new temporary queue. */
struct queue *tempq_alloc(unsigned int);
/* Called to free a temporary queue. */
void tempq_free(struct queue *);
/* Called to find a temporary queue by index. */
struct queue *tempq_get(unsigned int);
/* Called to find the index of a temporary queue. */
unsigned int tempq_index(struct queue *);
/* Called to move a temporary queue to a new index in the list. */
void tempq_move(struct queue *, unsigned int);
/* Called to find the next track to play. */
struct track *tempq_next();
/* Called to find the number of temporary queues currently allocated. */
unsigned int tempq_count();
/* Called to tell temporary queues that a track has been updated. */
void tempq_updated(struct track *);
#endif /* OCARINA_CORE_DECK_H */

View File

@ -4,6 +4,17 @@
#ifndef OCARINA_CORE_VERSION_H
#define OCARINA_CORE_VERSION_H
#ifdef CONFIG_TESTING
#define OCARINA_NAME "ocarina-test/"CONFIG_TESTING_DIR
#elif CONFIG_DEBUG
#define OCARINA_NAME "ocarina-debug"
#else
#define OCARINA_NAME "ocarina"
#endif
#define OCARINA_MAJOR_VERSION CONFIG_MAJOR
#define OCARINA_MINOR_VERSION CONFIG_MINOR
/*
* Scons sets the current version by passing the
* -DCONFIG_VERSION macro when Ocarina is compiled.

21
include/gui/artwork.h Normal file
View File

@ -0,0 +1,21 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#ifndef OCARINA_GUI_ARTWORK_H
#define OCARINA_GUI_ARTWORK_H
#include <core/tags/track.h>
#include <gui/builder.h>
/* Called to set artwork for a track. */
void gui_artwork_set_cover(void);
/* Called to import artwork for a track. */
void gui_artwork_import(struct track *, gchar *);
/* Called to get the cover image. */
static inline GtkImage *gui_artwork(void)
{
return GTK_IMAGE(gui_builder_widget("artwork"));
}
#endif /* OCARINA_GUI_ARTWORK_H */

View File

@ -3,14 +3,107 @@
*/
#ifndef OCARINA_GUI_AUDIO_H
#define OCARINA_GUI_AUDIO_H
#include <core/audio.h>
#include <gui/builder.h>
/* Audio callback functions. */
extern struct audio_ops audio_ops;
extern struct audio_callbacks audio_cb;
/* Called to initialize the GUI audio controls. */
void gui_audio_init();
#ifdef CONFIG_TESTING
void test_gui_audio_timeout();
#endif /* CONFIG_TESTING */
/* Called to stop the GUI audio timeout function. */
void gui_audio_deinit();
/* Called to update the current track position. */
int gui_audio_timeout();
int gui_audio_popover_timeout();
/* Called to get the label displaying the album tag. */
static inline GtkLabel *gui_album_tag(void)
{
return GTK_LABEL(gui_builder_widget("album_tag"));
}
/* Called to get the label displaying the artist tag. */
static inline GtkLabel *gui_artist_tag(void)
{
return GTK_LABEL(gui_builder_widget("artist_tag"));
}
/* Called to get the label displaying the title tag. */
static inline GtkLabel *gui_title_tag(void)
{
return GTK_LABEL(gui_builder_widget("title_tag"));
}
/* Called to get the label displaying the track's position. */
static inline GtkLabel *gui_position(void)
{
return GTK_LABEL(gui_builder_widget("position"));
}
/* Called to get the label displaying the track's duration. */
static inline GtkLabel *gui_duration(void)
{
return GTK_LABEL(gui_builder_widget("duration"));
}
/* Called to get the play button. */
static inline GtkButton *gui_play_button(void)
{
return GTK_BUTTON(gui_builder_widget("play_button"));
}
/* Called to get the pause button. */
static inline GtkButton *gui_pause_button(void)
{
return GTK_BUTTON(gui_builder_widget("pause_button"));
}
/* Called to get the previous button. */
static inline GtkButton *gui_prev_button(void)
{
return GTK_BUTTON(gui_builder_widget("prev_button"));
}
/* Called to get the next button. */
static inline GtkButton *gui_next_button(void)
{
return GTK_BUTTON(gui_builder_widget("next_button"));
}
/* Called to get the pause-after widgets. */
static inline GtkEntry *gui_pause_entry(void)
{
return GTK_ENTRY(gui_builder_widget("pause_entry"));
}
static inline GtkButton *gui_pause_down(void)
{
return GTK_BUTTON(gui_builder_widget("pause_down"));
}
static inline GtkButton *gui_pause_up(void)
{
return GTK_BUTTON(gui_builder_widget("pause_up"));
}
static inline GtkPopover *gui_pause_popover(void)
{
return GTK_POPOVER(gui_builder_widget("pause_popover"));
}
/* Called to get the seeking GtkAdjustment. */
static inline GtkAdjustment *gui_seek(void)
{
return GTK_ADJUSTMENT(gui_builder_object("seek"));
}
/* Called to get the volume button. */
static inline GtkScaleButton *gui_volume_button(void)
{
return GTK_SCALE_BUTTON(gui_builder_widget("volume_button"));
}
#endif /* OCARINA_GUI_AUDIO_H */

View File

@ -17,6 +17,9 @@ GObject *gui_builder_object(const char *);
/* Called to get a widget from the GTK builder. */
GtkWidget *gui_builder_widget(const char *);
/* Called to find the height of a GTK builder widget. */
int gui_builder_widget_height(const char *);
#ifdef CONFIG_TESTING
GtkBuilder *test_get_gui_builder();
#endif /* CONFIG_TESTING */

View File

@ -1,13 +0,0 @@
/*
* Copyright 2015 (c) Anna Schumaker.
*/
#ifndef OCARINA_GUI_COLLECTION_H
#define OCARINA_GUI_COLLECTION_H
/* Called to initialize the GUI collection code. */
void gui_collection_init();
/* Collection operations passed to core_init() */
extern struct queue_ops collection_ops;
#endif /* OCARINA_GUI_COLLECTION_H */

56
include/gui/filter.h Normal file
View File

@ -0,0 +1,56 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#ifndef OCARINA_GUI_FILTER_H
#define OCARINA_GUI_FILTER_H
#include <core/playlist.h>
#include <gui/builder.h>
enum gui_filter_how {
GUI_FILTER_DEFAULT,
GUI_FILTER_ALBUM,
GUI_FILTER_ARTIST,
GUI_FILTER_GENRE,
GUI_FILTER_TITLE,
};
/* Called to initialize the filter model. */
void gui_filter_init();
/* Called to deinitialize the filter model. */
void gui_filter_deinit();
/* Called to set the current playlist. */
void gui_filter_set_playlist(struct playlist *);
/* Called to get the filter model. */
GtkTreeModelFilter *gui_filter_get();
/* Called to convert a filter model path into a track. */
struct track *gui_filter_path_get_track(GtkTreePath *);
/* Called to load the track at path. */
void gui_filter_path_load_track(GtkTreePath *);
/* Called to convert a filter model path into a queue index. */
unsigned int gui_filter_path_get_index(GtkTreePath *);
/* Called to convert a playlist iterator index into a path. */
GtkTreePath *gui_filter_path_from_index(unsigned int);
/* Called to refilter a playlist. Pass NULL to refilter the current playlist */
void gui_filter_refilter(struct playlist *);
/* Called to access the filter search-entry. */
static inline GtkSearchEntry *gui_filter_search(void)
{
return GTK_SEARCH_ENTRY(gui_builder_widget("filter_search"));
}
/* Called to access the filter-how chooser. */
static inline GtkComboBox *gui_filter_how(void)
{
return GTK_COMBO_BOX(gui_builder_widget("filter_how"));
}
#endif /* OCARINA_GUI_FILTER_H */

View File

@ -1,10 +0,0 @@
/*
* Copyright 2016 (c) Anna Schumaker.
*/
#ifndef OCARINA_GUI_HISTORY_H
#define OCARINA_GUI_HISTORY_H
/* History operations passed to core_init() */
extern struct queue_ops history_ops;
#endif /* OCARINA_GUI_HISTORY_H */

Some files were not shown because too many files have changed in this diff Show More