From 0e2244ba907c4621918773cee0573f421272c91b Mon Sep 17 00:00:00 2001 From: Joshua Strobl Date: Fri, 7 May 2021 16:45:57 +0300 Subject: [PATCH] Implement Playlist functionality. My god... Too many changes to summarize. - Fixes #2. - Fixes #3. - Fixes #5. - Fixes #7. Start work on uncrustify config. --- .vscode/launch.json | 4 +- .vscode/settings.json | 7 +- README.md | 2 + jsc.cfg | 2631 +++++++++++++++++ meson.build | 1 + src/components/koto-action-bar.c | 355 +++ src/components/koto-action-bar.h | 58 + src/components/koto-cover-art-button.c | 217 ++ src/components/koto-cover-art-button.h | 40 + src/db/cartographer.c | 242 +- src/db/cartographer.h | 10 +- src/db/db.c | 4 +- src/indexer/album.c | 75 +- src/indexer/artist.c | 40 +- src/indexer/file-indexer.c | 60 +- src/indexer/structs.h | 12 +- src/indexer/track.c | 41 +- src/koto-button.c | 177 +- src/koto-button.h | 17 +- src/koto-dialog-container.c | 99 + ...reate-dialog.h => koto-dialog-container.h} | 19 +- src/koto-expander.c | 10 +- src/koto-expander.h | 3 +- src/koto-nav.c | 113 +- src/koto-nav.h | 6 + src/koto-playerbar.c | 60 +- src/koto-playerbar.h | 3 +- src/koto-track-item.c | 12 +- src/koto-track-item.h | 2 + src/koto-utils.c | 25 +- src/koto-utils.h | 2 + src/koto-window.c | 85 +- src/koto-window.h | 11 +- src/meson.build | 7 +- src/pages/music/album-view.c | 6 +- src/pages/music/artist-view.c | 2 +- src/pages/music/disc-view.c | 67 +- src/pages/music/disc-view.h | 1 + src/pages/music/music-local.c | 30 +- src/pages/music/music-local.h | 2 + src/pages/playlist/list.c | 510 ++++ src/pages/playlist/list.h | 55 + src/playback/engine.c | 43 +- src/playback/engine.h | 2 +- src/playlist/add-remove-track-popover.c | 249 ++ src/playlist/add-remove-track-popover.h | 48 + src/playlist/create-dialog.c | 99 - src/playlist/create-modify-dialog.c | 301 ++ src/playlist/create-modify-dialog.h | 44 + src/playlist/current.c | 2 + src/playlist/playlist.c | 488 ++- src/playlist/playlist.h | 38 +- theme/_button.scss | 2 + theme/_disc-view.scss | 12 +- theme/_player-bar.scss | 28 + theme/_vars.scss | 2 + theme/components/_cover-art-button.scss | 5 + theme/components/_gtk-overrides.scss | 63 + theme/main.scss | 16 +- theme/meson.build | 3 + theme/pages/_music-local.scss | 2 - theme/pages/_playlist-page.scss | 84 + 62 files changed, 6280 insertions(+), 374 deletions(-) create mode 100644 jsc.cfg create mode 100644 src/components/koto-action-bar.c create mode 100644 src/components/koto-action-bar.h create mode 100644 src/components/koto-cover-art-button.c create mode 100644 src/components/koto-cover-art-button.h create mode 100644 src/koto-dialog-container.c rename src/{playlist/create-dialog.h => koto-dialog-container.h} (50%) create mode 100644 src/pages/playlist/list.c create mode 100644 src/pages/playlist/list.h create mode 100644 src/playlist/add-remove-track-popover.c create mode 100644 src/playlist/add-remove-track-popover.h delete mode 100644 src/playlist/create-dialog.c create mode 100644 src/playlist/create-modify-dialog.c create mode 100644 src/playlist/create-modify-dialog.h create mode 100644 theme/components/_cover-art-button.scss create mode 100644 theme/components/_gtk-overrides.scss create mode 100644 theme/pages/_playlist-page.scss diff --git a/.vscode/launch.json b/.vscode/launch.json index dd20c46..d477032 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,7 +14,7 @@ "cwd": "${workspaceFolder}", "environment": [ {"name" : "G_MESSAGES_DEBUG", "value": "all" }, - { "name": "GTK_THEME", "value": "Default:dark" } + { "name": "GTK_THEME", "value": "Adwaita:dark" } ], "externalConsole": false, "MIMode": "gdb", @@ -37,7 +37,7 @@ "cwd": "${workspaceFolder}", "environment": [ {"name" : "G_MESSAGES_DEBUG", "value": "all" }, - { "name": "GTK_THEME", "value": "Default:dark" } + { "name": "GTK_THEME", "value": "Adwaita:dark" } ], "externalConsole": false, "linux": { diff --git a/.vscode/settings.json b/.vscode/settings.json index 5d4fac9..553253d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,10 @@ { "files.associations": { - "glib.h": "c" + "glib.h": "c", + "ios": "c", + "__node_handle": "c", + "gtk.h": "c", + "gtktreeview.h": "c", + "cartographer.h": "c" } } \ No newline at end of file diff --git a/README.md b/README.md index 76d6144..6ff5f96 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Koto is an in-development audiobook, music, and podcast manager that is designed ## Blog +- [Dev Diary 6: Koto April Progress Report (A-side)](https://joshuastrobl.com/2021/04/26/dev-diary-6-koto-april-progress-report-a-side/) +- [Dev Diary 5: Koto March Progress Report (B-side)](https://joshuastrobl.com/2021/04/08/dev-diary-5-koto-march-progress-report-b-side/) - [Dev Diary 4: Koto March Progress Report (A-side)](https://joshuastrobl.com/2021/03/26/dev-diary-4-koto-march-progress-report-a-side/) - [Dev Diary 3: Koto February Progress Report (B-side)](https://joshuastrobl.com/2021/03/05/dev-diary-3-koto-february-progress-report-b-side/) - [Dev Diary 2: Koto February Progress Report (A-side)](https://joshuastrobl.com/2021/02/17/dev-diary-2-koto-february-progress-report-a-side/) diff --git a/jsc.cfg b/jsc.cfg new file mode 100644 index 0000000..10cf04a --- /dev/null +++ b/jsc.cfg @@ -0,0 +1,2631 @@ +# Uncrustify_d-0.71.0-250-9c62bb00 + +# +# General options +# + +# The type of line endings. +# +# Default: auto +newlines = auto # lf/crlf/cr/auto + +# The original size of tabs in the input. +# +# Default: 8 +input_tab_size = 4 # unsigned number + +# The size of tabs in the output (only used if align_with_tabs=true). +# +# Default: 8 +output_tab_size = 4 # unsigned number + +# The ASCII value of the string escape char, usually 92 (\) or (Pawn) 94 (^). +# +# Default: 92 +string_escape_char = 92 # unsigned number + +# Alternate string escape char (usually only used for Pawn). +# Only works right before the quote char. +string_escape_char2 = 0 # unsigned number + +# Replace tab characters found in string literals with the escape sequence \t +# instead. +string_replace_tab_chars = false # true/false + +# Allow interpreting '>=' and '>>=' as part of a template in code like +# 'void f(list>=val);'. If true, 'assert(x<0 && y>=3)' will be broken. +# Improvements to template detection may make this option obsolete. +tok_split_gte = false # true/false + +# Disable formatting of NL_CONT ('\\n') ended lines (e.g. multiline macros) +disable_processing_nl_cont = false # true/false + +# Specify the marker used in comments to disable processing of part of the +# file. +# The comment should be used alone in one line. +# +# Default: *INDENT-OFF* +disable_processing_cmt = " *INDENT-OFF*" # string + +# Specify the marker used in comments to (re)enable processing in a file. +# The comment should be used alone in one line. +# +# Default: *INDENT-ON* +enable_processing_cmt = " *INDENT-ON*" # string + +# Enable parsing of digraphs. +enable_digraphs = false # true/false + +# Add or remove the UTF-8 BOM (recommend 'remove'). +utf8_bom = ignore # ignore/add/remove/force + +# If the file contains bytes with values between 128 and 255, but is not +# UTF-8, then output as UTF-8. +utf8_byte = false # true/false + +# Force the output encoding to UTF-8. +utf8_force = false # true/false + +# Add or remove space between 'do' and '{'. +sp_do_brace_open = force # ignore/add/remove/force + +# Add or remove space between '}' and 'while'. +sp_brace_close_while = force # ignore/add/remove/force + +# Add or remove space between 'while' and '('. +sp_while_paren_open = force # ignore/add/remove/force + +# +# Spacing options +# + +# Add or remove space around non-assignment symbolic operators ('+', '/', '%', +# '<<', and so forth). +sp_arith = force # ignore/add/remove/force + +# Add or remove space around arithmetic operators '+' and '-'. +# +# Overrides sp_arith. +sp_arith_additive = force # ignore/add/remove/force + +# Add or remove space around assignment operator '=', '+=', etc. +sp_assign = force # ignore/add/remove/force + +# Add or remove space around assignment operator '=' in a prototype. +# +# If set to ignore, use sp_assign. +sp_assign_default = ignore # ignore/add/remove/force + +# Add or remove space in 'NS_ENUM ('. +sp_enum_paren = ignore # ignore/add/remove/force + +# Add or remove space around assignment '=' in enum. +sp_enum_assign = ignore # ignore/add/remove/force + +# Add or remove space before assignment '=' in enum. +# +# Overrides sp_enum_assign. +sp_enum_before_assign = ignore # ignore/add/remove/force + +# Add or remove space after assignment '=' in enum. +# +# Overrides sp_enum_assign. +sp_enum_after_assign = ignore # ignore/add/remove/force + +# Add or remove space around assignment ':' in enum. +sp_enum_colon = ignore # ignore/add/remove/force + +# Add or remove space around preprocessor '##' concatenation operator. +# +# Default: add +sp_pp_concat = add # ignore/add/remove/force + +# Add or remove space after preprocessor '#' stringify operator. +# Also affects the '#@' charizing operator. +sp_pp_stringify = ignore # ignore/add/remove/force + +# Add or remove space before preprocessor '#' stringify operator +# as in '#define x(y) L#y'. +sp_before_pp_stringify = add # ignore/add/remove/force + +# Add or remove space around boolean operators '&&' and '||'. +sp_bool = force # ignore/add/remove/force + +# Add or remove space around compare operator '<', '>', '==', etc. +sp_compare = force # ignore/add/remove/force + +# Add or remove space inside '(' and ')'. +sp_inside_paren = ignore # ignore/add/remove/force + +# Add or remove space between nested parentheses, i.e. '((' vs. ') )'. +sp_paren_paren = remove # ignore/add/remove/force + +# Add or remove space between back-to-back parentheses, i.e. ')(' vs. ') ('. +sp_cparen_oparen = force # ignore/add/remove/force + +# Whether to balance spaces inside nested parentheses. +sp_balance_nested_parens = false # true/false + +# Add or remove space between ')' and '{'. +sp_paren_brace = force # ignore/add/remove/force + +# Add or remove space between nested braces, i.e. '{{' vs '{ {'. +sp_brace_brace = ignore # ignore/add/remove/force + +# Add or remove space before pointer star '*'. +sp_before_ptr_star = ignore # ignore/add/remove/force + +# Add or remove space before pointer star '*' that isn't followed by a +# variable name. If set to ignore, sp_before_ptr_star is used instead. +sp_before_unnamed_ptr_star = ignore # ignore/add/remove/force + +# Add or remove space between pointer stars '*'. +sp_between_ptr_star = remove # ignore/add/remove/force + +# Add or remove space after pointer star '*', if followed by a word. +# +# Overrides sp_type_func. +sp_after_ptr_star = remove # ignore/add/remove/force + +# Add or remove space after pointer caret '^', if followed by a word. +sp_after_ptr_block_caret = ignore # ignore/add/remove/force + +# Add or remove space after pointer star '*', if followed by a qualifier. +sp_after_ptr_star_qualifier = remove # ignore/add/remove/force + +# Add or remove space after a pointer star '*', if followed by a function +# prototype or function definition. +# +# Overrides sp_after_ptr_star and sp_type_func. +sp_after_ptr_star_func = remove # ignore/add/remove/force + +# Add or remove space after a pointer star '*', if followed by an open +# parenthesis, as in 'void* (*)(). +sp_ptr_star_paren = force # ignore/add/remove/force + +# Add or remove space before a pointer star '*', if followed by a function +# prototype or function definition. +sp_before_ptr_star_func = force # ignore/add/remove/force + +# Add or remove space before a reference sign '&'. +sp_before_byref = ignore # ignore/add/remove/force + +# Add or remove space before a reference sign '&' that isn't followed by a +# variable name. If set to ignore, sp_before_byref is used instead. +sp_before_unnamed_byref = ignore # ignore/add/remove/force + +# Add or remove space after reference sign '&', if followed by a word. +# +# Overrides sp_type_func. +sp_after_byref = remove # ignore/add/remove/force + +# Add or remove space after a reference sign '&', if followed by a function +# prototype or function definition. +# +# Overrides sp_after_byref and sp_type_func. +sp_after_byref_func = remove # ignore/add/remove/force + +# Add or remove space before a reference sign '&', if followed by a function +# prototype or function definition. +sp_before_byref_func = remove # ignore/add/remove/force + +# Add or remove space between type and word. In cases where total removal of +# whitespace would be a syntax error, a value of 'remove' is treated the same +# as 'force'. +# +# This also affects some other instances of space following a type that are +# not covered by other options; for example, between the return type and +# parenthesis of a function type template argument, between the type and +# parenthesis of an array parameter, or between 'decltype(...)' and the +# following word. +# +# Default: force +sp_after_type = force # ignore/add/remove/force + +# Add or remove space between 'decltype(...)' and word. +# +# Overrides sp_after_type. +sp_after_decltype = ignore # ignore/add/remove/force + +# Add or remove space between 'template' and '<'. +# If set to ignore, sp_before_angle is used. +sp_template_angle = ignore # ignore/add/remove/force + +# Add or remove space before '<'. +sp_before_angle = ignore # ignore/add/remove/force + +# Add or remove space inside '<' and '>'. +sp_inside_angle = ignore # ignore/add/remove/force + +# Add or remove space inside '<>'. +sp_inside_angle_empty = ignore # ignore/add/remove/force + +# Add or remove space between '>' and ':'. +sp_angle_colon = ignore # ignore/add/remove/force + +# Add or remove space after '>'. +sp_after_angle = ignore # ignore/add/remove/force + +# Add or remove space between '>' and '(' as found in 'new List(foo);'. +sp_angle_paren = remove # ignore/add/remove/force + +# Add or remove space between '>' and '()' as found in 'new List();'. +sp_angle_paren_empty = remove # ignore/add/remove/force + +# Add or remove space between '>' and a word as in 'List m;' or +# 'template static ...'. +sp_angle_word = force # ignore/add/remove/force + +# Add or remove space between '>' and '>' in '>>' (template stuff). +# +# Default: add +sp_angle_shift = add # ignore/add/remove/force + +# (C++11) Permit removal of the space between '>>' in 'foo >'. Note +# that sp_angle_shift cannot remove the space without this option. +sp_permit_cpp11_shift = false # true/false + +# Add or remove space before '(' of control statements ('if', 'for', 'switch', +# 'while', etc.). +sp_before_sparen = force # ignore/add/remove/force + +# Add or remove space inside '(' and ')' of control statements. +sp_inside_sparen = remove # ignore/add/remove/force + +# Add or remove space after '(' of control statements. +# +# Overrides sp_inside_sparen. +sp_inside_sparen_open = remove # ignore/add/remove/force + +# Add or remove space before ')' of control statements. +# +# Overrides sp_inside_sparen. +sp_inside_sparen_close = remove # ignore/add/remove/force + +# Add or remove space after ')' of control statements. +sp_after_sparen = force # ignore/add/remove/force + +# Add or remove space between ')' and '{' of of control statements. +sp_sparen_brace = force # ignore/add/remove/force + +# Add or remove space before empty statement ';' on 'if', 'for' and 'while'. +sp_special_semi = ignore # ignore/add/remove/force + +# Add or remove space before ';'. +# +# Default: remove +sp_before_semi = remove # ignore/add/remove/force + +# Add or remove space before ';' in non-empty 'for' statements. +sp_before_semi_for = remove # ignore/add/remove/force + +# Add or remove space before a semicolon of an empty part of a for statement. +sp_before_semi_for_empty = ignore # ignore/add/remove/force + +# Add or remove space after ';', except when followed by a comment. +# +# Default: add +sp_after_semi = add # ignore/add/remove/force + +# Add or remove space after ';' in non-empty 'for' statements. +# +# Default: force +sp_after_semi_for = force # ignore/add/remove/force + +# Add or remove space after the final semicolon of an empty part of a for +# statement, as in 'for ( ; ; )'. +sp_after_semi_for_empty = ignore # ignore/add/remove/force + +# Add or remove space before '[' (except '[]'). +sp_before_square = ignore # ignore/add/remove/force + +# Add or remove space before '[' for a variable definition. +# +# Default: remove +sp_before_vardef_square = remove # ignore/add/remove/force + +# Add or remove space before '[' for asm block. +sp_before_square_asm_block = ignore # ignore/add/remove/force + +# Add or remove space before '[]'. +sp_before_squares = remove # ignore/add/remove/force + +# Add or remove space inside a non-empty '[' and ']'. +sp_inside_square = remove # ignore/add/remove/force + +# Add or remove space inside '[]'. +sp_inside_square_empty = remove # ignore/add/remove/force + +# Add or remove space after ',', i.e. 'a,b' vs. 'a, b'. +sp_after_comma = force # ignore/add/remove/force + +# Add or remove space before ','. +# +# Default: remove +sp_before_comma = remove # ignore/add/remove/force + +# Add or remove space between an open parenthesis and comma, +# i.e. '(,' vs. '( ,'. +# +# Default: force +sp_paren_comma = force # ignore/add/remove/force + +# Add or remove space before the variadic '...' when preceded by a +# non-punctuator. +sp_before_ellipsis = ignore # ignore/add/remove/force + +# Add or remove space between a type and '...'. +sp_type_ellipsis = ignore # ignore/add/remove/force + +# Add or remove space between ')' and '...'. +sp_paren_ellipsis = ignore # ignore/add/remove/force + +# Add or remove space between ')' and a qualifier such as 'const'. +sp_paren_qualifier = ignore # ignore/add/remove/force + +# Add or remove space between ')' and 'noexcept'. +sp_paren_noexcept = ignore # ignore/add/remove/force + +# Add or remove space after class ':'. +sp_after_class_colon = ignore # ignore/add/remove/force + +# Add or remove space before class ':'. +sp_before_class_colon = ignore # ignore/add/remove/force + +# Add or remove space after class constructor ':'. +sp_after_constr_colon = ignore # ignore/add/remove/force + +# Add or remove space before class constructor ':'. +sp_before_constr_colon = ignore # ignore/add/remove/force + +# Add or remove space before case ':'. +# +# Default: remove +sp_before_case_colon = remove # ignore/add/remove/force + +# Add or remove space between 'operator' and operator sign. +sp_after_operator = ignore # ignore/add/remove/force + +# Add or remove space between the operator symbol and the open parenthesis, as +# in 'operator ++('. +sp_after_operator_sym = force # ignore/add/remove/force + +# Overrides sp_after_operator_sym when the operator has no arguments, as in +# 'operator *()'. +sp_after_operator_sym_empty = ignore # ignore/add/remove/force + +# Add or remove space after C/D cast, i.e. 'cast(int)a' vs. 'cast(int) a' or +# '(int)a' vs. '(int) a'. +sp_after_cast = force # ignore/add/remove/force + +# Add or remove spaces inside cast parentheses. +sp_inside_paren_cast = remove # ignore/add/remove/force + +# Add or remove space between 'sizeof' and '('. +sp_sizeof_paren = remove # ignore/add/remove/force + +# Add or remove space between 'sizeof' and '...'. +sp_sizeof_ellipsis = ignore # ignore/add/remove/force + +# Add or remove space between 'sizeof...' and '('. +sp_sizeof_ellipsis_paren = remove # ignore/add/remove/force + +# Add or remove space between 'decltype' and '('. +sp_decltype_paren = force # ignore/add/remove/force + +# Add or remove space inside enum '{' and '}'. +sp_inside_braces_enum = force # ignore/add/remove/force + +# Add or remove space inside struct/union '{' and '}'. +sp_inside_braces_struct = force # ignore/add/remove/force + +# Add or remove space after open brace in an unnamed temporary +# direct-list-initialization. +sp_after_type_brace_init_lst_open = ignore # ignore/add/remove/force + +# Add or remove space before close brace in an unnamed temporary +# direct-list-initialization. +sp_before_type_brace_init_lst_close = ignore # ignore/add/remove/force + +# Add or remove space inside an unnamed temporary direct-list-initialization. +sp_inside_type_brace_init_lst = ignore # ignore/add/remove/force + +# Add or remove space inside '{' and '}'. +sp_inside_braces = force # ignore/add/remove/force + +# Add or remove space inside '{}'. +sp_inside_braces_empty = force # ignore/add/remove/force + +# Add or remove space around trailing return operator '->'. +sp_trailing_return = remove # ignore/add/remove/force + +# Add or remove space between return type and function name. A minimum of 1 +# is forced except for pointer return types. +sp_type_func = force # ignore/add/remove/force + +# Add or remove space between type and open brace of an unnamed temporary +# direct-list-initialization. +sp_type_brace_init_lst = ignore # ignore/add/remove/force + +# Add or remove space between function name and '(' on function declaration. +sp_func_proto_paren = remove # ignore/add/remove/force + +# Add or remove space between function name and '()' on function declaration +# without parameters. +sp_func_proto_paren_empty = remove # ignore/add/remove/force + +# Add or remove space between function name and '(' with a typedef specifier. +sp_func_type_paren = remove # ignore/add/remove/force + +# Add or remove space between alias name and '(' of a non-pointer function type typedef. +sp_func_def_paren = ignore # ignore/add/remove/force + +# Add or remove space between function name and '()' on function definition +# without parameters. +sp_func_def_paren_empty = remove # ignore/add/remove/force + +# Add or remove space inside empty function '()'. +# Overrides sp_after_angle unless use_sp_after_angle_always is set to true. +sp_inside_fparens = remove # ignore/add/remove/force + +# Add or remove space inside function '(' and ')'. +sp_inside_fparen = ignore # ignore/add/remove/force + +# Add or remove space inside the first parentheses in a function type, as in +# 'void (*x)(...)'. +sp_inside_tparen = ignore # ignore/add/remove/force + +# Add or remove space between the ')' and '(' in a function type, as in +# 'void (*x)(...)'. +sp_after_tparen_close = force # ignore/add/remove/force + +# Add or remove space between ']' and '(' when part of a function call. +sp_square_fparen = force # ignore/add/remove/force + +# Add or remove space between ')' and '{' of function. +sp_fparen_brace = force # ignore/add/remove/force + +# Add or remove space between ')' and '{' of a function call in object +# initialization. +# +# Overrides sp_fparen_brace. +sp_fparen_brace_initializer = force # ignore/add/remove/force + +# Add or remove space between function name and '(' on function calls. +sp_func_call_paren = remove # ignore/add/remove/force + +# Add or remove space between function name and '()' on function calls without +# parameters. If set to ignore (the default), sp_func_call_paren is used. +sp_func_call_paren_empty = remove # ignore/add/remove/force + +# Add or remove space between the user function name and '(' on function +# calls. You need to set a keyword to be a user function in the config file, +# like: +# set func_call_user tr _ i18n +sp_func_call_user_paren = ignore # ignore/add/remove/force + +# Add or remove space inside user function '(' and ')'. +sp_func_call_user_inside_fparen = remove # ignore/add/remove/force + +# Add or remove space between nested parentheses with user functions, +# i.e. '((' vs. '( ('. +sp_func_call_user_paren_paren = remove # ignore/add/remove/force + +# Add or remove space between a constructor/destructor and the open +# parenthesis. +sp_func_class_paren = remove # ignore/add/remove/force + +# Add or remove space between a constructor without parameters or destructor +# and '()'. +sp_func_class_paren_empty = remove # ignore/add/remove/force + +# Add or remove space between 'return' and '('. +sp_return_paren = force # ignore/add/remove/force + +# Add or remove space between 'return' and '{'. +sp_return_brace = force # ignore/add/remove/force + +# Add or remove space between '__attribute__' and '('. +sp_attribute_paren = ignore # ignore/add/remove/force + +# Add or remove space between 'defined' and '(' in '#if defined (FOO)'. +sp_defined_paren = force # ignore/add/remove/force + +# Add or remove space between 'throw' and '(' in 'throw (something)'. +sp_throw_paren = force # ignore/add/remove/force + +# Add or remove space between 'throw' and anything other than '(' as in +# '@throw [...];'. +sp_after_throw = force # ignore/add/remove/force + +# Add or remove space between 'catch' and '(' in 'catch (something) { }'. +# If set to ignore, sp_before_sparen is used. +sp_catch_paren = force # ignore/add/remove/force + +# Add or remove space between 'super' and '(' in 'super (something)'. +# +# Default: remove +sp_super_paren = remove # ignore/add/remove/force + +# Add or remove space between 'this' and '(' in 'this (something)'. +# +# Default: remove +sp_this_paren = remove # ignore/add/remove/force + +# Add or remove space between a macro name and its definition. +sp_macro = ignore # ignore/add/remove/force + +# Add or remove space between a macro function ')' and its definition. +sp_macro_func = ignore # ignore/add/remove/force + +# Add or remove space between 'else' and '{' if on the same line. +sp_else_brace = force # ignore/add/remove/force + +# Add or remove space between '}' and 'else' if on the same line. +sp_brace_else = force # ignore/add/remove/force + +# Add or remove space between '}' and the name of a typedef on the same line. +sp_brace_typedef = ignore # ignore/add/remove/force + +# Add or remove space before the '{' of a 'catch' statement, if the '{' and +# 'catch' are on the same line, as in 'catch (decl) {'. +sp_catch_brace = force # ignore/add/remove/force + +# Add or remove space between '}' and 'catch' if on the same line. +sp_brace_catch = force # ignore/add/remove/force + +# Add or remove space between 'finally' and '{' if on the same line. +sp_finally_brace = force # ignore/add/remove/force + +# Add or remove space between '}' and 'finally' if on the same line. +sp_brace_finally = force # ignore/add/remove/force + +# Add or remove space between 'try' and '{' if on the same line. +sp_try_brace = force # ignore/add/remove/force + +# Add or remove space between get/set and '{' if on the same line. +sp_getset_brace = force # ignore/add/remove/force + +# Add or remove space between a variable and '{' for a namespace. +# +# Default: add +sp_word_brace_ns = force # ignore/add/remove/force + +# Add or remove space before the '::' operator. +sp_before_dc = ignore # ignore/add/remove/force + +# Add or remove space after the '::' operator. +sp_after_dc = ignore # ignore/add/remove/force + +# Add or remove space after the '!' (not) unary operator. +# +# Default: remove +sp_not = remove # ignore/add/remove/force + +# Add or remove space after the '~' (invert) unary operator. +# +# Default: remove +sp_inv = remove # ignore/add/remove/force + +# Add or remove space after the '&' (address-of) unary operator. This does not +# affect the spacing after a '&' that is part of a type. +# +# Default: remove +sp_addr = remove # ignore/add/remove/force + +# Add or remove space around the '.' or '->' operators. +# +# Default: remove +sp_member = remove # ignore/add/remove/force + +# Add or remove space after the '*' (dereference) unary operator. This does +# not affect the spacing after a '*' that is part of a type. +# +# Default: remove +sp_deref = remove # ignore/add/remove/force + +# Add or remove space after '+' or '-', as in 'x = -5' or 'y = +7'. +# +# Default: remove +sp_sign = remove # ignore/add/remove/force + +# Add or remove space between '++' and '--' the word to which it is being +# applied, as in '(--x)' or 'y++;'. +# +# Default: remove +sp_incdec = remove # ignore/add/remove/force + +# Add or remove space before a backslash-newline at the end of a line. +# +# Default: add +sp_before_nl_cont = add # ignore/add/remove/force + +# Add or remove space around the ':' in 'b ? t : f'. +sp_cond_colon = force # ignore/add/remove/force + +# Add or remove space around the '?' in 'b ? t : f'. +sp_cond_question = force # ignore/add/remove/force + +# Fix the spacing between 'case' and the label. Only 'ignore' and 'force' make +# sense here. +sp_case_label = force # ignore/add/remove/force + +# Add or remove space between #else or #endif and a trailing comment. +sp_endif_cmt = remove # ignore/add/remove/force + +# Add or remove space after 'new', 'delete' and 'delete[]'. +sp_after_new = ignore # ignore/add/remove/force + +# Add or remove space between 'new' and '(' in 'new()'. +sp_between_new_paren = remove # ignore/add/remove/force + +# Add or remove space between ')' and type in 'new(foo) BAR'. +sp_after_newop_paren = force # ignore/add/remove/force + +# Add or remove space inside parenthesis of the new operator +# as in 'new(foo) BAR'. +sp_inside_newop_paren = remove # ignore/add/remove/force + +# Add or remove space before a trailing or embedded comment. +sp_before_tr_emb_cmt = force # ignore/add/remove/force + +# Number of spaces before a trailing or embedded comment. +sp_num_before_tr_emb_cmt = 1 # unsigned number + +# If true, vbrace tokens are dropped to the previous token and skipped. +sp_skip_vbrace_tokens = false # true/false + +# Add or remove space after 'noexcept'. +sp_after_noexcept = ignore # ignore/add/remove/force + +# Add or remove space after '_'. +sp_vala_after_translation = ignore # ignore/add/remove/force + +# If true, a is inserted after #define. +force_tab_after_define = false # true/false + +# +# Indenting options +# + +# The number of columns to indent per level. Usually 2, 3, 4, or 8. +# +# Default: 8 +indent_columns = 4 # unsigned number + +# The continuation indent. If non-zero, this overrides the indent of '(', '[' +# and '=' continuation indents. Negative values are OK; negative value is +# absolute and not increased for each '(' or '[' level. +# +# For FreeBSD, this is set to 4. +indent_continue = 0 # number + +# The continuation indent, only for class header line(s). If non-zero, this +# overrides the indent of 'class' continuation indents. +indent_continue_class_head = 0 # unsigned number + +# Whether to indent empty lines (i.e. lines which contain only spaces before +# the newline character). +indent_single_newlines = false # true/false + +# The continuation indent for func_*_param if they are true. If non-zero, this +# overrides the indent. +indent_param = 0 # unsigned number + +# How to use tabs when indenting code. +# +# 0: Spaces only +# 1: Indent with tabs to brace level, align with spaces (default) +# 2: Indent and align with tabs, using spaces when not on a tabstop +# +# Default: 1 +indent_with_tabs = 2 # unsigned number + +# Whether to indent comments that are not at a brace level with tabs on a +# tabstop. Requires indent_with_tabs=2. If false, will use spaces. +indent_cmt_with_tabs = false # true/false + +# Whether to indent strings broken by '\' so that they line up. +indent_align_string = false # true/false + +# The number of spaces to indent multi-line XML strings. +# Requires indent_align_string=true. +indent_xml_string = 0 # unsigned number + +# Spaces to indent '{' from level. +indent_brace = 0 # unsigned number + +# Whether braces are indented to the body level. +indent_braces = false # true/false + +# Whether to disable indenting function braces if indent_braces=true. +indent_braces_no_func = false # true/false + +# Whether to disable indenting class braces if indent_braces=true. +indent_braces_no_class = false # true/false + +# Whether to disable indenting struct braces if indent_braces=true. +indent_braces_no_struct = false # true/false + +# Whether to indent based on the size of the brace parent, +# i.e. 'if' => 3 spaces, 'for' => 4 spaces, etc. +indent_brace_parent = false # true/false + +# Whether to indent based on the open parenthesis instead of the open brace +# in '({\n'. +indent_paren_open_brace = false # true/false + +# Whether to indent the body of a 'namespace'. +indent_namespace = true # true/false + +# Whether to indent only the first namespace, and not any nested namespaces. +# Requires indent_namespace=true. +indent_namespace_single_indent = false # true/false + +# The number of spaces to indent a namespace block. +# If set to zero, use the value indent_columns +indent_namespace_level = 0 # unsigned number + +# If the body of the namespace is longer than this number, it won't be +# indented. Requires indent_namespace=true. 0 means no limit. +indent_namespace_limit = 0 # unsigned number + +# Whether the 'extern "C"' body is indented. +indent_extern = false # true/false + +# Whether the 'class' body is indented. +indent_class = true # true/false + +# Whether to indent the stuff after a leading base class colon. +indent_class_colon = false # true/false + +# Whether to indent based on a class colon instead of the stuff after the +# colon. Requires indent_class_colon=true. +indent_class_on_colon = false # true/false + +# Whether to indent the stuff after a leading class initializer colon. +indent_constr_colon = false # true/false + +# Virtual indent from the ':' for member initializers. +# +# Default: 2 +indent_ctor_init_leading = 2 # unsigned number + +# Additional indent for constructor initializer list. +# Negative values decrease indent down to the first column. +indent_ctor_init = 0 # number + +# Whether to indent 'if' following 'else' as a new block under the 'else'. +# If false, 'else\nif' is treated as 'else if' for indenting purposes. +indent_else_if = false # true/false + +# Amount to indent variable declarations after a open brace. +# +# <0: Relative +# >=0: Absolute +indent_var_def_blk = 0 # number + +# Whether to indent continued variable declarations instead of aligning. +indent_var_def_cont = false # true/false + +# Whether to indent continued shift expressions ('<<' and '>>') instead of +# aligning. Set align_left_shift=false when enabling this. +indent_shift = false # true/false + +# Whether to force indentation of function definitions to start in column 1. +indent_func_def_force_col1 = false # true/false + +# Whether to indent continued function call parameters one indent level, +# rather than aligning parameters under the open parenthesis. +indent_func_call_param = false # true/false + +# Whether to indent continued function definition parameters one indent level, +# rather than aligning parameters under the open parenthesis. +indent_func_def_param = true # true/false + +# for function definitions, only if indent_func_def_param is false +# Allows to align params when appropriate and indent them when not +# behave as if it was true if paren position is more than this value +# if paren position is more than the option value +indent_func_def_param_paren_pos_threshold = 0 # unsigned number + +# Whether to indent continued function call prototype one indent level, +# rather than aligning parameters under the open parenthesis. +indent_func_proto_param = true # true/false + +# Whether to indent continued function call declaration one indent level, +# rather than aligning parameters under the open parenthesis. +indent_func_class_param = true # true/false + +# Whether to indent continued class variable constructors one indent level, +# rather than aligning parameters under the open parenthesis. +indent_func_ctor_var_param = false # true/false + +# Whether to indent continued template parameter list one indent level, +# rather than aligning parameters under the open parenthesis. +indent_template_param = false # true/false + +# Double the indent for indent_func_xxx_param options. +# Use both values of the options indent_columns and indent_param. +indent_func_param_double = false # true/false + +# Indentation column for standalone 'const' qualifier on a function +# prototype. +indent_func_const = 0 # unsigned number + +# Indentation column for standalone 'throw' qualifier on a function +# prototype. +indent_func_throw = 0 # unsigned number + +# How to indent within a macro followed by a brace on the same line +# This allows reducing the indent in macros that have (for example) +# `do { ... } while (0)` blocks bracketing them. +# +# true: add an indent for the brace on the same line as the macro +# false: do not add an indent for the brace on the same line as the macro +# +# Default: true +indent_macro_brace = true # true/false + +# The number of spaces to indent a continued '->' or '.'. +# Usually set to 0, 1, or indent_columns. +indent_member = 0 # unsigned number + +# Whether lines broken at '.' or '->' should be indented by a single indent. +# The indent_member option will not be effective if this is set to true. +indent_member_single = false # true/false + +# Spaces to indent single line ('//') comments on lines before code. +indent_sing_line_comments = 0 # unsigned number + +# When opening a paren for a control statement (if, for, while, etc), increase +# the indent level by this value. Negative values decrease the indent level. +indent_sparen_extra = 0 # number + +# Whether to indent trailing single line ('//') comments relative to the code +# instead of trying to keep the same absolute column. +indent_relative_single_line_comments = false # true/false + +# Spaces to indent 'case' from 'switch'. Usually 0 or indent_columns. +indent_switch_case = 4 # unsigned number + +# indent 'break' with 'case' from 'switch'. +indent_switch_break_with_case = false # true/false + +# Whether to indent preprocessor statements inside of switch statements. +# +# Default: true +indent_switch_pp = true # true/false + +# Spaces to shift the 'case' line, without affecting any other lines. +# Usually 0. +indent_case_shift = 0 # unsigned number + +# Spaces to indent '{' from 'case'. By default, the brace will appear under +# the 'c' in case. Usually set to 0 or indent_columns. Negative values are OK. +indent_case_brace = 0 # number + +# Whether to indent comments found in first column. +indent_col1_comment = false # true/false + +# Whether to indent multi string literal in first column. +indent_col1_multi_string_literal = false # true/false + +# How to indent goto labels. +# +# >0: Absolute column where 1 is the leftmost column +# <=0: Subtract from brace indent +# +# Default: 1 +indent_label = 1 # number + +# How to indent access specifiers that are followed by a +# colon. +# +# >0: Absolute column where 1 is the leftmost column +# <=0: Subtract from brace indent +# +# Default: 1 +indent_access_spec = 1 # number + +# Whether to indent the code after an access specifier by one level. +# If true, this option forces 'indent_access_spec=0'. +indent_access_spec_body = false # true/false + +# If an open parenthesis is followed by a newline, whether to indent the next +# line so that it lines up after the open parenthesis (not recommended). +indent_paren_nl = false # true/false + +# How to indent a close parenthesis after a newline. +# +# 0: Indent to body level (default) +# 1: Align under the open parenthesis +# 2: Indent to the brace level +indent_paren_close = 2 # unsigned number + +# Whether to indent the open parenthesis of a function definition, +# if the parenthesis is on its own line. +indent_paren_after_func_def = false # true/false + +# Whether to indent the open parenthesis of a function declaration, +# if the parenthesis is on its own line. +indent_paren_after_func_decl = false # true/false + +# Whether to indent the open parenthesis of a function call, +# if the parenthesis is on its own line. +indent_paren_after_func_call = false # true/false + +# Whether to indent a comma when inside a parenthesis. +# If true, aligns under the open parenthesis. +indent_comma_paren = false # true/false + +# Whether to indent a Boolean operator when inside a parenthesis. +# If true, aligns under the open parenthesis. +indent_bool_paren = false # true/false + +# Whether to indent a semicolon when inside a for parenthesis. +# If true, aligns under the open for parenthesis. +indent_semicolon_for_paren = false # true/false + +# Whether to align the first expression to following ones +# if indent_bool_paren=true. +indent_first_bool_expr = false # true/false + +# Whether to align the first expression to following ones +# if indent_semicolon_for_paren=true. +indent_first_for_expr = false # true/false + +# If an open square is followed by a newline, whether to indent the next line +# so that it lines up after the open square (not recommended). +indent_square_nl = false # true/false + +# (ESQL/C) Whether to preserve the relative indent of 'EXEC SQL' bodies. +indent_preserve_sql = false # true/false + +# Whether to align continued statements at the '='. If false or if the '=' is +# followed by a newline, the next line is indent one tab. +# +# Default: true +indent_align_assign = true # true/false + +# If true, the indentation of the chunks after a '=' sequence will be set at +# LHS token indentation column before '='. +indent_off_after_assign = false # true/false + +# Whether to align continued statements at the '('. If false or the '(' is +# followed by a newline, the next line indent is one tab. +# +# Default: true +indent_align_paren = true # true/false + +# When indenting after virtual brace open and newline add further spaces to +# reach this minimum indent. +indent_min_vbrace_open = 0 # unsigned number + +# Whether to add further spaces after regular indent to reach next tabstop +# when indenting after virtual brace open and newline. +indent_vbrace_open_on_tabstop = false # true/false + +# How to indent after a brace followed by another token (not a newline). +# true: indent all contained lines to match the token +# false: indent all contained lines to match the brace +# +# Default: true +indent_token_after_brace = true # true/false + +# How to indent compound literals that are being returned. +# true: add both the indent from return & the compound literal open brace (ie: +# 2 indent levels) +# false: only indent 1 level, don't add the indent for the open brace, only add +# the indent for the return. +# +# Default: true +indent_compound_literal_return = true # true/false + +# How to indent the continuation of ternary operator. +# +# 0: Off (default) +# 1: When the `if_false` is a continuation, indent it under `if_false` +# 2: When the `:` is a continuation, indent it under `?` +indent_ternary_operator = 0 # unsigned number + +# Whether to indent the statments inside ternary operator. +indent_inside_ternary_operator = false # true/false + +# If true, the indentation of the chunks after a `return` sequence will be set at return indentation column. +indent_off_after_return = false # true/false + +# If true, the indentation of the chunks after a `return new` sequence will be set at return indentation column. +indent_off_after_return_new = false # true/false + +# If true, the tokens after return are indented with regular single indentation. By default (false) the indentation is after the return token. +indent_single_after_return = false # true/false + +# Whether to ignore indent and alignment for 'asm' blocks (i.e. assume they +# have their own indentation). +indent_ignore_asm_block = false # true/false + +# Don't indent the close parenthesis of a function definition, +# if the parenthesis is on its own line. +donot_indent_func_def_close_paren = false # true/false + +# +# Newline adding and removing options +# + +# Whether to collapse empty blocks between '{' and '}'. +# If true, overrides nl_inside_empty_func +nl_collapse_empty_body = true # true/false + +# Don't split one-line braced assignments, as in 'foo_t f = { 1, 2 };'. +nl_assign_leave_one_liners = false # true/false + +# Don't split one-line braced statements inside a 'class xx { }' body. +nl_class_leave_one_liners = true # true/false + +# Don't split one-line enums, as in 'enum foo { BAR = 15 };' +nl_enum_leave_one_liners = false # true/false + +# Don't split one-line get or set functions. +nl_getset_leave_one_liners = false # true/false + +# Don't split one-line function definitions, as in 'int foo() { return 0; }'. +# might modify nl_func_type_name +nl_func_leave_one_liners = false # true/false + +# Don't split one-line if/else statements, as in 'if(...) b++;'. +nl_if_leave_one_liners = false # true/false + +# Don't split one-line while statements, as in 'while(...) b++;'. +nl_while_leave_one_liners = false # true/false + +# Don't split one-line for statements, as in 'for(...) b++;'. +nl_for_leave_one_liners = false # true/false + +# Add or remove newlines at the start of the file. +nl_start_of_file = remove # ignore/add/remove/force + +# The minimum number of newlines at the start of the file (only used if +# nl_start_of_file is 'add' or 'force'). +nl_start_of_file_min = 0 # unsigned number + +# Add or remove newline at the end of the file. +nl_end_of_file = ignore # ignore/add/remove/force + +# The minimum number of newlines at the end of the file (only used if +# nl_end_of_file is 'add' or 'force'). +nl_end_of_file_min = 0 # unsigned number + +# Add or remove newline between '=' and '{'. +nl_assign_brace = ignore # ignore/add/remove/force + +# Add or remove newline between '[]' and '{'. +nl_tsquare_brace = ignore # ignore/add/remove/force + +# Add or remove newline between a function call's ')' and '{', as in +# 'list_for_each(item, &list) { }'. +nl_fcall_brace = remove # ignore/add/remove/force + +# Add or remove newline between 'enum' and '{'. +nl_enum_brace = remove # ignore/add/remove/force + +# Add or remove newline between 'enum' and 'class'. +nl_enum_class = remove # ignore/add/remove/force + +# Add or remove newline between 'enum class' and the identifier. +nl_enum_class_identifier = remove # ignore/add/remove/force + +# Add or remove newline between 'enum class' type and ':'. +nl_enum_identifier_colon = remove # ignore/add/remove/force + +# Add or remove newline between 'enum class identifier :' and type. +nl_enum_colon_type = remove # ignore/add/remove/force + +# Add or remove newline between 'struct and '{'. +nl_struct_brace = remove # ignore/add/remove/force + +# Add or remove newline between 'union' and '{'. +nl_union_brace = remove # ignore/add/remove/force + +# Add or remove newline between 'if' and '{'. +nl_if_brace = remove # ignore/add/remove/force + +# Add or remove newline between '}' and 'else'. +nl_brace_else = remove # ignore/add/remove/force + +# Add or remove newline between 'else if' and '{'. If set to ignore, +# nl_if_brace is used instead. +nl_elseif_brace = remove # ignore/add/remove/force + +# Add or remove newline between 'else' and '{'. +nl_else_brace = remove # ignore/add/remove/force + +# Add or remove newline between 'else' and 'if'. +nl_else_if = remove # ignore/add/remove/force + +# Add or remove newline before '{' opening brace +nl_before_opening_brace_func_class_def = remove # ignore/add/remove/force + +# Add or remove newline before 'if'/'else if' closing parenthesis. +nl_before_if_closing_paren = remove # ignore/add/remove/force + +# Add or remove newline between '}' and 'finally'. +nl_brace_finally = remove # ignore/add/remove/force + +# Add or remove newline between 'finally' and '{'. +nl_finally_brace = remove # ignore/add/remove/force + +# Add or remove newline between 'try' and '{'. +nl_try_brace = remove # ignore/add/remove/force + +# Add or remove newline between get/set and '{'. +nl_getset_brace = remove # ignore/add/remove/force + +# Add or remove newline between 'for' and '{'. +nl_for_brace = remove # ignore/add/remove/force + +# Add or remove newline before the '{' of a 'catch' statement, as in +# 'catch (decl) {'. +nl_catch_brace = remove # ignore/add/remove/force + +# Add or remove newline between '}' and 'catch'. +nl_brace_catch = remove # ignore/add/remove/force + +# Add or remove newline between '}' and ']'. +nl_brace_square = remove # ignore/add/remove/force + +# Add or remove newline between '}' and ')' in a function invocation. +nl_brace_fparen = remove # ignore/add/remove/force + +# Add or remove newline between 'while' and '{'. +nl_while_brace = remove # ignore/add/remove/force + +# Add or remove newline between two open or close braces. Due to general +# newline/brace handling, REMOVE may not work. +nl_brace_brace = ignore # ignore/add/remove/force + +# Add or remove newline between 'do' and '{'. +nl_do_brace = remove # ignore/add/remove/force + +# Add or remove newline between '}' and 'while' of 'do' statement. +nl_brace_while = remove # ignore/add/remove/force + +# Add or remove newline between 'switch' and '{'. +nl_switch_brace = remove # ignore/add/remove/force + +# Add or remove newline between 'synchronized' and '{'. +nl_synchronized_brace = remove # ignore/add/remove/force + +# Add a newline between ')' and '{' if the ')' is on a different line than the +# if/for/etc. +# +# Overrides nl_for_brace, nl_if_brace, nl_switch_brace, nl_while_switch and +# nl_catch_brace. +nl_multi_line_cond = false # true/false + +# Add a newline after '(' if an if/for/while/switch condition spans multiple +# lines +nl_multi_line_sparen_open = force # ignore/add/remove/force + +# Add a newline before ')' if an if/for/while/switch condition spans multiple +# lines. Overrides nl_before_if_closing_paren if both are specified. +nl_multi_line_sparen_close = force # ignore/add/remove/force + +# Force a newline in a define after the macro name for multi-line defines. +nl_multi_line_define = false # true/false + +# Whether to add a newline before 'case', and a blank line before a 'case' +# statement that follows a ';' or '}'. +nl_before_case = false # true/false + +# Whether to add a newline after a 'case' statement. +nl_after_case = false # true/false + +# Add or remove newline between a case ':' and '{'. +# +# Overrides nl_after_case. +nl_case_colon_brace = remove # ignore/add/remove/force + +# Add or remove newline between ')' and 'throw'. +nl_before_throw = remove # ignore/add/remove/force + +# Add or remove newline between 'namespace' and '{'. +nl_namespace_brace = remove # ignore/add/remove/force + +# Add or remove newline after 'template<...>' of a template class. +nl_template_class = remove # ignore/add/remove/force + +# Add or remove newline after 'template<...>' of a template class declaration. +# +# Overrides nl_template_class. +nl_template_class_decl = ignore # ignore/add/remove/force + +# Add or remove newline after 'template<>' of a specialized class declaration. +# +# Overrides nl_template_class_decl. +nl_template_class_decl_special = ignore # ignore/add/remove/force + +# Add or remove newline after 'template<...>' of a template class definition. +# +# Overrides nl_template_class. +nl_template_class_def = ignore # ignore/add/remove/force + +# Add or remove newline after 'template<>' of a specialized class definition. +# +# Overrides nl_template_class_def. +nl_template_class_def_special = ignore # ignore/add/remove/force + +# Add or remove newline after 'template<...>' of a template function. +nl_template_func = ignore # ignore/add/remove/force + +# Add or remove newline after 'template<...>' of a template function +# declaration. +# +# Overrides nl_template_func. +nl_template_func_decl = ignore # ignore/add/remove/force + +# Add or remove newline after 'template<>' of a specialized function +# declaration. +# +# Overrides nl_template_func_decl. +nl_template_func_decl_special = ignore # ignore/add/remove/force + +# Add or remove newline after 'template<...>' of a template function +# definition. +# +# Overrides nl_template_func. +nl_template_func_def = ignore # ignore/add/remove/force + +# Add or remove newline after 'template<>' of a specialized function +# definition. +# +# Overrides nl_template_func_def. +nl_template_func_def_special = ignore # ignore/add/remove/force + +# Add or remove newline after 'template<...>' of a template variable. +nl_template_var = ignore # ignore/add/remove/force + +# Add or remove newline between 'template<...>' and 'using' of a templated +# type alias. +nl_template_using = ignore # ignore/add/remove/force + +# Add or remove newline between 'class' and '{'. +nl_class_brace = remove # ignore/add/remove/force + +# Add or remove newline before or after (depending on pos_class_comma, +# may not be IGNORE) each',' in the base class list. +nl_class_init_args = ignore # ignore/add/remove/force + +# Add or remove newline after each ',' in the constructor member +# initialization. Related to nl_constr_colon, pos_constr_colon and +# pos_constr_comma. +nl_constr_init_args = ignore # ignore/add/remove/force + +# Add or remove newline before first element, after comma, and after last +# element, in 'enum'. +nl_enum_own_lines = force # ignore/add/remove/force + +# Add or remove newline between return type and function name in a function +# definition. +# might be modified by nl_func_leave_one_liners +nl_func_type_name = remove # ignore/add/remove/force + +# Add or remove newline between return type and function name inside a class +# definition. If set to ignore, nl_func_type_name or nl_func_proto_type_name +# is used instead. +nl_func_type_name_class = ignore # ignore/add/remove/force + +# Add or remove newline between class specification and '::' +# in 'void A::f() { }'. Only appears in separate member implementation (does +# not appear with in-line implementation). +nl_func_class_scope = ignore # ignore/add/remove/force + +# Add or remove newline between function scope and name, as in +# 'void A :: f() { }'. +nl_func_scope_name = ignore # ignore/add/remove/force + +# Add or remove newline between return type and function name in a prototype. +nl_func_proto_type_name = ignore # ignore/add/remove/force + +# Add or remove newline between a function name and the opening '(' in the +# declaration. +nl_func_paren = remove # ignore/add/remove/force + +# Overrides nl_func_paren for functions with no parameters. +nl_func_paren_empty = remove # ignore/add/remove/force + +# Add or remove newline between a function name and the opening '(' in the +# definition. +nl_func_def_paren = remove # ignore/add/remove/force + +# Overrides nl_func_def_paren for functions with no parameters. +nl_func_def_paren_empty = remove # ignore/add/remove/force + +# Add or remove newline between a function name and the opening '(' in the +# call. +nl_func_call_paren = ignore # ignore/add/remove/force + +# Overrides nl_func_call_paren for functions with no parameters. +nl_func_call_paren_empty = ignore # ignore/add/remove/force + +# Add or remove newline after '(' in a function declaration. +nl_func_decl_start = remove # ignore/add/remove/force + +# Add or remove newline after '(' in a function definition. +nl_func_def_start = remove # ignore/add/remove/force + +# Overrides nl_func_decl_start when there is only one parameter. +nl_func_decl_start_single = remove # ignore/add/remove/force + +# Overrides nl_func_def_start when there is only one parameter. +nl_func_def_start_single = remove # ignore/add/remove/force + +# Whether to add a newline after '(' in a function declaration if '(' and ')' +# are in different lines. If false, nl_func_decl_start is used instead. +nl_func_decl_start_multi_line = false # true/false + +# Whether to add a newline after '(' in a function definition if '(' and ')' +# are in different lines. If false, nl_func_def_start is used instead. +nl_func_def_start_multi_line = true # true/false + +# Add or remove newline after each ',' in a function declaration. +nl_func_decl_args = ignore # ignore/add/remove/force + +# Add or remove newline after each ',' in a function definition. +nl_func_def_args = remove # ignore/add/remove/force + +# Add or remove newline after each ',' in a function call. +nl_func_call_args = ignore # ignore/add/remove/force + +# Whether to add a newline after each ',' in a function declaration if '(' +# and ')' are in different lines. If false, nl_func_decl_args is used instead. +nl_func_decl_args_multi_line = true # true/false + +# Whether to add a newline after each ',' in a function definition if '(' +# and ')' are in different lines. If false, nl_func_def_args is used instead. +nl_func_def_args_multi_line = true # true/false + +# Add or remove newline before the ')' in a function declaration. +nl_func_decl_end = add # ignore/add/remove/force + +# Add or remove newline before the ')' in a function definition. +nl_func_def_end = ignore # ignore/add/remove/force + +# Overrides nl_func_decl_end when there is only one parameter. +nl_func_decl_end_single = remove # ignore/add/remove/force + +# Overrides nl_func_def_end when there is only one parameter. +nl_func_def_end_single = remove # ignore/add/remove/force + +# Whether to add a newline before ')' in a function declaration if '(' and ')' +# are in different lines. If false, nl_func_decl_end is used instead. +nl_func_decl_end_multi_line = false # true/false + +# Whether to add a newline before ')' in a function definition if '(' and ')' +# are in different lines. If false, nl_func_def_end is used instead. +nl_func_def_end_multi_line = false # true/false + +# Add or remove newline between '()' in a function declaration. +nl_func_decl_empty = remove # ignore/add/remove/force + +# Add or remove newline between '()' in a function definition. +nl_func_def_empty = remove # ignore/add/remove/force + +# Add or remove newline between '()' in a function call. +nl_func_call_empty = remove # ignore/add/remove/force + +# Whether to add a newline after '(' in a function call, +# has preference over nl_func_call_start_multi_line. +nl_func_call_start = ignore # ignore/add/remove/force + +# Whether to add a newline before ')' in a function call. +nl_func_call_end = ignore # ignore/add/remove/force + +# Whether to add a newline after '(' in a function call if '(' and ')' are in +# different lines. +nl_func_call_start_multi_line = true # true/false + +# Whether to add a newline after each ',' in a function call if '(' and ')' +# are in different lines. +nl_func_call_args_multi_line = true # true/false + +# Whether to add a newline before ')' in a function call if '(' and ')' are in +# different lines. +nl_func_call_end_multi_line = false # true/false + +# Whether to respect nl_func_call_XXX option incase of closure args. +nl_func_call_args_multi_line_ignore_closures = false # true/false + +# Whether to add a newline after '<' of a template parameter list. +nl_template_start = false # true/false + +# Whether to add a newline after each ',' in a template parameter list. +nl_template_args = false # true/false + +# Whether to add a newline before '>' of a template parameter list. +nl_template_end = false # true/false + +# Add or remove newline between function signature and '{'. +nl_fdef_brace = ignore # ignore/add/remove/force + +# Add or remove newline between function signature and '{', +# if signature ends with ')'. Overrides nl_fdef_brace. +nl_fdef_brace_cond = ignore # ignore/add/remove/force + +# Add or remove newline between 'return' and the return expression. +nl_return_expr = remove # ignore/add/remove/force + +# Whether to add a newline after semicolons, except in 'for' statements. +nl_after_semicolon = false # true/false + +# Whether to add a newline after the type in an unnamed temporary +# direct-list-initialization. +nl_type_brace_init_lst = ignore # ignore/add/remove/force + +# Whether to add a newline after the open brace in an unnamed temporary +# direct-list-initialization. +nl_type_brace_init_lst_open = ignore # ignore/add/remove/force + +# Whether to add a newline before the close brace in an unnamed temporary +# direct-list-initialization. +nl_type_brace_init_lst_close = ignore # ignore/add/remove/force + +# Whether to add a newline after '{'. This also adds a newline before the +# matching '}'. +nl_after_brace_open = true # true/false + +# Whether to add a newline between the open brace and a trailing single-line +# comment. Requires nl_after_brace_open=true. +nl_after_brace_open_cmt = false # true/false + +# Whether to add a newline after a virtual brace open with a non-empty body. +# These occur in un-braced if/while/do/for statement bodies. +nl_after_vbrace_open = true # true/false + +# Whether to add a newline after a virtual brace open with an empty body. +# These occur in un-braced if/while/do/for statement bodies. +nl_after_vbrace_open_empty = false # true/false + +# Whether to add a newline after '}'. Does not apply if followed by a +# necessary ';'. +nl_after_brace_close = true # true/false + +# Whether to add a newline after a virtual brace close, +# as in 'if (foo) a++; return;'. +nl_after_vbrace_close = false # true/false + +# Add or remove newline between the close brace and identifier, +# as in 'struct { int a; } b;'. Affects enumerations, unions and +# structures. If set to ignore, uses nl_after_brace_close. +nl_brace_struct_var = ignore # ignore/add/remove/force + +# Whether to alter newlines in '#define' macros. +nl_define_macro = false # true/false + +# Whether to alter newlines between consecutive parenthesis closes. The number +# of closing parentheses in a line will depend on respective open parenthesis +# lines. +nl_squeeze_paren_close = false # true/false + +# Whether to remove blanks after '#ifxx' and '#elxx', or before '#elxx' and +# '#endif'. Does not affect top-level #ifdefs. +nl_squeeze_ifdef = false # true/false + +# Makes the nl_squeeze_ifdef option affect the top-level #ifdefs as well. +nl_squeeze_ifdef_top_level = false # true/false + +# Add or remove blank line before 'if'. +nl_before_if = ignore # ignore/add/remove/force + +# Add or remove blank line after 'if' statement. Add/Force work only if the +# next token is not a closing brace. +nl_after_if = ignore # ignore/add/remove/force + +# Add or remove blank line before 'for'. +nl_before_for = ignore # ignore/add/remove/force + +# Add or remove blank line after 'for' statement. +nl_after_for = ignore # ignore/add/remove/force + +# Add or remove blank line before 'while'. +nl_before_while = ignore # ignore/add/remove/force + +# Add or remove blank line after 'while' statement. +nl_after_while = ignore # ignore/add/remove/force + +# Add or remove blank line before 'switch'. +nl_before_switch = ignore # ignore/add/remove/force + +# Add or remove blank line after 'switch' statement. +nl_after_switch = ignore # ignore/add/remove/force + +# Add or remove blank line before 'synchronized'. +nl_before_synchronized = ignore # ignore/add/remove/force + +# Add or remove blank line after 'synchronized' statement. +nl_after_synchronized = ignore # ignore/add/remove/force + +# Add or remove blank line before 'do'. +nl_before_do = ignore # ignore/add/remove/force + +# Add or remove blank line after 'do/while' statement. +nl_after_do = ignore # ignore/add/remove/force + +# Whether to put a blank line before 'return' statements, unless after an open +# brace. +nl_before_return = false # true/false + +# Whether to put a blank line after 'return' statements, unless followed by a +# close brace. +nl_after_return = false # true/false + +# Whether to put a blank line before a member '.' or '->' operators. +nl_before_member = ignore # ignore/add/remove/force + +# Whether to double-space commented-entries in 'struct'/'union'/'enum'. +nl_ds_struct_enum_cmt = false # true/false + +# Whether to force a newline before '}' of a 'struct'/'union'/'enum'. +# (Lower priority than eat_blanks_before_close_brace.) +nl_ds_struct_enum_close_brace = false # true/false + +# Add or remove newline before or after (depending on pos_class_colon) a class +# colon, as in 'class Foo : public Bar'. +nl_class_colon = ignore # ignore/add/remove/force + +# Add or remove newline around a class constructor colon. The exact position +# depends on nl_constr_init_args, pos_constr_colon and pos_constr_comma. +nl_constr_colon = ignore # ignore/add/remove/force + +# Whether to collapse a two-line namespace, like 'namespace foo\n{ decl; }' +# into a single line. If true, prevents other brace newline rules from turning +# such code into four lines. +nl_namespace_two_to_one_liner = false # true/false + +# Whether to remove a newline in simple unbraced if statements, turning them +# into one-liners, as in 'if(b)\n i++;' => 'if(b) i++;'. +nl_create_if_one_liner = false # true/false + +# Whether to remove a newline in simple unbraced for statements, turning them +# into one-liners, as in 'for (...)\n stmt;' => 'for (...) stmt;'. +nl_create_for_one_liner = false # true/false + +# Whether to remove a newline in simple unbraced while statements, turning +# them into one-liners, as in 'while (expr)\n stmt;' => 'while (expr) stmt;'. +nl_create_while_one_liner = false # true/false + +# Whether to collapse a function definition whose body (not counting braces) +# is only one line so that the entire definition (prototype, braces, body) is +# a single line. +nl_create_func_def_one_liner = false # true/false + +# Whether to collapse a function definition whose body (not counting braces) +# is only one line so that the entire definition (prototype, braces, body) is +# a single line. +nl_create_list_one_liner = false # true/false + +# Whether to split one-line simple unbraced if statements into two lines by +# adding a newline, as in 'if(b) i++;'. +nl_split_if_one_liner = false # true/false + +# Whether to split one-line simple unbraced for statements into two lines by +# adding a newline, as in 'for (...) stmt;'. +nl_split_for_one_liner = false # true/false + +# Whether to split one-line simple unbraced while statements into two lines by +# adding a newline, as in 'while (expr) stmt;'. +nl_split_while_one_liner = false # true/false + +# +# Blank line options +# + +# The maximum number of consecutive newlines (3 = 2 blank lines). +nl_max = 0 # unsigned number + +# The maximum number of consecutive newlines in a function. +nl_max_blank_in_func = 0 # unsigned number + +# The number of newlines inside an empty function body. +# This option is overridden by nl_collapse_empty_body=true +nl_inside_empty_func = 0 # unsigned number + +# The number of newlines before a function prototype. +nl_before_func_body_proto = 0 # unsigned number + +# The number of newlines before a multi-line function definition. +nl_before_func_body_def = 0 # unsigned number + +# The number of newlines before a class constructor/destructor prototype. +nl_before_func_class_proto = 0 # unsigned number + +# The number of newlines before a class constructor/destructor definition. +nl_before_func_class_def = 0 # unsigned number + +# The number of newlines after a function prototype. +nl_after_func_proto = 0 # unsigned number + +# The number of newlines after a function prototype, if not followed by +# another function prototype. +nl_after_func_proto_group = 0 # unsigned number + +# The number of newlines after a class constructor/destructor prototype. +nl_after_func_class_proto = 0 # unsigned number + +# The number of newlines after a class constructor/destructor prototype, +# if not followed by another constructor/destructor prototype. +nl_after_func_class_proto_group = 0 # unsigned number + +# Whether one-line method definitions inside a class body should be treated +# as if they were prototypes for the purposes of adding newlines. +# +# Requires nl_class_leave_one_liners=true. Overrides nl_before_func_body_def +# and nl_before_func_class_def for one-liners. +nl_class_leave_one_liner_groups = false # true/false + +# The number of newlines after '}' of a multi-line function body. +nl_after_func_body = 0 # unsigned number + +# The number of newlines after '}' of a multi-line function body in a class +# declaration. Also affects class constructors/destructors. +# +# Overrides nl_after_func_body. +nl_after_func_body_class = 0 # unsigned number + +# The number of newlines after '}' of a single line function body. Also +# affects class constructors/destructors. +# +# Overrides nl_after_func_body and nl_after_func_body_class. +nl_after_func_body_one_liner = 0 # unsigned number + +# The number of blank lines after a block of variable definitions at the top +# of a function body. +# +# 0: No change (default). +nl_func_var_def_blk = 0 # unsigned number + +# The number of newlines before a block of typedefs. If nl_after_access_spec +# is non-zero, that option takes precedence. +# +# 0: No change (default). +nl_typedef_blk_start = 0 # unsigned number + +# The number of newlines after a block of typedefs. +# +# 0: No change (default). +nl_typedef_blk_end = 0 # unsigned number + +# The maximum number of consecutive newlines within a block of typedefs. +# +# 0: No change (default). +nl_typedef_blk_in = 0 # unsigned number + +# The number of newlines before a block of variable definitions not at the top +# of a function body. If nl_after_access_spec is non-zero, that option takes +# precedence. +# +# 0: No change (default). +nl_var_def_blk_start = 0 # unsigned number + +# The number of newlines after a block of variable definitions not at the top +# of a function body. +# +# 0: No change (default). +nl_var_def_blk_end = 0 # unsigned number + +# The maximum number of consecutive newlines within a block of variable +# definitions. +# +# 0: No change (default). +nl_var_def_blk_in = 0 # unsigned number + +# The minimum number of newlines before a multi-line comment. +# Doesn't apply if after a brace open or another multi-line comment. +nl_before_block_comment = 0 # unsigned number + +# The minimum number of newlines before a single-line C comment. +# Doesn't apply if after a brace open or other single-line C comments. +nl_before_c_comment = 0 # unsigned number + +# Whether to force a newline after a multi-line comment. +nl_after_multiline_comment = false # true/false + +# Whether to force a newline after a label's colon. +nl_after_label_colon = false # true/false + +# The number of newlines after '}' or ';' of a struct/enum/union definition. +nl_after_struct = 0 # unsigned number + +# The number of newlines before a class definition. +nl_before_class = 0 # unsigned number + +# The number of newlines after '}' or ';' of a class definition. +nl_after_class = 0 # unsigned number + +# The number of newlines before a namespace. +nl_before_namespace = 0 # unsigned number + +# The number of newlines after '{' of a namespace. This also adds newlines +# before the matching '}'. +# +# 0: Apply eat_blanks_after_open_brace or eat_blanks_before_close_brace if +# applicable, otherwise no change. +# +# Overrides eat_blanks_after_open_brace and eat_blanks_before_close_brace. +nl_inside_namespace = 0 # unsigned number + +# The number of newlines after '}' of a namespace. +nl_after_namespace = 0 # unsigned number + +# The number of newlines before an access specifier label. This also includes +# the Qt-specific 'signals:' and 'slots:'. Will not change the newline count +# if after a brace open. +# +# 0: No change (default). +nl_before_access_spec = 0 # unsigned number + +# The number of newlines after an access specifier label. This also includes +# the Qt-specific 'signals:' and 'slots:'. Will not change the newline count +# if after a brace open. +# +# 0: No change (default). +# +# Overrides nl_typedef_blk_start and nl_var_def_blk_start. +nl_after_access_spec = 0 # unsigned number + +# The number of newlines between a function definition and the function +# comment, as in '// comment\n void foo() {...}'. +# +# 0: No change (default). +nl_comment_func_def = 0 # unsigned number + +# The number of newlines after a try-catch-finally block that isn't followed +# by a brace close. +# +# 0: No change (default). +nl_after_try_catch_finally = 0 # unsigned number + +# Whether to remove blank lines after '{'. +eat_blanks_after_open_brace = false # true/false + +# Whether to remove blank lines before '}'. +eat_blanks_before_close_brace = false # true/false + +# How aggressively to remove extra newlines not in preprocessor. +# +# 0: No change (default) +# 1: Remove most newlines not handled by other config +# 2: Remove all newlines and reformat completely by config +nl_remove_extra_newlines = 0 # unsigned number + +# The number of newlines before a whole-file #ifdef. +# +# 0: No change (default). +nl_before_whole_file_ifdef = 0 # unsigned number + +# The number of newlines after a whole-file #ifdef. +# +# 0: No change (default). +nl_after_whole_file_ifdef = 0 # unsigned number + +# The number of newlines before a whole-file #endif. +# +# 0: No change (default). +nl_before_whole_file_endif = 0 # unsigned number + +# The number of newlines after a whole-file #endif. +# +# 0: No change (default). +nl_after_whole_file_endif = 0 # unsigned number + +# +# Positioning options +# + +# The position of arithmetic operators in wrapped expressions. +pos_arith = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of assignment in wrapped expressions. Do not affect '=' +# followed by '{'. +pos_assign = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of Boolean operators in wrapped expressions. +pos_bool = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of comparison operators in wrapped expressions. +pos_compare = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of conditional operators, as in the '?' and ':' of +# 'expr ? stmt : stmt', in wrapped expressions. +pos_conditional = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of the comma in wrapped expressions. +pos_comma = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of the comma in enum entries. +pos_enum_comma = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of the comma in the base class list if there is more than one +# line. Affects nl_class_init_args. +pos_class_comma = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of the comma in the constructor initialization list. +# Related to nl_constr_colon, nl_constr_init_args and pos_constr_colon. +pos_constr_comma = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of trailing/leading class colon, between class and base class +# list. Affects nl_class_colon. +pos_class_colon = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of colons between constructor and member initialization. +# Related to nl_constr_colon, nl_constr_init_args and pos_constr_comma. +pos_constr_colon = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of shift operators in wrapped expressions. +pos_shift = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# +# Line splitting options +# + +# Try to limit code width to N columns. +code_width = 0 # unsigned number + +# Whether to fully split long 'for' statements at semi-colons. +ls_for_split_full = false # true/false + +# Whether to fully split long function prototypes/calls at commas. +# The option ls_code_width has priority over the option ls_func_split_full. +ls_func_split_full = false # true/false + +# Whether to split lines as close to code_width as possible and ignore some +# groupings. +# The option ls_code_width has priority over the option ls_func_split_full. +ls_code_width = false # true/false + +# +# Code alignment options (not left column spaces/tabs) +# + +# Whether to keep non-indenting tabs. +align_keep_tabs = false # true/false + +# Whether to use tabs for aligning. +align_with_tabs = false # true/false + +# Whether to bump out to the next tab when aligning. +align_on_tabstop = false # true/false + +# Whether to right-align numbers. +align_number_right = false # true/false + +# Whether to keep whitespace not required for alignment. +align_keep_extra_space = false # true/false + +# Whether to align variable definitions in prototypes and functions. +align_func_params = false # true/false + +# The span for aligning parameter definitions in function on parameter name. +# +# 0: Don't align (default). +align_func_params_span = 0 # unsigned number + +# The threshold for aligning function parameter definitions. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_func_params_thresh = 0 # number + +# The gap for aligning function parameter definitions. +align_func_params_gap = 0 # unsigned number + +# The span for aligning constructor value. +# +# 0: Don't align (default). +align_constr_value_span = 0 # unsigned number + +# The threshold for aligning constructor value. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_constr_value_thresh = 0 # number + +# The gap for aligning constructor value. +align_constr_value_gap = 0 # unsigned number + +# Whether to align parameters in single-line functions that have the same +# name. The function names must already be aligned with each other. +align_same_func_call_params = false # true/false + +# The span for aligning function-call parameters for single line functions. +# +# 0: Don't align (default). +align_same_func_call_params_span = 0 # unsigned number + +# The threshold for aligning function-call parameters for single line +# functions. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_same_func_call_params_thresh = 0 # number + +# The span for aligning variable definitions. +# +# 0: Don't align (default). +align_var_def_span = 0 # unsigned number + +# How to consider (or treat) the '*' in the alignment of variable definitions. +# +# 0: Part of the type 'void * foo;' (default) +# 1: Part of the variable 'void *foo;' +# 2: Dangling 'void *foo;' +# Dangling: the '*' will not be taken into account when aligning. +align_var_def_star_style = 0 # unsigned number + +# How to consider (or treat) the '&' in the alignment of variable definitions. +# +# 0: Part of the type 'long & foo;' (default) +# 1: Part of the variable 'long &foo;' +# 2: Dangling 'long &foo;' +# Dangling: the '&' will not be taken into account when aligning. +align_var_def_amp_style = 1 # unsigned number + +# The threshold for aligning variable definitions. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_var_def_thresh = 0 # number + +# The gap for aligning variable definitions. +align_var_def_gap = 0 # unsigned number + +# Whether to align the colon in struct bit fields. +align_var_def_colon = false # true/false + +# The gap for aligning the colon in struct bit fields. +align_var_def_colon_gap = 0 # unsigned number + +# Whether to align any attribute after the variable name. +align_var_def_attribute = false # true/false + +# Whether to align inline struct/enum/union variable definitions. +align_var_def_inline = false # true/false + +# The span for aligning on '=' in assignments. +# +# 0: Don't align (default). +align_assign_span = 0 # unsigned number + +# The span for aligning on '=' in function prototype modifier. +# +# 0: Don't align (default). +align_assign_func_proto_span = 0 # unsigned number + +# The threshold for aligning on '=' in assignments. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_assign_thresh = 0 # number + +# How to apply align_assign_span to function declaration "assignments", i.e. +# 'virtual void foo() = 0' or '~foo() = {default|delete}'. +# +# 0: Align with other assignments (default) +# 1: Align with each other, ignoring regular assignments +# 2: Don't align +align_assign_decl_func = 0 # unsigned number + +# The span for aligning on '=' in enums. +# +# 0: Don't align (default). +align_enum_equ_span = 0 # unsigned number + +# The threshold for aligning on '=' in enums. +# Use a negative number for absolute thresholds. +# +# 0: no limit (default). +align_enum_equ_thresh = 0 # number + +# The span for aligning class member definitions. +# +# 0: Don't align (default). +align_var_class_span = 0 # unsigned number + +# The threshold for aligning class member definitions. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_var_class_thresh = 0 # number + +# The gap for aligning class member definitions. +align_var_class_gap = 0 # unsigned number + +# The span for aligning struct/union member definitions. +# +# 0: Don't align (default). +align_var_struct_span = 0 # unsigned number + +# The threshold for aligning struct/union member definitions. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_var_struct_thresh = 0 # number + +# The gap for aligning struct/union member definitions. +align_var_struct_gap = 0 # unsigned number + +# The span for aligning struct initializer values. +# +# 0: Don't align (default). +align_struct_init_span = 0 # unsigned number + +# The span for aligning single-line typedefs. +# +# 0: Don't align (default). +align_typedef_span = 0 # unsigned number + +# The minimum space between the type and the synonym of a typedef. +align_typedef_gap = 0 # unsigned number + +# How to align typedef'd functions with other typedefs. +# +# 0: Don't mix them at all (default) +# 1: Align the open parenthesis with the types +# 2: Align the function type name with the other type names +align_typedef_func = 0 # unsigned number + +# How to consider (or treat) the '*' in the alignment of typedefs. +# +# 0: Part of the typedef type, 'typedef int * pint;' (default) +# 1: Part of type name: 'typedef int *pint;' +# 2: Dangling: 'typedef int *pint;' +# Dangling: the '*' will not be taken into account when aligning. +align_typedef_star_style = 0 # unsigned number + +# How to consider (or treat) the '&' in the alignment of typedefs. +# +# 0: Part of the typedef type, 'typedef int & intref;' (default) +# 1: Part of type name: 'typedef int &intref;' +# 2: Dangling: 'typedef int &intref;' +# Dangling: the '&' will not be taken into account when aligning. +align_typedef_amp_style = 0 # unsigned number + +# The span for aligning comments that end lines. +# +# 0: Don't align (default). +align_right_cmt_span = 0 # unsigned number + +# Minimum number of columns between preceding text and a trailing comment in +# order for the comment to qualify for being aligned. Must be non-zero to have +# an effect. +align_right_cmt_gap = 0 # unsigned number + +# If aligning comments, whether to mix with comments after '}' and #endif with +# less than three spaces before the comment. +align_right_cmt_mix = false # true/false + +# Whether to only align trailing comments that are at the same brace level. +align_right_cmt_same_level = false # true/false + +# Minimum column at which to align trailing comments. Comments which are +# aligned beyond this column, but which can be aligned in a lesser column, +# may be "pulled in". +# +# 0: Ignore (default). +align_right_cmt_at_col = 0 # unsigned number + +# The span for aligning function prototypes. +# +# 0: Don't align (default). +align_func_proto_span = 0 # unsigned number + +# The threshold for aligning function prototypes. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_func_proto_thresh = 0 # number + +# Minimum gap between the return type and the function name. +align_func_proto_gap = 0 # unsigned number + +# Whether to align function prototypes on the 'operator' keyword instead of +# what follows. +align_on_operator = false # true/false + +# Whether to mix aligning prototype and variable declarations. If true, +# align_var_def_XXX options are used instead of align_func_proto_XXX options. +align_mix_var_proto = false # true/false + +# Whether to align single-line functions with function prototypes. +# Uses align_func_proto_span. +align_single_line_func = false # true/false + +# Whether to align the open brace of single-line functions. +# Requires align_single_line_func=true. Uses align_func_proto_span. +align_single_line_brace = false # true/false + +# Gap for align_single_line_brace. +align_single_line_brace_gap = 0 # unsigned number + +# Whether to align macros wrapped with a backslash and a newline. This will +# not work right if the macro contains a multi-line comment. +align_nl_cont = false # true/false + +# Whether to align macro functions and variables together. +align_pp_define_together = false # true/false + +# The span for aligning on '#define' bodies. +# +# =0: Don't align (default) +# >0: Number of lines (including comments) between blocks +align_pp_define_span = 0 # unsigned number + +# The minimum space between label and value of a preprocessor define. +align_pp_define_gap = 0 # unsigned number + +# Whether to align lines that start with '<<' with previous '<<'. +# +# Default: true +align_left_shift = true # true/false + +# Whether to align comma-separated statements following '<<' (as used to +# initialize Eigen matrices). +align_eigen_comma_init = false # true/false + +# Whether to align text after 'asm volatile ()' colons. +align_asm_colon = false # true/false + +# +# Comment modification options +# + +# Try to wrap comments at N columns. +cmt_width = 0 # unsigned number + +# How to reflow comments. +# +# 0: No reflowing (apart from the line wrapping due to cmt_width) (default) +# 1: No touching at all +# 2: Full reflow +cmt_reflow_mode = 0 # unsigned number + +# Whether to convert all tabs to spaces in comments. If false, tabs in +# comments are left alone, unless used for indenting. +cmt_convert_tab_to_spaces = false # true/false + +# Whether to apply changes to multi-line comments, including cmt_width, +# keyword substitution and leading chars. +# +# Default: true +cmt_indent_multi = true # true/false + +# Whether to group c-comments that look like they are in a block. +cmt_c_group = false # true/false + +# Whether to put an empty '/*' on the first line of the combined c-comment. +cmt_c_nl_start = false # true/false + +# Whether to add a newline before the closing '*/' of the combined c-comment. +cmt_c_nl_end = false # true/false + +# Whether to put a star on subsequent comment lines. +cmt_star_cont = false # true/false + +# The number of spaces to insert at the start of subsequent comment lines. +cmt_sp_before_star_cont = 0 # unsigned number + +# The number of spaces to insert after the star on subsequent comment lines. +cmt_sp_after_star_cont = 0 # unsigned number + +# For multi-line comments with a '*' lead, remove leading spaces if the first +# and last lines of the comment are the same length. +# +# Default: true +cmt_multi_check_last = true # true/false + +# For multi-line comments with a '*' lead, remove leading spaces if the first +# and last lines of the comment are the same length AND if the length is +# bigger as the first_len minimum. +# +# Default: 4 +cmt_multi_first_len_minimum = 4 # unsigned number + +# Path to a file that contains text to insert at the beginning of a file if +# the file doesn't start with a C/C++ comment. If the inserted text contains +# '$(filename)', that will be replaced with the current file's name. +cmt_insert_file_header = "" # string + +# Path to a file that contains text to insert at the end of a file if the +# file doesn't end with a C/C++ comment. If the inserted text contains +# '$(filename)', that will be replaced with the current file's name. +cmt_insert_file_footer = "" # string + +# Path to a file that contains text to insert before a function definition if +# the function isn't preceded by a C/C++ comment. If the inserted text +# contains '$(function)', '$(javaparam)' or '$(fclass)', these will be +# replaced with, respectively, the name of the function, the javadoc '@param' +# and '@return' stuff, or the name of the class to which the member function +# belongs. +cmt_insert_func_header = "" # string + +# Path to a file that contains text to insert before a class if the class +# isn't preceded by a C/C++ comment. If the inserted text contains '$(class)', +# that will be replaced with the class name. +cmt_insert_class_header = "" # string + +# Path to a file that contains text to insert before an Objective-C message +# specification, if the method isn't preceded by a C/C++ comment. If the +# inserted text contains '$(message)' or '$(javaparam)', these will be +# replaced with, respectively, the name of the function, or the javadoc +# '@param' and '@return' stuff. +cmt_insert_oc_msg_header = "" # string + +# Whether a comment should be inserted if a preprocessor is encountered when +# stepping backwards from a function name. +# +# Applies to cmt_insert_oc_msg_header, cmt_insert_func_header and +# cmt_insert_class_header. +cmt_insert_before_preproc = false # true/false + +# Whether a comment should be inserted if a function is declared inline to a +# class definition. +# +# Applies to cmt_insert_func_header. +# +# Default: true +cmt_insert_before_inlines = true # true/false + +# Whether a comment should be inserted if the function is a class constructor +# or destructor. +# +# Applies to cmt_insert_func_header. +cmt_insert_before_ctor_dtor = false # true/false + +# +# Code modifying options (non-whitespace) +# + +# Add or remove braces on a single-line 'do' statement. +mod_full_brace_do = ignore # ignore/add/remove/force + +# Add or remove braces on a single-line 'for' statement. +mod_full_brace_for = ignore # ignore/add/remove/force + +# (Pawn) Add or remove braces on a single-line function definition. +mod_full_brace_function = ignore # ignore/add/remove/force + +# Add or remove braces on a single-line 'if' statement. Braces will not be +# removed if the braced statement contains an 'else'. +mod_full_brace_if = ignore # ignore/add/remove/force + +# Whether to enforce that all blocks of an 'if'/'else if'/'else' chain either +# have, or do not have, braces. If true, braces will be added if any block +# needs braces, and will only be removed if they can be removed from all +# blocks. +# +# Overrides mod_full_brace_if. +mod_full_brace_if_chain = false # true/false + +# Whether to add braces to all blocks of an 'if'/'else if'/'else' chain. +# If true, mod_full_brace_if_chain will only remove braces from an 'if' that +# does not have an 'else if' or 'else'. +mod_full_brace_if_chain_only = false # true/false + +# Add or remove braces on single-line 'while' statement. +mod_full_brace_while = ignore # ignore/add/remove/force + +# Add or remove braces on single-line 'using ()' statement. +mod_full_brace_using = ignore # ignore/add/remove/force + +# Don't remove braces around statements that span N newlines +mod_full_brace_nl = 0 # unsigned number + +# Whether to prevent removal of braces from 'if'/'for'/'while'/etc. blocks +# which span multiple lines. +# +# Affects: +# mod_full_brace_for +# mod_full_brace_if +# mod_full_brace_if_chain +# mod_full_brace_if_chain_only +# mod_full_brace_while +# mod_full_brace_using +# +# Does not affect: +# mod_full_brace_do +# mod_full_brace_function +mod_full_brace_nl_block_rem_mlcond = false # true/false + +# Add or remove unnecessary parenthesis on 'return' statement. +mod_paren_on_return = ignore # ignore/add/remove/force + +# (Pawn) Whether to change optional semicolons to real semicolons. +mod_pawn_semicolon = false # true/false + +# Whether to fully parenthesize Boolean expressions in 'while' and 'if' +# statement, as in 'if (a && b > c)' => 'if (a && (b > c))'. +mod_full_paren_if_bool = false # true/false + +# Whether to remove superfluous semicolons. +mod_remove_extra_semicolon = false # true/false + +# If a function body exceeds the specified number of newlines and doesn't have +# a comment after the close brace, a comment will be added. +mod_add_long_function_closebrace_comment = 0 # unsigned number + +# If a namespace body exceeds the specified number of newlines and doesn't +# have a comment after the close brace, a comment will be added. +mod_add_long_namespace_closebrace_comment = 0 # unsigned number + +# If a class body exceeds the specified number of newlines and doesn't have a +# comment after the close brace, a comment will be added. +mod_add_long_class_closebrace_comment = 0 # unsigned number + +# If a switch body exceeds the specified number of newlines and doesn't have a +# comment after the close brace, a comment will be added. +mod_add_long_switch_closebrace_comment = 0 # unsigned number + +# If an #ifdef body exceeds the specified number of newlines and doesn't have +# a comment after the #endif, a comment will be added. +mod_add_long_ifdef_endif_comment = 0 # unsigned number + +# If an #ifdef or #else body exceeds the specified number of newlines and +# doesn't have a comment after the #else, a comment will be added. +mod_add_long_ifdef_else_comment = 0 # unsigned number + +# Whether to take care of the case by the mod_sort_xx options. +mod_sort_case_sensitive = false # true/false + +# Whether to sort consecutive single-line 'import' statements. +mod_sort_import = false # true/false + +# Whether to sort consecutive single-line '#include' statements (C/C++) and +# '#import' statements (Objective-C). Be aware that this has the potential to +# break your code if your includes/imports have ordering dependencies. +mod_sort_include = false # true/false + +# Whether to prioritize '#include' and '#import' statements that contain +# filename without extension when sorting is enabled. +mod_sort_incl_import_prioritize_filename = false # true/false + +# Whether to prioritize '#include' and '#import' statements that does not +# contain extensions when sorting is enabled. +mod_sort_incl_import_prioritize_extensionless = false # true/false + +# Whether to prioritize '#include' and '#import' statements that contain +# angle over quotes when sorting is enabled. +mod_sort_incl_import_prioritize_angle_over_quotes = false # true/false + +# Whether to ignore file extension in '#include' and '#import' statements +# for sorting comparison. +mod_sort_incl_import_ignore_extension = false # true/false + +# Whether to group '#include' and '#import' statements when sorting is enabled. +mod_sort_incl_import_grouping_enabled = false # true/false + +# Whether to move a 'break' that appears after a fully braced 'case' before +# the close brace, as in 'case X: { ... } break;' => 'case X: { ... break; }'. +mod_move_case_break = false # true/false + +# Add or remove braces around a fully braced case statement. Will only remove +# braces if there are no variable declarations in the block. +mod_case_brace = ignore # ignore/add/remove/force + +# Whether to remove a void 'return;' that appears as the last statement in a +# function. +mod_remove_empty_return = false # true/false + +# Add or remove the comma after the last value of an enumeration. +mod_enum_last_comma = ignore # ignore/add/remove/force + +# +# Preprocessor options +# + +# Add or remove indentation of preprocessor directives inside #if blocks +# at brace level 0 (file-level). +pp_indent = ignore # ignore/add/remove/force + +# Whether to indent #if/#else/#endif at the brace level. If false, these are +# indented from column 1. +pp_indent_at_level = false # true/false + +# Specifies the number of columns to indent preprocessors per level +# at brace level 0 (file-level). If pp_indent_at_level=false, also specifies +# the number of columns to indent preprocessors per level +# at brace level > 0 (function-level). +# +# Default: 1 +pp_indent_count = 1 # unsigned number + +# Add or remove space after # based on pp_level of #if blocks. +pp_space = ignore # ignore/add/remove/force + +# Sets the number of spaces per level added with pp_space. +pp_space_count = 0 # unsigned number + +# The indent for '#region' and '#endregion' in C# and '#pragma region' in +# C/C++. Negative values decrease indent down to the first column. +pp_indent_region = 0 # number + +# Whether to indent the code between #region and #endregion. +pp_region_indent_code = false # true/false + +# If pp_indent_at_level=true, sets the indent for #if, #else and #endif when +# not at file-level. Negative values decrease indent down to the first column. +# +# =0: Indent preprocessors using output_tab_size +# >0: Column at which all preprocessors will be indented +pp_indent_if = 0 # number + +# Whether to indent the code between #if, #else and #endif. +pp_if_indent_code = false # true/false + +# Whether to indent '#define' at the brace level. If false, these are +# indented from column 1. +pp_define_at_level = false # true/false + +# Whether to ignore the '#define' body while formatting. +pp_ignore_define_body = false # true/false + +# Whether to indent case statements between #if, #else, and #endif. +# Only applies to the indent of the preprocesser that the case statements +# directly inside of. +# +# Default: true +pp_indent_case = true # true/false + +# Whether to indent whole function definitions between #if, #else, and #endif. +# Only applies to the indent of the preprocesser that the function definition +# is directly inside of. +# +# Default: true +pp_indent_func_def = true # true/false + +# Whether to indent extern C blocks between #if, #else, and #endif. +# Only applies to the indent of the preprocesser that the extern block is +# directly inside of. +# +# Default: true +pp_indent_extern = true # true/false + +# Whether to indent braces directly inside #if, #else, and #endif. +# Only applies to the indent of the preprocesser that the braces are directly +# inside of. +# +# Default: true +pp_indent_brace = true # true/false + +# +# Sort includes options +# + +# The regex for include category with priority 0. +include_category_0 = "" # string + +# The regex for include category with priority 1. +include_category_1 = "" # string + +# The regex for include category with priority 2. +include_category_2 = "" # string + +# +# Use or Do not Use options +# + +# true: indent_func_call_param will be used (default) +# false: indent_func_call_param will NOT be used +# +# Default: true +use_indent_func_call_param = true # true/false + +# The value of the indentation for a continuation line is calculated +# differently if the statement is: +# - a declaration: your case with QString fileName ... +# - an assignment: your case with pSettings = new QSettings( ... +# +# At the second case the indentation value might be used twice: +# - at the assignment +# - at the function call (if present) +# +# To prevent the double use of the indentation value, use this option with the +# value 'true'. +# +# true: indent_continue will be used only once +# false: indent_continue will be used every time (default) +use_indent_continue_only_once = false # true/false + +# Whether sp_after_angle takes precedence over sp_inside_fparen. This was the +# historic behavior, but is probably not the desired behavior, so this is off +# by default. +use_sp_after_angle_always = false # true/false + +# Whether to apply special formatting for Qt SIGNAL/SLOT macros. Essentially, +# this tries to format these so that they match Qt's normalized form (i.e. the +# result of QMetaObject::normalizedSignature), which can slightly improve the +# performance of the QObject::connect call, rather than how they would +# otherwise be formatted. +# +# See options_for_QT.cpp for details. +# +# Default: true +use_options_overriding_for_qt_macros = true # true/false + +# If true: the form feed character is removed from the list +# of whitespace characters. +# See https://en.cppreference.com/w/cpp/string/byte/isspace +use_form_feed_no_more_as_whitespace_character = false # true/false + +# +# Warn levels - 1: error, 2: warning (default), 3: note +# + +# Limit the number of loops. +# Used by uncrustify.cpp to exit from infinite loop. +# 0: no limit. +debug_max_number_of_loops = 0 # number + +# Set the number of the line to protocol; +# Used in the function prot_the_line if the 2. parameter is zero. +# 0: nothing protocol. +debug_line_number_to_protocol = 0 # number + +# Set the number of second(s) before terminating formatting the current file, +# 0: no timeout. +# only for linux +debug_timeout = 0 # number + +# Meaning of the settings: +# Ignore - do not do any changes +# Add - makes sure there is 1 or more space/brace/newline/etc +# Force - makes sure there is exactly 1 space/brace/newline/etc, +# behaves like Add in some contexts +# Remove - removes space/brace/newline/etc +# +# +# - Token(s) can be treated as specific type(s) with the 'set' option: +# `set tokenType tokenString [tokenString...]` +# +# Example: +# `set BOOL __AND__ __OR__` +# +# tokenTypes are defined in src/token_enum.h, use them without the +# 'CT_' prefix: 'CT_BOOL' => 'BOOL' +# +# +# - Token(s) can be treated as type(s) with the 'type' option. +# `type tokenString [tokenString...]` +# +# Example: +# `type int c_uint_8 Rectangle` +# +# This can also be achieved with `set TYPE int c_uint_8 Rectangle` +# +# +# To embed whitespace in tokenStrings use the '\' escape character, or quote +# the tokenStrings. These quotes are supported: "'` +# +# +# - Support for the auto detection of languages through the file ending can be +# added using the 'file_ext' command. +# `file_ext langType langString [langString..]` +# +# Example: +# `file_ext CPP .ch .cxx .cpp.in` +# +# langTypes are defined in uncrusify_types.h in the lang_flag_e enum, use +# them without the 'LANG_' prefix: 'LANG_CPP' => 'CPP' +# +# +# - Custom macro-based indentation can be set up using 'macro-open', +# 'macro-else' and 'macro-close'. +# `(macro-open | macro-else | macro-close) tokenString` +# +# Example: +# `macro-open BEGIN_TEMPLATE_MESSAGE_MAP` +# `macro-open BEGIN_MESSAGE_MAP` +# `macro-close END_MESSAGE_MAP` +# +# +# option(s) with 'not default' value: 0 +# diff --git a/meson.build b/meson.build index 332afbc..bc52839 100644 --- a/meson.build +++ b/meson.build @@ -3,6 +3,7 @@ project('koto', 'c', meson_version: '>= 0.57.0', default_options: [ 'warning_level=2', 'c_std=gnu11', + 'werror=true', ], ) diff --git a/src/components/koto-action-bar.c b/src/components/koto-action-bar.c new file mode 100644 index 0000000..32be399 --- /dev/null +++ b/src/components/koto-action-bar.c @@ -0,0 +1,355 @@ +/* koto-action-bar.c + * + * Copyright 2021 Joshua Strobl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "koto-action-bar.h" +#include "../db/cartographer.h" +#include "../pages/music/music-local.h" +#include "../playlist/add-remove-track-popover.h" +#include "../playback/engine.h" +#include "../koto-button.h" +#include "../koto-window.h" + +extern KotoAddRemoveTrackPopover *koto_add_remove_track_popup; +extern KotoCartographer *koto_maps; +extern KotoPageMusicLocal *music_local_page; +extern KotoPlaybackEngine *playback_engine; +extern KotoWindow *main_window; + +enum { + SIGNAL_CLOSED, + N_SIGNALS +}; + +static guint actionbar_signals[N_SIGNALS] = { 0 }; + +struct _KotoActionBar { + GObject parent_instance; + + GtkActionBar *main; + + GtkWidget *center_box_content; + GtkWidget *start_box_content; + GtkWidget *stop_box_content; + + KotoButton *close_button; + GtkWidget *go_to_artist; + GtkWidget *playlists; + GtkWidget *play_track; + GtkWidget *remove_from_playlist; + + GList *current_list; + gchar *current_album_uuid; + gchar *current_playlist_uuid; + KotoActionBarRelative relative; +}; + +struct _KotoActionBarClass { + GObjectClass parent_class; + + void (* closed) (KotoActionBar *self); +}; + +G_DEFINE_TYPE(KotoActionBar, koto_action_bar, G_TYPE_OBJECT); + +KotoActionBar* action_bar; + +static void koto_action_bar_class_init(KotoActionBarClass *c) { + GObjectClass *gobject_class = G_OBJECT_CLASS(c); + + actionbar_signals[SIGNAL_CLOSED] = g_signal_new( + "closed", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoActionBarClass, closed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0 + ); +} + +static void koto_action_bar_init(KotoActionBar *self) { + self->main = GTK_ACTION_BAR(gtk_action_bar_new()); // Create a new action bar + self->current_list = NULL; + + self->close_button = koto_button_new_with_icon(NULL, "window-close-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); + self->go_to_artist = gtk_button_new_with_label("Go to Artist"); + self->playlists = gtk_button_new_with_label("Playlists"); + self->play_track = gtk_button_new_with_label("Play"); + self->remove_from_playlist = gtk_button_new_with_label("Remove from Playlist"); + + gtk_widget_add_css_class(self->playlists, "suggested-action"); + gtk_widget_add_css_class(self->play_track, "suggested-action"); + gtk_widget_add_css_class(self->remove_from_playlist, "destructive-action"); + + gtk_widget_set_size_request(self->play_track, 160, -1); + + self->center_box_content = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + self->start_box_content = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + self->stop_box_content = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 20); + + gtk_box_prepend(GTK_BOX(self->start_box_content), GTK_WIDGET(self->go_to_artist)); + + gtk_box_prepend(GTK_BOX(self->center_box_content), GTK_WIDGET(self->play_track)); + + gtk_box_prepend(GTK_BOX(self->stop_box_content), GTK_WIDGET(self->playlists)); + gtk_box_append(GTK_BOX(self->stop_box_content), GTK_WIDGET(self->remove_from_playlist)); + gtk_box_append(GTK_BOX(self->stop_box_content), GTK_WIDGET(self->close_button)); + + gtk_action_bar_pack_start(self->main, self->start_box_content); + gtk_action_bar_pack_end(self->main, self->stop_box_content); + gtk_action_bar_set_center_widget(self->main, self->center_box_content); + + // Hide all the widgets by default + gtk_widget_hide(GTK_WIDGET(self->go_to_artist)); + gtk_widget_hide(GTK_WIDGET(self->playlists)); + gtk_widget_hide(GTK_WIDGET(self->play_track)); + gtk_widget_hide(GTK_WIDGET(self->remove_from_playlist)); + + // Set up bindings + + koto_button_add_click_handler(self->close_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_action_bar_handle_close_button_clicked), self); + g_signal_connect(self->go_to_artist, "clicked", G_CALLBACK(koto_action_bar_handle_go_to_artist_button_clicked), self); + g_signal_connect(self->playlists, "clicked", G_CALLBACK(koto_action_bar_handle_playlists_button_clicked), self); + g_signal_connect(self->play_track, "clicked", G_CALLBACK(koto_action_bar_handle_play_track_button_clicked), self); + g_signal_connect(self->remove_from_playlist, "clicked", G_CALLBACK(koto_action_bar_handle_remove_from_playlist_button_clicked), self); + + koto_action_bar_toggle_reveal(self, FALSE); // Hide by default +} + +void koto_action_bar_close(KotoActionBar *self) { + if (!KOTO_IS_ACTION_BAR(self)) { + return; + } + + gtk_widget_hide(GTK_WIDGET(koto_add_remove_track_popup)); // Hide the Add / Remove Track Playlists popover + koto_action_bar_toggle_reveal(self, FALSE); // Hide the action bar + + g_signal_emit( + self, + actionbar_signals[SIGNAL_CLOSED], + 0, + NULL + ); +} + +GtkActionBar* koto_action_bar_get_main(KotoActionBar *self) { + if (!KOTO_IS_ACTION_BAR(self)) { + return NULL; + } + + return self->main; +} + +void koto_action_bar_handle_close_button_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data) { + (void) gesture; (void) n_press; (void) x; (void) y; + koto_action_bar_close(data); +} + +void koto_action_bar_handle_go_to_artist_button_clicked(GtkButton *button, gpointer data) { + (void) button; + KotoActionBar *self = data; + + if (!KOTO_IS_ACTION_BAR(self)) { + return; + } + + if (self->current_list == NULL || (g_list_length(self->current_list) != 1)) { // Not a list or not exactly one item + return; + } + + KotoIndexedTrack *selected_track = g_list_nth_data(self->current_list, 0); // Get the first item + + if (!KOTO_IS_INDEXED_TRACK(selected_track)) { // Not a track + return; + } + + gchar *artist_uuid = NULL; + g_object_get( + selected_track, + "artist-uuid", + &artist_uuid, + NULL + ); + + koto_page_music_local_go_to_artist_by_uuid(music_local_page, artist_uuid); // Go to the artist + koto_window_go_to_page(main_window, "music.local"); // Navigate to the local music stack so we can see the substack page + koto_action_bar_close(self); // Close the action bar +} +void koto_action_bar_handle_playlists_button_clicked(GtkButton *button, gpointer data) { + (void) button; + KotoActionBar *self = data; + + if (!KOTO_IS_ACTION_BAR(self)) { + return; + } + + if (self->current_list == NULL || (g_list_length(self->current_list) == 0)) { // Not a list or no content + return; + } + + koto_add_remove_track_popover_set_tracks(koto_add_remove_track_popup, self->current_list); // Set the popover tracks + koto_add_remove_track_popover_set_pointing_to_widget(koto_add_remove_track_popup, GTK_WIDGET(self->playlists), GTK_POS_TOP); // Show the popover above the button + gtk_widget_show(GTK_WIDGET(koto_add_remove_track_popup)); +} + +void koto_action_bar_handle_play_track_button_clicked(GtkButton *button, gpointer data) { + (void) button; + KotoActionBar *self = data; + + if (!KOTO_IS_ACTION_BAR(self)) { + return; + } + + if (self->current_list == NULL || (g_list_length(self->current_list) != 1)) { // Not a list or not exactly 1 item selected + goto doclose; + } + + KotoIndexedTrack *track = g_list_nth_data(self->current_list, 0); // Get the first track + + if (!KOTO_IS_INDEXED_TRACK(track)) { // Not a track + goto doclose; + } + + koto_playback_engine_set_track_by_uuid(playback_engine, koto_indexed_track_get_uuid(track)); // Set the track to play + +doclose: + koto_action_bar_close(self); +} + +void koto_action_bar_handle_remove_from_playlist_button_clicked(GtkButton *button, gpointer data) { + (void) button; + KotoActionBar *self = data; + + if (!KOTO_IS_ACTION_BAR(self)) { + return; + } + + if (self->current_list == NULL || (g_list_length(self->current_list) == 0)) { // Not a list or no content + goto doclose; + } + + if (self->current_playlist_uuid == NULL || (g_strcmp0(self->current_playlist_uuid, "") == 0)) { // Not valid UUID + goto doclose; + } + + KotoPlaylist *playlist = koto_cartographer_get_playlist_by_uuid(koto_maps, self->current_playlist_uuid); + + if (!KOTO_IS_PLAYLIST(playlist)) { // Not a playlist + goto doclose; + } + + GList *cur_list; + for (cur_list = self->current_list; cur_list != NULL; cur_list = cur_list->next) { // For each KotoIndexedTrack + KotoIndexedTrack *track = cur_list->data; + koto_playlist_remove_track_by_uuid(playlist, koto_indexed_track_get_uuid(track)); // Remove this track + } + +doclose: + koto_action_bar_close(self); +} + +void koto_action_bar_set_tracks_in_album_selection(KotoActionBar *self, gchar *album_uuid, GList *tracks) { + if (!KOTO_IS_ACTION_BAR(self)) { + return; + } + + if (self->current_album_uuid != NULL && (g_strcmp0(self->current_album_uuid, "") != 0)) { // Album UUID currently set + g_free(self->current_album_uuid); + } + + if (self->current_playlist_uuid != NULL && (g_strcmp0(self->current_playlist_uuid, "") != 0)) { // Playlist UUID currently set + g_free(self->current_playlist_uuid); + } + + self->current_playlist_uuid = NULL; + self->current_album_uuid = g_strdup(album_uuid); + self->relative = KOTO_ACTION_BAR_IS_ALBUM_RELATIVE; + + g_list_free(self->current_list); + self->current_list = g_list_copy(tracks); + + koto_add_remove_track_popover_clear_tracks(koto_add_remove_track_popup); // Clear the current popover contents + koto_add_remove_track_popover_set_tracks(koto_add_remove_track_popup, self->current_list); // Set the associated tracks to remove + + koto_action_bar_toggle_go_to_artist_visibility(self, FALSE); + koto_action_bar_toggle_play_button_visibility(self, g_list_length(self->current_list) == 1); + + gtk_widget_show(GTK_WIDGET(self->playlists)); + gtk_widget_hide(GTK_WIDGET(self->remove_from_playlist)); +} + +void koto_action_bar_set_tracks_in_playlist_selection(KotoActionBar *self, gchar *playlist_uuid, GList *tracks) { + if (!KOTO_IS_ACTION_BAR(self)) { + return; + } + + if (self->current_album_uuid != NULL && (g_strcmp0(self->current_album_uuid, "") != 0)) { // Album UUID currently set + g_free(self->current_album_uuid); + } + + if (self->current_playlist_uuid != NULL && (g_strcmp0(self->current_playlist_uuid, "") != 0)) { // Playlist UUID currently set + g_free(self->current_playlist_uuid); + } + + self->current_album_uuid = NULL; + self->current_playlist_uuid = g_strdup(playlist_uuid); + self->relative = KOTO_ACTION_BAR_IS_PLAYLIST_RELATIVE; + + g_list_free(self->current_list); + self->current_list = g_list_copy(tracks); + + gboolean single_selected = g_list_length(tracks) == 1; + koto_action_bar_toggle_go_to_artist_visibility(self, single_selected); + koto_action_bar_toggle_play_button_visibility(self, single_selected); + gtk_widget_hide(GTK_WIDGET(self->playlists)); + gtk_widget_show(GTK_WIDGET(self->remove_from_playlist)); +} + +void koto_action_bar_toggle_go_to_artist_visibility(KotoActionBar *self, gboolean visible) { + if (!KOTO_IS_ACTION_BAR(self)) { + return; + } + + (visible) ? gtk_widget_show(GTK_WIDGET(self->go_to_artist)) : gtk_widget_hide(GTK_WIDGET(self->go_to_artist)); +} + +void koto_action_bar_toggle_play_button_visibility(KotoActionBar *self, gboolean visible) { + if (!KOTO_IS_ACTION_BAR(self)) { + return; + } + + (visible) ? gtk_widget_show(GTK_WIDGET(self->play_track)) : gtk_widget_hide(GTK_WIDGET(self->play_track)); +} + +void koto_action_bar_toggle_reveal(KotoActionBar *self, gboolean state) { + if (!KOTO_IS_ACTION_BAR(self)) { + return; + } + + gtk_action_bar_set_revealed(self->main, state); +} + +KotoActionBar* koto_action_bar_new() { + return g_object_new( + KOTO_TYPE_ACTION_BAR, + NULL + ); +} \ No newline at end of file diff --git a/src/components/koto-action-bar.h b/src/components/koto-action-bar.h new file mode 100644 index 0000000..4cc05ba --- /dev/null +++ b/src/components/koto-action-bar.h @@ -0,0 +1,58 @@ +/* koto-action-bar.h + * + * Copyright 2021 Joshua Strobl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include "../indexer/structs.h" + +G_BEGIN_DECLS + +typedef enum { + KOTO_ACTION_BAR_IS_ALBUM_RELATIVE = 1, + KOTO_ACTION_BAR_IS_PLAYLIST_RELATIVE = 2 +} KotoActionBarRelative; + +#define KOTO_TYPE_ACTION_BAR (koto_action_bar_get_type()) +#define KOTO_IS_ACTION_BAR(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_ACTION_BAR)) + +typedef struct _KotoActionBar KotoActionBar; +typedef struct _KotoActionBarClass KotoActionBarClass; + +GLIB_AVAILABLE_IN_ALL +GType koto_action_bar_get_type(void) G_GNUC_CONST; + +/** + * Action Bar Functions +**/ + +KotoActionBar* koto_action_bar_new(void); +void koto_action_bar_close(KotoActionBar *self); +GtkActionBar* koto_action_bar_get_main(KotoActionBar *self); +void koto_action_bar_handle_close_button_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data); +void koto_action_bar_handle_go_to_artist_button_clicked(GtkButton *button, gpointer data); +void koto_action_bar_handle_playlists_button_clicked(GtkButton *button, gpointer data); +void koto_action_bar_handle_play_track_button_clicked(GtkButton *button, gpointer data); +void koto_action_bar_handle_remove_from_playlist_button_clicked(GtkButton *button, gpointer data); +void koto_action_bar_set_tracks_in_album_selection(KotoActionBar *self, gchar *album_uuid, GList *tracks); +void koto_action_bar_set_tracks_in_playlist_selection(KotoActionBar *self, gchar *playlist_uuid, GList *tracks); +void koto_action_bar_toggle_go_to_artist_visibility(KotoActionBar *self, gboolean visible); +void koto_action_bar_toggle_play_button_visibility(KotoActionBar *self, gboolean visible); +void koto_action_bar_toggle_reveal(KotoActionBar *self, gboolean state); + +G_END_DECLS \ No newline at end of file diff --git a/src/components/koto-cover-art-button.c b/src/components/koto-cover-art-button.c new file mode 100644 index 0000000..bde2a1e --- /dev/null +++ b/src/components/koto-cover-art-button.c @@ -0,0 +1,217 @@ +/* koto-cover-art-button.c + * + * Copyright 2021 Joshua Strobl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "koto-cover-art-button.h" +#include "../koto-button.h" +#include "../koto-utils.h" + +struct _KotoCoverArtButton { + GObject parent_instance; + + GtkWidget *art; + GtkWidget *main; + GtkWidget *revealer; + KotoButton *play_pause_button; + + guint height; + guint width; +}; + +G_DEFINE_TYPE(KotoCoverArtButton, koto_cover_art_button, G_TYPE_OBJECT); + +enum { + PROP_0, + PROP_DESIRED_HEIGHT, + PROP_DESIRED_WIDTH, + PROP_ART_PATH, + N_PROPERTIES +}; + +static GParamSpec *props[N_PROPERTIES] = { NULL, }; +static void koto_cover_art_button_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec); +static void koto_cover_art_button_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec); + +static void koto_cover_art_button_class_init(KotoCoverArtButtonClass *c) { + GObjectClass *gobject_class; + gobject_class = G_OBJECT_CLASS(c); + gobject_class->get_property = koto_cover_art_button_get_property; + gobject_class->set_property = koto_cover_art_button_set_property; + + props[PROP_DESIRED_HEIGHT] = g_param_spec_uint( + "desired-height", + "Desired height", + "Desired height", + 0, + G_MAXUINT, + 0, + G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_WRITABLE + ); + + props[PROP_DESIRED_WIDTH] = g_param_spec_uint( + "desired-width", + "Desired width", + "Desired width", + 0, + G_MAXUINT, + 0, + G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_WRITABLE + ); + + props[PROP_ART_PATH] = g_param_spec_string( + "art-path", + "Path to art", + "Path to art", + NULL, + G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_WRITABLE + ); + + g_object_class_install_properties(gobject_class, N_PROPERTIES, props); +} + +static void koto_cover_art_button_init(KotoCoverArtButton *self) { + self->main = gtk_overlay_new(); // Create our overlay container + gtk_widget_add_css_class(self->main, "cover-art-button"); + self->revealer = gtk_revealer_new(); // Create a new revealer + gtk_revealer_set_transition_type(GTK_REVEALER(self->revealer), GTK_REVEALER_TRANSITION_TYPE_CROSSFADE); + gtk_revealer_set_transition_duration(GTK_REVEALER(self->revealer), 400); + + GtkWidget *controls = gtk_center_box_new(); // Create a center box for the controls + self->play_pause_button = koto_button_new_with_icon("", "media-playback-start-symbolic", "media-playback-pause-symbolic", KOTO_BUTTON_PIXBUF_SIZE_NORMAL); + gtk_center_box_set_center_widget(GTK_CENTER_BOX(controls), GTK_WIDGET(self->play_pause_button)); + + gtk_revealer_set_child(GTK_REVEALER(self->revealer), controls); + koto_cover_art_button_hide_overlay_controls(NULL, self); // Hide by default + gtk_overlay_add_overlay(GTK_OVERLAY(self->main), self->revealer); // Add our revealer as the overlay + + GtkEventController *motion_controller = gtk_event_controller_motion_new(); // Create our new motion event controller to track mouse leave and enter + g_signal_connect(motion_controller, "enter", G_CALLBACK(koto_cover_art_button_show_overlay_controls), self); + g_signal_connect(motion_controller, "leave", G_CALLBACK(koto_cover_art_button_hide_overlay_controls), self); + gtk_widget_add_controller(self->main, motion_controller); +} + +static void koto_cover_art_button_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec) { + (void) val; + + switch (prop_id) { + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_cover_art_button_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec) { + KotoCoverArtButton *self = KOTO_COVER_ART_BUTTON(obj); + + switch (prop_id) { + case PROP_ART_PATH: + koto_cover_art_button_set_art_path(self, g_value_get_string(val)); // Get the value and call our set_art_path with it + break; + case PROP_DESIRED_HEIGHT: + koto_cover_art_button_set_dimensions(self, g_value_get_uint(val), 0); + break; + case PROP_DESIRED_WIDTH: + koto_cover_art_button_set_dimensions(self, 0, g_value_get_uint(val)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +void koto_cover_art_button_hide_overlay_controls(GtkEventControllerFocus *controller, gpointer data) { + (void) controller; + KotoCoverArtButton* self = data; + gtk_revealer_set_reveal_child(GTK_REVEALER(self->revealer), FALSE); +} + +KotoButton* koto_cover_art_button_get_button(KotoCoverArtButton *self) { + if (!KOTO_IS_COVER_ART_BUTTON(self)) { + return NULL; + } + + return self->play_pause_button; +} + +GtkWidget* koto_cover_art_button_get_main(KotoCoverArtButton *self) { + if (!KOTO_IS_COVER_ART_BUTTON(self)) { + return NULL; + } + + return self->main; +} + +void koto_cover_art_button_set_art_path(KotoCoverArtButton *self, const gchar *art_path) { + if (!KOTO_IS_COVER_ART_BUTTON(self)) { + return; + } + + gboolean defined_artwork = (art_path != NULL || (g_strcmp0(art_path, "") != 0)); + + if (GTK_IS_IMAGE(self->art)) { // Already have an image + if (!defined_artwork) { // No art path or empty string + gtk_image_set_from_icon_name(GTK_IMAGE(self->art), "audio-x-generic-symbolic"); + } else { // Have an art path + gtk_image_set_from_file(GTK_IMAGE(self->art), g_strdup(art_path)); // Set from the file + } + } else { // If we don't have an image + self->art = koto_utils_create_image_from_filepath(defined_artwork ? g_strdup(art_path) : NULL, "audio-x-generic-symbolic", self->width, self->height); + gtk_overlay_set_child(GTK_OVERLAY(self->main), self->art); // Set the child + } +} + +void koto_cover_art_button_set_dimensions(KotoCoverArtButton *self, guint height, guint width) { + if (!KOTO_IS_COVER_ART_BUTTON(self)) { + return; + } + + if (height != 0) { + self->height = height; + } + + if (width != 0) { + self->width = width; + } + + if ((self->height != 0) && (self->width != 0)) { // Both height and width set + gtk_widget_set_size_request(self->main, self->width, self->height); // Update our widget + + if (GTK_IS_IMAGE(self->art)) { // Art is defined + gtk_widget_set_size_request(self->art, self->width, self->height); // Update our image as well + } + } +} + +void koto_cover_art_button_show_overlay_controls(GtkEventControllerFocus *controller, gpointer data) { + (void) controller; + KotoCoverArtButton* self = data; + + gtk_revealer_set_reveal_child(GTK_REVEALER(self->revealer), TRUE); +} + +KotoCoverArtButton* koto_cover_art_button_new(guint height, guint width, const gchar *art_path) { + return g_object_new(KOTO_TYPE_COVER_ART_BUTTON, + "desired-height", + height, + "desired-width", + width, + "art-path", + art_path, + NULL + ); +} \ No newline at end of file diff --git a/src/components/koto-cover-art-button.h b/src/components/koto-cover-art-button.h new file mode 100644 index 0000000..b62daf9 --- /dev/null +++ b/src/components/koto-cover-art-button.h @@ -0,0 +1,40 @@ +/* koto-cover-art-button.h + * + * Copyright 2021 Joshua Strobl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "../koto-button.h" + +G_BEGIN_DECLS + +#define KOTO_TYPE_COVER_ART_BUTTON (koto_cover_art_button_get_type()) +G_DECLARE_FINAL_TYPE(KotoCoverArtButton, koto_cover_art_button, KOTO, COVER_ART_BUTTON, GObject); +#define KOTO_IS_COVER_ART_BUTTON(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_COVER_ART_BUTTON)) + +/** + * Cover Art Functions +**/ + +KotoCoverArtButton* koto_cover_art_button_new(guint height, guint width, const gchar *art_path); +KotoButton* koto_cover_art_button_get_button(KotoCoverArtButton *self); +GtkWidget* koto_cover_art_button_get_main(KotoCoverArtButton *self); +void koto_cover_art_button_hide_overlay_controls(GtkEventControllerFocus *controller, gpointer data); +void koto_cover_art_button_set_art_path(KotoCoverArtButton *self, const gchar *art_path); +void koto_cover_art_button_set_dimensions(KotoCoverArtButton *self, guint height, guint width); +void koto_cover_art_button_show_overlay_controls(GtkEventControllerFocus *controller, gpointer data); + +G_END_DECLS \ No newline at end of file diff --git a/src/db/cartographer.c b/src/db/cartographer.c index 477d4e1..98da8dd 100644 --- a/src/db/cartographer.c +++ b/src/db/cartographer.c @@ -18,6 +18,20 @@ #include #include "cartographer.h" +enum { + SIGNAL_ALBUM_ADDED, + SIGNAL_ALBUM_REMOVED, + SIGNAL_ARTIST_ADDED, + SIGNAL_ARTIST_REMOVED, + SIGNAL_PLAYLIST_ADDED, + SIGNAL_PLAYLIST_REMOVED, + SIGNAL_TRACK_ADDED, + SIGNAL_TRACK_REMOVED, + N_SIGNALS +}; + +static guint cartographer_signals[N_SIGNALS] = { 0 }; + struct _KotoCartographer { GObject parent_instance; @@ -27,12 +41,130 @@ struct _KotoCartographer { GHashTable *tracks; }; +struct _KotoCartographerClass { + GObjectClass parent_class; + + void (* album_added) (KotoCartographer *cartographer, KotoIndexedAlbum *album); + void (* album_removed) (KotoCartographer *cartographer, KotoIndexedAlbum *album); + void (* artist_added) (KotoCartographer *cartographer, KotoIndexedArtist *artist); + void (* artist_removed) (KotoCartographer *cartographer, KotoIndexedArtist *artist); + void (* playlist_added) (KotoCartographer *cartographer, KotoPlaylist *playlist); + void (* playlist_removed) (KotoCartographer *cartographer, KotoPlaylist *playlist); + void (* track_added) (KotoCartographer *cartographer, KotoIndexedTrack *track); + void (* track_removed) (KotoCartographer *cartographer, KotoIndexedTrack *track); +}; + G_DEFINE_TYPE(KotoCartographer, koto_cartographer, G_TYPE_OBJECT); KotoCartographer *koto_maps = NULL; static void koto_cartographer_class_init(KotoCartographerClass *c) { - (void) c; + GObjectClass *gobject_class; + gobject_class = G_OBJECT_CLASS(c); + + cartographer_signals[SIGNAL_ALBUM_ADDED] = g_signal_new( + "album-added", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoCartographerClass, album_added), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + KOTO_TYPE_INDEXED_ALBUM + ); + + cartographer_signals[SIGNAL_ALBUM_REMOVED] = g_signal_new( + "album-removed", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoCartographerClass, album_removed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_CHAR + ); + + cartographer_signals[SIGNAL_ARTIST_ADDED] = g_signal_new( + "artist-added", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoCartographerClass, artist_added), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + KOTO_TYPE_INDEXED_ARTIST + ); + + cartographer_signals[SIGNAL_ARTIST_REMOVED] = g_signal_new( + "artist-removed", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoCartographerClass, artist_removed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_CHAR + ); + + cartographer_signals[SIGNAL_PLAYLIST_ADDED] = g_signal_new( + "playlist-added", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoCartographerClass, playlist_added), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + KOTO_TYPE_PLAYLIST + ); + + cartographer_signals[SIGNAL_PLAYLIST_REMOVED] = g_signal_new( + "playlist-removed", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoCartographerClass, playlist_removed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_CHAR + ); + + cartographer_signals[SIGNAL_TRACK_ADDED] = g_signal_new( + "track-added", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoCartographerClass, track_added), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + KOTO_TYPE_INDEXED_TRACK + ); + + cartographer_signals[SIGNAL_TRACK_REMOVED] = g_signal_new( + "track-removed", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoCartographerClass, track_removed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_CHAR + ); } static void koto_cartographer_init(KotoCartographer *self) { @@ -46,36 +178,80 @@ void koto_cartographer_add_album(KotoCartographer *self, KotoIndexedAlbum *album gchar *album_uuid = NULL; g_object_get(album, "uuid", &album_uuid, NULL); - if ((album_uuid != NULL) && (!koto_cartographer_has_album_by_uuid(self, album_uuid))) { // Don't have this album - g_hash_table_replace(self->albums, album_uuid, album); + if ((album_uuid == NULL) || koto_cartographer_has_album_by_uuid(self, album_uuid)) { // Have the album or invalid UUID + return; } + + g_hash_table_replace(self->albums, album_uuid, album); + + g_signal_emit( + self, + cartographer_signals[SIGNAL_ALBUM_ADDED], + 0, + album + ); } void koto_cartographer_add_artist(KotoCartographer *self, KotoIndexedArtist *artist) { gchar *artist_uuid = NULL; g_object_get(artist, "uuid", &artist_uuid, NULL); - if ((artist_uuid != NULL) && (!koto_cartographer_has_artist_by_uuid(self, artist_uuid))) { // Don't have this album - g_hash_table_replace(self->artists, artist_uuid, artist); + if ((artist_uuid == NULL) || koto_cartographer_has_artist_by_uuid(self, artist_uuid)) { // Have the artist or invalid UUID + return; } + + g_hash_table_replace(self->artists, artist_uuid, artist); + + g_signal_emit( + self, + cartographer_signals[SIGNAL_ARTIST_ADDED], + 0, + artist + ); } void koto_cartographer_add_playlist(KotoCartographer *self, KotoPlaylist *playlist) { gchar *playlist_uuid = NULL; g_object_get(playlist, "uuid", &playlist_uuid, NULL); - if ((playlist_uuid != NULL) && (!koto_cartographer_has_playlist_by_uuid(self, playlist_uuid))) { // Don't have this album - g_hash_table_replace(self->playlists, playlist_uuid, playlist); + if ((playlist_uuid == NULL) || koto_cartographer_has_playlist_by_uuid(self, playlist_uuid)) { // Have the playlist or invalid UUID + return; } + + g_hash_table_replace(self->playlists, playlist_uuid, playlist); + + if (koto_playlist_get_is_finalized(playlist)) { // Already finalized + koto_cartographer_emit_playlist_added(playlist, self); // Emit playlist-added immediately + } else { // Not finalized + g_signal_connect(playlist, "track-load-finalized", G_CALLBACK(koto_cartographer_emit_playlist_added), self); + } +} + +void koto_cartographer_emit_playlist_added(KotoPlaylist *playlist, KotoCartographer *self) { + g_signal_emit( + self, + cartographer_signals[SIGNAL_PLAYLIST_ADDED], + 0, + playlist + ); } void koto_cartographer_add_track(KotoCartographer *self, KotoIndexedTrack *track) { gchar *track_uuid = NULL; g_object_get(track, "uuid", &track_uuid, NULL); - if ((track_uuid != NULL) && (!koto_cartographer_has_playlist_by_uuid(self, track_uuid))) { // Don't have this album - g_hash_table_replace(self->tracks, track_uuid, track); + if ((track_uuid == NULL) || koto_cartographer_has_track_by_uuid(self, track_uuid)) { // Have the track or invalid UUID + return; } + + g_hash_table_replace(self->tracks, track_uuid, track); + + g_signal_emit( + self, + cartographer_signals[SIGNAL_TRACK_ADDED], + 0, + track + ); } KotoIndexedAlbum* koto_cartographer_get_album_by_uuid(KotoCartographer *self, gchar* album_uuid) { @@ -86,6 +262,10 @@ KotoIndexedArtist* koto_cartographer_get_artist_by_uuid(KotoCartographer *self, return g_hash_table_lookup(self->artists, artist_uuid); } +GHashTable* koto_cartographer_get_playlists(KotoCartographer *self) { + return self->playlists; +} + KotoPlaylist* koto_cartographer_get_playlist_by_uuid(KotoCartographer *self, gchar* playlist_uuid) { return g_hash_table_lookup(self->playlists, playlist_uuid); } @@ -153,57 +333,77 @@ gboolean koto_cartographer_has_track_by_uuid(KotoCartographer *self, gchar* trac void koto_cartographer_remove_album(KotoCartographer *self, KotoIndexedAlbum *album) { gchar *album_uuid = NULL; g_object_get(album, "uuid", &album_uuid, NULL); - return koto_cartographer_remove_album_by_uuid(self, album_uuid); + koto_cartographer_remove_album_by_uuid(self, album_uuid); } void koto_cartographer_remove_album_by_uuid(KotoCartographer *self, gchar* album_uuid) { if (album_uuid != NULL) { g_hash_table_remove(self->albums, album_uuid); - } - return; + g_signal_emit( + self, + cartographer_signals[SIGNAL_ALBUM_REMOVED], + 0, + album_uuid + ); + } } void koto_cartographer_remove_artist(KotoCartographer *self, KotoIndexedArtist *artist) { gchar *artist_uuid = NULL; g_object_get(artist, "uuid", &artist_uuid, NULL); - return koto_cartographer_remove_artist_by_uuid(self, artist_uuid); + koto_cartographer_remove_artist_by_uuid(self, artist_uuid); } void koto_cartographer_remove_artist_by_uuid(KotoCartographer *self, gchar* artist_uuid) { if (artist_uuid == NULL) { g_hash_table_remove(self->artists, artist_uuid); - } - return; + g_signal_emit( + self, + cartographer_signals[SIGNAL_ARTIST_REMOVED], + 0, + artist_uuid + ); + } } void koto_cartographer_remove_playlist(KotoCartographer *self, KotoPlaylist *playlist) { gchar *playlist_uuid = NULL; g_object_get(playlist, "uuid", &playlist_uuid, NULL); - return koto_cartographer_remove_playlist_by_uuid(self, playlist_uuid); + koto_cartographer_remove_playlist_by_uuid(self, playlist_uuid); } void koto_cartographer_remove_playlist_by_uuid(KotoCartographer *self, gchar* playlist_uuid) { if (playlist_uuid != NULL) { g_hash_table_remove(self->playlists, playlist_uuid); - } - return; + g_signal_emit( + self, + cartographer_signals[SIGNAL_PLAYLIST_REMOVED], + 0, + playlist_uuid + ); + } } void koto_cartographer_remove_track(KotoCartographer *self, KotoIndexedTrack *track) { gchar *track_uuid = NULL; g_object_get(track, "uuid", &track_uuid, NULL); - return koto_cartographer_remove_track_by_uuid(self, track_uuid); + koto_cartographer_remove_track_by_uuid(self, track_uuid); } void koto_cartographer_remove_track_by_uuid(KotoCartographer *self, gchar* track_uuid) { if (track_uuid != NULL) { g_hash_table_remove(self->tracks, track_uuid); - } - return; + g_signal_emit( + self, + cartographer_signals[SIGNAL_TRACK_REMOVED], + 0, + track_uuid + ); + } } KotoCartographer* koto_cartographer_new() { diff --git a/src/db/cartographer.h b/src/db/cartographer.h index 1c4ee7a..b7d675e 100644 --- a/src/db/cartographer.h +++ b/src/db/cartographer.h @@ -27,7 +27,12 @@ G_BEGIN_DECLS **/ #define KOTO_TYPE_CARTOGRAPHER koto_cartographer_get_type() -G_DECLARE_FINAL_TYPE(KotoCartographer, koto_cartographer, KOTO, CARTOGRAPHER, GObject); + +typedef struct _KotoCartographer KotoCartographer; +typedef struct _KotoCartographerClass KotoCartographerClass; + +GLIB_AVAILABLE_IN_ALL +GType koto_cartographer_get_type(void) G_GNUC_CONST; /** * Cartographer Functions @@ -40,9 +45,12 @@ void koto_cartographer_add_artist(KotoCartographer *self, KotoIndexedArtist *art void koto_cartographer_add_playlist(KotoCartographer *self, KotoPlaylist *playlist); void koto_cartographer_add_track(KotoCartographer *self, KotoIndexedTrack *track); +void koto_cartographer_emit_playlist_added(KotoPlaylist *playlist, KotoCartographer *self); + KotoIndexedAlbum* koto_cartographer_get_album_by_uuid(KotoCartographer *self, gchar* album_uuid); KotoIndexedArtist* koto_cartographer_get_artist_by_uuid(KotoCartographer *self, gchar* artist_uuid); KotoPlaylist* koto_cartographer_get_playlist_by_uuid(KotoCartographer *self, gchar* playlist_uuid); +GHashTable* koto_cartographer_get_playlists(KotoCartographer *self); KotoIndexedTrack* koto_cartographer_get_track_by_uuid(KotoCartographer *self, gchar* track_uuid); gboolean koto_cartographer_has_album(KotoCartographer *self, KotoIndexedAlbum *album); diff --git a/src/db/db.c b/src/db/db.c index fe98c50..0100353 100644 --- a/src/db/db.c +++ b/src/db/db.c @@ -38,8 +38,8 @@ int create_db_tables() { char *tables_creation_queries = "CREATE TABLE IF NOT EXISTS artists(id string UNIQUE PRIMARY KEY, path string, type int, name string, art_path string);" "CREATE TABLE IF NOT EXISTS albums(id string UNIQUE PRIMARY KEY, path string, artist_id string, name string, art_path string, FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE);" "CREATE TABLE IF NOT EXISTS tracks(id string UNIQUE PRIMARY KEY, path string, type int, artist_id string, album_id string, file_name string, name string, disc int, position int, FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE);" - "CREATE TABLE IF NOT EXISTS playlist_meta(id string UNIQUE PRIMARY KEY, name string, art_path string);" - "CREATE TABLE IF NOT EXISTS playlist_tracks(playlist_id string PRIMARY KEY, track_id string, position int, current int, FOREIGN KEY(playlist_id) REFERENCES playlist_meta(id), FOREIGN KEY(track_id) REFERENCES tracks(id) ON DELETE CASCADE);"; + "CREATE TABLE IF NOT EXISTS playlist_meta(id string UNIQUE PRIMARY KEY, name string, art_path string, preferred_model int);" + "CREATE TABLE IF NOT EXISTS playlist_tracks(position INTEGER PRIMARY KEY AUTOINCREMENT, playlist_id string, track_id string, current int, FOREIGN KEY(playlist_id) REFERENCES playlist_meta(id), FOREIGN KEY(track_id) REFERENCES tracks(id) ON DELETE CASCADE);"; gchar *create_tables_errmsg = NULL; int rc = sqlite3_exec(koto_db, tables_creation_queries, 0,0, &create_tables_errmsg); diff --git a/src/indexer/album.c b/src/indexer/album.c index 6d9fcde..645a633 100644 --- a/src/indexer/album.c +++ b/src/indexer/album.c @@ -373,14 +373,46 @@ static void koto_indexed_album_set_property(GObject *obj, guint prop_id, const G } gchar* koto_indexed_album_get_album_art(KotoIndexedAlbum *self) { - return g_strdup((self->has_album_art && (self->art_path != NULL) && (strcmp(self->art_path, "") != 0)) ? self->art_path : ""); + if (!KOTO_IS_INDEXED_ALBUM(self)) { // Not an album + return g_strdup(""); + } + + return g_strdup((self->has_album_art && (self->art_path != NULL) && (g_strcmp0(self->art_path, "") != 0)) ? self->art_path : ""); +} + +gchar *koto_indexed_album_get_album_name(KotoIndexedAlbum *self) { + if (!KOTO_IS_INDEXED_ALBUM(self)) { // Not an album + return NULL; + } + + if ((self->name == NULL) || g_strcmp0(self->name, "") == 0) { // Not set + return NULL; + } + + return g_strdup(self->name); // Return duplicate of the name +} + +gchar* koto_indexed_album_get_album_uuid(KotoIndexedAlbum *self) { + if ((self->uuid == NULL) || g_strcmp0(self->uuid, "") == 0) { // Not set + return NULL; + } + + return g_strdup(self->uuid); // Return a duplicate of the UUID } GList* koto_indexed_album_get_tracks(KotoIndexedAlbum *self) { + if (!KOTO_IS_INDEXED_ALBUM(self)) { // Not an album + return NULL; + } + return self->tracks; // Return } void koto_indexed_album_set_album_art(KotoIndexedAlbum *self, const gchar *album_art) { + if (!KOTO_IS_INDEXED_ALBUM(self)) { // Not an album + return; + } + if (album_art == NULL) { // Not valid album art return; } @@ -395,6 +427,10 @@ void koto_indexed_album_set_album_art(KotoIndexedAlbum *self, const gchar *album } void koto_indexed_album_remove_file(KotoIndexedAlbum *self, KotoIndexedTrack *track) { + if (!KOTO_IS_INDEXED_ALBUM(self)) { // Not an album + return; + } + if (track == NULL) { // Not a file return; } @@ -405,6 +441,10 @@ void koto_indexed_album_remove_file(KotoIndexedAlbum *self, KotoIndexedTrack *tr } void koto_indexed_album_set_album_name(KotoIndexedAlbum *self, const gchar *album_name) { + if (!KOTO_IS_INDEXED_ALBUM(self)) { // Not an album + return; + } + if (album_name == NULL) { // Not valid album name return; } @@ -417,6 +457,10 @@ void koto_indexed_album_set_album_name(KotoIndexedAlbum *self, const gchar *albu } void koto_indexed_album_set_artist_uuid(KotoIndexedAlbum *self, const gchar *artist_uuid) { + if (!KOTO_IS_INDEXED_ALBUM(self)) { // Not an album + return; + } + if (artist_uuid == NULL) { return; } @@ -429,6 +473,10 @@ void koto_indexed_album_set_artist_uuid(KotoIndexedAlbum *self, const gchar *art } void koto_indexed_album_set_as_current_playlist(KotoIndexedAlbum *self) { + if (!KOTO_IS_INDEXED_ALBUM(self)) { // Not an album + return; + } + if (self->tracks == NULL) { // No files to add to the playlist return; } @@ -436,11 +484,23 @@ void koto_indexed_album_set_as_current_playlist(KotoIndexedAlbum *self) { KotoPlaylist *new_album_playlist = koto_playlist_new(); // Create a new playlist g_object_set(new_album_playlist, "ephemeral", TRUE, NULL); // Set as ephemeral / temporary + // The following section effectively reverses our tracks, so the first is now last. + // It then adds them in reverse order, since our playlist add function will prepend to our queue + // This enables the preservation and defaulting of "newest" first everywhere else, while in albums preserving the rightful order of the album + // e.g. first track (0) being added last is actually first in the playlist's tracks + GList *reversed_tracks = g_list_copy(self->tracks); // Copy our tracks so we can reverse the order + reversed_tracks = g_list_reverse(reversed_tracks); // Actually reverse it + GList *t; - for (t = self->tracks; t != NULL; t = t->next) { // For each of the tracks - koto_playlist_add_track_by_uuid(new_album_playlist, (gchar*) t->data); // Add the UUID + for (t = reversed_tracks; t != NULL; t = t->next) { // For each of the tracks + gchar* track_uuid = t->data; + koto_playlist_add_track_by_uuid(new_album_playlist, track_uuid, FALSE, FALSE); // Add the UUID, skip commit to table since it is temporary } + g_list_free(t); + g_list_free(reversed_tracks); + + koto_playlist_apply_model(new_album_playlist, KOTO_PREFERRED_MODEL_TYPE_DEFAULT); // Ensure we are using our default model koto_current_playlist_set_playlist(current_playlist, new_album_playlist); // Set our new current playlist } @@ -491,16 +551,19 @@ gint koto_indexed_album_sort_tracks(gconstpointer track1_uuid, gconstpointer tra } void koto_indexed_album_update_path(KotoIndexedAlbum *self, const gchar* new_path) { - if (new_path == NULL) { + if (!KOTO_IS_INDEXED_ALBUM(self)) { // Not an album return; } - if (self->path != NULL) { + if ((new_path == NULL) || g_strcmp0(new_path, "") == 0) { + return; + } + + if ((self->path != NULL) && g_strcmp0(self->path, "") != 0) { g_free(self->path); } self->path = g_strdup(new_path); - koto_indexed_album_set_album_name(self, g_path_get_basename(self->path)); // Update our album name based on the base name if (!self->do_initial_index) { // Not doing our initial index diff --git a/src/indexer/artist.c b/src/indexer/artist.c index 0dfa990..a2ff8b2 100644 --- a/src/indexer/artist.c +++ b/src/indexer/artist.c @@ -151,7 +151,11 @@ static void koto_indexed_artist_set_property(GObject *obj, guint prop_id, const } void koto_indexed_artist_add_album(KotoIndexedArtist *self, gchar *album_uuid) { - if ((album_uuid == NULL) || (strcmp(album_uuid, "") == 0)) { // No album UUID really defined + if (!KOTO_IS_INDEXED_ARTIST(self)) { // Not an artist + return; + } + + if ((album_uuid == NULL) || g_strcmp0(album_uuid, "") == 0) { // No album UUID really defined return; } @@ -163,11 +167,27 @@ void koto_indexed_artist_add_album(KotoIndexedArtist *self, gchar *album_uuid) { } GList* koto_indexed_artist_get_albums(KotoIndexedArtist *self) { + if (!KOTO_IS_INDEXED_ARTIST(self)) { // Not an artist + return NULL; + } + return g_list_copy(self->albums); } +gchar* koto_indexed_artist_get_name(KotoIndexedArtist *self) { + if (!KOTO_IS_INDEXED_ARTIST(self)) { // Not an artist + return g_strdup(""); + } + + return g_strdup(g_strcmp0(self->artist_name, "") == 0 ? "" : self->artist_name); // Return artist name if set +} + void koto_indexed_artist_remove_album(KotoIndexedArtist *self, KotoIndexedAlbum *album) { - if (album == NULL) { // No album defined + if (!KOTO_IS_INDEXED_ARTIST(self)) { // Not an artist + return; + } + + if (!KOTO_INDEXED_ALBUM(album)) { // No album defined return; } @@ -177,11 +197,15 @@ void koto_indexed_artist_remove_album(KotoIndexedArtist *self, KotoIndexedAlbum } void koto_indexed_artist_update_path(KotoIndexedArtist *self, const gchar *new_path) { - if (new_path == NULL) { // No path really + if (!KOTO_IS_INDEXED_ARTIST(self)) { // Not an artist return; } - if (self->path != NULL) { // Already have a path set + if ((new_path == NULL) || g_strcmp0(new_path, "") == 0) { // No path really + return; + } + + if ((self->path != NULL) && g_strcmp0(self->path, "") != 0) { // Already have a path set g_free(self->path); // Free } @@ -190,11 +214,15 @@ void koto_indexed_artist_update_path(KotoIndexedArtist *self, const gchar *new_p } void koto_indexed_artist_set_artist_name(KotoIndexedArtist *self, const gchar *artist_name) { - if (artist_name == NULL) { // No artist name + if (!KOTO_IS_INDEXED_ARTIST(self)) { // Not an artist return; } - if (self->artist_name != NULL) { // Has artist name + if ((artist_name == NULL) || g_strcmp0(artist_name, "") == 0) { // No artist name + return; + } + + if ((self->artist_name != NULL) && g_strcmp0(self->artist_name, "") != 0) { // Has artist name g_free(self->artist_name); } diff --git a/src/indexer/file-indexer.c b/src/indexer/file-indexer.c index f88df44..b5568f7 100644 --- a/src/indexer/file-indexer.c +++ b/src/indexer/file-indexer.c @@ -22,6 +22,7 @@ #include #include "../db/cartographer.h" #include "../db/db.h" +#include "../playlist/playlist.h" #include "../koto-utils.h" #include "structs.h" @@ -242,6 +243,57 @@ int process_albums(void *data, int num_columns, char **fields, char **column_nam return 0; } +int process_playlists(void *data, int num_columns, char **fields, char **column_names) { + (void) data; (void) num_columns; (void) column_names; // Don't need any of the params + + gchar *playlist_uuid = g_strdup(koto_utils_unquote_string(fields[0])); // First column is UUID + gchar *playlist_name = g_strdup(koto_utils_unquote_string(fields[1])); // Second column is playlist name + gchar *playlist_art_path = g_strdup(koto_utils_unquote_string(fields[2])); // Third column is any art path + + KotoPlaylist *playlist = koto_playlist_new_with_uuid(playlist_uuid); // Create a playlist using the existing UUID + koto_playlist_set_name(playlist, playlist_name); // Add the playlist name + koto_playlist_set_artwork(playlist, playlist_art_path); // Add the playlist art path + + koto_cartographer_add_playlist(koto_maps, playlist); // Add to cartographer + + int playlist_tracks_rc = sqlite3_exec(koto_db, g_strdup_printf("SELECT * FROM playlist_tracks WHERE playlist_id=\"%s\" ORDER BY position ASC", playlist_uuid), process_playlists_tracks, playlist, NULL); // Process our playlist tracks + if (playlist_tracks_rc != SQLITE_OK) { // Failed to get our playlist tracks + g_critical("Failed to read our playlist tracks: %s", sqlite3_errmsg(koto_db)); + return 1; + } + + koto_playlist_mark_as_finalized(playlist); // Mark as finalized since loading should be complete + + g_free(playlist_uuid); + g_free(playlist_name); + g_free(playlist_art_path); + + return 0; +} + +int process_playlists_tracks(void *data, int num_columns, char **fields, char **column_names) { + (void) data; (void) num_columns; (void) column_names; // Don't need these + + gchar *playlist_uuid = g_strdup(koto_utils_unquote_string(fields[1])); + gchar *track_uuid = g_strdup(koto_utils_unquote_string(fields[2])); + gboolean current = g_strcmp0(koto_utils_unquote_string(fields[3]), "0"); + + KotoPlaylist *playlist = koto_cartographer_get_playlist_by_uuid(koto_maps, playlist_uuid); // Get the playlist + KotoIndexedTrack *track = koto_cartographer_get_track_by_uuid(koto_maps, track_uuid); // Get the track + + if (!KOTO_IS_PLAYLIST(playlist)) { + goto freeforret; + } + + koto_playlist_add_track(playlist, track, current, FALSE); // Add the track to the playlist but don't re-commit to the table + +freeforret: + g_free(playlist_uuid); + g_free(track_uuid); + + return 0; +} + int process_tracks(void *data, int num_columns, char **fields, char **column_names) { (void) num_columns; (void) column_names; // Don't need these @@ -287,6 +339,12 @@ void read_from_db(KotoIndexedLibrary *self) { } g_hash_table_foreach(self->music_artists, output_artists, NULL); + + int playlist_rc = sqlite3_exec(koto_db, "SELECT * FROM playlist_meta", process_playlists, self, NULL); // Process our playlists + if (playlist_rc != SQLITE_OK) { // Failed to get our playlists + g_critical("Failed to read our playlists: %s", sqlite3_errmsg(koto_db)); + return; + } } void start_indexing(KotoIndexedLibrary *self) { @@ -362,7 +420,6 @@ void index_folder(KotoIndexedLibrary *self, gchar *path, guint depth) { KotoIndexedArtist *artist = koto_indexed_library_get_artist(self, artist_name); // Get the artist if (artist == NULL) { - g_message("Failed to get artist by name of: %s", artist_name); continue; } @@ -422,7 +479,6 @@ void output_artists(gpointer artist_key, gpointer artist_ptr, gpointer data) { void output_track(gpointer data, gpointer user_data) { (void) user_data; - g_message("Track UUID: %s", g_strdup(data)); KotoIndexedTrack *track = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) data); if (track == NULL) { diff --git a/src/indexer/structs.h b/src/indexer/structs.h index 3224402..8698725 100644 --- a/src/indexer/structs.h +++ b/src/indexer/structs.h @@ -27,12 +27,15 @@ G_BEGIN_DECLS #define KOTO_TYPE_INDEXED_LIBRARY koto_indexed_library_get_type() G_DECLARE_FINAL_TYPE(KotoIndexedLibrary, koto_indexed_library, KOTO, INDEXED_LIBRARY, GObject); +#define KOTO_IS_INDEXED_LIBRARY(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_INDEXED_LIBRARY)) #define KOTO_TYPE_INDEXED_ARTIST koto_indexed_artist_get_type() G_DECLARE_FINAL_TYPE (KotoIndexedArtist, koto_indexed_artist, KOTO, INDEXED_ARTIST, GObject); +#define KOTO_IS_INDEXED_ARTIST(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_INDEXED_ARTIST)) #define KOTO_TYPE_INDEXED_ALBUM koto_indexed_album_get_type() G_DECLARE_FINAL_TYPE (KotoIndexedAlbum, koto_indexed_album, KOTO, INDEXED_ALBUM, GObject); +#define KOTO_IS_INDEXED_ALBUM(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_INDEXED_ALBUM)) #define KOTO_TYPE_INDEXED_TRACK koto_indexed_track_get_type() G_DECLARE_FINAL_TYPE(KotoIndexedTrack, koto_indexed_track, KOTO, INDEXED_TRACK, GObject); @@ -51,6 +54,8 @@ void koto_indexed_library_remove_artist(KotoIndexedLibrary *self, KotoIndexedArt void koto_indexed_library_set_path(KotoIndexedLibrary *self, gchar *path); int process_artists(void *data, int num_columns, char **fields, char **column_names); int process_albums(void *data, int num_columns, char **fields, char **column_names); +int process_playlists(void *data, int num_columns, char **fields, char **column_names); +int process_playlists_tracks(void *data, int num_columns, char **fields, char **column_names); int process_tracks(void *data, int num_columns, char **fields, char **column_names); void read_from_db(KotoIndexedLibrary *self); void start_indexing(KotoIndexedLibrary *self); @@ -68,6 +73,7 @@ void koto_indexed_artist_add_album(KotoIndexedArtist *self, gchar *album_uuid); void koto_indexed_artist_commit(KotoIndexedArtist *self); guint koto_indexed_artist_find_album_with_name(gconstpointer *album_data, gconstpointer *album_name_data); GList* koto_indexed_artist_get_albums(KotoIndexedArtist *self); +gchar* koto_indexed_artist_get_name(KotoIndexedArtist *self); void koto_indexed_artist_remove_album(KotoIndexedArtist *self, KotoIndexedAlbum *album); void koto_indexed_artist_remove_album_by_name(KotoIndexedArtist *self, gchar *album_name); void koto_indexed_artist_set_artist_name(KotoIndexedArtist *self, const gchar *artist_name); @@ -86,6 +92,8 @@ void koto_indexed_album_commit(KotoIndexedAlbum *self); void koto_indexed_album_find_album_art(KotoIndexedAlbum *self); void koto_indexed_album_find_tracks(KotoIndexedAlbum *self, magic_t magic_cookie, const gchar *path); gchar* koto_indexed_album_get_album_art(KotoIndexedAlbum *self); +gchar* koto_indexed_album_get_album_name(KotoIndexedAlbum *self); +gchar* koto_indexed_album_get_album_uuid(KotoIndexedAlbum *self); GList* koto_indexed_album_get_tracks(KotoIndexedAlbum *self); void koto_indexed_album_remove_file(KotoIndexedAlbum *self, KotoIndexedTrack *track); void koto_indexed_album_set_album_art(KotoIndexedAlbum *self, const gchar *album_art); @@ -103,8 +111,10 @@ KotoIndexedTrack* koto_indexed_track_new(KotoIndexedAlbum *album, const gchar *p KotoIndexedTrack* koto_indexed_track_new_with_uuid(const gchar *uuid); void koto_indexed_track_commit(KotoIndexedTrack *self); +gchar* koto_indexed_track_get_uuid(KotoIndexedTrack *self); void koto_indexed_track_parse_name(KotoIndexedTrack *self); -void koto_indexed_track_save_to_playlist(KotoIndexedTrack *self, gchar *playlist_uuid, guint position, gint current); +void koto_indexed_track_remove_from_playlist(KotoIndexedTrack *self, gchar *playlist_uuid); +void koto_indexed_track_save_to_playlist(KotoIndexedTrack *self, gchar *playlist_uuid, gint current); void koto_indexed_track_set_file_name(KotoIndexedTrack *self, gchar *new_file_name); void koto_indexed_track_set_cd(KotoIndexedTrack *self, guint cd); void koto_indexed_track_set_parsed_name(KotoIndexedTrack *self, gchar *new_parsed_name); diff --git a/src/indexer/track.c b/src/indexer/track.c index 476de59..9ccc55c 100644 --- a/src/indexer/track.c +++ b/src/indexer/track.c @@ -278,6 +278,14 @@ void koto_indexed_track_commit(KotoIndexedTrack *self) { g_free(commit_op_errmsg); } +gchar* koto_indexed_track_get_uuid(KotoIndexedTrack *self) { + if (!KOTO_IS_INDEXED_TRACK(self)) { + return NULL; + } + + return self->uuid; // Do not return a duplicate since otherwise comparison refs fail due to pointer positions being different +} + void koto_indexed_track_parse_name(KotoIndexedTrack *self) { gchar *copied_file_name = g_strdelimit(g_strdup(self->file_name), "_", ' '); // Replace _ with whitespace for starters @@ -335,14 +343,37 @@ void koto_indexed_track_parse_name(KotoIndexedTrack *self) { g_free(file_without_ext); } -void koto_indexed_track_save_to_playlist(KotoIndexedTrack *self, gchar *playlist_uuid, guint position, gint current) { +void koto_indexed_track_remove_from_playlist(KotoIndexedTrack *self, gchar *playlist_uuid) { + if (!KOTO_IS_INDEXED_TRACK(self)) { + return; + } + gchar *commit_op = g_strdup_printf( - "INSERT INTO playlist_tracks(playlist_id, track_id, position, current)" - "VALUES('%s', '%s', quote(\"%d\"), quote(\"%d\")" - "ON CONFLICT(playlist_id, track_id) DO UPDATE SET position=excluded.position, current=excluded.current;", + "DELETE FROM playlist_tracks WHERE track_id='%s' AND playlist_id='%s'", + self->uuid, + playlist_uuid + ); + + gchar *commit_op_errmsg = NULL; + int rc = sqlite3_exec(koto_db, commit_op, 0, 0, &commit_op_errmsg); + if (rc != SQLITE_OK) { + g_warning("Failed to remove track from playlist: %s", commit_op_errmsg); + } + + g_free(commit_op); + g_free(commit_op_errmsg); +} + +void koto_indexed_track_save_to_playlist(KotoIndexedTrack *self, gchar *playlist_uuid, gint current) { + if (!KOTO_IS_INDEXED_TRACK(self)) { + return; + } + + gchar *commit_op = g_strdup_printf( + "INSERT INTO playlist_tracks(playlist_id, track_id, current)" + "VALUES('%s', '%s', quote(\"%d\"))", playlist_uuid, self->uuid, - position, current ); diff --git a/src/koto-button.c b/src/koto-button.c index b45e475..84414fd 100644 --- a/src/koto-button.c +++ b/src/koto-button.c @@ -51,10 +51,11 @@ guint koto_get_pixbuf_size(KotoButtonPixbufSize s) { enum { PROP_BTN_0, - PROP_USE_FROM_FILE, PROP_PIX_SIZE, PROP_TEXT, PROP_BADGE_TEXT, + PROP_USE_FROM_FILE, + PROP_IMAGE_FILE_PATH, PROP_ICON_NAME, PROP_ALT_ICON_NAME, N_BTN_PROPERTIES @@ -70,11 +71,16 @@ struct _KotoButton { GtkWidget *badge_label; GtkWidget *button_label; + GtkGesture *left_click_gesture; + GtkGesture *right_click_gesture; + + gchar *image_file_path; gchar *badge_text; gchar *icon_name; gchar *alt_icon_name; gchar *text; + KotoButtonImagePosition image_position; gboolean use_from_file; gboolean currently_showing_alt; }; @@ -96,14 +102,6 @@ static void koto_button_class_init(KotoButtonClass *c) { gobject_class->set_property = koto_button_set_property; gobject_class->get_property = koto_button_get_property; - btn_props[PROP_USE_FROM_FILE] = g_param_spec_boolean( - "use-from-file", - "Use from a file / file name", - "Use from a file / file name", - FALSE, - G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE - ); - btn_props[PROP_PIX_SIZE] = g_param_spec_uint( "pixbuf-size", "Pixbuf Size", @@ -130,6 +128,22 @@ static void koto_button_class_init(KotoButtonClass *c) { G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE ); + btn_props[PROP_USE_FROM_FILE] = g_param_spec_boolean( + "use-from-file", + "Use from a file / file name", + "Use from a file / file name", + FALSE, + G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE + ); + + btn_props[PROP_IMAGE_FILE_PATH] = g_param_spec_string( + "image-file-path", + "File path to image", + "File path to image", + NULL, + G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE + ); + btn_props[PROP_ICON_NAME] = g_param_spec_string( "icon-name", "Icon Name", @@ -151,6 +165,16 @@ static void koto_button_class_init(KotoButtonClass *c) { static void koto_button_init(KotoButton *self) { self->currently_showing_alt = FALSE; + self->image_position = KOTO_BUTTON_IMAGE_POS_LEFT; + + self->left_click_gesture = gtk_gesture_click_new(); // Set up our left click gesture + self->right_click_gesture = gtk_gesture_click_new(); // Set up our right click gesture + + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(self->left_click_gesture), (int) KOTO_BUTTON_CLICK_TYPE_PRIMARY); // Only allow left clicks on left click gesture + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(self->right_click_gesture), (int) KOTO_BUTTON_CLICK_TYPE_SECONDARY); // Only allow right clicks on right click gesture + + gtk_widget_add_controller(GTK_WIDGET(self), GTK_EVENT_CONTROLLER(self->left_click_gesture)); // Add our left click gesture + gtk_widget_add_controller(GTK_WIDGET(self), GTK_EVENT_CONTROLLER(self->right_click_gesture)); // Add our right click gesture } static void koto_button_constructed(GObject *obj) { @@ -165,6 +189,9 @@ static void koto_button_get_property(GObject *obj, guint prop_id, GValue *val, G KotoButton *self = KOTO_BUTTON(obj); switch (prop_id) { + case PROP_IMAGE_FILE_PATH: + g_value_set_string(val, self->image_file_path); + break; case PROP_USE_FROM_FILE: g_value_set_boolean(val, self->use_from_file); break; @@ -193,23 +220,24 @@ static void koto_button_set_property(GObject *obj, guint prop_id, const GValue * KotoButton *self = KOTO_BUTTON(obj); switch (prop_id) { - case PROP_USE_FROM_FILE: - self->use_from_file = g_value_get_boolean(val); - break; case PROP_PIX_SIZE: koto_button_set_pixbuf_size(self, g_value_get_uint(val)); break; case PROP_TEXT: - if (val == NULL) { - koto_button_set_text(self, NULL); - } else { - koto_button_set_text(self, g_strdup(g_value_get_string(val))); + if (val != NULL) { + koto_button_set_text(self, (gchar*) g_value_get_string(val)); } break; case PROP_BADGE_TEXT: koto_button_set_badge_text(self, g_strdup(g_value_get_string(val))); break; + case PROP_USE_FROM_FILE: + self->use_from_file = g_value_get_boolean(val); + break; + case PROP_IMAGE_FILE_PATH: + koto_button_set_file_path(self, (gchar*) g_value_get_string(val)); + break; case PROP_ICON_NAME: koto_button_set_icon_name(self, g_strdup(g_value_get_string(val)), FALSE); if (!self->currently_showing_alt) { // Not showing alt @@ -228,10 +256,32 @@ static void koto_button_set_property(GObject *obj, guint prop_id, const GValue * } } +void koto_button_add_click_handler(KotoButton *self, KotoButtonClickType button, GCallback handler, gpointer user_data) { + if (!KOTO_IS_BUTTON(self)) { + return; + } + + if ((button != KOTO_BUTTON_CLICK_TYPE_PRIMARY) && (button != KOTO_BUTTON_CLICK_TYPE_SECONDARY)) { // Not valid type + return; + } + + g_signal_connect((button == KOTO_BUTTON_CLICK_TYPE_PRIMARY) ? self->left_click_gesture : self->right_click_gesture, "pressed", handler, user_data); +} + void koto_button_flip(KotoButton *self) { + if (!KOTO_IS_BUTTON(self)) { + return; + } + koto_button_show_image(self, !self->currently_showing_alt); } +void koto_button_hide_image(KotoButton *self) { + if (GTK_IS_WIDGET(self->button_pic)) { // Is a widget + gtk_widget_hide(self->button_pic); + } +} + void koto_button_set_badge_text(KotoButton *self, gchar *text) { if ((text == NULL) || (strcmp(text, "") == 0)) { // If the text is empty self->badge_text = g_strdup(""); @@ -256,6 +306,23 @@ void koto_button_set_badge_text(KotoButton *self, gchar *text) { g_object_notify_by_pspec(G_OBJECT(self), btn_props[PROP_BADGE_TEXT]); } +void koto_button_set_file_path(KotoButton *self, gchar *file_path) { + if (!KOTO_IS_BUTTON(self)) { // Not a button + return; + } + + if (file_path == NULL || (g_strcmp0(file_path, "") == 0)) { // Empty string or null + return; + } + + if (self->image_file_path != NULL && (g_strcmp0(self->image_file_path, "") != 0)) { // Not null and not empty + g_free(self->image_file_path); + } + + self->image_file_path = g_strdup(file_path); + koto_button_show_image(self, FALSE); +} + void koto_button_set_icon_name(KotoButton *self, gchar *icon_name, gboolean for_alt) { gchar *copied_icon_name = g_strdup(icon_name); @@ -291,6 +358,22 @@ void koto_button_set_icon_name(KotoButton *self, gchar *icon_name, gboolean for_ g_object_notify_by_pspec(G_OBJECT(self), for_alt ? btn_props[PROP_ALT_ICON_NAME] : btn_props[PROP_ICON_NAME]); } +void koto_button_set_image_position(KotoButton *self, KotoButtonImagePosition pos) { + if (self->image_position == pos) { // Is a different position that currently + return; + } + + if (GTK_IS_WIDGET(self->button_pic)) { // Button is already defined + if (pos == KOTO_BUTTON_IMAGE_POS_RIGHT) { // If we want to move the image to the right + gtk_box_reorder_child_after(GTK_BOX(self), self->button_pic, self->button_label); // Move image to after label + } else { // Moving image to left + gtk_box_reorder_child_after(GTK_BOX(self), self->button_label, self->button_pic); // Move label to after image + } + } + + self->image_position = pos; +} + void koto_button_set_pixbuf_size(KotoButton *self, guint size) { g_return_if_fail(size != self->pix_size); // If the sizes aren't different, return @@ -305,16 +388,14 @@ void koto_button_set_text(KotoButton *self, gchar *text) { return; } - gchar *copied_text = g_strdup(text); // Copy our text - - if (strcmp(copied_text, "") == 0) { // Clearing our text + if (self->text != NULL) { // Text defined g_free(self->text); // Free existing text } - self->text = copied_text; + self->text = g_strdup(text); if (GTK_IS_LABEL(self->button_label)) { // If we have a button label - if (strcmp(self->text, "") != 0) { // Have text set + if (g_strcmp0(self->text, "") != 0) { // Have text set gtk_label_set_text(GTK_LABEL(self->button_label), self->text); gtk_widget_show(self->button_label); // Show the label } else { // Have a label but no longer text @@ -322,7 +403,7 @@ void koto_button_set_text(KotoButton *self, gchar *text) { g_free(self->button_label); } } else { // If we do not have a button label - if (strcmp(self->text, "") != 0) { // If we have text + if ((self->text != NULL) && (g_strcmp0(self->text, "") != 0)) { // If we have text self->button_label = gtk_label_new(self->text); // Create our label gtk_label_set_xalign(GTK_LABEL(self->button_label), 0); @@ -342,15 +423,25 @@ void koto_button_show_image(KotoButton *self, gboolean use_alt) { return; } - if (use_alt && ((self->alt_icon_name == NULL) || (strcmp(self->alt_icon_name, "") == 0))) { // Don't have an alt icon set - return; - } else if (!use_alt && ((self->icon_name == NULL) || (strcmp(self->icon_name, "") == 0))) { // Don't have icon set - return; - } - if (self->use_from_file) { // Use from a file instead of icon name - // TODO: Add + if ((self->image_file_path == NULL) || g_strcmp0(self->image_file_path, "") == 0) { // Not set + return; + } + + if (GTK_IS_IMAGE(self->button_pic)) { // Already have an icon + gtk_image_set_from_file(GTK_IMAGE(self->button_pic), self->image_file_path); + } else { // Don't have an image yet + self->button_pic = gtk_image_new_from_file(self->image_file_path); // Create a new image from the file + gtk_box_prepend(GTK_BOX(self), self->button_pic); // Prepend to the box + } } else { // From icon name + + if (use_alt && ((self->alt_icon_name == NULL) || (strcmp(self->alt_icon_name, "") == 0))) { // Don't have an alt icon set + return; + } else if (!use_alt && ((self->icon_name == NULL) || (strcmp(self->icon_name, "") == 0))) { // Don't have icon set + return; + } + self->currently_showing_alt = use_alt; gchar *name = use_alt ? self->alt_icon_name : self->icon_name; @@ -358,12 +449,21 @@ void koto_button_show_image(KotoButton *self, gboolean use_alt) { gtk_image_set_from_icon_name(GTK_IMAGE(self->button_pic), name); // Just update the existing iamge } else { // Not an image self->button_pic = gtk_image_new_from_icon_name(name); // Get our new image - gtk_image_set_pixel_size(GTK_IMAGE(self->button_pic), self->pix_size); gtk_box_prepend(GTK_BOX(self), self->button_pic); // Prepend to the box } - - gtk_image_set_icon_size(GTK_IMAGE(self->button_pic), GTK_ICON_SIZE_INHERIT); // Inherit height of parent widget } + + gtk_image_set_pixel_size(GTK_IMAGE(self->button_pic), self->pix_size); + gtk_image_set_icon_size(GTK_IMAGE(self->button_pic), GTK_ICON_SIZE_INHERIT); // Inherit height of parent widget + gtk_widget_show(self->button_pic); // Ensure we actually are showing the image +} + +void koto_button_unflatten(KotoButton *self) { + if (!KOTO_IS_BUTTON(self)) { + return; + } + + gtk_widget_remove_css_class(GTK_WIDGET(self), "flat"); } KotoButton* koto_button_new_plain(gchar *label) { @@ -382,3 +482,16 @@ KotoButton* koto_button_new_with_icon(gchar *label, gchar *icon_name, gchar *alt NULL ); } + +KotoButton *koto_button_new_with_file(gchar *label, gchar *file_path, KotoButtonPixbufSize size) { + return g_object_new(KOTO_TYPE_BUTTON, + "button-text", label, + "use-from-file", + TRUE, + "image-file-path", + file_path, + "pixbuf-size", + koto_get_pixbuf_size(size), + NULL + ); +} diff --git a/src/koto-button.h b/src/koto-button.h index f99015c..2687991 100644 --- a/src/koto-button.h +++ b/src/koto-button.h @@ -23,6 +23,11 @@ G_BEGIN_DECLS +typedef enum { + KOTO_BUTTON_CLICK_TYPE_PRIMARY = 1, + KOTO_BUTTON_CLICK_TYPE_SECONDARY = 3 +} KotoButtonClickType; + typedef enum { KOTO_BUTTON_PIXBUF_SIZE_INVALID, KOTO_BUTTON_PIXBUF_SIZE_TINY, @@ -33,6 +38,11 @@ typedef enum { KOTO_BUTTON_PIXBUF_SIZE_GODLIKE } KotoButtonPixbufSize; +typedef enum { + KOTO_BUTTON_IMAGE_POS_LEFT, + KOTO_BUTTON_IMAGE_POS_RIGHT +} KotoButtonImagePosition; + #define NUM_BUILTIN_SIZES 7 #define KOTO_TYPE_BUTTON (koto_button_get_type()) @@ -43,14 +53,19 @@ guint koto_get_pixbuf_size(KotoButtonPixbufSize size); KotoButton* koto_button_new_plain(gchar *label); KotoButton* koto_button_new_with_icon(gchar *label, gchar *icon_name, gchar *alt_icon_name, KotoButtonPixbufSize size); -KotoButton* koto_button_new_with_pixbuf(gchar *label, GdkPixbuf *pix, KotoButtonPixbufSize size); +KotoButton *koto_button_new_with_file(gchar *label, gchar *file_path, KotoButtonPixbufSize size); +void koto_button_add_click_handler(KotoButton *self, KotoButtonClickType button, GCallback handler, gpointer user_data); void koto_button_flip(KotoButton *self); +void koto_button_hide_image(KotoButton *self); void koto_button_set_badge_text(KotoButton *self, gchar *text); +void koto_button_set_file_path(KotoButton *self, gchar *file_path); void koto_button_set_icon_name(KotoButton *self, gchar *icon_name, gboolean for_alt); +void koto_button_set_image_position(KotoButton *self, KotoButtonImagePosition pos); void koto_button_set_pixbuf(KotoButton *self, GdkPixbuf *pix); void koto_button_set_pixbuf_size(KotoButton *self, guint size); void koto_button_set_text(KotoButton *self, gchar *text); void koto_button_show_image(KotoButton *self, gboolean use_alt); +void koto_button_unflatten(KotoButton *self); G_END_DECLS diff --git a/src/koto-dialog-container.c b/src/koto-dialog-container.c new file mode 100644 index 0000000..6521650 --- /dev/null +++ b/src/koto-dialog-container.c @@ -0,0 +1,99 @@ +/* koto-dialog-container.c + * + * Copyright 2021 Joshua Strobl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "koto-button.h" +#include "koto-dialog-container.h" + +struct _KotoDialogContainer { + GtkBox parent_instance; + KotoButton *close_button; + GtkWidget *dialogs; +}; + +G_DEFINE_TYPE(KotoDialogContainer, koto_dialog_container, GTK_TYPE_BOX); + +static void koto_dialog_container_class_init(KotoDialogContainerClass *c) { + (void) c; +} + +static void koto_dialog_container_init(KotoDialogContainer *self) { + gtk_widget_add_css_class(GTK_WIDGET(self), "koto-dialog-container"); + + g_object_set(GTK_WIDGET(self), + "hexpand", + TRUE, + "vexpand", + TRUE, + NULL); + + self->close_button = koto_button_new_with_icon(NULL, "window-close-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_LARGE); + gtk_widget_set_halign(GTK_WIDGET(self->close_button), GTK_ALIGN_END); + gtk_box_prepend(GTK_BOX(self), GTK_WIDGET(self->close_button)); // Add our close button + + self->dialogs = gtk_stack_new(); + gtk_stack_set_transition_duration(GTK_STACK(self->dialogs), 0); // No transition timing + gtk_stack_set_transition_type(GTK_STACK(self->dialogs), GTK_STACK_TRANSITION_TYPE_NONE); // No transition + gtk_widget_set_halign(self->dialogs, GTK_ALIGN_CENTER); + gtk_widget_set_hexpand(self->dialogs, TRUE); + gtk_widget_set_valign(self->dialogs, GTK_ALIGN_CENTER); + gtk_widget_set_vexpand(self->dialogs, TRUE); + + gtk_box_append(GTK_BOX(self), self->dialogs); // Add the dialogs stack + + koto_button_add_click_handler(self->close_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_dialog_container_handle_close_click), self); + gtk_widget_hide(GTK_WIDGET(self)); // Hide by default +} + +void koto_dialog_container_add_dialog(KotoDialogContainer *self, gchar *dialog_name, GtkWidget *dialog) { + if (!KOTO_IS_DIALOG_CONTAINER(self)) { // Not a dialog container + return; + } + + gtk_stack_add_named(GTK_STACK(self->dialogs), dialog, dialog_name); // Add the dialog to the stack +} + +void koto_dialog_container_handle_close_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { + (void) gesture; (void) n_press; (void) x; (void) y; + koto_dialog_container_hide((KotoDialogContainer*) user_data); +} + +void koto_dialog_container_hide(KotoDialogContainer *self) { + if (!KOTO_IS_DIALOG_CONTAINER(self)) { // Not a dialog container + return; + } + + gtk_widget_hide(GTK_WIDGET(self)); +} + +void koto_dialog_container_show_dialog(KotoDialogContainer *self, gchar *dialog_name) { + if (!KOTO_IS_DIALOG_CONTAINER(self)) { // Not a dialog container + return; + } + + gtk_stack_set_visible_child_name(GTK_STACK(self->dialogs), dialog_name); // Set to the dialog name + gtk_widget_show(GTK_WIDGET(self)); // Ensure we show self +} + +KotoDialogContainer* koto_dialog_container_new() { + return g_object_new(KOTO_TYPE_DIALOG_CONTAINER, + "orientation", + GTK_ORIENTATION_VERTICAL, + NULL + ); +} diff --git a/src/playlist/create-dialog.h b/src/koto-dialog-container.h similarity index 50% rename from src/playlist/create-dialog.h rename to src/koto-dialog-container.h index c210b7b..f2a253b 100644 --- a/src/playlist/create-dialog.h +++ b/src/koto-dialog-container.h @@ -1,4 +1,4 @@ -/* create-dialog.h +/* koto-dialog-container.h * * Copyright 2021 Joshua Strobl * @@ -25,17 +25,18 @@ G_BEGIN_DECLS * Type Definition **/ -#define KOTO_TYPE_CREATE_PLAYLIST_DIALOG koto_create_playlist_dialog_get_type() -G_DECLARE_FINAL_TYPE(KotoCreatePlaylistDialog, koto_create_playlist_dialog, KOTO, CREATE_PLAYLIST_DIALOG, GObject); +#define KOTO_TYPE_DIALOG_CONTAINER koto_dialog_container_get_type() +G_DECLARE_FINAL_TYPE(KotoDialogContainer, koto_dialog_container, KOTO, DIALOG_CONTAINER, GtkBox); +#define KOTO_IS_DIALOG_CONTAINER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_DIALOG_CONTAINER)) /** - * Create Dialog Functions + * Functions **/ -KotoCreatePlaylistDialog* koto_create_playlist_dialog_new(); -GtkWidget* koto_create_playlist_dialog_get_content(KotoCreatePlaylistDialog *self); -void koto_create_playlist_dialog_handle_close(KotoCreatePlaylistDialog *self); -void koto_create_playlist_dialog_handle_create(KotoCreatePlaylistDialog *self); -void koto_create_playlist_dialog_handle_image_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data); +KotoDialogContainer* koto_dialog_container_new(); +void koto_dialog_container_add_dialog(KotoDialogContainer *self, gchar *dialog_name, GtkWidget *dialog); +void koto_dialog_container_handle_close_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data); +void koto_dialog_container_hide(KotoDialogContainer *self); +void koto_dialog_container_show_dialog(KotoDialogContainer *self, gchar *dialog_name); G_END_DECLS diff --git a/src/koto-expander.c b/src/koto-expander.c index 0a95ae0..000c80d 100644 --- a/src/koto-expander.c +++ b/src/koto-expander.c @@ -123,7 +123,7 @@ static void koto_expander_set_property(GObject *obj, guint prop_id, const GValue KotoExpander *self = KOTO_EXPANDER(obj); if (!GTK_IS_WIDGET(self->header_button)) { // Header Button is not a widget - KotoButton *new_button = koto_button_new_with_icon("Temporary Text", "emblem-favorite-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_SMALL); + KotoButton *new_button = koto_button_new_with_icon(NULL, "emblem-favorite-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_SMALL); if (GTK_IS_WIDGET(new_button)) { // Created our widget successfully self->header_button = new_button; @@ -175,9 +175,7 @@ static void koto_expander_init(KotoExpander *self) { self->constructed = TRUE; - GtkGesture *controller = gtk_gesture_click_new(); // Create a new GtkGestureClick - g_signal_connect(controller, "pressed", G_CALLBACK(koto_expander_toggle_content), self); - gtk_widget_add_controller(GTK_WIDGET(self->header_expand_button), GTK_EVENT_CONTROLLER(controller)); + koto_button_add_click_handler(self->header_expand_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_expander_toggle_content), self); } void koto_expander_set_secondary_button(KotoExpander *self, KotoButton *new_button) { @@ -215,6 +213,10 @@ void koto_expander_set_content(KotoExpander *self, GtkWidget *new_content) { g_object_notify_by_pspec(G_OBJECT(self), expander_props[PROP_CONTENT]); } +GtkWidget* koto_expander_get_content(KotoExpander *self) { + return self->content; +} + void koto_expander_toggle_content(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data) { (void) gesture; (void) n_press; (void) x; (void) y; KotoExpander* self = data; diff --git a/src/koto-expander.h b/src/koto-expander.h index 680c9f1..4f4fb2a 100644 --- a/src/koto-expander.h +++ b/src/koto-expander.h @@ -23,11 +23,12 @@ G_BEGIN_DECLS #define KOTO_TYPE_EXPANDER (koto_expander_get_type()) - G_DECLARE_FINAL_TYPE (KotoExpander, koto_expander, KOTO, EXPANDER, GtkBox) +#define KOTO_IS_EXPANDER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_EXPANDER)) KotoExpander* koto_expander_new(gchar *primary_icon_name, gchar *primary_label_text); KotoExpander* koto_expander_new_with_button(gchar *primary_icon_name, gchar *primary_label_text, KotoButton *secondary_button); +GtkWidget* koto_expander_get_content(KotoExpander *self); void koto_expander_set_icon_name(KotoExpander *self, const gchar *in); void koto_expander_set_label(KotoExpander *self, const gchar *label); void koto_expander_set_secondary_button(KotoExpander *self, KotoButton *new_button); diff --git a/src/koto-nav.c b/src/koto-nav.c index f40f2ff..bdde19c 100644 --- a/src/koto-nav.c +++ b/src/koto-nav.c @@ -16,12 +16,16 @@ */ #include +#include "db/cartographer.h" +#include "indexer/structs.h" +#include "playlist/playlist.h" #include "koto-config.h" #include "koto-button.h" #include "koto-expander.h" #include "koto-nav.h" #include "koto-window.h" +extern KotoCartographer *koto_maps; extern KotoWindow *main_window; struct _KotoNav { @@ -46,6 +50,10 @@ struct _KotoNav { KotoButton *music_local; KotoButton *music_radio; + // Playlists + + GHashTable *playlist_buttons; + // Podcasts KotoButton *podcasts_local; @@ -63,6 +71,7 @@ static void koto_nav_class_init(KotoNavClass *c) { } static void koto_nav_init(KotoNav *self) { + self->playlist_buttons = g_hash_table_new(g_str_hash, g_str_equal); self->win = gtk_scrolled_window_new(); gtk_widget_set_hexpand_set(self->win, TRUE); // using hexpand-set works, hexpand seems to break it by causing it to take up way too much space gtk_widget_set_size_request(self->win, 300, -1); @@ -88,19 +97,7 @@ static void koto_nav_init(KotoNav *self) { koto_nav_create_audiobooks_section(self); koto_nav_create_music_section(self); koto_nav_create_podcasts_section(self); - - KotoButton *playlist_add_button = koto_button_new_with_icon("", "list-add-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_SMALL); - KotoExpander *pl_expander = koto_expander_new_with_button("playlist-symbolic", "Playlists", playlist_add_button); - - if (pl_expander != NULL) { - self->playlists_expander = pl_expander; - gtk_box_append(GTK_BOX(self->content), GTK_WIDGET(self->playlists_expander)); - } - - GtkGesture *playlist_add_gesture = gtk_gesture_click_new(); // Create a gesture for clicking on the playlist add - gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(playlist_add_gesture), 1); // Only allow left click - g_signal_connect(playlist_add_gesture, "pressed", G_CALLBACK(koto_nav_handle_playlist_add_click), NULL); - gtk_widget_add_controller(GTK_WIDGET(playlist_add_button), GTK_EVENT_CONTROLLER(playlist_add_gesture)); + koto_nav_create_playlist_section(self); } void koto_nav_create_audiobooks_section(KotoNav *self) { @@ -135,6 +132,24 @@ void koto_nav_create_music_section(KotoNav *self) { gtk_box_append(GTK_BOX(new_content), GTK_WIDGET(self->music_radio)); koto_expander_set_content(m_expander, new_content); + koto_button_add_click_handler(self->music_local, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_nav_handle_local_music_click), NULL); +} + +void koto_nav_create_playlist_section(KotoNav *self) { + KotoButton *playlist_add_button = koto_button_new_with_icon("", "list-add-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_SMALL); + KotoExpander *pl_expander = koto_expander_new_with_button("playlist-symbolic", "Playlists", playlist_add_button); + + self->playlists_expander = pl_expander; + gtk_box_append(GTK_BOX(self->content), GTK_WIDGET(self->playlists_expander)); + + // TODO: Turn into ListBox to sort playlists + GtkWidget *playlist_list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + + koto_expander_set_content(self->playlists_expander, playlist_list); + koto_button_add_click_handler(playlist_add_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_nav_handle_playlist_add_click), NULL); + + g_signal_connect(koto_maps, "playlist-added", G_CALLBACK(koto_nav_handle_playlist_added), self); + g_signal_connect(koto_maps, "playlist-removed", G_CALLBACK(koto_nav_handle_playlist_removed), self); } void koto_nav_create_podcasts_section(KotoNav *self) { @@ -155,8 +170,76 @@ void koto_nav_create_podcasts_section(KotoNav *self) { void koto_nav_handle_playlist_add_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { (void) gesture; (void) n_press; (void) x; (void) y; (void) user_data; - g_message("plz"); - koto_window_show_create_playlist_dialog(main_window); + koto_window_show_dialog(main_window, "create-modify-playlist"); +} + +void koto_nav_handle_local_music_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { + (void) gesture; (void) n_press; (void) x; (void) y; (void) user_data; + koto_window_go_to_page(main_window, "music.local"); // Go to the playlist page +} + +void koto_nav_handle_playlist_button_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { + (void) gesture; (void) n_press; (void) x; (void) y; + gchar *playlist_uuid = user_data; + koto_window_go_to_page(main_window, playlist_uuid); // Go to the playlist page +} + +void koto_nav_handle_playlist_added(KotoCartographer *carto, KotoPlaylist *playlist, gpointer user_data) { + (void) carto; + g_return_if_fail(KOTO_IS_PLAYLIST(playlist)); + + KotoNav *self = user_data; + g_return_if_fail(KOTO_IS_NAV(self)); + + gchar *playlist_uuid = koto_playlist_get_uuid(playlist); // Get the UUID for a playlist + + if (g_hash_table_contains(self->playlist_buttons, playlist_uuid)) { // Already added button + g_free(playlist_uuid); + return; + } + + gchar *playlist_name = koto_playlist_get_name(playlist); + gchar *playlist_art_path = koto_playlist_get_artwork(playlist); // Get any file path for it + KotoButton *playlist_button = NULL; + + if ((playlist_art_path != NULL) && g_strcmp0(playlist_art_path, "") != 0) { // Have a file associated + playlist_button = koto_button_new_with_file(playlist_name, playlist_art_path, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); + } else { // No file associated + playlist_button = koto_button_new_with_icon(playlist_name, "audio-x-generic-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); + } + + if (KOTO_IS_BUTTON(playlist_button)) { + g_hash_table_insert(self->playlist_buttons, playlist_uuid, playlist_button); // Add the button + + // TODO: Make this a ListBox and sort the playlists alphabetically + GtkBox *playlist_expander_content = GTK_BOX(koto_expander_get_content(self->playlists_expander)); + + if (GTK_IS_BOX(playlist_expander_content)) { + gtk_box_append(playlist_expander_content, GTK_WIDGET(playlist_button)); + + koto_button_add_click_handler(playlist_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_nav_handle_playlist_button_click), playlist_uuid); + koto_window_handle_playlist_added(koto_maps, playlist, main_window); // TODO: MOVE THIS + } + } +} + +void koto_nav_handle_playlist_removed(KotoCartographer *carto, gchar *playlist_uuid, gpointer user_data) { + (void) carto; + KotoNav *self = user_data; + + if (!g_hash_table_contains(self->playlist_buttons, playlist_uuid)) { // Does not contain this + return; + } + + KotoButton *playlist_btn = g_hash_table_lookup(self->playlist_buttons, playlist_uuid); // Get the playlist button + + if (!KOTO_IS_BUTTON(playlist_btn)) { // Not a playlist button + return; + } + + GtkBox *playlist_expander_content = GTK_BOX(koto_expander_get_content(self->playlists_expander)); + gtk_box_remove(playlist_expander_content, GTK_WIDGET(playlist_btn)); // Remove the button + g_hash_table_remove(self->playlist_buttons, playlist_uuid); // Remove from the playlist buttons hash table } GtkWidget* koto_nav_get_nav(KotoNav *self) { diff --git a/src/koto-nav.h b/src/koto-nav.h index 2961fe5..d63f365 100644 --- a/src/koto-nav.h +++ b/src/koto-nav.h @@ -17,6 +17,8 @@ #pragma once #include +#include "db/cartographer.h" +#include "indexer/structs.h" G_BEGIN_DECLS @@ -27,8 +29,12 @@ G_DECLARE_FINAL_TYPE (KotoNav, koto_nav, KOTO, NAV, GObject) KotoNav* koto_nav_new (void); void koto_nav_create_audiobooks_section(KotoNav *self); void koto_nav_create_music_section(KotoNav *self); +void koto_nav_create_playlist_section(KotoNav *self); void koto_nav_create_podcasts_section(KotoNav *self); void koto_nav_handle_playlist_add_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data); +void koto_nav_handle_playlist_added(KotoCartographer *carto, KotoPlaylist *playlist, gpointer user_data); +void koto_nav_handle_playlist_removed(KotoCartographer *carto, gchar *playlist_uuid, gpointer user_data); +void koto_nav_handle_local_music_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data); GtkWidget* koto_nav_get_nav(KotoNav *self); diff --git a/src/koto-playerbar.c b/src/koto-playerbar.c index 9a5a695..b45ea85 100644 --- a/src/koto-playerbar.c +++ b/src/koto-playerbar.c @@ -18,6 +18,7 @@ #include #include #include "db/cartographer.h" +#include "playlist/add-remove-track-popover.h" #include "playlist/current.h" #include "playlist/playlist.h" #include "playback/engine.h" @@ -25,6 +26,7 @@ #include "koto-config.h" #include "koto-playerbar.h" +extern KotoAddRemoveTrackPopover *koto_add_remove_track_popup; extern KotoCurrentPlaylist *current_playlist; extern KotoCartographer *koto_maps; extern KotoPlaybackEngine *playback_engine; @@ -82,7 +84,10 @@ static void koto_playerbar_class_init(KotoPlayerBarClass *c) { static void koto_playerbar_constructed(GObject *obj) { KotoPlayerBar *self = KOTO_PLAYERBAR(obj); self->main = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_add_css_class(self->main, "player-bar"); + self->progress_bar = gtk_scale_new_with_range(GTK_ORIENTATION_HORIZONTAL, 0, 120, 1); // Default to 120 as random max + gtk_scale_set_draw_value(GTK_SCALE(self->progress_bar), FALSE); gtk_scale_set_digits(GTK_SCALE(self->progress_bar), 0); gtk_range_set_increments(GTK_RANGE(self->progress_bar), 1, 1); @@ -94,7 +99,6 @@ static void koto_playerbar_constructed(GObject *obj) { g_signal_connect(press_controller, "begin", G_CALLBACK(koto_playerbar_handle_progressbar_gesture_begin), self); g_signal_connect(press_controller, "end", G_CALLBACK(koto_playerbar_handle_progressbar_gesture_end), self); g_signal_connect(press_controller, "pressed", G_CALLBACK(koto_playerbar_handle_progressbar_pressed), self); - //g_signal_connect(press_controller, "unpaired-release", G_CALLBACK(koto_playerbar_handle_progressbar_unpaired_release), self); gtk_widget_add_controller(GTK_WIDGET(self->progress_bar), GTK_EVENT_CONTROLLER(press_controller)); @@ -102,11 +106,15 @@ static void koto_playerbar_constructed(GObject *obj) { self->controls = gtk_center_box_new(); gtk_center_box_set_baseline_position(GTK_CENTER_BOX(self->controls), GTK_BASELINE_POSITION_CENTER); - gtk_widget_add_css_class(self->main, "player-bar"); + + self->primary_controls_section = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_widget_add_css_class(self->primary_controls_section, "playerbar-primary-controls"); self->playback_section = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); - self->primary_controls_section = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_widget_add_css_class(self->playback_section, "playerbar-info"); + self->secondary_controls_section = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_widget_add_css_class(self->secondary_controls_section, "playerbar-secondary-controls"); gtk_center_box_set_start_widget(GTK_CENTER_BOX(self->controls), GTK_WIDGET(self->primary_controls_section)); gtk_center_box_set_center_widget(GTK_CENTER_BOX(self->controls), GTK_WIDGET(self->playback_section)); @@ -155,6 +163,7 @@ void koto_playerbar_create_playback_details(KotoPlayerBar* bar) { gtk_image_set_from_paintable(GTK_IMAGE(bar->artwork), GDK_PAINTABLE(audio_paintable)); } else { // Not an image bar->artwork = gtk_image_new_from_paintable(GDK_PAINTABLE(audio_paintable)); + gtk_widget_add_css_class(bar->artwork, "circular"); gtk_widget_set_size_request(bar->artwork, 96, 96); gtk_box_append(GTK_BOX(bar->playback_section), bar->artwork); } @@ -162,8 +171,13 @@ void koto_playerbar_create_playback_details(KotoPlayerBar* bar) { } bar->playback_title = gtk_label_new("Title"); + gtk_label_set_xalign(GTK_LABEL(bar->playback_title), 0); + bar->playback_album = gtk_label_new("Album"); + gtk_label_set_xalign(GTK_LABEL(bar->playback_album), 0); + bar->playback_artist = gtk_label_new("Artist"); + gtk_label_set_xalign(GTK_LABEL(bar->playback_artist), 0); gtk_box_append(GTK_BOX(bar->playback_details_section), GTK_WIDGET(bar->playback_title)); gtk_box_append(GTK_BOX(bar->playback_details_section), GTK_WIDGET(bar->playback_album)); @@ -179,26 +193,17 @@ void koto_playerbar_create_primary_controls(KotoPlayerBar* bar) { if (KOTO_IS_BUTTON(bar->back_button)) { gtk_box_append(GTK_BOX(bar->primary_controls_section), GTK_WIDGET(bar->back_button)); - - GtkGesture *back_controller = gtk_gesture_click_new(); // Create a new GtkGestureClick - g_signal_connect(back_controller, "pressed", G_CALLBACK(koto_playerbar_go_backwards), NULL); - gtk_widget_add_controller(GTK_WIDGET(bar->back_button), GTK_EVENT_CONTROLLER(back_controller)); + koto_button_add_click_handler(bar->back_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_go_backwards), bar); } if (KOTO_IS_BUTTON(bar->play_pause_button)) { gtk_box_append(GTK_BOX(bar->primary_controls_section), GTK_WIDGET(bar->play_pause_button)); - - GtkGesture *controller = gtk_gesture_click_new(); // Create a new GtkGestureClick - g_signal_connect(controller, "pressed", G_CALLBACK(koto_playerbar_toggle_play_pause), NULL); - gtk_widget_add_controller(GTK_WIDGET(bar->play_pause_button), GTK_EVENT_CONTROLLER(controller)); + koto_button_add_click_handler(bar->play_pause_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_toggle_play_pause), bar); } if (KOTO_IS_BUTTON(bar->forward_button)) { gtk_box_append(GTK_BOX(bar->primary_controls_section), GTK_WIDGET(bar->forward_button)); - - GtkGesture *forwards_controller = gtk_gesture_click_new(); // Create a new GtkGestureClick - g_signal_connect(forwards_controller, "pressed", G_CALLBACK(koto_playerbar_go_forwards), NULL); - gtk_widget_add_controller(GTK_WIDGET(bar->forward_button), GTK_EVENT_CONTROLLER(forwards_controller)); + koto_button_add_click_handler(bar->forward_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_go_forwards), bar); } } @@ -212,22 +217,17 @@ void koto_playerbar_create_secondary_controls(KotoPlayerBar* bar) { if (KOTO_IS_BUTTON(bar->repeat_button)) { gtk_box_append(GTK_BOX(bar->secondary_controls_section), GTK_WIDGET(bar->repeat_button)); - - GtkGesture *controller = gtk_gesture_click_new(); // Create a new GtkGestureClick - g_signal_connect(controller, "pressed", G_CALLBACK(koto_playerbar_toggle_track_repeat), bar); - gtk_widget_add_controller(GTK_WIDGET(bar->repeat_button), GTK_EVENT_CONTROLLER(controller)); + koto_button_add_click_handler(bar->repeat_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_toggle_track_repeat), bar); } if (KOTO_IS_BUTTON(bar->shuffle_button)) { gtk_box_append(GTK_BOX(bar->secondary_controls_section), GTK_WIDGET(bar->shuffle_button)); - - GtkGesture *controller = gtk_gesture_click_new(); // Create a new GtkGestureClick - g_signal_connect(controller, "pressed", G_CALLBACK(koto_playerbar_toggle_playlist_shuffle), bar); - gtk_widget_add_controller(GTK_WIDGET(bar->shuffle_button), GTK_EVENT_CONTROLLER(controller)); + koto_button_add_click_handler(bar->shuffle_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_toggle_playlist_shuffle), bar); } if (KOTO_IS_BUTTON(bar->playlist_button)) { gtk_box_append(GTK_BOX(bar->secondary_controls_section), GTK_WIDGET(bar->playlist_button)); + koto_button_add_click_handler(bar->playlist_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_handle_playlist_button_clicked), bar); } if (KOTO_IS_BUTTON(bar->eq_button)) { @@ -285,6 +285,18 @@ void koto_playerbar_handle_is_paused(KotoPlaybackEngine *engine, gpointer user_d koto_button_show_image(bar->play_pause_button, FALSE); // Set to FALSE to show play as the next action } +void koto_playerbar_handle_playlist_button_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data) { + (void) gesture; (void) n_press; (void) x; (void) y; + KotoPlayerBar *self = data; + + if (!KOTO_IS_PLAYERBAR(self)) { // Not a playerbar + return; + } + + koto_add_remove_track_popover_set_pointing_to_widget(koto_add_remove_track_popup, GTK_WIDGET(self->playlist_button), GTK_POS_TOP); // Position above the playlist button + gtk_widget_show(GTK_WIDGET(koto_add_remove_track_popup)); +} + void koto_playerbar_handle_progressbar_gesture_begin(GtkGesture *gesture, GdkEventSequence *seq, gpointer data) { (void) gesture; (void) seq; KotoPlayerBar *bar = data; @@ -421,7 +433,7 @@ void koto_playerbar_set_progressbar_duration(KotoPlayerBar* bar, gint64 duration } } -void koto_playerbar_set_progressbar_value(KotoPlayerBar* bar, gint64 progress) { +void koto_playerbar_set_progressbar_value(KotoPlayerBar* bar, double progress) { gtk_range_set_value(GTK_RANGE(bar->progress_bar), progress); } diff --git a/src/koto-playerbar.h b/src/koto-playerbar.h index c45c0e9..11fc947 100644 --- a/src/koto-playerbar.h +++ b/src/koto-playerbar.h @@ -35,6 +35,7 @@ void koto_playerbar_go_backwards(GtkGestureClick *gesture, int n_press, double x void koto_playerbar_go_forwards(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data); void koto_playerbar_handle_is_playing(KotoPlaybackEngine *engine, gpointer user_data); void koto_playerbar_handle_is_paused(KotoPlaybackEngine *engine, gpointer user_data); +void koto_playerbar_handle_playlist_button_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data); void koto_playerbar_handle_progressbar_scroll_begin(GtkEventControllerScroll *controller, gpointer data); void koto_playerbar_handle_progressbar_gesture_begin(GtkGesture *gesture, GdkEventSequence *seq, gpointer data); void koto_playerbar_handle_progressbar_gesture_end(GtkGesture *gesture, GdkEventSequence *seq, gpointer data); @@ -47,7 +48,7 @@ void koto_playerbar_handle_track_shuffle(KotoPlaybackEngine *engine, gpointer us void koto_playerbar_handle_volume_button_change(GtkScaleButton *button, double value, gpointer user_data); void koto_playerbar_reset_progressbar(KotoPlayerBar* bar); void koto_playerbar_set_progressbar_duration(KotoPlayerBar* bar, gint64 duration); -void koto_playerbar_set_progressbar_value(KotoPlayerBar* bar, gint64 progress); +void koto_playerbar_set_progressbar_value(KotoPlayerBar* bar, gdouble progress); void koto_playerbar_toggle_play_pause(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data); void koto_playerbar_toggle_playlist_shuffle(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data); void koto_playerbar_toggle_track_repeat(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data); diff --git a/src/koto-track-item.c b/src/koto-track-item.c index dd97e69..98874cc 100644 --- a/src/koto-track-item.c +++ b/src/koto-track-item.c @@ -16,15 +16,18 @@ */ #include +#include "indexer/structs.h" +#include "playlist/add-remove-track-popover.h" #include "koto-button.h" #include "koto-track-item.h" +extern KotoAddRemoveTrackPopover *koto_add_remove_track_popup; + struct _KotoTrackItem { GtkBox parent_instance; KotoIndexedTrack *track; GtkWidget *track_label; - KotoButton *add_to_playlist_button; }; struct _KotoTrackItemClass { @@ -91,14 +94,15 @@ static void koto_track_item_init(KotoTrackItem *self) { self->track_label = gtk_label_new(NULL); // Create with no track name gtk_label_set_xalign(GTK_LABEL(self->track_label), 0.0); - self->add_to_playlist_button = koto_button_new_with_icon(NULL, "playlist-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_TINY); - gtk_widget_add_css_class(GTK_WIDGET(self), "track-item"); gtk_widget_set_hexpand(GTK_WIDGET(self), TRUE); gtk_widget_set_hexpand(GTK_WIDGET(self->track_label), TRUE); gtk_box_prepend(GTK_BOX(self), self->track_label); - gtk_box_append(GTK_BOX(self), GTK_WIDGET(self->add_to_playlist_button)); +} + +KotoIndexedTrack* koto_track_item_get_track(KotoTrackItem *self) { + return self->track; } void koto_track_item_set_track(KotoTrackItem *self, KotoIndexedTrack *track) { diff --git a/src/koto-track-item.h b/src/koto-track-item.h index 33eaa11..0c9a54f 100644 --- a/src/koto-track-item.h +++ b/src/koto-track-item.h @@ -28,6 +28,8 @@ G_BEGIN_DECLS G_DECLARE_FINAL_TYPE(KotoTrackItem, koto_track_item, KOTO, TRACK_ITEM, GtkBox) KotoTrackItem* koto_track_item_new(KotoIndexedTrack *track); +void koto_track_item_handle_add_to_playlist_button_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data); +KotoIndexedTrack* koto_track_item_get_track(KotoTrackItem *self); void koto_track_item_set_track(KotoTrackItem *self, KotoIndexedTrack *track); G_END_DECLS diff --git a/src/koto-utils.c b/src/koto-utils.c index 130452b..6420b68 100644 --- a/src/koto-utils.c +++ b/src/koto-utils.c @@ -18,6 +18,25 @@ #include #include +extern GtkWindow *main_window; + +GtkFileChooserNative* koto_utils_create_image_file_chooser(gchar *file_chooser_label) { + GtkFileChooserNative* chooser = gtk_file_chooser_native_new( + file_chooser_label, + main_window, + GTK_FILE_CHOOSER_ACTION_OPEN, + "Choose", + "Cancel" + ); + + GtkFileFilter *image_filter = gtk_file_filter_new(); // Create our file filter + gtk_file_filter_add_mime_type(image_filter, "image/*"); // Only allow for images + gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(chooser), image_filter); // Only allow picking images + gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(chooser), FALSE); + + return chooser; +} + GtkWidget* koto_utils_create_image_from_filepath(gchar *filepath, gchar *fallback_icon, guint width, guint height) { GtkWidget* image = NULL; @@ -68,7 +87,11 @@ gchar* koto_utils_get_filename_without_extension(gchar *filename) { return stripped_file_name; } -gchar *koto_utils_replace_string_all(gchar *str, gchar *find, gchar *repl) { +void koto_utils_push_queue_element_to_store(gpointer data, gpointer user_data) { + g_list_store_append(G_LIST_STORE(user_data), data); +} + +gchar* koto_utils_replace_string_all(gchar *str, gchar *find, gchar *repl) { gchar *cleaned_string = ""; gchar **split = g_strsplit(str, find, -1); // Split on find diff --git a/src/koto-utils.h b/src/koto-utils.h index 58d16b7..b00152a 100644 --- a/src/koto-utils.h +++ b/src/koto-utils.h @@ -21,8 +21,10 @@ G_BEGIN_DECLS +GtkFileChooserNative* koto_utils_create_image_file_chooser(gchar *file_chooser_label); GtkWidget* koto_utils_create_image_from_filepath(gchar *filepath, gchar *fallback_icon, guint width, guint height); gchar* koto_utils_get_filename_without_extension(gchar *filename); +void koto_utils_push_queue_element_to_store(gpointer data, gpointer user_data); gchar *koto_utils_replace_string_all(gchar *str, gchar *find, gchar *repl); gchar* koto_utils_unquote_string(gchar *s); diff --git a/src/koto-window.c b/src/koto-window.c index 939535b..9a535ab 100644 --- a/src/koto-window.c +++ b/src/koto-window.c @@ -16,17 +16,27 @@ */ #include +#include "components/koto-action-bar.h" +#include "db/cartographer.h" #include "indexer/structs.h" #include "pages/music/music-local.h" +#include "pages/playlist/list.h" #include "playback/engine.h" +#include "playlist/add-remove-track-popover.h" #include "playlist/current.h" -#include "playlist/create-dialog.h" +#include "playlist/create-modify-dialog.h" #include "koto-config.h" +#include "koto-dialog-container.h" #include "koto-nav.h" #include "koto-playerbar.h" #include "koto-window.h" +extern KotoActionBar *action_bar; +extern KotoAddRemoveTrackPopover *koto_add_remove_track_popup; +extern KotoCartographer *koto_maps; +extern KotoCreateModifyPlaylistDialog *playlist_create_modify_dialog; extern KotoCurrentPlaylist *current_playlist; +extern KotoPageMusicLocal *music_local_page; extern KotoPlaybackEngine *playback_engine; struct _KotoWindow { @@ -34,7 +44,7 @@ struct _KotoWindow { KotoIndexedLibrary *library; KotoCurrentPlaylist *current_playlist; - KotoCreatePlaylistDialog *playlist_create_dialog; + KotoDialogContainer *dialogs; GtkWidget *overlay; GtkWidget *header_bar; @@ -66,14 +76,18 @@ static void koto_window_init (KotoWindow *self) { create_new_headerbar(self); // Create our headerbar self->overlay = gtk_overlay_new(); // Create our overlay - self->playlist_create_dialog = koto_create_playlist_dialog_new(); // Create our Create Playlist dialog + self->dialogs = koto_dialog_container_new(); // Create our dialog container self->primary_layout = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_widget_add_css_class(self->primary_layout, "primary-layout"); gtk_widget_set_hexpand(self->primary_layout, TRUE); gtk_widget_set_vexpand(self->primary_layout, TRUE); + playlist_create_modify_dialog = koto_create_modify_playlist_dialog_new(); // Create our Create Playlist dialog + koto_dialog_container_add_dialog(self->dialogs, "create-modify-playlist", GTK_WIDGET(playlist_create_modify_dialog)); + gtk_overlay_set_child(GTK_OVERLAY(self->overlay), self->primary_layout); // Add our primary layout to the overlay + gtk_overlay_add_overlay(GTK_OVERLAY(self->overlay), GTK_WIDGET(self->dialogs)); // Add the stack as our overlay self->content_layout = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); gtk_widget_add_css_class(self->content_layout, "content-layout"); @@ -97,9 +111,20 @@ static void koto_window_init (KotoWindow *self) { gtk_box_prepend(GTK_BOX(self->primary_layout), self->content_layout); + koto_add_remove_track_popup = koto_add_remove_track_popover_new(); // Create our popover for adding and removing tracks + action_bar = koto_action_bar_new(); // Create our Koto Action Bar + + if (KOTO_IS_ACTION_BAR(action_bar)) { // Is an action bar + GtkActionBar *bar = koto_action_bar_get_main(action_bar); + + if (GTK_IS_ACTION_BAR(bar)) { + gtk_box_append(GTK_BOX(self->primary_layout), GTK_WIDGET(bar)); // Add the action + } + } + self->player_bar = koto_playerbar_new(); - if (self->player_bar != NULL) { + if (KOTO_IS_PLAYERBAR(self->player_bar)) { // Is a playerbar GtkWidget *playerbar_main = koto_playerbar_get_main(self->player_bar); gtk_box_append(GTK_BOX(self->primary_layout), playerbar_main); } @@ -116,6 +141,42 @@ static void koto_window_init (KotoWindow *self) { g_thread_new("load-library", (void*) load_library, self); } +void koto_window_add_page(KotoWindow *self, gchar *page_name, GtkWidget *page) { + gtk_stack_add_named(GTK_STACK(self->pages), page, page_name); +} + +void koto_window_go_to_page(KotoWindow *self, gchar *page_name) { + gtk_stack_set_visible_child_name(GTK_STACK(self->pages), page_name); +} + +void koto_window_handle_playlist_added(KotoCartographer *carto, KotoPlaylist *playlist, gpointer user_data) { + (void) carto; + + if (!KOTO_IS_PLAYLIST(playlist)) { + return; + } + + KotoWindow *self = user_data; + + gchar *playlist_uuid = koto_playlist_get_uuid(playlist); + KotoPlaylistPage *playlist_page = koto_playlist_page_new(playlist_uuid); // Create our new Playlist Page + koto_window_add_page(self, playlist_uuid, koto_playlist_page_get_main(playlist_page)); // Get the GtkScrolledWindow "main" content of the playlist page and add that as a page to our stack by the playlist UUID +} + +void koto_window_hide_dialogs(KotoWindow *self) { + koto_dialog_container_hide(self->dialogs); // Hide the dialog container +} + +void koto_window_remove_page(KotoWindow *self, gchar *page_name) { + GtkWidget *page = gtk_stack_get_child_by_name(GTK_STACK(self->pages), page_name); + g_return_if_fail(page != NULL); + gtk_stack_remove(GTK_STACK(self->pages), page); +} + +void koto_window_show_dialog(KotoWindow *self, gchar *dialog_name) { + koto_dialog_container_show_dialog(self->dialogs, dialog_name); +} + void create_new_headerbar(KotoWindow *self) { self->header_bar = gtk_header_bar_new(); gtk_widget_add_css_class(self->header_bar, "hdr"); @@ -136,26 +197,18 @@ void create_new_headerbar(KotoWindow *self) { gtk_window_set_titlebar(GTK_WINDOW(self), self->header_bar); } -void koto_window_hide_create_playlist_dialog(KotoWindow *self) { - gtk_overlay_remove_overlay(GTK_OVERLAY(self->overlay), koto_create_playlist_dialog_get_content(self->playlist_create_dialog)); -} - -void koto_window_show_create_playlist_dialog(KotoWindow *self) { - gtk_overlay_add_overlay(GTK_OVERLAY(self->overlay), koto_create_playlist_dialog_get_content(self->playlist_create_dialog)); -} - void load_library(KotoWindow *self) { KotoIndexedLibrary *lib = koto_indexed_library_new(g_get_user_special_dir(G_USER_DIRECTORY_MUSIC)); if (lib != NULL) { self->library = lib; - KotoPageMusicLocal* l = koto_page_music_local_new(); + music_local_page = koto_page_music_local_new(); // TODO: Remove and do some fancy state loading - gtk_stack_add_named(GTK_STACK(self->pages), GTK_WIDGET(l), "music.local"); - gtk_stack_set_visible_child_name(GTK_STACK(self->pages), "music.local"); + koto_window_add_page(self, "music.local", GTK_WIDGET(music_local_page)); + koto_window_go_to_page(self, "music.local"); gtk_widget_show(self->pages); // Do not remove this. Will cause sporadic hiding of the local page content otherwise. - koto_page_music_local_set_library(l, self->library); + koto_page_music_local_set_library(music_local_page, self->library); } g_thread_exit(0); diff --git a/src/koto-window.h b/src/koto-window.h index 3dbca29..aab9609 100644 --- a/src/koto-window.h +++ b/src/koto-window.h @@ -18,6 +18,8 @@ #pragma once #include +#include "db/cartographer.h" +#include "playlist/playlist.h" G_BEGIN_DECLS @@ -25,10 +27,15 @@ G_BEGIN_DECLS G_DECLARE_FINAL_TYPE (KotoWindow, koto_window, KOTO, WINDOW, GtkApplicationWindow) -void koto_window_show_create_playlist_dialog(KotoWindow *self); -void koto_window_hide_create_playlist_dialog(KotoWindow *self); +void koto_window_add_page(KotoWindow *self, gchar *page_name, GtkWidget *page); +void koto_window_go_to_page(KotoWindow *self, gchar *page_name); +void koto_window_handle_playlist_added(KotoCartographer *carto, KotoPlaylist *playlist, gpointer user_data); +void koto_window_hide_dialogs(KotoWindow *self); +void koto_window_remove_page(KotoWindow *self, gchar *page_name); +void koto_window_show_dialog(KotoWindow *self, gchar *dialog_name); void create_new_headerbar(KotoWindow *self); +void handle_album_added(); void load_library(KotoWindow *self); void set_optimal_default_window_size(KotoWindow *self); diff --git a/src/meson.build b/src/meson.build index a6c9ac5..8e22d0b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,6 +1,8 @@ add_project_arguments('-Db_sanitize=address', language: 'c') koto_sources = [ + 'components/koto-action-bar.c', + 'components/koto-cover-art-button.c', 'db/cartographer.c', 'db/db.c', 'indexer/album.c', @@ -11,15 +13,18 @@ koto_sources = [ 'pages/music/artist-view.c', 'pages/music/disc-view.c', 'pages/music/music-local.c', + 'pages/playlist/list.c', 'playback/engine.c', 'playback/media-keys.c', 'playback/mimes.c', 'playback/mpris.c', - 'playlist/create-dialog.c', + 'playlist/add-remove-track-popover.c', + 'playlist/create-modify-dialog.c', 'playlist/current.c', 'playlist/playlist.c', 'main.c', 'koto-button.c', + 'koto-dialog-container.c', 'koto-expander.c', 'koto-nav.c', 'koto-playerbar.c', diff --git a/src/pages/music/album-view.c b/src/pages/music/album-view.c index 57f1118..d3d5e22 100644 --- a/src/pages/music/album-view.c +++ b/src/pages/music/album-view.c @@ -82,9 +82,11 @@ static void koto_album_view_init(KotoAlbumView *self) { self->discs = gtk_list_box_new(); // Create our list of our tracks gtk_list_box_set_selection_mode(GTK_LIST_BOX(self->discs), GTK_SELECTION_NONE); + gtk_list_box_set_show_separators(GTK_LIST_BOX(self->discs), FALSE); gtk_list_box_set_sort_func(GTK_LIST_BOX(self->discs), koto_album_view_sort_discs, NULL, NULL); // Ensure we can sort our discs gtk_widget_add_css_class(self->discs, "discs-list"); gtk_widget_set_can_focus(self->discs, FALSE); + gtk_widget_set_focusable(self->discs, FALSE); gtk_widget_set_size_request(self->discs, 600, -1); gtk_box_append(GTK_BOX(self->main), self->album_tracks_box); // Add the tracks box to the art info combo box @@ -116,9 +118,7 @@ static void koto_album_view_init(KotoAlbumView *self) { g_signal_connect(motion_controller, "leave", G_CALLBACK(koto_album_view_hide_overlay_controls), self); gtk_widget_add_controller(self->album_overlay_container, motion_controller); - GtkGesture *controller = gtk_gesture_click_new(); // Create a new GtkGestureClick - g_signal_connect(controller, "pressed", G_CALLBACK(koto_album_view_toggle_album_playback), self); - gtk_widget_add_controller(GTK_WIDGET(self->play_pause_button), GTK_EVENT_CONTROLLER(controller)); + koto_button_add_click_handler(self->play_pause_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_album_view_toggle_album_playback), self); } GtkWidget* koto_album_view_get_main(KotoAlbumView *self) { diff --git a/src/pages/music/artist-view.c b/src/pages/music/artist-view.c index 8cdcf48..1622dba 100644 --- a/src/pages/music/artist-view.c +++ b/src/pages/music/artist-view.c @@ -122,7 +122,7 @@ static void koto_artist_view_constructed(GObject *obj) { gtk_widget_set_halign(self->favorites_list, GTK_ALIGN_START); self->album_list = gtk_flow_box_new(); // Create our list of our albums - gtk_flow_box_set_activate_on_single_click(GTK_FLOW_BOX(self->album_list), FALSE); + //gtk_flow_box_set_activate_on_single_click(GTK_FLOW_BOX(self->album_list), FALSE); gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(self->album_list), GTK_SELECTION_NONE); gtk_widget_add_css_class(self->album_list, "album-list"); diff --git a/src/pages/music/disc-view.c b/src/pages/music/disc-view.c index 9071262..ef6a201 100644 --- a/src/pages/music/disc-view.c +++ b/src/pages/music/disc-view.c @@ -16,11 +16,13 @@ */ #include +#include "../../components/koto-action-bar.h" #include "../../db/cartographer.h" #include "../../indexer/structs.h" #include "../../koto-track-item.h" #include "disc-view.h" +extern KotoActionBar *action_bar; extern KotoCartographer *koto_maps; struct _KotoDiscView { @@ -121,31 +123,17 @@ static void koto_disc_view_init(KotoDiscView *self) { gtk_box_append(GTK_BOX(self->header), self->label); gtk_box_append(GTK_BOX(self), self->header); -} - -void koto_disc_view_set_album(KotoDiscView *self, KotoIndexedAlbum *album) { - if (album == NULL) { - return; - } - - if (self->album != NULL) { - g_free(self->album); - } - - self->album = album; - - if (GTK_IS_LIST_BOX(self->list)) { // Already have a listbox - gtk_box_remove(GTK_BOX(self), self->list); // Remove the box - g_object_unref(self->list); // Unref the list - } self->list = gtk_list_box_new(); // Create our list of our tracks + gtk_list_box_set_activate_on_single_click(GTK_LIST_BOX(self->list), FALSE); gtk_list_box_set_selection_mode(GTK_LIST_BOX(self->list), GTK_SELECTION_MULTIPLE); gtk_widget_add_css_class(self->list, "track-list"); + gtk_widget_set_can_focus(self->list, FALSE); + gtk_widget_set_focusable(self->list, FALSE); gtk_widget_set_size_request(self->list, 600, -1); gtk_box_append(GTK_BOX(self), self->list); - g_list_foreach(koto_indexed_album_get_tracks(self->album), koto_disc_view_list_tracks, self); + g_signal_connect(self->list, "selected-rows-changed", G_CALLBACK(koto_disc_view_handle_selected_rows_changed), self); } void koto_disc_view_list_tracks(gpointer data, gpointer selfptr) { @@ -163,6 +151,49 @@ void koto_disc_view_list_tracks(gpointer data, gpointer selfptr) { gtk_list_box_append(GTK_LIST_BOX(self->list), GTK_WIDGET(track_item)); // Add to our tracks list box } +void koto_disc_view_handle_selected_rows_changed(GtkListBox *box, gpointer user_data) { + KotoDiscView *self = user_data; + + gchar *album_uuid = koto_indexed_album_get_album_uuid(self->album); // Get the UUID + + if ((album_uuid == NULL) || g_strcmp0(album_uuid, "") == 0) { // Not set + return; + } + + GList *selected_rows = gtk_list_box_get_selected_rows(box); // Get the selected rows + + if (g_list_length(selected_rows) == 0) { // No rows selected + koto_action_bar_toggle_reveal(action_bar, FALSE); // Close the action bar + return; + } + + GList *selected_tracks = NULL; // Create our list of KotoIndexedTracks + GList *cur_selected_rows; + for (cur_selected_rows = selected_rows; cur_selected_rows != NULL; cur_selected_rows = cur_selected_rows->next) { // Iterate over the rows + KotoTrackItem *track_item = (KotoTrackItem*) gtk_list_box_row_get_child(cur_selected_rows->data); + selected_tracks = g_list_append(selected_tracks, koto_track_item_get_track(track_item)); // Add the KotoIndexedTrack to our list + } + + g_list_free(cur_selected_rows); + + koto_action_bar_set_tracks_in_album_selection(action_bar, album_uuid, selected_tracks); // Set our album selection + koto_action_bar_toggle_reveal(action_bar, TRUE); // Show the action bar +} + +void koto_disc_view_set_album(KotoDiscView *self, KotoIndexedAlbum *album) { + if (album == NULL) { + return; + } + + if (self->album != NULL) { + g_free(self->album); + } + + self->album = album; + + g_list_foreach(koto_indexed_album_get_tracks(self->album), koto_disc_view_list_tracks, self); +} + void koto_disc_view_set_disc_number(KotoDiscView *self, guint disc_number) { if (disc_number == 0) { return; diff --git a/src/pages/music/disc-view.h b/src/pages/music/disc-view.h index b2233f6..683f9a5 100644 --- a/src/pages/music/disc-view.h +++ b/src/pages/music/disc-view.h @@ -28,6 +28,7 @@ G_BEGIN_DECLS G_DECLARE_FINAL_TYPE(KotoDiscView, koto_disc_view, KOTO, DISC_VIEW, GtkBox) KotoDiscView* koto_disc_view_new(KotoIndexedAlbum *album, guint *disc); +void koto_disc_view_handle_selected_rows_changed(GtkListBox *box, gpointer user_data); void koto_disc_view_list_tracks(gpointer data, gpointer selfptr); void koto_disc_view_set_album(KotoDiscView *self, KotoIndexedAlbum *album); void koto_disc_view_set_disc_label_visible(KotoDiscView *self, gboolean visible); diff --git a/src/pages/music/music-local.c b/src/pages/music/music-local.c index 4cd4756..4f19ef5 100644 --- a/src/pages/music/music-local.c +++ b/src/pages/music/music-local.c @@ -49,6 +49,8 @@ struct _KotoPageMusicLocalClass { G_DEFINE_TYPE(KotoPageMusicLocal, koto_page_music_local, GTK_TYPE_BOX); +KotoPageMusicLocal *music_local_page; + static void koto_page_music_local_constructed(GObject *obj); static void koto_page_music_local_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec); static void koto_page_music_local_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec); @@ -145,6 +147,32 @@ void koto_page_music_local_add_artist(KotoPageMusicLocal *self, KotoIndexedArtis gtk_stack_add_named(GTK_STACK(self->stack), koto_artist_view_get_main(artist_view), artist_name); } +void koto_page_music_local_go_to_artist_by_name(KotoPageMusicLocal *self, gchar *artist_name) { + gtk_stack_set_visible_child_name(GTK_STACK(self->stack), artist_name); +} + +void koto_page_music_local_go_to_artist_by_uuid(KotoPageMusicLocal *self, gchar *artist_uuid) { + KotoIndexedArtist *artist = koto_cartographer_get_artist_by_uuid(koto_maps, artist_uuid); // Get the artist + + if (!KOTO_IS_INDEXED_ARTIST(artist)) { // No artist for this UUID + return; + } + + gchar *artist_name = NULL; + g_object_get( + artist, + "name", + &artist_name, + NULL + ); + + if (artist_name == NULL || (g_strcmp0(artist_name, "") == 0)) { // Failed to get the artist name + return; + } + + koto_page_music_local_go_to_artist_by_name(self, artist_name); +} + void koto_page_music_local_handle_artist_click(GtkListBox *box, GtkListBoxRow *row, gpointer data) { (void) box; KotoPageMusicLocal *self = (KotoPageMusicLocal*) data; @@ -152,7 +180,7 @@ void koto_page_music_local_handle_artist_click(GtkListBox *box, GtkListBoxRow *r gchar *artist_name; g_object_get(btn, "button-text", &artist_name, NULL); - gtk_stack_set_visible_child_name(GTK_STACK(self->stack), artist_name); + koto_page_music_local_go_to_artist_by_name(self, artist_name); } void koto_page_music_local_set_library(KotoPageMusicLocal *self, KotoIndexedLibrary *lib) { diff --git a/src/pages/music/music-local.h b/src/pages/music/music-local.h index dc80510..4e37e5a 100644 --- a/src/pages/music/music-local.h +++ b/src/pages/music/music-local.h @@ -31,6 +31,8 @@ G_DECLARE_FINAL_TYPE (KotoPageMusicLocal, koto_page_music_local, KOTO, PAGE_MUSI KotoPageMusicLocal* koto_page_music_local_new(); void koto_page_music_local_add_artist(KotoPageMusicLocal *self, KotoIndexedArtist *artist); void koto_page_music_local_handle_artist_click(GtkListBox *box, GtkListBoxRow *row, gpointer data); +void koto_page_music_local_go_to_artist_by_name(KotoPageMusicLocal *self, gchar *artist_name); +void koto_page_music_local_go_to_artist_by_uuid(KotoPageMusicLocal *self, gchar *artist_uuid); void koto_page_music_local_set_library(KotoPageMusicLocal *self, KotoIndexedLibrary *lib); int koto_page_music_local_sort_artists(GtkListBoxRow *artist1, GtkListBoxRow *artist2, gpointer user_data); diff --git a/src/pages/playlist/list.c b/src/pages/playlist/list.c new file mode 100644 index 0000000..0fbc1dd --- /dev/null +++ b/src/pages/playlist/list.c @@ -0,0 +1,510 @@ +/* list.c + * + * Copyright 2021 Joshua Strobl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "../../components/koto-action-bar.h" +#include "../../components/koto-cover-art-button.h" +#include "../../db/cartographer.h" +#include "../../playlist/current.h" +#include "../../playlist/playlist.h" +#include "../../koto-button.h" +#include "../../koto-window.h" +#include "../../playlist/create-modify-dialog.h" +#include "list.h" + +extern KotoActionBar *action_bar; +extern KotoCartographer *koto_maps; +extern KotoCreateModifyPlaylistDialog *playlist_create_modify_dialog; +extern KotoCurrentPlaylist *current_playlist; +extern KotoWindow *main_window; + +enum { + PROP_0, + PROP_PLAYLIST_UUID, + N_PROPERTIES +}; + +static GParamSpec *props[N_PROPERTIES] = { NULL, }; + +struct _KotoPlaylistPage { + GObject parent_instance; + KotoPlaylist *playlist; + gchar *uuid; + + GtkWidget *main; // Our Scrolled Window + GtkWidget *content; // Content inside scrolled window + GtkWidget *header; + + KotoCoverArtButton *playlist_image; + GtkWidget *name_label; + GtkWidget *tracks_count_label; + GtkWidget *type_label; + KotoButton *favorite_button; + KotoButton *edit_button; + + GtkListItemFactory *item_factory; + GListModel *model; + GtkSelectionModel *selection_model; + + GtkWidget *track_list_content; + GtkWidget *track_list_header; + GtkWidget *track_list_view; + + KotoButton *track_num_button; + KotoButton *track_title_button; + KotoButton *track_album_button; + KotoButton *track_artist_button; + + GtkSizeGroup *track_pos_size_group; + GtkSizeGroup *track_name_size_group; + GtkSizeGroup *track_album_size_group; + GtkSizeGroup *track_artist_size_group; +}; + +struct _KotoPlaylistPageClass { + GObjectClass parent_class; +}; + +G_DEFINE_TYPE(KotoPlaylistPage, koto_playlist_page, G_TYPE_OBJECT); + +static void koto_playlist_page_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec); +static void koto_playlist_page_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec); + +static void koto_playlist_page_class_init(KotoPlaylistPageClass *c) { + GObjectClass *gobject_class; + gobject_class = G_OBJECT_CLASS(c); + gobject_class->get_property = koto_playlist_page_get_property; + gobject_class->set_property = koto_playlist_page_set_property; + + props[PROP_PLAYLIST_UUID] = g_param_spec_string( + "uuid", + "UUID of associated Playlist", + "UUID of associated Playlist", + NULL, + G_PARAM_CONSTRUCT_ONLY|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE + ); + + g_object_class_install_properties(gobject_class, N_PROPERTIES, props); +} + +static void koto_playlist_page_init(KotoPlaylistPage *self) { + self->track_name_size_group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + self->track_pos_size_group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + self->track_album_size_group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + self->track_artist_size_group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + + self->main = gtk_scrolled_window_new(); // Create our scrolled window + self->content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_add_css_class(self->content, "playlist-page"); + + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(self->main), self->content); + + self->header = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); // Create a horizontal box + gtk_widget_add_css_class(self->header, "playlist-page-header"); + gtk_box_prepend(GTK_BOX(self->content), self->header); + + self->playlist_image = koto_cover_art_button_new(220, 220, NULL); // Create our Cover Art Button with no art by default + KotoButton *cover_art_button = koto_cover_art_button_get_button(self->playlist_image); // Get the button for the cover art + koto_button_add_click_handler(cover_art_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playlist_page_handle_cover_art_clicked), self); + + GtkWidget *info_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_size_request(info_box, -1, 220); + gtk_widget_add_css_class(info_box, "playlist-page-header-info"); + gtk_widget_set_hexpand(info_box, TRUE); + + self->type_label = gtk_label_new(NULL); // Create our type label + gtk_widget_set_halign(self->type_label, GTK_ALIGN_START); + + self->name_label = gtk_label_new(NULL); + gtk_widget_set_halign(self->name_label, GTK_ALIGN_START); + + self->tracks_count_label = gtk_label_new(NULL); + gtk_widget_set_halign(self->tracks_count_label, GTK_ALIGN_START); + gtk_widget_set_valign(self->tracks_count_label, GTK_ALIGN_END); + + gtk_box_append(GTK_BOX(info_box), self->type_label); + gtk_box_append(GTK_BOX(info_box), self->name_label); + gtk_box_append(GTK_BOX(info_box), self->tracks_count_label); + + self->favorite_button = koto_button_new_with_icon(NULL, "emblem-favorite-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); + self->edit_button = koto_button_new_with_icon(NULL, "emblem-system-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); + koto_button_add_click_handler(self->edit_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playlist_page_handle_edit_button_clicked), self); // Set up our binding + + gtk_box_append(GTK_BOX(self->header), koto_cover_art_button_get_main(self->playlist_image)); // Add the Cover Art Button main overlay + gtk_box_append(GTK_BOX(self->header), info_box); // Add our info box + gtk_box_append(GTK_BOX(self->header), GTK_WIDGET(self->favorite_button)); // Add the favorite button + gtk_box_append(GTK_BOX(self->header), GTK_WIDGET(self->edit_button)); // Add the edit button + + self->item_factory = gtk_signal_list_item_factory_new(); // Create a new signal list item factory + g_signal_connect(self->item_factory, "setup", G_CALLBACK(koto_playlist_page_setup_track_item), self); + g_signal_connect(self->item_factory, "bind", G_CALLBACK(koto_playlist_page_bind_track_item), self); + + self->track_list_content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_add_css_class((self->track_list_content), "track-list-content"); + gtk_widget_set_hexpand(self->track_list_content, TRUE); // Expand horizontally + gtk_widget_set_vexpand(self->track_list_content, TRUE); // Expand vertically + + self->track_list_header = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_widget_add_css_class(self->track_list_header, "track-list-header"); + koto_playlist_page_create_tracks_header(self); // Create our tracks header content + + self->track_list_view = gtk_list_view_new(NULL, self->item_factory); // Create our list view with no model yet + gtk_widget_add_css_class(self->track_list_view, "track-list-columned"); + gtk_widget_set_hexpand(self->track_list_view, TRUE); // Expand horizontally + gtk_widget_set_vexpand(self->track_list_view, TRUE); // Expand vertically + + gtk_box_append(GTK_BOX(self->track_list_content), self->track_list_header); + gtk_box_append(GTK_BOX(self->track_list_content), self->track_list_view); + + gtk_box_append(GTK_BOX(self->content), self->track_list_content); + + g_signal_connect(action_bar, "closed", G_CALLBACK(koto_playlist_page_handle_action_bar_closed), self); // Handle closed action bar +} + +static void koto_playlist_page_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec) { + KotoPlaylistPage *self = KOTO_PLAYLIST_PAGE(obj); + + switch (prop_id) { + case PROP_PLAYLIST_UUID: + g_value_set_string(val, self->uuid); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_playlist_page_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec) { + KotoPlaylistPage *self = KOTO_PLAYLIST_PAGE(obj); + switch (prop_id) { + case PROP_PLAYLIST_UUID: + koto_playlist_page_set_playlist_uuid(self, g_strdup(g_value_get_string(val))); // Call to our playlist UUID set function + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +void koto_playlist_page_bind_track_item(GtkListItemFactory *factory, GtkListItem *item, KotoPlaylistPage *self) { + (void) factory; + + GtkWidget *track_position_label = gtk_widget_get_first_child(gtk_list_item_get_child(item)); + GtkWidget *track_name_label = gtk_widget_get_next_sibling(track_position_label); + GtkWidget *track_album_label = gtk_widget_get_next_sibling(track_name_label); + GtkWidget *track_artist_label = gtk_widget_get_next_sibling(track_album_label); + + KotoIndexedTrack *track = gtk_list_item_get_item(item); // Get the track UUID from our model + + g_return_if_fail(KOTO_IS_INDEXED_TRACK(track)); + + gchar *track_name = NULL; + gchar *album_uuid = NULL; + gchar *artist_uuid = NULL; + + g_object_get( + track, + "parsed-name", + &track_name, + "album-uuid", + &album_uuid, + "artist-uuid", + &artist_uuid, + NULL + ); + + guint track_position = koto_playlist_get_position_of_track(self->playlist, track); + + gtk_label_set_label(GTK_LABEL(track_position_label), g_strdup_printf("%u", track_position)); // Set the track position + gtk_label_set_label(GTK_LABEL(track_name_label), track_name); // Set our track name + + KotoIndexedAlbum *album = koto_cartographer_get_album_by_uuid(koto_maps, album_uuid); + + if (KOTO_IS_INDEXED_ALBUM(album)) { + gtk_label_set_label(GTK_LABEL(track_album_label), koto_indexed_album_get_album_name(album)); // Get the name of the album and set it to the label + } + + KotoIndexedArtist *artist = koto_cartographer_get_artist_by_uuid(koto_maps, artist_uuid); + + if (KOTO_IS_INDEXED_ARTIST(artist)) { + gtk_label_set_label(GTK_LABEL(track_artist_label), koto_indexed_artist_get_name(artist)); // Get the name of the artist and set it to the label + } +} + +void koto_playlist_page_create_tracks_header(KotoPlaylistPage *self) { + self->track_num_button = koto_button_new_with_icon("#", "pan-down-symbolic", "pan-up-symbolic", KOTO_BUTTON_PIXBUF_SIZE_SMALL); + koto_button_add_click_handler(self->track_num_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playlist_page_handle_track_num_clicked), self); + koto_button_set_image_position(self->track_num_button, KOTO_BUTTON_IMAGE_POS_RIGHT); // Move the image to the right + gtk_size_group_add_widget(self->track_pos_size_group, GTK_WIDGET(self->track_num_button)); + + self->track_title_button = koto_button_new_plain("Title"); + koto_button_add_click_handler(self->track_title_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playlist_page_handle_track_name_clicked), self); + gtk_size_group_add_widget(self->track_name_size_group, GTK_WIDGET(self->track_title_button)); + + self->track_album_button = koto_button_new_plain("Album"); + + gtk_widget_set_margin_start(GTK_WIDGET(self->track_album_button), 50); + koto_button_add_click_handler(self->track_album_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playlist_page_handle_track_album_clicked), self); + gtk_size_group_add_widget(self->track_album_size_group, GTK_WIDGET(self->track_album_button)); + + self->track_artist_button = koto_button_new_plain("Artist"); + + gtk_widget_set_margin_start(GTK_WIDGET(self->track_artist_button), 50); + koto_button_add_click_handler(self->track_artist_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playlist_page_handle_track_artist_clicked), self); + gtk_size_group_add_widget(self->track_artist_size_group, GTK_WIDGET(self->track_artist_button)); + + gtk_box_append(GTK_BOX(self->track_list_header), GTK_WIDGET(self->track_num_button)); + gtk_box_append(GTK_BOX(self->track_list_header), GTK_WIDGET(self->track_title_button)); + gtk_box_append(GTK_BOX(self->track_list_header), GTK_WIDGET(self->track_album_button)); + gtk_box_append(GTK_BOX(self->track_list_header), GTK_WIDGET(self->track_artist_button)); +} + +GtkWidget* koto_playlist_page_get_main(KotoPlaylistPage *self) { + return self->main; +} + +void koto_playlist_page_handle_action_bar_closed(KotoActionBar *bar, gpointer data) { + (void) bar; + KotoPlaylistPage *self = data; + + if (!KOTO_IS_PLAYLIST(self->playlist)) { // No playlist set + return; + } + + gtk_selection_model_unselect_all(self->selection_model); + gtk_widget_grab_focus(GTK_WIDGET(main_window)); // Focus on the window +} + +void koto_playlist_page_handle_cover_art_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { + (void) gesture; (void) n_press; (void) x; (void) y; + KotoPlaylistPage *self = user_data; + + if (!KOTO_IS_PLAYLIST(self->playlist)) { // No playlist set + return; + } + + koto_current_playlist_set_playlist(current_playlist, self->playlist); +} + +void koto_playlist_page_handle_edit_button_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { + (void) gesture; (void) n_press; (void) x; (void) y; + KotoPlaylistPage *self = user_data; + koto_create_modify_playlist_dialog_set_playlist_uuid(playlist_create_modify_dialog, koto_playlist_get_uuid(self->playlist)); + koto_window_show_dialog(main_window, "create-modify-playlist"); +} + +void koto_playlist_page_handle_track_album_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { + (void) gesture; (void) n_press; (void) x; (void) y; + KotoPlaylistPage *self = user_data; + gtk_widget_add_css_class(GTK_WIDGET(self->track_album_button), "active"); + koto_button_hide_image(self->track_num_button); // Go back to hiding the image + koto_playlist_page_set_playlist_model(self, KOTO_PREFERRED_MODEL_TYPE_SORT_BY_ALBUM); +} + +void koto_playlist_page_handle_track_artist_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { + (void) gesture; (void) n_press; (void) x; (void) y; + KotoPlaylistPage *self = user_data; + gtk_widget_add_css_class(GTK_WIDGET(self->track_artist_button), "active"); + koto_button_hide_image(self->track_num_button); // Go back to hiding the image + koto_playlist_page_set_playlist_model(self, KOTO_PREFERRED_MODEL_TYPE_SORT_BY_ARTIST); +} + +void koto_playlist_page_handle_track_name_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { + (void) gesture; (void) n_press; (void) x; (void) y; + KotoPlaylistPage *self = user_data; + gtk_widget_add_css_class(GTK_WIDGET(self->track_title_button), "active"); + koto_button_hide_image(self->track_num_button); // Go back to hiding the image + koto_playlist_page_set_playlist_model(self, KOTO_PREFERRED_MODEL_TYPE_SORT_BY_TRACK_NAME); +} + +void koto_playlist_page_handle_track_num_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { + (void) gesture; (void) n_press; (void) x; (void) y; + KotoPlaylistPage *self = user_data; + + KotoPreferredModelType current_model = koto_playlist_get_current_model(self->playlist); + + if (current_model == KOTO_PREFERRED_MODEL_TYPE_DEFAULT) { // Set to newest currently + koto_playlist_page_set_playlist_model(self, KOTO_PREFERRED_MODEL_TYPE_OLDEST_FIRST); // Sort reversed (oldest) + koto_button_show_image(self->track_num_button, TRUE); // Use inverted value (pan-up-symbolic) + } else { + koto_playlist_page_set_playlist_model(self, KOTO_PREFERRED_MODEL_TYPE_DEFAULT); // Sort newest + koto_button_show_image(self->track_num_button, FALSE); // Use pan down default + } +} + +void koto_playlist_page_handle_tracks_selected(GtkSelectionModel *model, guint position, guint n_items, gpointer user_data) { + (void) position; + KotoPlaylistPage *self = user_data; + + if (n_items == 0) { // No items selected + koto_action_bar_toggle_reveal(action_bar, FALSE); // Hide the action bar + return; + } + + GtkBitset *selected_items_bitset = gtk_selection_model_get_selection(model); // Get the selected items as a GtkBitSet + GtkBitsetIter iter; + GList *selected_tracks = NULL; + GList *selected_tracks_pos = NULL; + + guint first_track_pos; + if (!gtk_bitset_iter_init_first(&iter, selected_items_bitset, &first_track_pos)) { // Failed to get the first item + return; + } + + selected_tracks_pos = g_list_append(selected_tracks_pos, GUINT_TO_POINTER(first_track_pos)); + + gboolean have_more_items = TRUE; + while (have_more_items) { // While we are able to get selected items + guint track_pos; + have_more_items = gtk_bitset_iter_next(&iter, &track_pos); + if (have_more_items) { // Got the next track + selected_tracks_pos = g_list_append(selected_tracks_pos, GUINT_TO_POINTER(track_pos)); + } + } + + GList *cur_pos_list; + for (cur_pos_list = selected_tracks_pos; cur_pos_list != NULL; cur_pos_list = cur_pos_list->next) { // Iterate over every position that we accumulated + KotoIndexedTrack *selected_track = g_list_model_get_item(self->model, GPOINTER_TO_UINT(cur_pos_list->data)); // Get the KotoIndexedTrack in the GListModel for this current position + selected_tracks = g_list_append(selected_tracks, selected_track); // Add to selected tracks + } + + koto_action_bar_set_tracks_in_playlist_selection(action_bar, self->uuid, selected_tracks); // Set the tracks for the playlist selection + koto_action_bar_toggle_reveal(action_bar, TRUE); // Show the items +} + +void koto_playlist_page_set_playlist_uuid(KotoPlaylistPage *self, gchar *playlist_uuid) { + g_return_if_fail(KOTO_IS_PLAYLIST_PAGE(self)); + g_return_if_fail(g_strcmp0(playlist_uuid, "") != 0); // Return if empty string + g_return_if_fail(koto_cartographer_has_playlist_by_uuid(koto_maps, playlist_uuid)); // Don't have a playlist with this UUID + + self->uuid = g_strdup(playlist_uuid); // Duplicate the playlist UUID + KotoPlaylist *playlist = koto_cartographer_get_playlist_by_uuid(koto_maps, self->uuid); + self->playlist = playlist; + koto_playlist_page_set_playlist_model(self, KOTO_PREFERRED_MODEL_TYPE_DEFAULT); // TODO: Enable this to be changed + koto_playlist_page_update_header(self); // Update our header +} + +void koto_playlist_page_set_playlist_model(KotoPlaylistPage *self, KotoPreferredModelType model) { + g_return_if_fail(KOTO_IS_PLAYLIST_PAGE(self)); + + koto_playlist_apply_model(self->playlist, model); // Apply our new model + self->model = G_LIST_MODEL(koto_playlist_get_store(self->playlist)); // Get the latest generated model / store and cast it as a GListModel + + if (model != KOTO_PREFERRED_MODEL_TYPE_SORT_BY_ALBUM) { // Not sorting by album currently + gtk_widget_remove_css_class(GTK_WIDGET(self->track_album_button), "active"); + } + + if (model != KOTO_PREFERRED_MODEL_TYPE_SORT_BY_ARTIST) { // Not sorting by artist currently + gtk_widget_remove_css_class(GTK_WIDGET(self->track_artist_button), "active"); + } + + if (model != KOTO_PREFERRED_MODEL_TYPE_SORT_BY_TRACK_NAME) { // Not sorting by track name + gtk_widget_remove_css_class(GTK_WIDGET(self->track_title_button), "active"); + } + + self->selection_model = GTK_SELECTION_MODEL(gtk_multi_selection_new(self->model)); + g_signal_connect(self->selection_model, "selection-changed", G_CALLBACK(koto_playlist_page_handle_tracks_selected), self); // Bind to our selection changed + + gtk_list_view_set_model(GTK_LIST_VIEW(self->track_list_view), self->selection_model); // Set our multi selection model to our provided model +} + +void koto_playlist_page_setup_track_item(GtkListItemFactory *factory, GtkListItem *item, gpointer user_data) { + (void) factory; + KotoPlaylistPage *self = user_data; + + GtkWidget *item_content = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); // Have a horizontal box for our content + gtk_widget_add_css_class(item_content, "track-list-columned-item"); + + GtkWidget *track_number = gtk_label_new(NULL); // Our track number + gtk_label_set_xalign(GTK_LABEL(track_number), 0); + gtk_widget_add_css_class(track_number, "track-column-number"); + gtk_widget_set_halign(track_number, GTK_ALIGN_END); + gtk_widget_set_hexpand(track_number, FALSE); + gtk_widget_set_size_request(track_number, 150, -1); + gtk_box_append(GTK_BOX(item_content), track_number); + gtk_size_group_add_widget(self->track_pos_size_group, track_number); + + GtkWidget *track_name = gtk_label_new(NULL); // Our track name + gtk_label_set_xalign(GTK_LABEL(track_name), 0); + gtk_widget_add_css_class(track_name, "track-column-name"); + gtk_widget_set_halign(track_name, GTK_ALIGN_START); + gtk_widget_set_hexpand(track_name, FALSE); + gtk_widget_set_size_request(track_name, 350, -1); + gtk_box_append(GTK_BOX(item_content), track_name); + gtk_size_group_add_widget(self->track_name_size_group, track_name); + + GtkWidget *track_album = gtk_label_new(NULL); // Our track album + gtk_label_set_xalign(GTK_LABEL(track_album), 0); + gtk_widget_add_css_class(track_album, "track-column-album"); + gtk_widget_set_halign(track_album, GTK_ALIGN_START); + gtk_widget_set_hexpand(track_album, FALSE); + gtk_widget_set_margin_start(track_album, 50); + gtk_widget_set_size_request(track_album, 350, -1); + gtk_box_append(GTK_BOX(item_content), track_album); + gtk_size_group_add_widget(self->track_album_size_group, track_album); + + GtkWidget *track_artist = gtk_label_new(NULL); // Our track artist + gtk_label_set_xalign(GTK_LABEL(track_artist), 0); + gtk_widget_add_css_class(track_artist, "track-column-artist"); + gtk_widget_set_halign(track_artist, GTK_ALIGN_START); + gtk_widget_set_hexpand(track_artist, TRUE); + gtk_widget_set_margin_start(track_artist, 50); + gtk_widget_set_size_request(track_artist, 350, -1); + gtk_box_append(GTK_BOX(item_content), track_artist); + gtk_size_group_add_widget(self->track_artist_size_group, track_artist); + + gtk_list_item_set_child(item, item_content); +} + +void koto_playlist_page_update_header(KotoPlaylistPage *self) { + g_return_if_fail(KOTO_IS_PLAYLIST_PAGE(self)); + g_return_if_fail(KOTO_IS_PLAYLIST(self->playlist)); // Not a valid playlist + + gboolean ephemeral = TRUE; + g_object_get( + self->playlist, + "ephemeral", + &ephemeral, + NULL + ); + + gtk_label_set_text(GTK_LABEL(self->type_label), ephemeral ? "Generated playlist" : "Curated playlist"); + + gtk_label_set_text(GTK_LABEL(self->name_label), koto_playlist_get_name(self->playlist)); // Set the name label to our playlist name + guint track_count = koto_playlist_get_length(self->playlist); // Get the number of tracks + gtk_label_set_text(GTK_LABEL(self->tracks_count_label), g_strdup_printf(track_count != 1 ? "%u tracks" : "%u track", track_count)); // Set the text to "N tracks" where N is the number + + gchar *artwork = koto_playlist_get_artwork(self->playlist); + + if ((artwork != NULL) && g_strcmp0(artwork, "") != 0) { // Have artwork + koto_cover_art_button_set_art_path(self->playlist_image, artwork); // Update our artwork + } + + KotoPreferredModelType current_model = koto_playlist_get_current_model(self->playlist); // Get the current model + if (current_model == KOTO_PREFERRED_MODEL_TYPE_OLDEST_FIRST) { + koto_button_show_image(self->track_num_button, TRUE); // Immediately use pan-up-symbolic + } +} + +KotoPlaylistPage* koto_playlist_page_new(gchar *playlist_uuid) { + return g_object_new(KOTO_TYPE_PLAYLIST_PAGE, + "uuid", + playlist_uuid, + NULL + ); +} diff --git a/src/pages/playlist/list.h b/src/pages/playlist/list.h new file mode 100644 index 0000000..5bf634b --- /dev/null +++ b/src/pages/playlist/list.h @@ -0,0 +1,55 @@ +/* list.h + * + * Copyright 2021 Joshua Strobl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include "../../components/koto-action-bar.h" +#include "../../playlist/playlist.h" +#include "../../koto-utils.h" + +G_BEGIN_DECLS + +#define KOTO_TYPE_PLAYLIST_PAGE koto_playlist_page_get_type() +G_DECLARE_FINAL_TYPE (KotoPlaylistPage, koto_playlist_page, KOTO, PLAYLIST_PAGE, GObject); +#define KOTO_IS_PLAYLIST_PAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_PLAYLIST_PAGE)) + +KotoPlaylistPage* koto_playlist_page_new(gchar *playlist_uuid); +void koto_playlist_page_add_track(KotoPlaylistPage* self, const gchar *track_uuid); +void koto_playlist_page_create_tracks_header(KotoPlaylistPage *self); +void koto_playlist_page_bind_track_item(GtkListItemFactory *factory, GtkListItem *item, KotoPlaylistPage *self); +void koto_playlist_page_remove_track(KotoPlaylistPage *self, const gchar *track_uuid); +GtkWidget* koto_playlist_page_get_main(KotoPlaylistPage *self); +void koto_playlist_page_handle_action_bar_closed(KotoActionBar *bar, gpointer data); +void koto_playlist_page_handle_cover_art_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data); +void koto_playlist_page_handle_edit_button_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data); +void koto_playlist_page_handle_track_album_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data); +void koto_playlist_page_handle_track_artist_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data); +void koto_playlist_page_handle_track_name_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data); +void koto_playlist_page_handle_track_num_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data); +void koto_playlist_page_handle_tracks_selected(GtkSelectionModel *model, guint position, guint n_items, gpointer user_data); +void koto_playlist_page_hide_popover(KotoPlaylistPage *self); +void koto_playlist_page_setup_track_item(GtkListItemFactory *factory, GtkListItem *item, gpointer user_data); +void koto_playlist_page_set_playlist_uuid(KotoPlaylistPage *self, gchar *playlist_uuid); +void koto_playlist_page_set_playlist_model(KotoPlaylistPage *self, KotoPreferredModelType model); +void koto_playlist_page_show_popover(KotoPlaylistPage *self); +void koto_playlist_page_update_header(KotoPlaylistPage *self); + +void koto_playlist_page_handle_listitem_activate(GtkListView *list, guint position, gpointer user_data); + +G_END_DECLS diff --git a/src/playback/engine.c b/src/playback/engine.c index 7b5e403..f5f600a 100644 --- a/src/playback/engine.c +++ b/src/playback/engine.c @@ -36,7 +36,6 @@ enum { N_SIGNALS }; -static glong NS = 1000000000; static guint playback_engine_signals[N_SIGNALS] = { 0 }; extern KotoCartographer *koto_maps; @@ -51,12 +50,16 @@ struct _KotoPlaybackEngine { GstElement *suppress_video; GstBus *monitor; + GstQuery *duration_query; + GstQuery *position_query; + KotoIndexedTrack *current_track; gboolean is_muted; gboolean is_repeat_enabled; gboolean is_playing; + gboolean is_playing_specific_track; gboolean tick_duration_timer_running; gboolean tick_track_timer_running; @@ -197,12 +200,17 @@ static void koto_playback_engine_init(KotoPlaybackEngine *self) { gst_bin_add(GST_BIN(self->player), self->playbin); self->monitor = gst_bus_new(); // Get the bus for the playbin + self->duration_query = gst_query_new_duration(GST_FORMAT_TIME); // Create our re-usable query for querying the duration + self->position_query = gst_query_new_position(GST_FORMAT_TIME); // Create our re-usable query for querying the position + if (GST_IS_BUS(self->monitor)) { gst_bus_add_watch(self->monitor, koto_playback_engine_monitor_changed, self); gst_element_set_bus(self->player, self->monitor); // Set our bus to monitor changes } self->is_muted = FALSE; + self->is_playing = FALSE; + self->is_playing_specific_track = FALSE; self->is_repeat_enabled = FALSE; self->tick_duration_timer_running = FALSE; self->tick_track_timer_running = FALSE; @@ -219,6 +227,10 @@ void koto_playback_engine_backwards(KotoPlaybackEngine *self) { return; } + if (self->is_repeat_enabled || self->is_playing_specific_track) { // Repeat enabled or playing a specific track + return; + } + koto_playback_engine_set_track_by_uuid(self, koto_playlist_go_to_previous(playlist)); } @@ -245,7 +257,7 @@ void koto_playback_engine_forwards(KotoPlaybackEngine *self) { if (self->is_repeat_enabled) { // Is repeat enabled koto_playback_engine_set_position(self, 0); // Set position back to 0 to repeat the track - } else { // Repeat not enabled + } else if (!self->is_repeat_enabled && !self->is_playing_specific_track) { // Repeat not enabled and we are not playing a specific track koto_playback_engine_set_track_by_uuid(self, koto_playlist_go_to_next(playlist)); } } @@ -256,27 +268,32 @@ KotoIndexedTrack* koto_playback_engine_get_current_track(KotoPlaybackEngine *sel gint64 koto_playback_engine_get_duration(KotoPlaybackEngine *self) { gint64 duration = 0; - if (gst_element_query_duration(self->player, GST_FORMAT_TIME, &duration)) { - duration = duration / NS; // Divide by NS to get seconds + if (gst_element_query(self->player, self->duration_query)) { // Able to query our duration + gst_query_parse_duration(self->duration_query, NULL, &duration); // Get the duration + duration = duration / GST_SECOND; // Divide by NS to get seconds } return duration; } -gint64 koto_playback_engine_get_progress(KotoPlaybackEngine *self) { - gint64 progress = 0; - if (gst_element_query_position(self->player, GST_FORMAT_TIME, &progress)) { - progress = progress / NS; // Divide by NS to get seconds +gdouble koto_playback_engine_get_progress(KotoPlaybackEngine *self) { + gdouble progress = 0.0; + gint64 gstprog = 0; + if (gst_element_query(self->playbin, self->position_query)) { // Able to get our position + gst_query_parse_position(self->position_query, NULL, &gstprog); // Get the progress + + if (gstprog < 1) { // Less than a second + return 0.0; + } + + progress = gstprog / GST_SECOND; // Divide by GST_SECOND then again by 100. } return progress; } GstState koto_playback_engine_get_state(KotoPlaybackEngine *self) { - GstState current_state; - gst_element_get_state(self->player, ¤t_state, NULL, GST_SECOND); // Get the current state, allowing up to a second to get it - - return current_state; + return GST_STATE(self->player); } gboolean koto_playback_engine_get_track_repeat(KotoPlaybackEngine *self) { @@ -361,7 +378,7 @@ void koto_playback_engine_pause(KotoPlaybackEngine *self) { } void koto_playback_engine_set_position(KotoPlaybackEngine *self, int position) { - gst_element_seek_simple(self->playbin, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, position * NS); + gst_element_seek_simple(self->playbin, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, position * GST_SECOND); } void koto_playback_engine_set_track_repeat(KotoPlaybackEngine *self, gboolean enable_repeat) { diff --git a/src/playback/engine.h b/src/playback/engine.h index fcc4ad5..698ab82 100644 --- a/src/playback/engine.h +++ b/src/playback/engine.h @@ -48,7 +48,7 @@ void koto_playback_engine_forwards(KotoPlaybackEngine *self); KotoIndexedTrack* koto_playback_engine_get_current_track(KotoPlaybackEngine *self); gint64 koto_playback_engine_get_duration(KotoPlaybackEngine *self); GstState koto_playback_engine_get_state(KotoPlaybackEngine *self); -gint64 koto_playback_engine_get_progress(KotoPlaybackEngine *self); +gdouble koto_playback_engine_get_progress(KotoPlaybackEngine *self); gboolean koto_playback_engine_get_track_repeat(KotoPlaybackEngine *self); gboolean koto_playback_engine_get_track_shuffle(KotoPlaybackEngine *self); void koto_playback_engine_mute(KotoPlaybackEngine *self); diff --git a/src/playlist/add-remove-track-popover.c b/src/playlist/add-remove-track-popover.c new file mode 100644 index 0000000..8f6338a --- /dev/null +++ b/src/playlist/add-remove-track-popover.c @@ -0,0 +1,249 @@ +/* add-remove-track-popover.c + * + * Copyright 2021 Joshua Strobl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "../db/cartographer.h" +#include "../playback/engine.h" +#include "../playlist/playlist.h" +#include "add-remove-track-popover.h" + +extern KotoCartographer *koto_maps; + +struct _KotoAddRemoveTrackPopover { + GtkPopover parent_instance; + GtkWidget *list_box; + GHashTable *checkbox_to_playlist_uuid; + GHashTable *playlist_uuid_to_checkbox; + GList *tracks; + + GHashTable *checkbox_to_signal_ids; +}; + +G_DEFINE_TYPE(KotoAddRemoveTrackPopover, koto_add_remove_track_popover, GTK_TYPE_POPOVER); + +KotoAddRemoveTrackPopover *koto_add_remove_track_popup = NULL; + +static void koto_add_remove_track_popover_class_init(KotoAddRemoveTrackPopoverClass *c) { + (void) c; +} + +static void koto_add_remove_track_popover_init(KotoAddRemoveTrackPopover *self) { + self->list_box = gtk_list_box_new(); // Create our new GtkListBox + gtk_list_box_set_selection_mode(GTK_LIST_BOX(self->list_box), GTK_SELECTION_NONE); + + self->checkbox_to_playlist_uuid = g_hash_table_new(g_str_hash, g_str_equal); + self->playlist_uuid_to_checkbox = g_hash_table_new(g_str_hash, g_str_equal); + self->checkbox_to_signal_ids = g_hash_table_new(g_str_hash, g_str_equal), + self->tracks = NULL; // Initialize our tracks + + gtk_popover_set_autohide(GTK_POPOVER(self), TRUE); // Ensure autohide is enabled + gtk_popover_set_child(GTK_POPOVER(self), self->list_box); + + g_signal_connect(koto_maps, "playlist-added", G_CALLBACK(koto_add_remove_track_popover_handle_playlist_added), self); + g_signal_connect(koto_maps, "playlist-removed", G_CALLBACK(koto_add_remove_track_popover_handle_playlist_removed), self); +} + +void koto_add_remove_track_popover_add_playlist(KotoAddRemoveTrackPopover *self, KotoPlaylist *playlist) { + if (!KOTO_JS_ADD_REMOVE_TRACK_POPOVER(self)) { + return; + } + + if (!KOTO_IS_PLAYLIST(playlist)) { // Not a playlist + return; + } + + gchar *playlist_uuid = koto_playlist_get_uuid(playlist); // Get the UUID of the playlist + + if (GTK_IS_CHECK_BUTTON(g_hash_table_lookup(self->playlist_uuid_to_checkbox, playlist_uuid))) { // Already have a check button for this + g_free(playlist_uuid); + return; + } + + GtkWidget *playlist_button = gtk_check_button_new_with_label(koto_playlist_get_name(playlist)); // Create our GtkCheckButton + g_hash_table_insert(self->checkbox_to_playlist_uuid, playlist_button, playlist_uuid); + g_hash_table_insert(self->playlist_uuid_to_checkbox, playlist_uuid, playlist_button); + + gulong playlist_sig_id = g_signal_connect(playlist_button, "toggled", G_CALLBACK(koto_add_remove_track_popover_handle_checkbutton_toggle), self); + g_hash_table_insert(self->checkbox_to_signal_ids, playlist_button, GUINT_TO_POINTER(playlist_sig_id)); // Add our GSignal handler ID + + gtk_list_box_append(GTK_LIST_BOX(self->list_box), playlist_button); // Add the playlist to the list box +} + +void koto_add_remove_track_popover_clear_tracks(KotoAddRemoveTrackPopover *self) { + if (!KOTO_JS_ADD_REMOVE_TRACK_POPOVER(self)) { + return; + } + + if (self->tracks != NULL) { // Is a list + g_list_free(self->tracks); + self->tracks = NULL; + } +} + +void koto_add_remove_track_popover_remove_playlist(KotoAddRemoveTrackPopover *self, gchar *playlist_uuid) { + if (!KOTO_JS_ADD_REMOVE_TRACK_POPOVER(self)) { + return; + } + + if (!g_hash_table_contains(self->playlist_uuid_to_checkbox, playlist_uuid)) { // Playlist not added + return; + } + + GtkCheckButton *btn = GTK_CHECK_BUTTON(g_hash_table_lookup(self->playlist_uuid_to_checkbox, playlist_uuid)); // Get the check button + + if (GTK_IS_CHECK_BUTTON(btn)) { // Is a check button + g_hash_table_remove(self->checkbox_to_playlist_uuid, btn); // Remove uuid based on btn + gtk_list_box_remove(GTK_LIST_BOX(self->list_box), GTK_WIDGET(btn)); // Remove the button from the list box + } + + g_hash_table_remove(self->playlist_uuid_to_checkbox, playlist_uuid); +} + +void koto_add_remove_track_popover_handle_checkbutton_toggle(GtkCheckButton *btn, gpointer user_data) { + KotoAddRemoveTrackPopover *self = user_data; + + if (!KOTO_JS_ADD_REMOVE_TRACK_POPOVER(self)) { + return; + } + + gboolean should_add = gtk_check_button_get_active(btn); // Get the now active state + gchar *playlist_uuid = g_hash_table_lookup(self->checkbox_to_playlist_uuid, btn); // Get the playlist UUID for this button + + KotoPlaylist *playlist = koto_cartographer_get_playlist_by_uuid(koto_maps, playlist_uuid); // Get the playlist + + if (!KOTO_IS_PLAYLIST(playlist)) { // Failed to get the playlist + return; + } + + GList *pos; + for (pos = self->tracks; pos != NULL; pos = pos->next) { // Iterate over our KotoIndexedTracks + KotoIndexedTrack *track = pos->data; + + if (!KOTO_INDEXED_TRACK(track)) { // Not a track + continue; // Skip this + } + + gchar *track_uuid = koto_indexed_track_get_uuid(track); // Get the track + + if (should_add) { // Should be adding + koto_playlist_add_track_by_uuid(playlist, track_uuid, FALSE, TRUE); // Add the track to the playlist + } else { // Should be removing + koto_playlist_remove_track_by_uuid(playlist, track_uuid); // Remove the track from the playlist + } + } + + gtk_popover_popdown(GTK_POPOVER(self)); // Temporary to hopefully prevent a bork +} + +void koto_add_remove_track_popover_handle_playlist_added(KotoCartographer *carto, KotoPlaylist *playlist, gpointer user_data) { + (void) carto; + KotoAddRemoveTrackPopover *self = user_data; + + if (!KOTO_JS_ADD_REMOVE_TRACK_POPOVER(self)) { + return; + } + + koto_add_remove_track_popover_add_playlist(self, playlist); +} + +void koto_add_remove_track_popover_handle_playlist_removed(KotoCartographer *carto, gchar *playlist_uuid, gpointer user_data) { + (void) carto; + KotoAddRemoveTrackPopover *self = user_data; + + if (!KOTO_JS_ADD_REMOVE_TRACK_POPOVER(self)) { + return; + } + + koto_add_remove_track_popover_remove_playlist(self, playlist_uuid); +} + +void koto_add_remove_track_popover_set_pointing_to_widget(KotoAddRemoveTrackPopover *self, GtkWidget *widget, GtkPositionType pos) { + if (!KOTO_JS_ADD_REMOVE_TRACK_POPOVER(self)) { + return; + } + + if (!GTK_IS_WIDGET(widget)) { // Not a widget + return; + } + + GtkWidget* existing_parent = gtk_widget_get_parent(GTK_WIDGET(self)); + + if (existing_parent != NULL) { + g_object_ref(GTK_WIDGET(self)); // Increment widget ref since unparent will do an unref + gtk_widget_unparent(GTK_WIDGET(self)); // Unparent the popup + } + + gtk_widget_insert_after(GTK_WIDGET(self), widget, gtk_widget_get_last_child(widget)); + gtk_popover_set_position(GTK_POPOVER(self), pos); +} + +void koto_add_remove_track_popover_set_tracks(KotoAddRemoveTrackPopover *self, GList *tracks) { + if (!KOTO_JS_ADD_REMOVE_TRACK_POPOVER(self)) { + return; + } + + gint tracks_len = g_list_length(tracks); + + if (tracks_len == 0) { // No tracks + return; + } + + self->tracks = g_list_copy(tracks); + GHashTable *playlists = koto_cartographer_get_playlists(koto_maps); // Get our playlists + GHashTableIter playlists_iter; + gpointer uuid, playlist_ptr; + + g_hash_table_iter_init(&playlists_iter, playlists); // Init our HashTable iterator + + while (g_hash_table_iter_next(&playlists_iter, &uuid, &playlist_ptr)) { // While we are iterating through our playlists + KotoPlaylist *playlist = playlist_ptr; + gboolean should_be_checked = FALSE; + + if (tracks_len > 1) { // More than one track + GList *pos; + for (pos = self->tracks; pos != NULL; pos = pos->next) { // Iterate over our tracks + should_be_checked = (koto_playlist_get_position_of_track(playlist, pos->data) != -1); + + if (!should_be_checked) { // Should not be checked + break; + } + } + } else { + KotoIndexedTrack *track = g_list_nth_data(self->tracks, 0); // Get the first track + + if (KOTO_IS_INDEXED_TRACK(track)) { + gint pos = koto_playlist_get_position_of_track(playlist, track); + should_be_checked = (pos != -1); + } + } + + GtkCheckButton *playlist_check = g_hash_table_lookup(self->playlist_uuid_to_checkbox, uuid); // Get the GtkCheckButton for this playlist + + if (GTK_IS_CHECK_BUTTON(playlist_check)) { // Is a checkbox + gpointer sig_id_ptr = g_hash_table_lookup(self->checkbox_to_signal_ids, playlist_check); + gulong check_button_sig_id = GPOINTER_TO_UINT(sig_id_ptr); + g_signal_handler_block(playlist_check, check_button_sig_id); // Temporary ignore toggled signal, since set_active calls toggled + gtk_check_button_set_active(playlist_check, should_be_checked); // Set active to our should_be_checked bool + g_signal_handler_unblock(playlist_check, check_button_sig_id); // Unblock the signal + } + } +} + +KotoAddRemoveTrackPopover* koto_add_remove_track_popover_new() { + return g_object_new(KOTO_TYPE_ADD_REMOVE_TRACK_POPOVER, NULL); +} \ No newline at end of file diff --git a/src/playlist/add-remove-track-popover.h b/src/playlist/add-remove-track-popover.h new file mode 100644 index 0000000..c301e01 --- /dev/null +++ b/src/playlist/add-remove-track-popover.h @@ -0,0 +1,48 @@ +/* add-remove-track-popover.h + * + * Copyright 2021 Joshua Strobl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include "../db/cartographer.h" +#include "playlist.h" + +G_BEGIN_DECLS + +/** + * Type Definition +**/ + +#define KOTO_TYPE_ADD_REMOVE_TRACK_POPOVER koto_add_remove_track_popover_get_type() +G_DECLARE_FINAL_TYPE(KotoAddRemoveTrackPopover, koto_add_remove_track_popover, KOTO, ADD_REMOVE_TRACK_POPOVER, GtkPopover); +#define KOTO_JS_ADD_REMOVE_TRACK_POPOVER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_ADD_REMOVE_TRACK_POPOVER)) + +/** + * Functions +**/ + +KotoAddRemoveTrackPopover* koto_add_remove_track_popover_new(); +void koto_add_remove_track_popover_add_playlist(KotoAddRemoveTrackPopover *self, KotoPlaylist *playlist); +void koto_add_remove_track_popover_clear_tracks(KotoAddRemoveTrackPopover *self); +void koto_add_remove_track_popover_remove_playlist(KotoAddRemoveTrackPopover *self, gchar *playlist_uuid); +void koto_add_remove_track_popover_handle_checkbutton_toggle(GtkCheckButton *btn, gpointer user_data); +void koto_add_remove_track_popover_handle_playlist_added(KotoCartographer *carto, KotoPlaylist *playlist, gpointer user_data); +void koto_add_remove_track_popover_handle_playlist_removed(KotoCartographer *carto, gchar *playlist_uuid, gpointer user_data); +void koto_add_remove_track_popover_set_pointing_to_widget(KotoAddRemoveTrackPopover *self, GtkWidget *widget, GtkPositionType pos); +void koto_add_remove_track_popover_set_tracks(KotoAddRemoveTrackPopover *self, GList *tracks); + +G_END_DECLS \ No newline at end of file diff --git a/src/playlist/create-dialog.c b/src/playlist/create-dialog.c deleted file mode 100644 index 6907a0a..0000000 --- a/src/playlist/create-dialog.c +++ /dev/null @@ -1,99 +0,0 @@ -/* create-dialog.h - * - * Copyright 2021 Joshua Strobl - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include -#include "../db/cartographer.h" -#include "../db/db.h" -#include "create-dialog.h" - -extern GtkWindow *main_window; - -struct _KotoCreatePlaylistDialog { - GObject parent_instance; - GtkWidget *content; - GtkWidget *playlist_image; - GtkFileChooserNative *playlist_file_chooser; - GtkWidget *name_entry; -}; - -G_DEFINE_TYPE(KotoCreatePlaylistDialog, koto_create_playlist_dialog, G_TYPE_OBJECT); - -static void koto_create_playlist_dialog_class_init(KotoCreatePlaylistDialogClass *c) { - (void) c; -} - -static void koto_create_playlist_dialog_init(KotoCreatePlaylistDialog *self) { - self->content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); - gtk_widget_set_halign(self->content, GTK_ALIGN_CENTER); - gtk_widget_set_valign(self->content, GTK_ALIGN_CENTER); - - GtkIconTheme *default_icon_theme = gtk_icon_theme_get_for_display(gdk_display_get_default()); // Get the icon theme for this display - - if (default_icon_theme != NULL) { - gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self->content)); - GtkIconPaintable* audio_paintable = gtk_icon_theme_lookup_icon(default_icon_theme, "insert-image-symbolic", NULL, 96, scale_factor, GTK_TEXT_DIR_NONE, GTK_ICON_LOOKUP_PRELOAD); - - if (GTK_IS_ICON_PAINTABLE(audio_paintable)) { - self->playlist_image = gtk_image_new_from_paintable(GDK_PAINTABLE(audio_paintable)); - gtk_widget_set_size_request(self->playlist_image, 96, 96); - gtk_box_append(GTK_BOX(self->content), self->playlist_image); // Add our image to our content - - GtkGesture *image_click_controller = gtk_gesture_click_new(); // Create a click gesture for the image clicking - gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(image_click_controller), 1); // Only allow left click - g_signal_connect(GTK_EVENT_CONTROLLER(image_click_controller), "pressed", G_CALLBACK(koto_create_playlist_dialog_handle_image_click), self); - - gtk_widget_add_controller(self->playlist_image, GTK_EVENT_CONTROLLER(image_click_controller)); - } - } - - self->playlist_file_chooser = gtk_file_chooser_native_new( - "Choose playlist image", - main_window, - GTK_FILE_CHOOSER_ACTION_OPEN, - "Choose", - "Cancel" - ); - - GtkFileFilter *image_filter = gtk_file_filter_new(); // Create our file filter - gtk_file_filter_add_pattern(image_filter, "image/*"); // Only allow for images - gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(self->playlist_file_chooser), image_filter); // Only allow picking images - gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(self->playlist_file_chooser), FALSE); - - self->name_entry = gtk_entry_new(); // Create our text entry for the name of the playlist - gtk_entry_set_placeholder_text(GTK_ENTRY(self->name_entry), "Name of playlist"); - gtk_entry_set_input_purpose(GTK_ENTRY(self->name_entry), GTK_INPUT_PURPOSE_NAME); - gtk_entry_set_input_hints(GTK_ENTRY(self->name_entry), GTK_INPUT_HINT_SPELLCHECK & GTK_INPUT_HINT_NO_EMOJI & GTK_INPUT_HINT_PRIVATE); - - gtk_box_append(GTK_BOX(self->content), self->name_entry); -} - -GtkWidget* koto_create_playlist_dialog_get_content(KotoCreatePlaylistDialog *self) { - return self->content; -} - -void koto_create_playlist_dialog_handle_image_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { - (void) gesture; (void) n_press; (void) x; (void) y; - - KotoCreatePlaylistDialog *self = user_data; - - gtk_native_dialog_show(GTK_NATIVE_DIALOG(self->playlist_file_chooser)); // Show our file chooser -} - -KotoCreatePlaylistDialog* koto_create_playlist_dialog_new() { - return g_object_new(KOTO_TYPE_CREATE_PLAYLIST_DIALOG, NULL); -} diff --git a/src/playlist/create-modify-dialog.c b/src/playlist/create-modify-dialog.c new file mode 100644 index 0000000..da20b1e --- /dev/null +++ b/src/playlist/create-modify-dialog.c @@ -0,0 +1,301 @@ +/* create-modify-dialog.c + * + * Copyright 2021 Joshua Strobl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include "../db/cartographer.h" +#include "../playlist/playlist.h" +#include "../koto-utils.h" +#include "../koto-window.h" +#include "create-modify-dialog.h" + +extern KotoCartographer *koto_maps; +extern KotoWindow *main_window; + +enum { + PROP_DIALOG_0, + PROP_PLAYLIST_UUID, + N_PROPS +}; + +static GParamSpec *dialog_props[N_PROPS] = { NULL, }; + + +struct _KotoCreateModifyPlaylistDialog { + GtkBox parent_instance; + GtkWidget *playlist_image; + GtkWidget *name_entry; + + GtkWidget *create_button; + + gchar *playlist_image_path; + gchar *playlist_uuid; +}; + +G_DEFINE_TYPE(KotoCreateModifyPlaylistDialog, koto_create_modify_playlist_dialog, GTK_TYPE_BOX); + +KotoCreateModifyPlaylistDialog *playlist_create_modify_dialog; + +static void koto_create_modify_playlist_dialog_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec); +static void koto_create_modify_playlist_dialog_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec); + +static void koto_create_modify_playlist_dialog_class_init(KotoCreateModifyPlaylistDialogClass *c) { + GObjectClass *gobject_class; + gobject_class = G_OBJECT_CLASS(c); + gobject_class->set_property = koto_create_modify_playlist_dialog_set_property; + gobject_class->get_property = koto_create_modify_playlist_dialog_get_property; + + dialog_props[PROP_PLAYLIST_UUID] = g_param_spec_string( + "playlist-uuid", + "Playlist UUID", + "Playlist UUID", + NULL, + G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE + ); + + g_object_class_install_properties(gobject_class, N_PROPS, dialog_props); +} + +static void koto_create_modify_playlist_dialog_init(KotoCreateModifyPlaylistDialog *self) { + self->playlist_image_path = NULL; + + gtk_widget_set_halign(GTK_WIDGET(self), GTK_ALIGN_CENTER); + gtk_widget_set_valign(GTK_WIDGET(self), GTK_ALIGN_CENTER); + + self->playlist_image = gtk_image_new_from_icon_name("insert-image-symbolic"); + gtk_image_set_pixel_size(GTK_IMAGE(self->playlist_image), 220); + gtk_widget_set_size_request(self->playlist_image, 220, 220); + gtk_box_append(GTK_BOX(self), self->playlist_image); // Add our image + + GtkDropTarget *target = gtk_drop_target_new(G_TYPE_FILE, GDK_ACTION_COPY); + g_signal_connect(GTK_EVENT_CONTROLLER(target), "drop", G_CALLBACK(koto_create_modify_playlist_dialog_handle_drop), self); + gtk_widget_add_controller(self->playlist_image, GTK_EVENT_CONTROLLER(target)); + + GtkGesture *image_click_controller = gtk_gesture_click_new(); // Create a click gesture for the image clicking + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(image_click_controller), 1); // Only allow left click + g_signal_connect(GTK_EVENT_CONTROLLER(image_click_controller), "pressed", G_CALLBACK(koto_create_modify_playlist_dialog_handle_image_click), self); + + gtk_widget_add_controller(self->playlist_image, GTK_EVENT_CONTROLLER(image_click_controller)); + + self->name_entry = gtk_entry_new(); // Create our text entry for the name of the playlist + gtk_entry_set_placeholder_text(GTK_ENTRY(self->name_entry), "Name of playlist"); + gtk_entry_set_input_purpose(GTK_ENTRY(self->name_entry), GTK_INPUT_PURPOSE_NAME); + gtk_entry_set_input_hints(GTK_ENTRY(self->name_entry), GTK_INPUT_HINT_SPELLCHECK & GTK_INPUT_HINT_NO_EMOJI & GTK_INPUT_HINT_PRIVATE); + + gtk_box_append(GTK_BOX(self), self->name_entry); + + self->create_button = gtk_button_new_with_label("Create"); + gtk_widget_add_css_class(self->create_button, "suggested-action"); + g_signal_connect(self->create_button, "clicked", G_CALLBACK(koto_create_modify_playlist_dialog_handle_create_click), self); + gtk_box_append(GTK_BOX(self), self->create_button); // Add the create button +} + +static void koto_create_modify_playlist_dialog_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec){ + KotoCreateModifyPlaylistDialog *self = KOTO_CREATE_MODIFY_PLAYLIST_DIALOG(obj); + + switch (prop_id) { + case PROP_PLAYLIST_UUID: + g_value_set_string(val, (self->playlist_uuid != NULL) ? g_strdup(self->playlist_uuid) : NULL); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_create_modify_playlist_dialog_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec) { + KotoCreateModifyPlaylistDialog *self = KOTO_CREATE_MODIFY_PLAYLIST_DIALOG(obj); + (void) self; (void) val; + + switch (prop_id) { + case PROP_PLAYLIST_UUID: + // TODO: Implement + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +void koto_create_modify_playlist_dialog_handle_chooser_response(GtkNativeDialog *native, int response, gpointer user_data) { + if (response != GTK_RESPONSE_ACCEPT) { // Not accept + g_object_unref(native); + return; + } + + KotoCreateModifyPlaylistDialog *self = user_data; + if (!KOTO_IS_CURRENT_MODIFY_PLAYLIST(self)) { + return; + } + + GFile *file = gtk_file_chooser_get_file(GTK_FILE_CHOOSER(native)); + gchar *file_path = g_file_get_path(file); // Get the absolute path + + if (file_path != NULL) { + self->playlist_image_path = g_strdup(file_path); + gtk_image_set_from_file(GTK_IMAGE(self->playlist_image), self->playlist_image_path); // Set the file path + g_free(file_path); + } + + g_object_unref(file); + g_object_unref(native); +} + +void koto_create_modify_playlist_dialog_handle_create_click(GtkButton *button, gpointer user_data) { + (void) button; + + KotoCreateModifyPlaylistDialog *self = user_data; + + if (!KOTO_IS_CURRENT_MODIFY_PLAYLIST(self)) { + return; + } + + if (gtk_entry_get_text_length(GTK_ENTRY(self->name_entry)) == 0) { // No text + gtk_widget_grab_focus(GTK_WIDGET(self->name_entry)); // Select the name entry + return; + } + + KotoPlaylist *playlist = NULL; + gboolean modify_existing_playlist = ((self->playlist_uuid != NULL) && (g_strcmp0(self->playlist_uuid, "") != 0)); + + if (modify_existing_playlist) { // Modifying an existing playlist + playlist = koto_cartographer_get_playlist_by_uuid(koto_maps, self->playlist_uuid); + } else { // Creating a new playlist + playlist = koto_playlist_new(); // Create a new playlist with a new UUID + } + + if (!KOTO_IS_PLAYLIST(playlist)) { // If this isn't a playlist + return; + } + + koto_playlist_set_name(playlist, gtk_entry_buffer_get_text(gtk_entry_get_buffer(GTK_ENTRY(self->name_entry)))); // Set the name to the text we get from the entry buffer + koto_playlist_set_artwork(playlist, self->playlist_image_path); // Add the playlist path if any + koto_playlist_commit(playlist); // Save the playlist to the database + + if (!modify_existing_playlist) { // Not a new playlist + koto_cartographer_add_playlist(koto_maps, playlist); // Add to cartographer + koto_playlist_mark_as_finalized(playlist); // Ensure our tracks loaded finalized signal is emitted for the new playlist + } + + koto_create_modify_playlist_dialog_reset(self); + koto_window_hide_dialogs(main_window); // Hide the dialogs +} + +gboolean koto_create_modify_playlist_dialog_handle_drop(GtkDropTarget *target, const GValue *val, double x, double y, gpointer user_data) { + (void) target; (void) x; (void) y; + + if (!G_VALUE_HOLDS(val, G_TYPE_FILE)) { // Not a file + return FALSE; + } + + KotoCreateModifyPlaylistDialog *self = user_data; + + if (!KOTO_IS_CURRENT_MODIFY_PLAYLIST(self)) { // No dialog + return FALSE; + } + + GFile *dropped_file = g_value_get_object(val); // Get the GValue + gchar *file_path = g_file_get_path(dropped_file); // Get the absolute path + g_object_unref(dropped_file); // Unref the file + + if (file_path == NULL) { + return FALSE; // Failed to get the path so immediately return false + } + + magic_t magic_cookie = magic_open(MAGIC_MIME); + + if (magic_cookie == NULL) { + return FALSE; + } + + if (magic_load(magic_cookie, NULL) != 0) { + goto cookie_closure; + } + + const char *mime_type = magic_file(magic_cookie, file_path); + + if ((mime_type != NULL) && g_str_has_prefix(mime_type, "image/")) { // Is an image + self->playlist_image_path = g_strdup(file_path); + gtk_image_set_from_file(GTK_IMAGE(self->playlist_image), self->playlist_image_path); // Set the file path + g_free(file_path); + return TRUE; + } + +cookie_closure: + magic_close(magic_cookie); + return FALSE; +} + +void koto_create_modify_playlist_dialog_handle_image_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) { + (void) gesture; (void) n_press; (void) x; (void) y; + + KotoCreateModifyPlaylistDialog *self = user_data; + + GtkFileChooserNative* chooser = koto_utils_create_image_file_chooser("Choose playlist image"); + g_signal_connect(chooser, "response", G_CALLBACK(koto_create_modify_playlist_dialog_handle_chooser_response), self); + gtk_native_dialog_show(GTK_NATIVE_DIALOG(chooser)); // Show our file chooser +} + +void koto_create_modify_playlist_dialog_reset(KotoCreateModifyPlaylistDialog *self) { + if (!KOTO_IS_CURRENT_MODIFY_PLAYLIST(self)) { + return; + } + + gtk_entry_buffer_set_text(gtk_entry_get_buffer(GTK_ENTRY(self->name_entry)), "", -1); // Reset existing buffer to empty string + gtk_entry_set_placeholder_text(GTK_ENTRY(self->name_entry), "Name of playlist"); // Reset placeholder + gtk_image_set_from_icon_name(GTK_IMAGE(self->playlist_image), "insert-image-symbolic"); // Reset the image + gtk_button_set_label(GTK_BUTTON(self->create_button), "Create"); +} + +void koto_create_modify_playlist_dialog_set_playlist_uuid(KotoCreateModifyPlaylistDialog *self, gchar *playlist_uuid) { + if ((playlist_uuid == NULL) || g_strcmp0(playlist_uuid, "") == 0) { // Is an empty string or not a string at all + return; + } + + KotoPlaylist *playlist = koto_cartographer_get_playlist_by_uuid(koto_maps, playlist_uuid); + + if (!KOTO_IS_PLAYLIST(playlist)) { + return; + } + + self->playlist_uuid = g_strdup(playlist_uuid); + gtk_entry_buffer_set_text(gtk_entry_get_buffer(GTK_ENTRY(self->name_entry)), koto_playlist_get_name(playlist), -1); // Update the input buffer + gtk_entry_set_placeholder_text(GTK_ENTRY(self->name_entry), ""); // Clear placeholder + + gchar *art = koto_playlist_get_artwork(playlist); + + if ((art == NULL) || (g_strcmp0(art, "") == 0)) { // Is an empty string or not set + gtk_image_set_from_icon_name(GTK_IMAGE(self->playlist_image), "insert-image-symbolic"); // Reset the image + } else { + gtk_image_set_from_file(GTK_IMAGE(self->playlist_image), art); + g_free(art); + } + + gtk_button_set_label(GTK_BUTTON(self->create_button), "Save"); +} + +KotoCreateModifyPlaylistDialog* koto_create_modify_playlist_dialog_new(char *playlist_uuid) { + (void) playlist_uuid; + + return g_object_new(KOTO_TYPE_CREATE_MODIFY_PLAYLIST_DIALOG, + "orientation", + GTK_ORIENTATION_VERTICAL, + "spacing", + 40, + NULL); +} \ No newline at end of file diff --git a/src/playlist/create-modify-dialog.h b/src/playlist/create-modify-dialog.h new file mode 100644 index 0000000..3d9ad35 --- /dev/null +++ b/src/playlist/create-modify-dialog.h @@ -0,0 +1,44 @@ +/* create-modify-dialog.h + * + * Copyright 2021 Joshua Strobl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include + +G_BEGIN_DECLS + +/** + * Type Definition +**/ + +#define KOTO_TYPE_CREATE_MODIFY_PLAYLIST_DIALOG koto_create_modify_playlist_dialog_get_type() +G_DECLARE_FINAL_TYPE(KotoCreateModifyPlaylistDialog, koto_create_modify_playlist_dialog, KOTO, CREATE_MODIFY_PLAYLIST_DIALOG, GtkBox); +#define KOTO_IS_CURRENT_MODIFY_PLAYLIST(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_CREATE_MODIFY_PLAYLIST_DIALOG)) + +/** + * Functions +**/ + +KotoCreateModifyPlaylistDialog* koto_create_modify_playlist_dialog_new(); +void koto_create_modify_playlist_dialog_handle_chooser_response(GtkNativeDialog *native, int response, gpointer user_data); +void koto_create_modify_playlist_dialog_handle_create_click(GtkButton *button, gpointer user_data); +gboolean koto_create_modify_playlist_dialog_handle_drop(GtkDropTarget *target, const GValue *val, double x, double y, gpointer user_data); +void koto_create_modify_playlist_dialog_handle_image_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data); +void koto_create_modify_playlist_dialog_reset(KotoCreateModifyPlaylistDialog *self); +void koto_create_modify_playlist_dialog_set_playlist_uuid(KotoCreateModifyPlaylistDialog *self, gchar *playlist_uuid); + +G_END_DECLS diff --git a/src/playlist/current.c b/src/playlist/current.c index b0309c2..fb3b333 100644 --- a/src/playlist/current.c +++ b/src/playlist/current.c @@ -112,6 +112,8 @@ void koto_current_playlist_set_playlist(KotoCurrentPlaylist *self, KotoPlaylist } self->current_playlist = playlist; + // TODO: Saved state + koto_playlist_set_position(self->current_playlist, -1); // Reset our position, use -1 since "next" song is then 0 g_object_ref(playlist); // Increment the reference g_object_notify_by_pspec(G_OBJECT(self), props[PROP_CURRENT_PLAYLIST]); } diff --git a/src/playlist/playlist.c b/src/playlist/playlist.c index 164238e..9c2b360 100644 --- a/src/playlist/playlist.c +++ b/src/playlist/playlist.c @@ -16,29 +16,16 @@ */ #include +#include #include #include #include "../db/cartographer.h" +#include "../koto-utils.h" #include "playlist.h" extern KotoCartographer *koto_maps; extern sqlite3 *koto_db; -struct _KotoPlaylist { - GObject parent_instance; - gchar *uuid; - gchar *name; - gchar *art_path; - gint current_position; - gboolean ephemeral; - gboolean is_shuffle_enabled; - - GQueue *tracks; - GQueue *played_tracks; -}; - -G_DEFINE_TYPE(KotoPlaylist, koto_playlist, G_TYPE_OBJECT); - enum { PROP_0, PROP_UUID, @@ -49,7 +36,47 @@ enum { N_PROPERTIES, }; +enum { + SIGNAL_TRACK_ADDED, + SIGNAL_TRACK_LOAD_FINALIZED, + SIGNAL_TRACK_REMOVED, + N_SIGNALS +}; + +struct _KotoPlaylist { + GObject parent_instance; + gchar *uuid; + gchar *name; + gchar *art_path; + gint current_position; + gchar *current_uuid; + + KotoPreferredModelType model; + + gboolean ephemeral; + gboolean is_shuffle_enabled; + gboolean finalized; + + GListStore *store; + GQueue *sorted_tracks; + + GQueue *tracks; // This is effectively our vanilla value that should never change + GQueue *played_tracks; +}; + +struct _KotoPlaylistClass { + GObjectClass parent_class; + + void (* track_added) (KotoPlaylist *playlist, gchar *track_uuid); + void (* track_load_finalized) (KotoPlaylist *playlist); + void (* track_removed) (KotoPlaylist *playlist, gchar *track_uuid); +}; + +G_DEFINE_TYPE(KotoPlaylist, koto_playlist, G_TYPE_OBJECT); + static GParamSpec *props[N_PROPERTIES] = { NULL }; +static guint playlist_signals[N_SIGNALS] = { 0 }; + static void koto_playlist_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec);; static void koto_playlist_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec); @@ -100,6 +127,44 @@ static void koto_playlist_class_init(KotoPlaylistClass *c) { ); g_object_class_install_properties(gobject_class, N_PROPERTIES, props); + + playlist_signals[SIGNAL_TRACK_ADDED] = g_signal_new( + "track-added", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoPlaylistClass, track_added), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_CHAR + ); + + playlist_signals[SIGNAL_TRACK_LOAD_FINALIZED] = g_signal_new( + "track-load-finalized", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoPlaylistClass, track_load_finalized), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0 + ); + + playlist_signals[SIGNAL_TRACK_REMOVED] = g_signal_new( + "track-removed", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoPlaylistClass, track_removed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_CHAR + ); } static void koto_playlist_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec) { @@ -154,9 +219,16 @@ static void koto_playlist_set_property(GObject *obj, guint prop_id, const GValue static void koto_playlist_init(KotoPlaylist *self) { self->current_position = -1; // Default to -1 so first time incrementing puts it at 0 + self->current_uuid = NULL; + self->model = KOTO_PREFERRED_MODEL_TYPE_DEFAULT; // Default to default model self->is_shuffle_enabled = FALSE; - self->played_tracks = g_queue_new(); // Set as an empty GQueue + self->ephemeral = FALSE; + self->finalized = FALSE; + self->tracks = g_queue_new(); // Set as an empty GQueue + self->played_tracks = g_queue_new(); // Set as an empty GQueue + self->sorted_tracks = g_queue_new(); // Set as an empty GQueue + self->store = g_list_store_new(KOTO_TYPE_INDEXED_TRACK); } void koto_playlist_add_to_played_tracks(KotoPlaylist *self, gchar *uuid) { @@ -167,17 +239,62 @@ void koto_playlist_add_to_played_tracks(KotoPlaylist *self, gchar *uuid) { g_queue_push_tail(self->played_tracks, uuid); // Add to end } -void koto_playlist_add_track(KotoPlaylist *self, KotoIndexedTrack *track) { - gchar *uuid = NULL; - g_object_get(track, "uuid", &uuid, NULL); // Get the UUID - koto_playlist_add_track_by_uuid(self, uuid); // Add by the file's UUID +void koto_playlist_add_track(KotoPlaylist *self, KotoIndexedTrack *track, gboolean current, gboolean commit_to_table) { + koto_playlist_add_track_by_uuid(self, koto_indexed_track_get_uuid(track), current, commit_to_table); } -void koto_playlist_add_track_by_uuid(KotoPlaylist *self, const gchar *uuid) { - gchar *dup_uuid = g_strdup(uuid); +void koto_playlist_add_track_by_uuid(KotoPlaylist *self, gchar *uuid, gboolean current, gboolean commit_to_table) { + KotoIndexedTrack *track = koto_cartographer_get_track_by_uuid(koto_maps, uuid); // Get the track - g_queue_push_tail(self->tracks, dup_uuid); // Append the UUID to the tracks - // TODO: Add to table + if (!KOTO_IS_INDEXED_TRACK(track)) { + return; + } + + GList *found_tracks_uuids = g_queue_find_custom(self->tracks, uuid, koto_playlist_compare_track_uuids); + if (found_tracks_uuids != NULL) { // Is somewhere in the tracks already + g_list_free(found_tracks_uuids); + return; + } + + g_list_free(found_tracks_uuids); + + g_queue_push_tail(self->tracks, uuid); // Prepend the UUID to the tracks + g_queue_push_tail(self->sorted_tracks, uuid); // Also add to our sorted tracks + g_list_store_append(self->store, track); // Add to the store + + if (self->finalized) { // Is already finalized + koto_playlist_apply_model(self, self->model); // Re-apply our current model to ensure our "sorted tracks" is sorted, as is our GLIstStore used in the playlist page + } + + if (commit_to_table) { + koto_indexed_track_save_to_playlist(track, self->uuid, (g_strcmp0(self->current_uuid, uuid) == 0) ? 1 : 0); // Call to save the playlist to the track + } + + if (current && (g_queue_get_length(self->tracks) > 1)) { // Is current and NOT the first item + self->current_uuid = uuid; // Mark this as current UUID + } + + g_signal_emit( + self, + playlist_signals[SIGNAL_TRACK_ADDED], + 0, + uuid + ); +} + +void koto_playlist_apply_model(KotoPlaylist *self, KotoPreferredModelType preferred_model) { + GList *sort_user_data = NULL; + sort_user_data = g_list_prepend(sort_user_data, GUINT_TO_POINTER(preferred_model)); // Prepend our preferred model first + sort_user_data = g_list_prepend(sort_user_data, self); // Prepend ourself + + g_queue_sort(self->sorted_tracks, koto_playlist_model_sort_by_uuid, sort_user_data); // Sort tracks, which is by UUID + g_list_store_sort(self->store, koto_playlist_model_sort_by_track, sort_user_data); // Sort tracks by indexed tracks + + self->model = preferred_model; // Update our preferred model + + /*if (self->current_position != -1) { // Have a position set + koto_playlist_set_track_as_current(self, self->current_uuid); // Update the position based on the new model just by setting it as current again + }*/ } void koto_playlist_commit(KotoPlaylist *self) { @@ -186,8 +303,8 @@ void koto_playlist_commit(KotoPlaylist *self) { } gchar *commit_op = g_strdup_printf( - "INSERT INTO playlist_meta(id, name, art_path)" - "VALUES('%s', quote(\"%s\"), quote(\"%s\")" + "INSERT INTO playlist_meta(id, name, art_path, preferred_model)" + "VALUES('%s', quote(\"%s\"), quote(\"%s\"), 0)" "ON CONFLICT(id) DO UPDATE SET name=excluded.name, art_path=excluded.art_path;", self->uuid, self->name, @@ -215,22 +332,30 @@ void koto_playlist_commit_tracks(gpointer data, gpointer user_data) { gchar *playlist_uuid = self->uuid; // Get the playlist UUID gchar *current_track = g_queue_peek_nth(self->tracks, self->current_position); // Get the UUID of the current track - koto_indexed_track_save_to_playlist(track, playlist_uuid, g_queue_index(self->tracks, data), (data == current_track) ? 1 : 0); // Call to save the playlist to the track + //koto_indexed_track_save_to_playlist(track, playlist_uuid, (data == current_track) ? 1 : 0); // Call to save the playlist to the track g_free(playlist_uuid); g_free(current_track); } } +gint koto_playlist_compare_track_uuids(gconstpointer a, gconstpointer b) { + return g_strcmp0(a, b); +} + gchar* koto_playlist_get_artwork(KotoPlaylist *self) { - return g_strdup(self->art_path); // Return a duplicate of our art path + return (self->art_path == NULL) ? NULL : g_strdup(self->art_path); // Return a duplicate of our art path +} + +KotoPreferredModelType koto_playlist_get_current_model(KotoPlaylist *self) { + return self->model; } guint koto_playlist_get_current_position(KotoPlaylist *self) { return self->current_position; } -gchar* koto_playlist_get_current_uuid(KotoPlaylist *self) { - return g_queue_peek_nth(self->tracks, self->current_position); +gboolean koto_playlist_get_is_finalized(KotoPlaylist *self) { + return self->finalized; } guint koto_playlist_get_length(KotoPlaylist *self) { @@ -241,12 +366,35 @@ gchar* koto_playlist_get_name(KotoPlaylist *self) { return (self->name == NULL) ? NULL : g_strdup(self->name); } +gint koto_playlist_get_position_of_track(KotoPlaylist *self, KotoIndexedTrack *track) { + if (!KOTO_IS_PLAYLIST(self)) { + return -1; + } + + if (!G_IS_LIST_STORE(self->store)) { + return -1; + } + + if (!KOTO_IS_INDEXED_TRACK(track)) { + return -1; + } + + gint position = -1; + guint found_pos = 0; + + if (g_list_store_find(self->store , track, &found_pos)) { // Found the item + position = (gint) found_pos; // Cast our found position from guint to gint + } + + return position; +} + gchar* koto_playlist_get_random_track(KotoPlaylist *self) { gchar *track_uuid = NULL; - guint tracks_len = g_queue_get_length(self->tracks); + guint tracks_len = g_queue_get_length(self->sorted_tracks); if (tracks_len == g_queue_get_length(self->played_tracks)) { // Played all tracks - track_uuid = g_list_nth_data(self->tracks->head, 0); // Get the first + track_uuid = g_list_nth_data(self->sorted_tracks->head, 0); // Get the first g_queue_clear(self->played_tracks); // Clear our played tracks } else { // Have not played all tracks GRand* rando_calrissian = g_rand_new(); // Create a new RNG @@ -255,7 +403,7 @@ gchar* koto_playlist_get_random_track(KotoPlaylist *self) { while (track_uuid == NULL) { // Haven't selected a track yet attempt++; gint32 selected_item = g_rand_int_range(rando_calrissian, 0, (gint32) tracks_len); - gchar *selected_track = g_queue_peek_nth(self->tracks, (guint) selected_item); // Get the UUID of the selected item + gchar *selected_track = g_queue_peek_nth(self->sorted_tracks, (guint) selected_item); // Get the UUID of the selected item if (g_queue_index(self->played_tracks, selected_track) == -1) { // Haven't played the track self->current_position = (gint) selected_item; @@ -274,6 +422,10 @@ gchar* koto_playlist_get_random_track(KotoPlaylist *self) { return track_uuid; } +GListStore* koto_playlist_get_store(KotoPlaylist *self) { + return self->store; +} + GQueue* koto_playlist_get_tracks(KotoPlaylist *self) { return self->tracks; } @@ -283,22 +435,38 @@ gchar* koto_playlist_get_uuid(KotoPlaylist *self) { } gchar* koto_playlist_go_to_next(KotoPlaylist *self) { + if (!KOTO_IS_PLAYLIST(self)) { + return NULL; + } + if (self->is_shuffle_enabled) { // Shuffling enabled gchar *random_track_uuid = koto_playlist_get_random_track(self); // Get a random track koto_playlist_add_to_played_tracks(self, random_track_uuid); return random_track_uuid; } - gchar *current_uuid = koto_playlist_get_current_uuid(self); // Get the current UUID + if (self->current_uuid == NULL || (g_strcmp0(self->current_uuid, "") == 0)) { + self->current_position++; + } else { // Have a UUID currently + KotoIndexedTrack *track = koto_cartographer_get_track_by_uuid(koto_maps, self->current_uuid); - if (current_uuid == self->tracks->tail->data) { // Current UUID matches the last item in the playlist - return NULL; + if (!KOTO_IS_INDEXED_TRACK(track)) { + return NULL; + } + + gint pos_of_song = koto_playlist_get_position_of_track(self, track); // Get the position of the current track based on the current model + + if ((guint) pos_of_song == (g_queue_get_length(self->sorted_tracks) - 1)) { // At end + return NULL; + } + + self->current_position = pos_of_song+1; // Increment our position based on position of song } - self->current_position++; // Increment our position - current_uuid = koto_playlist_get_current_uuid(self); // Return the new UUID - koto_playlist_add_to_played_tracks(self, current_uuid); - return current_uuid; + self->current_uuid = g_queue_peek_nth(self->sorted_tracks, self->current_position); + koto_playlist_add_to_played_tracks(self, self->current_uuid); + + return self->current_uuid; } gchar* koto_playlist_go_to_previous(KotoPlaylist *self) { @@ -306,45 +474,217 @@ gchar* koto_playlist_go_to_previous(KotoPlaylist *self) { return koto_playlist_get_random_track(self); // Get a random track } - gchar *current_uuid = koto_playlist_get_current_uuid(self); // Get the current UUID - - if (current_uuid == self->tracks->head->data) { // Current UUID matches the first item in the playlist + if (self->current_uuid == NULL || (g_strcmp0(self->current_uuid, "") == 0)) { return NULL; } - self->current_position--; // Decrement our position - return koto_playlist_get_current_uuid(self); // Return the new UUID + KotoIndexedTrack *track = koto_cartographer_get_track_by_uuid(koto_maps, self->current_uuid); + + if (!KOTO_IS_INDEXED_TRACK(track)) { + return NULL; + } + + gint pos_of_song = koto_playlist_get_position_of_track(self, track); // Get the position of the current track based on the current model + + if (pos_of_song == 0) { + return NULL; + } + + self->current_position = pos_of_song - 1; // Decrement our position based on position of song + self->current_uuid = g_queue_peek_nth(self->sorted_tracks, self->current_position); + + return self->current_uuid; +} + +void koto_playlist_mark_as_finalized(KotoPlaylist *self) { + if (self->finalized) { // Already finalized + return; + } + + self->finalized = TRUE; + koto_playlist_apply_model(self, self->model); // Re-apply our model to enforce mass sort + + g_signal_emit( + self, + playlist_signals[SIGNAL_TRACK_LOAD_FINALIZED], + 0 + ); +} + +gint koto_playlist_model_sort_by_uuid(gconstpointer first_item, gconstpointer second_item, gpointer data_list) { + KotoIndexedTrack *first_track = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) first_item); + KotoIndexedTrack *second_track = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) second_item); + + return koto_playlist_model_sort_by_track(first_track, second_track, data_list); +} + +gint koto_playlist_model_sort_by_track(gconstpointer first_item, gconstpointer second_item, gpointer data_list) { + KotoIndexedTrack *first_track = (KotoIndexedTrack*) first_item; + KotoIndexedTrack *second_track = (KotoIndexedTrack*) second_item; + + GList* ptr_list = data_list; + KotoPlaylist *self = g_list_nth_data(ptr_list, 0); // First item in the GPtrArray is a pointer to our playlist + KotoPreferredModelType model = GPOINTER_TO_UINT(g_list_nth_data(ptr_list, 1)); // Second item in the GPtrArray is a pointer to our KotoPreferredModelType + + if ( + (model == KOTO_PREFERRED_MODEL_TYPE_DEFAULT) || // Newest first model + (model == KOTO_PREFERRED_MODEL_TYPE_OLDEST_FIRST) // Oldest first + ) { + gint first_track_pos = g_queue_index(self->tracks, koto_indexed_track_get_uuid(first_track)); + gint second_track_pos = g_queue_index(self->tracks, koto_indexed_track_get_uuid(second_track)); + + if (first_track_pos == -1) { // First track isn't in tracks + return 1; + } + + if (second_track_pos == -1) { // Second track isn't in tracks + return -1; + } + + if (model == KOTO_PREFERRED_MODEL_TYPE_DEFAULT) { // Newest first + return (first_track_pos < second_track_pos) ? 1 : -1; // Display first at end, not beginning + } else { + return (first_track_pos < second_track_pos) ? -1 : 1; // Display at beginning, not end + } + } + + if (model == KOTO_PREFERRED_MODEL_TYPE_SORT_BY_ALBUM) { // Sort by album name + gchar *first_album_uuid = NULL; + gchar *second_album_uuid = NULL; + + g_object_get( + first_track, + "album-uuid", + &first_album_uuid, + NULL + ); + + g_object_get( + second_track, + "album-uuid", + &second_album_uuid, + NULL + ); + + if (g_strcmp0(first_album_uuid, second_album_uuid) == 0) { // Identical albums + g_free(first_album_uuid); + g_free(second_album_uuid); + return 0; // Don't get too granular, just consider them equal + } + + KotoIndexedAlbum *first_album = koto_cartographer_get_album_by_uuid(koto_maps, first_album_uuid); + KotoIndexedAlbum *second_album = koto_cartographer_get_album_by_uuid(koto_maps, second_album_uuid); + + g_free(first_album_uuid); + g_free(second_album_uuid); + + if (!KOTO_IS_INDEXED_ALBUM(first_album) && !KOTO_IS_INDEXED_ALBUM(second_album)) { // Neither are valid albums + return 0; // Just consider them as equal + } + + return g_utf8_collate(koto_indexed_album_get_album_name(first_album), koto_indexed_album_get_album_name(second_album)); + } + + if (model == KOTO_PREFERRED_MODEL_TYPE_SORT_BY_ARTIST) { // Sort by artist name + gchar *first_artist_uuid = NULL; + gchar *second_artist_uuid = NULL; + + g_object_get( + first_track, + "artist-uuid", + &first_artist_uuid, + NULL + ); + + g_object_get( + second_track, + "artist-uuid", + &second_artist_uuid, + NULL + ); + + KotoIndexedArtist *first_artist = koto_cartographer_get_artist_by_uuid(koto_maps, first_artist_uuid); + KotoIndexedArtist *second_artist = koto_cartographer_get_artist_by_uuid(koto_maps, second_artist_uuid); + + g_free(first_artist_uuid); + g_free(second_artist_uuid); + + if (!KOTO_IS_INDEXED_ARTIST(first_artist) && !KOTO_IS_INDEXED_ARTIST(second_artist)) { // Neither are valid artists + return 0; // Just consider them as equal + } + + return g_utf8_collate(koto_indexed_artist_get_name(first_artist), koto_indexed_artist_get_name(second_artist)); + } + + if (model == KOTO_PREFERRED_MODEL_TYPE_SORT_BY_TRACK_NAME) { // Track name + gchar *first_track_name = NULL; + gchar *second_track_name = NULL; + + g_object_get( + first_track, + "parsed-name", + &first_track_name, + NULL + ); + + g_object_get( + second_track, + "parsed-name", + &second_track_name, + NULL + ); + + gint ret = g_utf8_collate(first_track_name, second_track_name); + g_free(first_track_name); + g_free(second_track_name); + + return ret; + } + + return 0; } void koto_playlist_remove_from_played_tracks(KotoPlaylist *self, gchar *uuid) { g_queue_remove(self->played_tracks, uuid); } -void koto_playlist_remove_track(KotoPlaylist *self, KotoIndexedTrack *track) { - gchar *track_uuid = NULL; - g_object_get(track, "uuid", &track_uuid, NULL); - - if (track_uuid != NULL) { - koto_playlist_remove_track_by_uuid(self, track_uuid); - g_free(track_uuid); - } - - return; -} - void koto_playlist_remove_track_by_uuid(KotoPlaylist *self, gchar *uuid) { - if (uuid == NULL) { + if (!KOTO_IS_PLAYLIST(self)) { return; } gint file_index = g_queue_index(self->tracks, uuid); // Get the position of this uuid - if (file_index == -1) { // Does not exist in our tracks list + if (file_index != -1) { // Have in tracks + g_queue_pop_nth(self->tracks, file_index); // Remove nth where it is the file index + } + + gint file_index_in_sorted = g_queue_index(self->sorted_tracks, uuid); // Get position in sorted tracks + + if (file_index_in_sorted != -1) { // Have in sorted tracks + g_queue_pop_nth(self->sorted_tracks, file_index_in_sorted); // Remove nth where it is the index in sorted tracks + } + + KotoIndexedTrack *track = koto_cartographer_get_track_by_uuid(koto_maps, uuid); // Get the track + + if (!KOTO_IS_INDEXED_TRACK(track)) { // Is not a track return; } - g_queue_pop_nth(self->tracks, file_index); // Remove nth where it is the file index - return; + guint position = 0; + + if (g_list_store_find(self->store, track, &position)) { // Got the position + g_list_store_remove(self->store, position); // Remove from the store + } + + koto_indexed_track_remove_from_playlist(track, self->uuid); + + g_signal_emit( + self, + playlist_signals[SIGNAL_TRACK_REMOVED], + 0, + uuid + ); } void koto_playlist_set_artwork(KotoPlaylist *self, const gchar *path) { @@ -376,11 +716,10 @@ void koto_playlist_set_artwork(KotoPlaylist *self, const gchar *path) { free_cookie: magic_close(cookie); // Close and free the cookie to the cookie monster - return; } void koto_playlist_set_name(KotoPlaylist *self, const gchar *name) { - if (name == NULL) { // No actual name + if (name == NULL) { return; } @@ -389,7 +728,16 @@ void koto_playlist_set_name(KotoPlaylist *self, const gchar *name) { } self->name = g_strdup(name); - return; +} + +void koto_playlist_set_position(KotoPlaylist *self, gint position) { + self->current_position = position; +} + +void koto_playlist_set_track_as_current(KotoPlaylist *self, gchar *track_uuid) { + gint position_of_track = g_queue_index(self->sorted_tracks, track_uuid); // Get the position of the UUID in our tracks + g_return_if_fail(position_of_track != -1); + self->current_position = position_of_track; } void koto_playlist_set_uuid(KotoPlaylist *self, const gchar *uuid) { @@ -402,7 +750,17 @@ void koto_playlist_set_uuid(KotoPlaylist *self, const gchar *uuid) { } self->uuid = g_strdup(uuid); // Set the new UUID - return; +} + +void koto_playlist_tracks_queue_push_to_store(gpointer data, gpointer user_data) { + gchar *track_uuid = (gchar *) data; + KotoIndexedTrack *track = koto_cartographer_get_track_by_uuid(koto_maps, track_uuid); + + if (!KOTO_IS_INDEXED_TRACK(track)) { // Not a track + return; + } + + g_list_store_append(G_LIST_STORE(user_data), track); } void koto_playlist_unmap(KotoPlaylist *self) { diff --git a/src/playlist/playlist.h b/src/playlist/playlist.h index ca8cfd7..65d05cc 100644 --- a/src/playlist/playlist.h +++ b/src/playlist/playlist.h @@ -16,19 +16,34 @@ */ #pragma once -#include +#include +#include #include "../indexer/structs.h" G_BEGIN_DECLS +typedef enum { + KOTO_PREFERRED_MODEL_TYPE_DEFAULT, // Considered to be newest first + KOTO_PREFERRED_MODEL_TYPE_OLDEST_FIRST, + KOTO_PREFERRED_MODEL_TYPE_SORT_BY_ALBUM, + KOTO_PREFERRED_MODEL_TYPE_SORT_BY_ARTIST, + KOTO_PREFERRED_MODEL_TYPE_SORT_BY_TRACK_NAME +} KotoPreferredModelType; + /** * Type Definition **/ #define KOTO_TYPE_PLAYLIST koto_playlist_get_type() -G_DECLARE_FINAL_TYPE(KotoPlaylist, koto_playlist, KOTO, PLAYLIST, GObject); +#define KOTO_PLAYLIST(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), KOTO_TYPE_PLAYLIST, KotoPlaylist)) #define KOTO_IS_PLAYLIST(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_PLAYLIST)) +typedef struct _KotoPlaylist KotoPlaylist; +typedef struct _KotoPlaylistClass KotoPlaylistClass; + +GLIB_AVAILABLE_IN_ALL +GType koto_playlist_get_type(void) G_GNUC_CONST; + /** * Playlist Functions **/ @@ -36,27 +51,36 @@ G_DECLARE_FINAL_TYPE(KotoPlaylist, koto_playlist, KOTO, PLAYLIST, GObject); KotoPlaylist* koto_playlist_new(); KotoPlaylist* koto_playlist_new_with_uuid(const gchar *uuid); void koto_playlist_add_to_played_tracks(KotoPlaylist *self, gchar *uuid); -void koto_playlist_add_track(KotoPlaylist *self, KotoIndexedTrack *track); -void koto_playlist_add_track_by_uuid(KotoPlaylist *self, const gchar *uuid); +void koto_playlist_add_track(KotoPlaylist *self, KotoIndexedTrack *track, gboolean current, gboolean commit_to_table); +void koto_playlist_add_track_by_uuid(KotoPlaylist *self, gchar *uuid, gboolean current, gboolean commit_to_table); +void koto_playlist_apply_model(KotoPlaylist *self, KotoPreferredModelType preferred_model); void koto_playlist_commit(KotoPlaylist *self); void koto_playlist_commit_tracks(gpointer data, gpointer user_data); +gint koto_playlist_compare_track_uuids(gconstpointer a, gconstpointer b); gchar* koto_playlist_get_artwork(KotoPlaylist *self); +KotoPreferredModelType koto_playlist_get_current_model(KotoPlaylist *self); guint koto_playlist_get_current_position(KotoPlaylist *self); -gchar* koto_playlist_get_current_uuid(KotoPlaylist *self); guint koto_playlist_get_length(KotoPlaylist *self); +gboolean koto_playlist_get_is_finalized(KotoPlaylist *self); gchar* koto_playlist_get_name(KotoPlaylist *self); +gint koto_playlist_get_position_of_track(KotoPlaylist *self, KotoIndexedTrack *track); +GListStore* koto_playlist_get_store(KotoPlaylist *self); GQueue* koto_playlist_get_tracks(KotoPlaylist *self); gchar* koto_playlist_get_uuid(KotoPlaylist *self); gchar* koto_playlist_go_to_next(KotoPlaylist *self); gchar* koto_playlist_go_to_previous(KotoPlaylist *self); +void koto_playlist_mark_as_finalized(KotoPlaylist *self); +gint koto_playlist_model_sort_by_uuid(gconstpointer first_item, gconstpointer second_item, gpointer data_list); +gint koto_playlist_model_sort_by_track(gconstpointer first_item, gconstpointer second_item, gpointer model_ptr); void koto_playlist_remove_from_played_tracks(KotoPlaylist *self, gchar *uuid); -void koto_playlist_remove_track(KotoPlaylist *self, KotoIndexedTrack *track); void koto_playlist_remove_track_by_uuid(KotoPlaylist *self, gchar *uuid); void koto_playlist_set_artwork(KotoPlaylist *self, const gchar *path); void koto_playlist_save_state(KotoPlaylist *self); void koto_playlist_set_name(KotoPlaylist *self, const gchar *name); -void koto_playlist_set_position(KotoPlaylist *self, guint pos); +void koto_playlist_set_position(KotoPlaylist *self, gint position); +void koto_playlist_set_track_as_current(KotoPlaylist *self, gchar *track_uuid); void koto_playlist_set_uuid(KotoPlaylist *self, const gchar *uuid); +void koto_playlist_tracks_queue_push_to_store(gpointer data, gpointer user_data); void koto_playlist_unmap(KotoPlaylist *self); G_END_DECLS diff --git a/theme/_button.scss b/theme/_button.scss index 85e078d..9b95dc6 100644 --- a/theme/_button.scss +++ b/theme/_button.scss @@ -1,3 +1,5 @@ +@import "vars"; + .koto-button { & > image { margin-right: 10px; diff --git a/theme/_disc-view.scss b/theme/_disc-view.scss index 983fd0e..624c83c 100644 --- a/theme/_disc-view.scss +++ b/theme/_disc-view.scss @@ -8,12 +8,14 @@ & .track-list { & > row { - &:nth-child(odd):not(:hover) { - background-color: $midnight; - } + &:not(:active):not(:selected) { // Neither active nor selected, see gtk overrides + &:nth-child(odd):not(:hover) { + background-color: $midnight; + } - &:nth-child(even), &:hover { - background-color: $grey; + &:nth-child(even), &:hover { + background-color: $grey; + } } } } diff --git a/theme/_player-bar.scss b/theme/_player-bar.scss index 8cf13e8..4c4a26b 100644 --- a/theme/_player-bar.scss +++ b/theme/_player-bar.scss @@ -14,4 +14,32 @@ color: white; } } + + .playerbar-info { // Central info section + & > box { // Info labels + margin-left: 2ex; + + & > label { + margin-top: 6px; + margin-bottom: 6px; + + &:nth-child(1) { // Title + font-size: x-large; + font-weight: bold; + } + + &:not(:nth-child(1)) { // Album and Artist + font-size: large; + } + + &:nth-child(2) { // Album + + } + + &:nth-child(3) { // Artist + + } + } + } + } } diff --git a/theme/_vars.scss b/theme/_vars.scss index 4996f31..fc9994a 100644 --- a/theme/_vars.scss +++ b/theme/_vars.scss @@ -2,6 +2,8 @@ $grey: #2e2e2e; $midnight: #1d1d1d; $darkgrey: #666666; $green: #60E078; +$palewhite: #cccccc; +$red : #FF4652; $itempadding: 40px; $halvedpadding: $itempadding / 2; diff --git a/theme/components/_cover-art-button.scss b/theme/components/_cover-art-button.scss new file mode 100644 index 0000000..6af84db --- /dev/null +++ b/theme/components/_cover-art-button.scss @@ -0,0 +1,5 @@ +.cover-art-button { + & > revealer > box { // Inner controls + background-color: rgba(0,0,0,0.75); + } +} \ No newline at end of file diff --git a/theme/components/_gtk-overrides.scss b/theme/components/_gtk-overrides.scss new file mode 100644 index 0000000..07e6e5c --- /dev/null +++ b/theme/components/_gtk-overrides.scss @@ -0,0 +1,63 @@ +@import '../vars'; + +@mixin selected-row-styling { + color: $midnight; + background-color: $green; + border: 0; // Don't have a border + border-image: none; // GTK uses an image which is weird + border-image-width: 0; + outline: none; + outline-offset: 0; + outline-style: none; +} + +button { + &.destructive-action { + background-color: $red; + background-image: none; + } + + &.suggested-action { // Adwaita makes it blue but we want it green + color: $midnight; + background-color: $green; + background-image: none; + } +} + +listview { + background-color: transparent; +} + +list:not(.discs-list), listview { + &:not(.track-list) > row { // Rows which are now in the track list + &:active, &:selected { // Active or selected + @include selected-row-styling; + } + } + + &.track-list > row { + &:selected { // Only selected rows + @include selected-row-styling; + } + } +} + +range { + &.dragging { // Dragging a range + & > trough { + & > slider { + background-color: $green; + } + } + } +} + +scale { // Progress bar + highlight { + background-color: $green; + } + + slider { // Slider + outline-color: $green; + } +} \ No newline at end of file diff --git a/theme/main.scss b/theme/main.scss index f6edc03..e624686 100644 --- a/theme/main.scss +++ b/theme/main.scss @@ -1,4 +1,7 @@ -@import 'pages/music-local.scss'; +@import 'components/cover-art-button'; +@import 'components/gtk-overrides'; +@import 'pages/music-local'; +@import 'pages/playlist-page'; @import 'button'; @import 'disc-view'; @@ -15,4 +18,15 @@ window { background-color: $midnight; background-image: none; } + + .koto-dialog-container { + background-color: transparentize($midnight, 0.5); + padding: 20px; + } + + // All the classes we want consistent padding applied to for its primary content + .artist-view-content, // Has the albums + .playlist-page { // Individual playlists + padding: $itempadding; + } } diff --git a/theme/meson.build b/theme/meson.build index f17aa7b..143c5f3 100644 --- a/theme/meson.build +++ b/theme/meson.build @@ -9,7 +9,10 @@ theme = custom_target('Theme generation', '@INPUT@', '@OUTPUT@', ], depend_files: files([ + 'components/_cover-art-button.scss', + 'components/_gtk-overrides.scss', 'pages/_music-local.scss', + 'pages/_playlist-page.scss', '_button.scss', '_disc-view.scss', '_expander.scss', diff --git a/theme/pages/_music-local.scss b/theme/pages/_music-local.scss index 0ddfbc2..cc0754f 100644 --- a/theme/pages/_music-local.scss +++ b/theme/pages/_music-local.scss @@ -16,8 +16,6 @@ & > stack { & > .artist-view { & > viewport > .artist-view-content { - padding: $itempadding; - & > .album-strip { margin-bottom: $itempadding; & > flowboxchild { diff --git a/theme/pages/_playlist-page.scss b/theme/pages/_playlist-page.scss new file mode 100644 index 0000000..cb18ec3 --- /dev/null +++ b/theme/pages/_playlist-page.scss @@ -0,0 +1,84 @@ +@import '../vars'; + +.playlist-page { + .playlist-page-header { // Our header + & > .playlist-page-header-info { // Our info centerbox + margin-left: 40px; + + & > label { // All labels + color: $palewhite; + } + + & > label:nth-child(1) { // First item (type of playlist) + font-size: 3ex; + font-weight: 700; + margin-top: 30px; + margin-bottom: 10px; + } + + & > label:nth-child(2), + & > label:nth-child(3) { + font-weight: 800; + } + + & > label:nth-child(2) { // Second item (playlist name) + font-size: 10ex; + } + + & > label:nth-child(3) { // Third item (number of tracks) + font-size: 4ex; + margin-top: 40px; + } + } + + & > .koto-button { + + } + } + + .track-list-content { // Our Track List + & > .track-list-header, + .track-list-columned-item { + font-size: x-large; + padding: 3ex 2ex; + } + + & > .track-list-header { // Headers + font-weight: bold; + + .koto-button { // All Koto buttons in our header + &.active { // Is active + color: $green; + } + } + } + + & > .track-list-columned { // Column content + & > row { + & > .track-list-columned-item { // Track rows + font-size: x-large; + } + + &:nth-child(odd):not(:selected) { + background-color: $midnight; + } + } + } + + .track-column-number { // Column section within header and track items + + } + + .track-column-name { // Name section within header and track items + + } + + .track-column-album { // Album section within headers and track items + + } + + .track-column-artist { // Artist section within headers and track items + + } + } +} \ No newline at end of file