diff --git a/.clang-format b/.clang-format deleted file mode 100644 index f8b5523..0000000 --- a/.clang-format +++ /dev/null @@ -1,258 +0,0 @@ ---- -Language: Cpp -# BasedOnStyle: Chromium -AccessModifierOffset: -1 -AlignAfterOpenBracket: AlwaysBreak -AlignArrayOfStructures: None -AlignConsecutiveAssignments: - Enabled: true - AcrossEmptyLines: false - AcrossComments: false - AlignCompound: false - PadOperators: true -AlignConsecutiveBitFields: - Enabled: false - AcrossEmptyLines: false - AcrossComments: false - AlignCompound: false - PadOperators: false -AlignConsecutiveDeclarations: - Enabled: true - AcrossEmptyLines: false - AcrossComments: false - AlignCompound: false - PadOperators: false -AlignConsecutiveMacros: - Enabled: true - AcrossEmptyLines: false - AcrossComments: false - AlignCompound: false - PadOperators: false -AlignEscapedNewlines: Left -AlignOperands: Align -AlignTrailingComments: - Kind: Always - OverEmptyLines: 0 -AllowAllArgumentsOnNextLine: false -AllowAllParametersOfDeclarationOnNextLine: false -AllowShortBlocksOnASingleLine: Always -AllowShortCaseLabelsOnASingleLine: false -AllowShortEnumsOnASingleLine: true -AllowShortFunctionsOnASingleLine: Inline -AllowShortIfStatementsOnASingleLine: WithoutElse -AllowShortLambdasOnASingleLine: All -AllowShortLoopsOnASingleLine: true -AlwaysBreakAfterDefinitionReturnType: None -AlwaysBreakAfterReturnType: None -AlwaysBreakBeforeMultilineStrings: true -AlwaysBreakTemplateDeclarations: Yes -AttributeMacros: - - __capability -BinPackArguments: true -BinPackParameters: false -BitFieldColonSpacing: Both -BraceWrapping: - AfterCaseLabel: false - AfterClass: false - AfterControlStatement: Never - AfterEnum: false - AfterExternBlock: false - AfterFunction: false - AfterNamespace: false - AfterObjCDeclaration: false - AfterStruct: false - AfterUnion: false - BeforeCatch: false - BeforeElse: false - BeforeLambdaBody: false - BeforeWhile: false - IndentBraces: false - SplitEmptyFunction: true - SplitEmptyRecord: true - SplitEmptyNamespace: true -BreakAfterAttributes: Never -BreakAfterJavaFieldAnnotations: false -BreakArrays: true -BreakBeforeBinaryOperators: None -BreakBeforeConceptDeclarations: Always -BreakBeforeBraces: Attach -BreakBeforeInlineASMColon: OnlyMultiline -BreakBeforeTernaryOperators: true -BreakConstructorInitializers: BeforeColon -BreakInheritanceList: BeforeColon -BreakStringLiterals: true -ColumnLimit: 160 -CommentPragmas: "^ IWYU pragma:" -CompactNamespaces: false -ConstructorInitializerIndentWidth: 4 -ContinuationIndentWidth: 4 -Cpp11BracedListStyle: true -DerivePointerAlignment: false -DisableFormat: false -EmptyLineAfterAccessModifier: Never -EmptyLineBeforeAccessModifier: LogicalBlock -ExperimentalAutoDetectBinPacking: false -FixNamespaceComments: false -ForEachMacros: - - foreach - - Q_FOREACH - - BOOST_FOREACH -IfMacros: - - KJ_IF_MAYBE -IncludeBlocks: Regroup -IncludeCategories: - - Regex: '^' - Priority: 2 - SortPriority: 0 - CaseSensitive: false - - Regex: '^<.*\.h>' - Priority: 1 - SortPriority: 0 - CaseSensitive: false - - Regex: "^<.*" - Priority: 2 - SortPriority: 0 - CaseSensitive: false - - Regex: ".*" - Priority: 3 - SortPriority: 0 - CaseSensitive: false -IncludeIsMainRegex: "([-_](test|unittest))?$" -IncludeIsMainSourceRegex: "" -IndentAccessModifiers: true -IndentCaseBlocks: false -IndentCaseLabels: true -IndentExternBlock: AfterExternBlock -IndentGotoLabels: true -IndentPPDirectives: None -IndentRequiresClause: true -IndentWidth: 2 -IndentWrappedFunctionNames: false -InsertBraces: false -InsertNewlineAtEOF: true -InsertTrailingCommas: None -IntegerLiteralSeparator: - Binary: 0 - BinaryMinDigits: 0 - Decimal: 0 - DecimalMinDigits: 0 - Hex: 0 - HexMinDigits: 0 -JavaScriptQuotes: Leave -JavaScriptWrapImports: true -KeepEmptyLinesAtTheStartOfBlocks: false -LambdaBodyIndentation: Signature -LineEnding: LF -MacroBlockBegin: "" -MacroBlockEnd: "" -MaxEmptyLinesToKeep: 1 -NamespaceIndentation: All -ObjCBinPackProtocolList: Never -ObjCBlockIndentWidth: 2 -ObjCBreakBeforeNestedBlockParam: true -ObjCSpaceAfterProperty: false -ObjCSpaceBeforeProtocolList: true -PackConstructorInitializers: NextLine -PenaltyBreakAssignment: 2 -PenaltyBreakBeforeFirstCallParameter: 1 -PenaltyBreakComment: 300 -PenaltyBreakFirstLessLess: 120 -PenaltyBreakOpenParenthesis: 0 -PenaltyBreakString: 1000 -PenaltyBreakTemplateDeclaration: 10 -PenaltyExcessCharacter: 1000000 -PenaltyIndentedWhitespace: 0 -PenaltyReturnTypeOnItsOwnLine: 200 -PointerAlignment: Left -PPIndentWidth: -1 -QualifierAlignment: Leave -RawStringFormats: - - Language: Cpp - Delimiters: - - cc - - CC - - cpp - - Cpp - - CPP - - "c++" - - "C++" - CanonicalDelimiter: "" - BasedOnStyle: google - - Language: TextProto - Delimiters: - - pb - - PB - - proto - - PROTO - EnclosingFunctions: - - EqualsProto - - EquivToProto - - PARSE_PARTIAL_TEXT_PROTO - - PARSE_TEST_PROTO - - PARSE_TEXT_PROTO - - ParseTextOrDie - - ParseTextProtoOrDie - - ParseTestProto - - ParsePartialTestProto - CanonicalDelimiter: pb - BasedOnStyle: google -ReferenceAlignment: Pointer -ReflowComments: true -RemoveBracesLLVM: false -RemoveSemicolon: false -RequiresClausePosition: OwnLine -RequiresExpressionIndentation: OuterScope -SeparateDefinitionBlocks: Leave -ShortNamespaceLines: 1 -SortIncludes: CaseSensitive -SortJavaStaticImport: Before -SortUsingDeclarations: LexicographicNumeric -SpaceAfterCStyleCast: true -SpaceAfterLogicalNot: false -SpaceAfterTemplateKeyword: true -SpaceAroundPointerQualifiers: Default -SpaceBeforeAssignmentOperators: true -SpaceBeforeCaseColon: false -SpaceBeforeCpp11BracedList: true -SpaceBeforeCtorInitializerColon: true -SpaceBeforeInheritanceColon: true -SpaceBeforeParens: ControlStatements -SpaceBeforeParensOptions: - AfterControlStatements: true - AfterForeachMacros: true - AfterFunctionDefinitionName: false - AfterFunctionDeclarationName: false - AfterIfMacros: true - AfterOverloadedOperator: false - AfterRequiresInClause: false - AfterRequiresInExpression: false - BeforeNonEmptyParentheses: false -SpaceBeforeRangeBasedForLoopColon: true -SpaceBeforeSquareBrackets: false -SpaceInEmptyBlock: false -SpaceInEmptyParentheses: false -SpacesBeforeTrailingComments: 2 -SpacesInAngles: Never -SpacesInConditionalStatement: false -SpacesInContainerLiterals: true -SpacesInCStyleCastParentheses: false -SpacesInLineCommentPrefix: - Minimum: 1 - Maximum: -1 -SpacesInParentheses: false -SpacesInSquareBrackets: false -Standard: Auto -StatementAttributeLikeMacros: - - Q_EMIT -StatementMacros: - - Q_UNUSED - - QT_REQUIRE_VERSION -TabWidth: 8 -UseTab: Never -WhitespaceSensitiveMacros: - - BOOST_PP_STRINGIZE - - CF_SWIFT_NAME - - NS_SWIFT_NAME - - PP_STRINGIZE - - STRINGIZE ---- diff --git a/.clangd b/.clangd deleted file mode 100644 index 359a391..0000000 --- a/.clangd +++ /dev/null @@ -1,2 +0,0 @@ -CompileFlags: - Add: [-std=c++20] diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..09d0c70 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: JoshStrobl +patreon: joshuastrobl +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: joshuastrobl +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/koto-alpha-image.png b/.github/koto-alpha-image.png new file mode 100644 index 0000000..061257a Binary files /dev/null and b/.github/koto-alpha-image.png differ diff --git a/.gitignore b/.gitignore index 983430f..4beaadd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,90 +1,4 @@ -# This file is used to ignore files which are generated -# ---------------------------------------------------------------------------- - -*~ -*.autosave -*.a -*.core -*.moc -*.o -*.obj -*.orig -*.rej -*.so -*.so.* -*_pch.h.cpp -*_resource.rc -*.qm -.#* -*.*# -core -!core/ -tags -.DS_Store -.directory -*.debug -Makefile* -*.prl -*.app -moc_*.cpp -ui_*.h -qrc_*.cpp -Thumbs.db -*.res -*.rc -/.qmake.cache -/.qmake.stash - -# qtcreator generated files -*.pro.user* -CMakeLists.txt.user* - -# xemacs temporary files -*.flc - -# Vim temporary files -.*.swp - -# Visual Studio generated files -*.ib_pdb_index -*.idb -*.ilk -*.pdb -*.sln -*.suo -*.vcproj -*vcproj.*.*.user -*.ncb -*.sdf -*.opensdf -*.vcxproj -*vcxproj.* - -# MinGW generated files -*.Debug -*.Release - -# Python byte code -*.pyc - -# Binaries -# -------- -*.dll -*.exe - -.cache/ -.ccls-cache/ -.idea/ -.kdev4/ -.qt/ -.rcc/ -.zed/ -bin/ -build*/ -cmake-build-debug/ -**/qmldir -**/meta_types -**/qmltypes -**/*.qrc -CMakeCache.txt -*.kdev4 +DesiredSettings.md +builddir +.buildconfig +src/theme/style.css diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..04ba0dd --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,17 @@ +{ + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/**", + "/usr/include/**" + ], + "defines": [], + "compilerPath": "/usr/bin/gcc", + "cStandard": "gnu17", + "cppStandard": "c++20", + "intelliSenseMode": "linux-gcc-x64" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d477032 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,50 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch (GDB)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/builddir/src/com.github.joshstrobl.koto", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + {"name" : "G_MESSAGES_DEBUG", "value": "all" }, + { "name": "GTK_THEME", "value": "Adwaita:dark" } + ], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ], + "preLaunchTask": "Meson Configure and Build" + }, + { + "name": "Launch", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/builddir/src/com.github.joshstrobl.koto", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + {"name" : "G_MESSAGES_DEBUG", "value": "all" }, + { "name": "GTK_THEME", "value": "Adwaita:dark" } + ], + "externalConsole": false, + "linux": { + "MIMode": "gdb", + "miDebuggerPath": "" + }, + "preLaunchTask": "Meson Configure and Build" + }, + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2ea2f84 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,22 @@ +{ + "files.associations": { + "glib.h": "c", + "ios": "c", + "__node_handle": "c", + "gtk.h": "c", + "gtktreeview.h": "c", + "cartographer.h": "c", + "structs.h": "c", + "gst.h": "c", + "player.h": "c", + "config.h": "c", + "toml.h": "c", + "chrono": "c", + "sqlite3.h": "c", + "unistd.h": "c", + "ui.h": "c", + "koto-utils.h": "c", + "random": "c", + "add-remove-track-popover.h": "c" + } +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..1359393 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,92 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Clean builddir", + "type": "shell", + "command": "rm", + "args": [ + "-rf", + "builddir" + ], + "problemMatcher": [] + }, + { + "label": "Format", + "type": "shell", + "command": "uncrustify", + "args": [ + "-c", + "jsc.cfg", + "--no-backup", + "**/*.c", + "**/*.h" + ] + }, + { + "label": "Meson Configure and Build", + "type": "shell", + "command": "", + "dependsOrder": "sequence", + "dependsOn": ["Meson Configure", "Meson Compile"] + }, + { + "label": "Meson Configure", + "type": "shell", + "command": "meson", + "args": [ + "--prefix=/usr", + "--libdir=\"libdir\"", + "--sysconfdir=/etc", + "builddir" + ], + "problemMatcher": [] + }, + { + "label": "Meson Compile", + "type": "shell", + "command": "meson", + "args": [ + "compile", + "-C", + "builddir", + ], + "problemMatcher": [], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "Meson Dist", + "type": "shell", + "command": "meson", + "args": [ + "dist", + "-C", + "builddir", + "--formats", + "xztar", + "--include-subprojects" + ], + "problemMatcher": [] + }, + { + "label": "Meson Install", + "type": "shell", + "command": "sudo", + "args": [ + "meson", + "install", + "-C", + "builddir", + "--destdir", + "/", + "--no-rebuild" + ], + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index 544751b..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -cmake_minimum_required(VERSION 3.16) - -project(koto VERSION 0.1 LANGUAGES CXX) - -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_STANDARD 23) - -add_subdirectory(desktop) diff --git a/COPYING b/COPYING index d0a1fa1..d645695 100644 --- a/COPYING +++ b/COPYING @@ -1,373 +1,202 @@ -Mozilla Public License Version 2.0 -================================== -1. Definitions --------------- - -1.1. "Contributor" - means each individual or legal entity that creates, contributes to - the creation of, or owns Covered Software. - -1.2. "Contributor Version" - means the combination of the Contributions of others (if any) used - by a Contributor and that particular Contributor's Contribution. - -1.3. "Contribution" - means Covered Software of a particular Contributor. - -1.4. "Covered Software" - means Source Code Form to which the initial Contributor has attached - the notice in Exhibit A, the Executable Form of such Source Code - Form, and Modifications of such Source Code Form, in each case - including portions thereof. - -1.5. "Incompatible With Secondary Licenses" - means - - (a) that the initial Contributor has attached the notice described - in Exhibit B to the Covered Software; or - - (b) that the Covered Software was made available under the terms of - version 1.1 or earlier of the License, but not also under the - terms of a Secondary License. - -1.6. "Executable Form" - means any form of the work other than Source Code Form. - -1.7. "Larger Work" - means a work that combines Covered Software with other material, in - a separate file or files, that is not Covered Software. - -1.8. "License" - means this document. - -1.9. "Licensable" - means having the right to grant, to the maximum extent possible, - whether at the time of the initial grant or subsequently, any and - all of the rights conveyed by this License. - -1.10. "Modifications" - means any of the following: - - (a) any file in Source Code Form that results from an addition to, - deletion from, or modification of the contents of Covered - Software; or - - (b) any new file in Source Code Form that contains any Covered - Software. - -1.11. "Patent Claims" of a Contributor - means any patent claim(s), including without limitation, method, - process, and apparatus claims, in any patent Licensable by such - Contributor that would be infringed, but for the grant of the - License, by the making, using, selling, offering for sale, having - made, import, or transfer of either its Contributions or its - Contributor Version. - -1.12. "Secondary License" - means either the GNU General Public License, Version 2.0, the GNU - Lesser General Public License, Version 2.1, the GNU Affero General - Public License, Version 3.0, or any later versions of those - licenses. - -1.13. "Source Code Form" - means the form of the work preferred for making modifications. - -1.14. "You" (or "Your") - means an individual or a legal entity exercising rights under this - License. For legal entities, "You" includes any entity that - controls, is controlled by, or is under common control with You. For - purposes of this definition, "control" means (a) the power, direct - or indirect, to cause the direction or management of such entity, - whether by contract or otherwise, or (b) ownership of more than - fifty percent (50%) of the outstanding shares or beneficial - ownership of such entity. - -2. License Grants and Conditions --------------------------------- - -2.1. Grants - -Each Contributor hereby grants You a world-wide, royalty-free, -non-exclusive license: - -(a) under intellectual property rights (other than patent or trademark) - Licensable by such Contributor to use, reproduce, make available, - modify, display, perform, distribute, and otherwise exploit its - Contributions, either on an unmodified basis, with Modifications, or - as part of a Larger Work; and - -(b) under Patent Claims of such Contributor to make, use, sell, offer - for sale, have made, import, and otherwise transfer either its - Contributions or its Contributor Version. - -2.2. Effective Date - -The licenses granted in Section 2.1 with respect to any Contribution -become effective for each Contribution on the date the Contributor first -distributes such Contribution. - -2.3. Limitations on Grant Scope - -The licenses granted in this Section 2 are the only rights granted under -this License. No additional rights or licenses will be implied from the -distribution or licensing of Covered Software under this License. -Notwithstanding Section 2.1(b) above, no patent license is granted by a -Contributor: - -(a) for any code that a Contributor has removed from Covered Software; - or - -(b) for infringements caused by: (i) Your and any other third party's - modifications of Covered Software, or (ii) the combination of its - Contributions with other software (except as part of its Contributor - Version); or - -(c) under Patent Claims infringed by Covered Software in the absence of - its Contributions. - -This License does not grant any rights in the trademarks, service marks, -or logos of any Contributor (except as may be necessary to comply with -the notice requirements in Section 3.4). - -2.4. Subsequent Licenses - -No Contributor makes additional grants as a result of Your choice to -distribute the Covered Software under a subsequent version of this -License (see Section 10.2) or under the terms of a Secondary License (if -permitted under the terms of Section 3.3). - -2.5. Representation - -Each Contributor represents that the Contributor believes its -Contributions are its original creation(s) or it has sufficient rights -to grant the rights to its Contributions conveyed by this License. - -2.6. Fair Use - -This License is not intended to limit any rights You have under -applicable copyright doctrines of fair use, fair dealing, or other -equivalents. - -2.7. Conditions - -Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted -in Section 2.1. - -3. Responsibilities -------------------- - -3.1. Distribution of Source Form - -All distribution of Covered Software in Source Code Form, including any -Modifications that You create or to which You contribute, must be under -the terms of this License. You must inform recipients that the Source -Code Form of the Covered Software is governed by the terms of this -License, and how they can obtain a copy of this License. You may not -attempt to alter or restrict the recipients' rights in the Source Code -Form. - -3.2. Distribution of Executable Form - -If You distribute Covered Software in Executable Form then: - -(a) such Covered Software must also be made available in Source Code - Form, as described in Section 3.1, and You must inform recipients of - the Executable Form how they can obtain a copy of such Source Code - Form by reasonable means in a timely manner, at a charge no more - than the cost of distribution to the recipient; and - -(b) You may distribute such Executable Form under the terms of this - License, or sublicense it under different terms, provided that the - license for the Executable Form does not attempt to limit or alter - the recipients' rights in the Source Code Form under this License. - -3.3. Distribution of a Larger Work - -You may create and distribute a Larger Work under terms of Your choice, -provided that You also comply with the requirements of this License for -the Covered Software. If the Larger Work is a combination of Covered -Software with a work governed by one or more Secondary Licenses, and the -Covered Software is not Incompatible With Secondary Licenses, this -License permits You to additionally distribute such Covered Software -under the terms of such Secondary License(s), so that the recipient of -the Larger Work may, at their option, further distribute the Covered -Software under the terms of either this License or such Secondary -License(s). - -3.4. Notices - -You may not remove or alter the substance of any license notices -(including copyright notices, patent notices, disclaimers of warranty, -or limitations of liability) contained within the Source Code Form of -the Covered Software, except that You may alter any license notices to -the extent required to remedy known factual inaccuracies. - -3.5. Application of Additional Terms - -You may choose to offer, and to charge a fee for, warranty, support, -indemnity or liability obligations to one or more recipients of Covered -Software. However, You may do so only on Your own behalf, and not on -behalf of any Contributor. You must make it absolutely clear that any -such warranty, support, indemnity, or liability obligation is offered by -You alone, and You hereby agree to indemnify every Contributor for any -liability incurred by such Contributor as a result of warranty, support, -indemnity or liability terms You offer. You may include additional -disclaimers of warranty and limitations of liability specific to any -jurisdiction. - -4. Inability to Comply Due to Statute or Regulation ---------------------------------------------------- - -If it is impossible for You to comply with any of the terms of this -License with respect to some or all of the Covered Software due to -statute, judicial order, or regulation then You must: (a) comply with -the terms of this License to the maximum extent possible; and (b) -describe the limitations and the code they affect. Such description must -be placed in a text file included with all distributions of the Covered -Software under this License. Except to the extent prohibited by statute -or regulation, such description must be sufficiently detailed for a -recipient of ordinary skill to be able to understand it. - -5. Termination --------------- - -5.1. The rights granted under this License will terminate automatically -if You fail to comply with any of its terms. However, if You become -compliant, then the rights granted under this License from a particular -Contributor are reinstated (a) provisionally, unless and until such -Contributor explicitly and finally terminates Your grants, and (b) on an -ongoing basis, if such Contributor fails to notify You of the -non-compliance by some reasonable means prior to 60 days after You have -come back into compliance. Moreover, Your grants from a particular -Contributor are reinstated on an ongoing basis if such Contributor -notifies You of the non-compliance by some reasonable means, this is the -first time You have received notice of non-compliance with this License -from such Contributor, and You become compliant prior to 30 days after -Your receipt of the notice. - -5.2. If You initiate litigation against any entity by asserting a patent -infringement claim (excluding declaratory judgment actions, -counter-claims, and cross-claims) alleging that a Contributor Version -directly or indirectly infringes any patent, then the rights granted to -You by any and all Contributors for the Covered Software under Section -2.1 of this License shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all -end user license agreements (excluding distributors and resellers) which -have been validly granted by You or Your distributors under this License -prior to termination shall survive termination. - -************************************************************************ -* * -* 6. Disclaimer of Warranty * -* ------------------------- * -* * -* Covered Software is provided under this License on an "as is" * -* basis, without warranty of any kind, either expressed, implied, or * -* statutory, including, without limitation, warranties that the * -* Covered Software is free of defects, merchantable, fit for a * -* particular purpose or non-infringing. The entire risk as to the * -* quality and performance of the Covered Software is with You. * -* Should any Covered Software prove defective in any respect, You * -* (not any Contributor) assume the cost of any necessary servicing, * -* repair, or correction. This disclaimer of warranty constitutes an * -* essential part of this License. No use of any Covered Software is * -* authorized under this License except under this disclaimer. * -* * -************************************************************************ - -************************************************************************ -* * -* 7. Limitation of Liability * -* -------------------------- * -* * -* Under no circumstances and under no legal theory, whether tort * -* (including negligence), contract, or otherwise, shall any * -* Contributor, or anyone who distributes Covered Software as * -* permitted above, be liable to You for any direct, indirect, * -* special, incidental, or consequential damages of any character * -* including, without limitation, damages for lost profits, loss of * -* goodwill, work stoppage, computer failure or malfunction, or any * -* and all other commercial damages or losses, even if such party * -* shall have been informed of the possibility of such damages. This * -* limitation of liability shall not apply to liability for death or * -* personal injury resulting from such party's negligence to the * -* extent applicable law prohibits such limitation. Some * -* jurisdictions do not allow the exclusion or limitation of * -* incidental or consequential damages, so this exclusion and * -* limitation may not apply to You. * -* * -************************************************************************ - -8. Litigation -------------- - -Any litigation relating to this License may be brought only in the -courts of a jurisdiction where the defendant maintains its principal -place of business and such litigation shall be governed by laws of that -jurisdiction, without reference to its conflict-of-law provisions. -Nothing in this Section shall prevent a party's ability to bring -cross-claims or counter-claims. - -9. Miscellaneous ----------------- - -This License represents the complete agreement concerning the subject -matter hereof. If any provision of this License is held to be -unenforceable, such provision shall be reformed only to the extent -necessary to make it enforceable. Any law or regulation which provides -that the language of a contract shall be construed against the drafter -shall not be used to construe this License against a Contributor. - -10. Versions of the License ---------------------------- - -10.1. New Versions - -Mozilla Foundation is the license steward. Except as provided in Section -10.3, no one other than the license steward has the right to modify or -publish new versions of this License. Each version will be given a -distinguishing version number. - -10.2. Effect of New Versions - -You may distribute the Covered Software under the terms of the version -of the License under which You originally received the Covered Software, -or under the terms of any subsequent version published by the license -steward. - -10.3. Modified Versions - -If you create software not governed by this License, and you want to -create a new license for such software, you may create and use a -modified version of this License if you rename the license and remove -any references to the name of the license steward (except to note that -such modified license differs from this License). - -10.4. Distributing Source Code Form that is Incompatible With Secondary -Licenses - -If You choose to distribute Source Code Form that is Incompatible With -Secondary Licenses under the terms of this version of the License, the -notice described in Exhibit B of this License must be attached. - -Exhibit A - Source Code Form License Notice -------------------------------------------- - - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at https://mozilla.org/MPL/2.0/. - -If it is not possible or desirable to put the notice in a particular -file, then You may include the notice in a location (such as a LICENSE -file in a relevant directory) where a recipient would be likely to look -for such a notice. - -You may add additional accurate notices of copyright ownership. - -Exhibit B - "Incompatible With Secondary Licenses" Notice ---------------------------------------------------------- - - This Source Code Form is "Incompatible With Secondary Licenses", as - defined by the Mozilla Public License, v. 2.0. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.md b/README.md index 2bd8c81..90c97d4 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,26 @@ -# Koto Qt +# Koto -Work in progress implementation of Koto in Qt6, Kirigami, and C++. +![koto alpha](https://raw.githubusercontent.com/JoshStrobl/koto/master/.github/koto-alpha-image.png) -To clone this repository on [Radicle](https://radicle.xyz), simple run: +Koto is an in-development audiobook, music, and podcast manager that is designed *for* and caters *to* a modern desktop +Linux experience. **Nothing to see here yet**. -``` -rad clone rad://z2RuqdobvbGje3mWhtMQ9cUVNfizp -``` +## Blog -## LICENSE +- [Dev Diary 13: Koto - The Resurrection](https://joshuastrobl.com/2024/11/01/dev-diary-13-koto-the-resurrection) +- [Dev Diary 12: Koto August Progress Report](https://joshuastrobl.com/2021/09/06/dev-diary-12-koto-august-progress-report) +- [Dev Diary 11: Koto July Progress Report](https://joshuastrobl.com/2021/08/08/dev-diary-11-koto-july-progress-report/) +- [Dev Diary 10: Koto June Progress Report](https://joshuastrobl.com/2021/07/08/dev-diary-10-koto-june-progress-report/) +- [Dev Diary 9: Koto May Progress Report (B-side)](https://joshuastrobl.com/2021/06/10/dev-diary-9-koto-may-progress-report-b-side/) +- [Dev Diary 8: Koto May Progress Report (A-side)](https://joshuastrobl.com/2021/05/27/dev-diary-8-koto-may-progress-report-a-side/) +- [Dev Diary 7: Koto April Progress Report (B-side)](https://joshuastrobl.com/2021/05/07/dev-diary-7-koto-april-progress-report-b-side/) +- [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/) +- [Dev Diary 1: Koto - Foundations](https://joshuastrobl.com/2021/01/25/dev-diary-1-koto-foundations/) -Licensed under the Mozilla Public License 2.0 (MPL-2.0). +## License + +Koto is licensed under the Apache 2.0 license. diff --git a/Taskfile.yml b/Taskfile.yml deleted file mode 100644 index 674989e..0000000 --- a/Taskfile.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: "3" - -tasks: - sdbus-gen-desktop: - cmds: - - sdbus-c++-xml2cpp desktop/dbus/schema.xml --adaptor=desktop/dbus/daemon-server.h --proxy=desktop/dbus/daemon-client.h - setup-desktop: - desc: "Run cmake configuration for desktop Koto" - cmds: - - cmake -S . -B build - build: cmake --build build - build-watch-desktop: watchman-make -p '**/*.cpp' '**/*.h' --run "task cook-desktop" - cook-desktop: - cmds: - - task setup-desktop - - task build - install: sudo make install -C build diff --git a/build-aux/meson/postinstall.py b/build-aux/meson/postinstall.py new file mode 100755 index 0000000..cfa1e2d --- /dev/null +++ b/build-aux/meson/postinstall.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 + +from os import environ, path +from subprocess import call + +prefix = environ.get('MESON_INSTALL_PREFIX', '/usr/local') +datadir = path.join(prefix, 'share') +destdir = environ.get('DESTDIR', '') + +# Package managers set this so we don't need to run +if not destdir: + print('Updating desktop database...') + call(['update-desktop-database', '-q', path.join(datadir, 'applications')]) + diff --git a/com.github.joshstrobl.koto.json b/com.github.joshstrobl.koto.json new file mode 100644 index 0000000..2d66e1e --- /dev/null +++ b/com.github.joshstrobl.koto.json @@ -0,0 +1,40 @@ +{ + "app-id" : "com.github.joshstrobl.koto", + "runtime" : "org.gnome.Platform", + "runtime-version" : "40", + "sdk" : "org.gnome.Sdk", + "command" : "com.github.joshstrobl.koto", + "finish-args" : [ + "--share=network", + "--share=ipc", + "--socket=fallback-x11", + "--socket=wayland" + ], + "cleanup" : [ + "/include", + "/lib/pkgconfig", + "/man", + "/share/doc", + "/share/gtk-doc", + "/share/man", + "/share/pkgconfig", + "*.la", + "*.a" + ], + "modules" : [ + { + "name" : "koto", + "builddir" : true, + "buildsystem" : "meson", + "sources" : [ + { + "type" : "git", + "url" : "https://github.com/JoshStrobl/koto.git" + } + ] + } + ], + "build-options" : { + "env" : { } + } +} diff --git a/data/com.github.joshstrobl.koto.appdata.xml.in b/data/com.github.joshstrobl.koto.appdata.xml.in new file mode 100644 index 0000000..04164f4 --- /dev/null +++ b/data/com.github.joshstrobl.koto.appdata.xml.in @@ -0,0 +1,33 @@ + + + com.github.joshstrobl.koto.desktop + CC0-1.0 + Apache-2.0 + Koto + Koto is an in-development audiobook, music, and podcast manager. + +

Koto is an in-development audiobook, music, and podcast manager that is designed for and caters to a modern desktop Linux experience.

+
+ com.github.joshstrobl.koto.desktop + https://github.com/JoshStrobl/koto + https://github.com/JoshStrobl/koto/issues + https://patreon.com/joshuastrobl + https://liberapay.com/joshuastrobl + Joshua Strobl + joshua.strobl_AT_outlook.com + + mild + + + com.github.joshstrobl.koto + + + pointing + touch + 1600 + + + keyboard + 1366 + +
diff --git a/KotoQt/data/com.github.joshstrobl.koto.desktop b/data/com.github.joshstrobl.koto.desktop.in similarity index 100% rename from KotoQt/data/com.github.joshstrobl.koto.desktop rename to data/com.github.joshstrobl.koto.desktop.in diff --git a/data/com.github.joshstrobl.koto.gschema.xml b/data/com.github.joshstrobl.koto.gschema.xml new file mode 100644 index 0000000..7ff368a --- /dev/null +++ b/data/com.github.joshstrobl.koto.gschema.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/data/genres/business-and-personal-finance.png b/data/genres/business-and-personal-finance.png new file mode 100644 index 0000000..789aadb Binary files /dev/null and b/data/genres/business-and-personal-finance.png differ diff --git a/data/genres/foreign-languages.png b/data/genres/foreign-languages.png new file mode 100644 index 0000000..5499f4e Binary files /dev/null and b/data/genres/foreign-languages.png differ diff --git a/data/genres/mystery-and-thriller.png b/data/genres/mystery-and-thriller.png new file mode 100644 index 0000000..d42cded Binary files /dev/null and b/data/genres/mystery-and-thriller.png differ diff --git a/data/genres/sci-fi.png b/data/genres/sci-fi.png new file mode 100644 index 0000000..959b6c8 Binary files /dev/null and b/data/genres/sci-fi.png differ diff --git a/data/genres/travel.png b/data/genres/travel.png new file mode 100644 index 0000000..41ba389 Binary files /dev/null and b/data/genres/travel.png differ diff --git a/data/meson.build b/data/meson.build new file mode 100644 index 0000000..99a118c --- /dev/null +++ b/data/meson.build @@ -0,0 +1,41 @@ +desktop_file = i18n.merge_file( + input: 'com.github.joshstrobl.koto.desktop.in', + output: 'com.github.joshstrobl.koto.desktop', + type: 'desktop', + po_dir: '../po', + install: true, + install_dir: join_paths(get_option('datadir'), 'applications') +) + +desktop_utils = find_program('desktop-file-validate', required: false) +if desktop_utils.found() + test('Validate desktop file', desktop_utils, + args: [desktop_file] + ) +endif + +appstream_file = i18n.merge_file( + input: 'com.github.joshstrobl.koto.appdata.xml.in', + output: 'com.github.joshstrobl.koto.appdata.xml', + po_dir: '../po', + install: true, + install_dir: join_paths(get_option('datadir'), 'appdata') +) + +appstream_util = find_program('appstream-util', required: false) +if appstream_util.found() + test('Validate appstream file (relaxed)', appstream_util, + args: ['validate-relax', appstream_file] + ) +endif + +install_data('com.github.joshstrobl.koto.gschema.xml', + install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas') +) + +compile_schemas = find_program('glib-compile-schemas', required: false) +if compile_schemas.found() + test('Validate schema file', compile_schemas, + args: ['--strict', '--dry-run', meson.current_source_dir()] + ) +endif diff --git a/data/vectors/business-and-personal-finance.svg b/data/vectors/business-and-personal-finance.svg new file mode 100644 index 0000000..7bb925c --- /dev/null +++ b/data/vectors/business-and-personal-finance.svg @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/vectors/foreign-languages.svg b/data/vectors/foreign-languages.svg new file mode 100644 index 0000000..c423fb9 --- /dev/null +++ b/data/vectors/foreign-languages.svg @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + Bonjour + Hello + + + + diff --git a/data/vectors/multimedia-backwards-jump.svg b/data/vectors/multimedia-backwards-jump.svg new file mode 100644 index 0000000..f5c9016 --- /dev/null +++ b/data/vectors/multimedia-backwards-jump.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/data/vectors/multimedia-forwards-jump.svg b/data/vectors/multimedia-forwards-jump.svg new file mode 100644 index 0000000..9db39bf --- /dev/null +++ b/data/vectors/multimedia-forwards-jump.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/data/vectors/mystery-and-thriller.svg b/data/vectors/mystery-and-thriller.svg new file mode 100644 index 0000000..9b7dad0 --- /dev/null +++ b/data/vectors/mystery-and-thriller.svg @@ -0,0 +1,572 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/vectors/sci-fi.svg b/data/vectors/sci-fi.svg new file mode 100644 index 0000000..5b4161e --- /dev/null +++ b/data/vectors/sci-fi.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + diff --git a/data/vectors/travel.svg b/data/vectors/travel.svg new file mode 100644 index 0000000..d70a1bc --- /dev/null +++ b/data/vectors/travel.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/desktop/CMakeLists.txt b/desktop/CMakeLists.txt deleted file mode 100644 index 4dd9a9a..0000000 --- a/desktop/CMakeLists.txt +++ /dev/null @@ -1,49 +0,0 @@ -find_package(Qt6 6.4 REQUIRED COMPONENTS Quick QuickControls2 Sql) -find_package(ECM REQUIRED NO_MODULE) -find_package(KF6Baloo) -find_package(KF6FileMetaData) -set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake) - -include(KDEInstallDirs) -include(ECMFindQmlModule) -include(ECMQmlModule) - -qt_standard_project_setup() - -qt_add_executable(com.github.joshstrobl.koto - config/config.cpp - config/library.cpp - config/ui_prefs.cpp - datalake/album.cpp - datalake/artist.cpp - datalake/cartographer.cpp - datalake/database.cpp - datalake/indexer.cpp - datalake/models.cpp - datalake/track.cpp - datalake/cartographer.hpp - datalake/structs.hpp - main.cpp - datalake/models.cpp -) - -target_include_directories(com.github.joshstrobl.koto PUBLIC datalake includes) -ecm_add_qml_module(com.github.joshstrobl.koto URI "com.github.joshstrobl.koto" GENERATE_PLUGIN_SOURCE) - -ecm_target_qml_sources(com.github.joshstrobl.koto - SOURCES - qml/PlayerBar/PlayerBar.qml - qml/PrimaryNavigation.qml - qml/HomePage.qml - qml/Main.qml - qml/Root.qml -) - -target_link_libraries(com.github.joshstrobl.koto - PRIVATE KF6::Baloo KF6::FileMetaData Qt6::Quick Qt6::QuickControls2 Qt6::Sql -) - -install(FILES com.github.joshstrobl.koto.desktop DESTINATION ${KDE_INSTALL_APPDIR}) -install(TARGETS com.github.joshstrobl.koto ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) - -ecm_finalize_qml_module(com.github.joshstrobl.koto) diff --git a/desktop/config/config.cpp b/desktop/config/config.cpp deleted file mode 100644 index 8fb1adb..0000000 --- a/desktop/config/config.cpp +++ /dev/null @@ -1,98 +0,0 @@ -#include "config.hpp" - -#include -#include -#include -#include - -namespace fs = std::filesystem; - -KotoConfig::KotoConfig() { - // Define our application's config location - auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::StandardLocation::AppConfigLocation)); - auto configDirPath = configDir.absolutePath(); - this->i_configDirPath = configDirPath; - this->i_libraries = QList(); - - fs::path filePath {}; - auto configPathStd = configDirPath.toStdString(); - filePath /= configPathStd; - filePath /= "config.toml"; - this->i_configPath = QString {filePath.c_str()}; - - if (QFileInfo::exists(i_configPath)) { - this->parseConfigFile(filePath); - } else { - this->bootstrap(); - } -} - -KotoConfig& KotoConfig::instance() { - static KotoConfig _instance; - return _instance; -} - -void KotoConfig::bootstrap() { - this->i_uiPreferences = new KotoUiPreferences(); - - auto musicDir = QDir(QStandardPaths::writableLocation(QStandardPaths::StandardLocation::MusicLocation)); - auto musicLibrary = new KotoLibraryConfig("Music", musicDir.absolutePath().toStdString(), KotoLibraryType::Music); - - this->i_libraries.append(musicLibrary); - - this->save(); -} - -QString KotoConfig::getConfigDirPath() { - return QString {this->i_configDirPath}; -} - -KotoUiPreferences* KotoConfig::getUiPreferences() { - return this->i_uiPreferences; -} - -QList KotoConfig::getLibraries() { - return this->i_libraries; -} - -void KotoConfig::parseConfigFile(std::string filePath) { - auto data = toml::parse(filePath); - std::optional ui_prefs; - - if (data.contains("preferences.ui")) { - auto ui_prefs_at = data.at("preferences.ui"); - if (ui_prefs_at.is_table()) ui_prefs = ui_prefs_at.as_table(); - } - - auto prefs = new KotoUiPreferences(ui_prefs); - this->i_uiPreferences = prefs; - - for (const auto& lib_value : toml::find>(data, "libraries")) { - auto lib = new KotoLibraryConfig(lib_value); - this->i_libraries.append(lib); - } -} - -void KotoConfig::save() { - toml::ordered_value config_table(toml::ordered_table {}); - config_table["preferences.ui"] = this->i_uiPreferences->serialize(); - - toml::ordered_value libraries_array(toml::ordered_array {}); - for (auto lib : this->i_libraries) { - auto lib_table = lib->serialize(); - libraries_array.push_back(lib_table); - } - config_table["libraries"] = libraries_array; - - auto configContent = toml::format(config_table); - - auto config_dir = QDir {this->i_configDirPath}; - if (!config_dir.exists()) config_dir.mkpath("."); - auto config_file = QFile {this->i_configPath}; - - auto out = QTextStream {&config_file}; - if (config_file.open(QIODevice::WriteOnly | QIODevice::Text)) { - out << configContent.c_str(); - config_file.close(); - } -} diff --git a/desktop/config/config.hpp b/desktop/config/config.hpp deleted file mode 100644 index 1acb3ce..0000000 --- a/desktop/config/config.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include -#include - -#include "library.hpp" -#include "ui_prefs.hpp" - -class KotoConfig { - public: - KotoConfig(); - static KotoConfig& instance(); - static KotoConfig* create() { return &instance(); } - void save(); - - QString getConfigDirPath(); - QList getLibraries(); - KotoUiPreferences* getUiPreferences(); - - private: - void bootstrap(); - void parseConfigFile(std::string filePath); - - QString i_configDirPath; - QString i_configPath; - QList i_libraries; - KotoUiPreferences* i_uiPreferences; -}; diff --git a/desktop/config/library.cpp b/desktop/config/library.cpp deleted file mode 100644 index 3022366..0000000 --- a/desktop/config/library.cpp +++ /dev/null @@ -1,60 +0,0 @@ -#include "library.hpp" - -#include -#include - -KotoLibraryConfig::KotoLibraryConfig(std::string name, fs::path path, KotoLibraryType type) { - this->i_name = name; - this->i_path = path; - this->i_type = type; - qDebug() << "Library: " << this->i_name.c_str() << " at " << this->i_path.c_str(); -} - -KotoLibraryConfig::~KotoLibraryConfig() {} - -KotoLibraryConfig::KotoLibraryConfig(const toml::value& v) { - this->i_name = toml::find(v, "name"); - this->i_path = toml::find(v, "path"); - this->i_type = libraryTypeFromString(toml::find(v, "type")); -} - -std::string KotoLibraryConfig::getName() { - return this->i_name; -} - -fs::path KotoLibraryConfig::getPath() { - return this->i_path; -} - -KotoLibraryType KotoLibraryConfig::getType() { - return this->i_type; -} - -toml::ordered_value KotoLibraryConfig::serialize() { - toml::ordered_value library_table(toml::ordered_table {}); - library_table["name"] = this->i_name; - library_table["path"] = this->i_path.string(); - auto stringifiedType = libraryTypeToString(this->i_type); - library_table["type"] = stringifiedType; - return library_table; -} - -std::string libraryTypeToString(KotoLibraryType type) { - switch (type) { - case KotoLibraryType::Audiobooks: - return std::string {"audiobooks"}; - case KotoLibraryType::Music: - return std::string {"music"}; - case KotoLibraryType::Podcasts: - return std::string {"podcasts"}; - default: - return std::string {"unknown"}; - } -} - -KotoLibraryType libraryTypeFromString(const std::string& type) { - if (type == "audiobooks") return KotoLibraryType::Audiobooks; - if (type == "music") return KotoLibraryType::Music; - if (type == "podcasts") return KotoLibraryType::Podcasts; - throw std::invalid_argument("Unknown KotoLibraryType: " + type); -} diff --git a/desktop/config/library.hpp b/desktop/config/library.hpp deleted file mode 100644 index bfbb16c..0000000 --- a/desktop/config/library.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once -#include -#include - -#include "includes/toml.hpp" - -namespace fs = std::filesystem; - -enum class KotoLibraryType { - Audiobooks, - Music, - Podcasts, -}; - -KotoLibraryType libraryTypeFromString(const std::string& type); -std::string libraryTypeToString(KotoLibraryType type); - -class KotoLibraryConfig { - public: - KotoLibraryConfig(std::string name, fs::path path, KotoLibraryType type); - KotoLibraryConfig(const toml::value& v); - ~KotoLibraryConfig(); - std::string getName(); - fs::path getPath(); - KotoLibraryType getType(); - toml::ordered_value serialize(); - - private: - std::string i_name; - fs::path i_path; - KotoLibraryType i_type; -}; diff --git a/desktop/config/ui_prefs.cpp b/desktop/config/ui_prefs.cpp deleted file mode 100644 index 24bad2f..0000000 --- a/desktop/config/ui_prefs.cpp +++ /dev/null @@ -1,71 +0,0 @@ -#include "ui_prefs.hpp" - -KotoUiPreferences::KotoUiPreferences() - : i_albumInfoShowDescription(true), i_albumInfoShowGenre(true), i_albumInfoShowNarrator(true), i_albumInfoShowYear(true), i_lastUsedVolume(0.5) {} - -KotoUiPreferences::KotoUiPreferences(std::optional v) { - // No UI prefs provided - if (!v.has_value()) return; - toml::value& uiPrefs = v.value(); - auto showDescription = toml::find_or(uiPrefs, "album_info_show_description", false); - auto showGenre = toml::find_or(uiPrefs, "album_info_show_genre", false); - auto showNarrator = toml::find_or(uiPrefs, "album_info_show_narrator", false); - auto showYear = toml::find_or(uiPrefs, "album_info_show_year", false); - auto lastUsedVolume = toml::find_or(uiPrefs, "last_used_volume", 0.5); - - this->setAlbumInfoShowDescription(showDescription); - this->setAlbumInfoShowGenre(showGenre); - this->setAlbumInfoShowNarrator(showNarrator); - this->setAlbumInfoShowYear(showYear); - this->setLastUsedVolume(lastUsedVolume); -} - -bool KotoUiPreferences::getAlbumInfoShowDescription() { - return this->i_albumInfoShowDescription; -} - -bool KotoUiPreferences::getAlbumInfoShowGenre() { - return this->i_albumInfoShowGenre; -} - -bool KotoUiPreferences::getAlbumInfoShowNarrator() { - return this->i_albumInfoShowNarrator; -} - -bool KotoUiPreferences::getAlbumInfoShowYear() { - return this->i_albumInfoShowYear; -} - -float KotoUiPreferences::getLastUsedVolume() { - return this->i_lastUsedVolume; -} - -toml::ordered_value KotoUiPreferences::serialize() { - toml::ordered_value ui_prefs_table(toml::ordered_table {}); - ui_prefs_table["album_info_show_description"] = this->i_albumInfoShowDescription; - ui_prefs_table["album_info_show_genre"] = this->i_albumInfoShowGenre; - ui_prefs_table["album_info_show_narrator"] = this->i_albumInfoShowNarrator; - ui_prefs_table["album_info_show_year"] = this->i_albumInfoShowYear; - ui_prefs_table["last_used_volume"] = this->i_lastUsedVolume; - return ui_prefs_table; -} - -void KotoUiPreferences::setAlbumInfoShowDescription(bool show) { - this->i_albumInfoShowDescription = show; -} - -void KotoUiPreferences::setAlbumInfoShowGenre(bool show) { - this->i_albumInfoShowGenre = show; -} - -void KotoUiPreferences::setAlbumInfoShowNarrator(bool show) { - this->i_albumInfoShowNarrator = show; -} - -void KotoUiPreferences::setAlbumInfoShowYear(bool show) { - this->i_albumInfoShowYear = show; -} - -void KotoUiPreferences::setLastUsedVolume(float volume) { - this->i_lastUsedVolume = volume; -} diff --git a/desktop/config/ui_prefs.hpp b/desktop/config/ui_prefs.hpp deleted file mode 100644 index a5a5978..0000000 --- a/desktop/config/ui_prefs.hpp +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once -#include -#include -#include -#include - -#include "includes/toml.hpp" - -class KotoUiPreferences { - public: - KotoUiPreferences(); - KotoUiPreferences(std::optional v); - ~KotoUiPreferences(); - - bool getAlbumInfoShowDescription(); - bool getAlbumInfoShowGenre(); - bool getAlbumInfoShowNarrator(); - bool getAlbumInfoShowYear(); - float getLastUsedVolume(); - - toml::ordered_value serialize(); - - void setAlbumInfoShowDescription(bool show); - void setAlbumInfoShowGenre(bool show); - void setAlbumInfoShowNarrator(bool show); - void setAlbumInfoShowYear(bool show); - void setLastUsedVolume(float volume); - - private: - bool i_albumInfoShowDescription; - bool i_albumInfoShowGenre; - bool i_albumInfoShowNarrator; - bool i_albumInfoShowYear; - float i_lastUsedVolume; -}; diff --git a/desktop/datalake/album.cpp b/desktop/datalake/album.cpp deleted file mode 100644 index 3e80def..0000000 --- a/desktop/datalake/album.cpp +++ /dev/null @@ -1,119 +0,0 @@ -#include - -#include "database.hpp" -#include "structs.hpp" - -KotoAlbum::KotoAlbum() { - this->uuid = QUuid::createUuid(); - this->tracks = QList(); -} - -KotoAlbum* KotoAlbum::fromDb(const QSqlQuery& query, const QSqlRecord& record) { - KotoAlbum* album = new KotoAlbum(); - album->uuid = QUuid {query.value(record.indexOf("id")).toString()}; - album->artist_uuid = QUuid {query.value(record.indexOf("artist_id")).toString()}; - album->title = QString {query.value(record.indexOf("name")).toString()}; - album->year = query.value(record.indexOf("year")).toInt(); - album->description = QString {query.value(record.indexOf("description")).toString()}; - album->narrator = QString {query.value(record.indexOf("narrator")).toString()}; - album->album_art_path = QString {query.value(record.indexOf("art_path")).toString()}; - album->genres = QList {query.value(record.indexOf("genres")).toString().split(", ")}; - return album; -} - -KotoAlbum::~KotoAlbum() { - for (auto track : this->tracks) { delete track; } - this->tracks.clear(); -} - -void KotoAlbum::addTrack(KotoTrack* track) { - this->tracks.append(track); -} - -void KotoAlbum::commit() { - QSqlQuery query(KotoDatabase::instance().getDatabase()); - - query.prepare( - "INSERT INTO albums(id, artist_id, name, description, narrator, art_path, genres, year) " - "VALUES (:id, :artist_id, :name, :description, :narrator, :art_path, :genres, :year) " - "ON CONFLICT(id) DO UPDATE SET artist_id = :artist_id, name = :name, description = :description, narrator = :narrator, art_path = " - ":art_path, genres = :genres, year = :year"); - - query.bindValue(":id", this->uuid.toString()); - query.bindValue(":artist_id", this->artist_uuid.toString()); - query.bindValue(":name", this->title); - query.bindValue(":year", this->year.value_or(NULL)); - query.bindValue(":description", this->description); - query.bindValue(":art_path", this->album_art_path); - query.bindValue(":narrator", this->narrator); - query.bindValue(":genres", this->genres.join(", ")); - query.exec(); -} - -QString KotoAlbum::getAlbumArtPath() { - return QString {this->album_art_path}; -} - -QString KotoAlbum::getDescription() { - return QString {this->description}; -} - -QList KotoAlbum::getGenres() { - return QList {this->genres}; -} - -QString KotoAlbum::getPath() { - return this->path; -} - -QString KotoAlbum::getNarrator() { - return QString {this->narrator}; -} - -QString KotoAlbum::getTitle() { - return QString {this->title}; -} - -QList KotoAlbum::getTracks() { - return QList {this->tracks}; -} - -std::optional KotoAlbum::getYear() { - return this->year; -} - -int KotoAlbum::getYearQml() { - return this->year.value_or(0); -} - -void KotoAlbum::removeTrack(KotoTrack* track) { - this->tracks.removeOne(track); -} - -void KotoAlbum::setAlbumArtPath(QString str) { - this->album_art_path = QString {path}; -} - -void KotoAlbum::setDescription(QString str) { - this->description = QString {str}; -} - -void KotoAlbum::setGenres(QList list) { - this->genres = QList {list}; -} - -void KotoAlbum::setNarrator(QString str) { - this->narrator = QString {str}; -} - -void KotoAlbum::setPath(QString str) { - this->path = QString {str}; -} - -void KotoAlbum::setTitle(QString str) { - this->title = QString {str}; -} - -void KotoAlbum::setYear(int num) { - this->year = num; -} diff --git a/desktop/datalake/artist.cpp b/desktop/datalake/artist.cpp deleted file mode 100644 index 45dd72f..0000000 --- a/desktop/datalake/artist.cpp +++ /dev/null @@ -1,91 +0,0 @@ -#include - -#include "database.hpp" -#include "structs.hpp" - -KotoArtist::KotoArtist() { - this->uuid = QUuid::createUuid(); -} - -KotoArtist* KotoArtist::fromDb(const QSqlQuery& query, const QSqlRecord& record) { - KotoArtist* artist = new KotoArtist(); - artist->uuid = QUuid {query.value(record.indexOf("id")).toString()}; - artist->name = QString {query.value(record.indexOf("name")).toString()}; - artist->path = QString {query.value(record.indexOf("art_path")).toString()}; - return artist; -} - -KotoArtist::~KotoArtist() { - for (auto album : this->albums) { delete album; } - for (auto track : this->tracks) { delete track; } - this->albums.clear(); - this->tracks.clear(); -} - -void KotoArtist::addAlbum(KotoAlbum* album) { - this->albums.append(album); -} - -void KotoArtist::addTrack(KotoTrack* track) { - this->tracks.append(track); - if (!track->album_uuid.has_value()) return; - for (auto album : this->albums) { - if (album->uuid == track->album_uuid.value()) { - album->addTrack(track); - return; - } - } -} - -void KotoArtist::commit() { - QSqlQuery query(KotoDatabase::instance().getDatabase()); - query.prepare("INSERT INTO artists(id, name, art_path) VALUES (:id, :name, :art_path) ON CONFLICT(id) DO UPDATE SET name = :name, art_path = :art_path"); - query.bindValue(":id", this->uuid.toString()); - query.bindValue(":name", this->name); - query.bindValue(":art_path", this->path); - query.exec(); -} - -QList KotoArtist::getAlbums() { - return QList {this->albums}; -} - -std::optional KotoArtist::getAlbumByName(QString name) { - for (auto album : this->albums) { - if (album->getTitle().contains(name)) { return std::optional {album}; } - } - - return std::nullopt; -} - -QString KotoArtist::getName() { - return QString {this->name}; -} - -QString KotoArtist::getPath() { - return QString {this->path}; -} - -QList KotoArtist::getTracks() { - return QList {this->tracks}; -} - -QUuid KotoArtist::getUuid() { - return this->uuid; -} - -void KotoArtist::removeAlbum(KotoAlbum* album) { - this->albums.removeOne(album); -} - -void KotoArtist::removeTrack(KotoTrack* track) { - this->tracks.removeOne(track); -} - -void KotoArtist::setName(QString str) { - this->name = QString {str}; -} - -void KotoArtist::setPath(QString str) { - this->path = QString {str}; -} diff --git a/desktop/datalake/cartographer.cpp b/desktop/datalake/cartographer.cpp deleted file mode 100644 index 7f47b5e..0000000 --- a/desktop/datalake/cartographer.cpp +++ /dev/null @@ -1,63 +0,0 @@ -#include "cartographer.hpp" - -#include - -Cartographer::Cartographer(QObject* parent) - : QObject(parent), - i_albums(QHash()), - i_artists_model(new KotoArtistModel(QList())), - i_artists_by_name(QHash()), - i_tracks(QHash()) {} - -Cartographer& Cartographer::instance() { - static Cartographer _instance(nullptr); - return _instance; -} - -void Cartographer::addAlbum(KotoAlbum* album) { - this->i_albums.insert(album->uuid, album); -} - -void Cartographer::addArtist(KotoArtist* artist) { - this->i_artists_model->addArtist(artist); - this->i_artists_by_name.insert(artist->getName(), artist); -} - -void Cartographer::addTrack(KotoTrack* track) { - this->i_tracks.insert(track->uuid, track); -} - -std::optional Cartographer::getAlbum(QUuid uuid) { - auto album = this->i_albums.value(uuid, nullptr); - return album ? std::optional {album} : std::nullopt; -} - -std::optional Cartographer::getArtist(QUuid uuid) { - for (auto artist : this->i_artists_model->getArtists()) { - if (artist->uuid == uuid) { return std::optional {artist}; } - } - return std::nullopt; -} - -QList Cartographer::getArtists() { - return this->i_artists_model->getArtists(); -} - -KotoArtistModel* Cartographer::getArtistsModel() { - // if (this->i_artists_model == nullptr) { this->i_artists_model = new KotoArtistModel(this->i_artists); } - return this->i_artists_model; -} - -std::optional Cartographer::getArtist(QString name) { - auto artist = this->i_artists_by_name.value(name, nullptr); - return artist ? std::optional {artist} : std::nullopt; -} - -std::optional Cartographer::getTrack(QUuid uuid) { - auto track = this->i_tracks.value(uuid, nullptr); - return track ? std::optional {track} : std::nullopt; -} - -QList Cartographer::getTracks() { - return this->i_tracks.values(); -} diff --git a/desktop/datalake/cartographer.hpp b/desktop/datalake/cartographer.hpp deleted file mode 100644 index a7b99fb..0000000 --- a/desktop/datalake/cartographer.hpp +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include -#include - -#include "structs.hpp" - -class Cartographer : public QObject { - Q_OBJECT - QML_ELEMENT - QML_SINGLETON - // Q_PROPERTY(QQmlListProperty albums READ getAlbumsQml) - Q_PROPERTY(KotoArtistModel* artists READ getArtistsModel CONSTANT) - // Q_PROPERTY(QQmlListProperty tracks READ getTracksQml) - - public: - Cartographer(QObject* parent); - static Cartographer& instance(); - static Cartographer* create(QQmlEngine*, QJSEngine*) { - QQmlEngine::setObjectOwnership(&instance(), QQmlEngine::CppOwnership); - return &instance(); - } - - void addAlbum(KotoAlbum* album); - void addArtist(KotoArtist* artist); - void addTrack(KotoTrack* track); - - // QQmlListProperty getAlbumsQml(); - KotoArtistModel* getArtistsModel(); - // QQmlListProperty getTracksQml(); - - std::optional getAlbum(QUuid uuid); - QList getAlbums(); - std::optional getArtist(QUuid uuid); - QList getArtists(); - std::optional getArtist(QString name); - std::optional getTrack(QUuid uuid); - QList getTracks(); - - private: - QHash i_albums; - KotoArtistModel* i_artists_model; - QHash i_artists_by_name; - QHash i_tracks; -}; diff --git a/desktop/datalake/database.cpp b/desktop/datalake/database.cpp deleted file mode 100644 index f470ccd..0000000 --- a/desktop/datalake/database.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "database.hpp" - -#include -#include -#include -#include -#include - -#include "cartographer.hpp" -#include "config/config.hpp" - -KotoDatabase::KotoDatabase() { - QString dbPath = QDir(KotoConfig::instance().getConfigDirPath()).filePath("koto.db"); - this->shouldBootstrap = !QFileInfo::exists(dbPath); - - this->db = QSqlDatabase::addDatabase("QSQLITE"); - std::cout << "Database path: " << dbPath.toStdString() << std::endl; - - this->db.setDatabaseName(dbPath); -} - -KotoDatabase& KotoDatabase::instance() { - static KotoDatabase _instance; - return _instance; -} - -void KotoDatabase::connect() { - if (!this->db.open()) { - std::cerr << "Failed to open database" << std::endl; - QCoreApplication::quit(); - } - - if (this->shouldBootstrap) this->bootstrap(); -} - -void KotoDatabase::disconnect() { - this->db.close(); -} - -QSqlDatabase KotoDatabase::getDatabase() { - return this->db; -} - -bool KotoDatabase::requiredBootstrap() { - return this->shouldBootstrap; -} - -void KotoDatabase::bootstrap() { - QSqlQuery query(this->db); - - query.exec("CREATE TABLE IF NOT EXISTS artists(id string UNIQUE PRIMARY KEY, name string, art_path string);"); - query.exec( - "CREATE TABLE IF NOT EXISTS albums(id string UNIQUE PRIMARY KEY, artist_id string, name string, description string, narrator string, art_path string, " - "genres strings, year int, FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE);"); - query.exec( - "CREATE TABLE IF NOT EXISTS tracks(id string UNIQUE PRIMARY KEY, artist_id string, album_id string, name string, disc int, position int, duration int, " - "genres string, FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE, FOREIGN KEY (album_id) REFERENCES albums(id) ON DELETE CASCADE);"); - query.exec( - "CREATE TABLE IF NOT EXISTS libraries_albums(id string, album_id string, path string, PRIMARY KEY (id, album_id) FOREIGN KEY(album_id) REFERENCES " - "albums(id) " - "ON DELETE CASCADE);"); - query.exec( - "CREATE TABLE IF NOT EXISTS libraries_artists(id string, artist_id string, path string, PRIMARY KEY(id, artist_id) FOREIGN KEY(artist_id) REFERENCES " - "artists(id) ON DELETE CASCADE);"); - query.exec( - "CREATE TABLE IF NOT EXISTS libraries_tracks(id string, track_id string, path string, PRIMARY KEY(id, track_id) FOREIGN KEY(track_id) REFERENCES " - "tracks(id) " - "ON DELETE CASCADE);"); - query.exec( - "CREATE TABLE IF NOT EXISTS playlist_meta(id string UNIQUE PRIMARY KEY, name string, art_path string, preferred_model int, album_id string, track_id " - "string, " - "playback_position_of_track int);"); - query.exec( - "CREATE TABLE IF NOT EXISTS playlist_tracks(position INTEGER PRIMARY KEY AUTOINCREMENT, playlist_id string, track_id string, FOREIGN KEY(playlist_id) " - "REFERENCES playlist_meta(id), FOREIGN KEY(track_id) REFERENCES tracks(id) ON DELETE CASCADE);"); -} - -void KotoDatabase::load() { - QSqlQuery query(this->db); - - query.exec("SELECT * FROM artists;"); - while (query.next()) { - KotoArtist* artist = KotoArtist::fromDb(query, query.record()); - Cartographer::instance().addArtist(artist); - } - - query.exec("SELECT * FROM albums;"); - while (query.next()) { - KotoAlbum* album = KotoAlbum::fromDb(query, query.record()); - auto artist = Cartographer::instance().getArtist(album->artist_uuid); - if (artist.has_value()) { artist.value()->addAlbum(album); } - Cartographer::instance().addAlbum(album); - } - - query.exec("SELECT * FROM tracks;"); - while (query.next()) { - KotoTrack* track = KotoTrack::fromDb(query, query.record()); - auto artist = Cartographer::instance().getArtist(track->artist_uuid); - if (artist.has_value()) { artist.value()->addTrack(track); } - Cartographer::instance().addTrack(track); - } -} diff --git a/desktop/datalake/database.hpp b/desktop/datalake/database.hpp deleted file mode 100644 index b5df848..0000000 --- a/desktop/datalake/database.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include - -class KotoDatabase { - public: - KotoDatabase(); - static KotoDatabase& instance(); - static KotoDatabase* create() { return &instance(); } - - void connect(); - void disconnect(); - QSqlDatabase getDatabase(); - void load(); - bool requiredBootstrap(); - - private: - void bootstrap(); - bool shouldBootstrap; - QSqlDatabase db; -}; diff --git a/desktop/datalake/indexer.cpp b/desktop/datalake/indexer.cpp deleted file mode 100644 index 00cdfdb..0000000 --- a/desktop/datalake/indexer.cpp +++ /dev/null @@ -1,88 +0,0 @@ -#include "indexer.hpp" - -#include -#include -#include -#include -#include - -FileIndexer::FileIndexer(KotoLibraryConfig* config) { - this->i_root = QString {config->getPath().c_str()}; -} - -FileIndexer::~FileIndexer() = default; - -void FileIndexer::index() { - QMimeDatabase db; - KFileMetaData::ExtractorCollection extractors; - - QStringList root_dirs {this->i_root.split(QDir::separator())}; - - QDirIterator it {this->i_root, QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories}; - - while (it.hasNext()) { - QString path = it.next(); - QFileInfo info {path}; - - if (info.isDir()) { - auto diffPath = info.dir().relativeFilePath(this->i_root); - auto diffDirs = diffPath.split(".."); - auto diffDirsSize = diffDirs.size() - 1; - - // This is going to be an artist - if (diffDirsSize == 0) { - auto artist = new KotoArtist(); - artist->setName(info.fileName()); - artist->setPath(path); - artist->commit(); - this->i_artists.append(artist); - Cartographer::instance().addArtist(artist); - continue; - } else if (diffDirsSize == 1) { - auto album = new KotoAlbum(); - album->setTitle(info.fileName()); - - auto artistDir = QDir(info.dir()); - auto artistName = artistDir.dirName(); - auto artistOptional = Cartographer::instance().getArtist(artistName); - - if (artistOptional.has_value()) { - auto artist = artistOptional.value(); - album->artist_uuid = artist->uuid; - artist->addAlbum(album); - } - - album->commit(); - Cartographer::instance().addAlbum(album); - continue; - } - } - - // This is a file - QMimeType mime = db.mimeTypeForFile(info); - if (mime.name().startsWith("audio/")) { - auto extractorList = extractors.fetchExtractors(mime.name()); - if (extractorList.isEmpty()) { continue; } - - auto result = KFileMetaData::SimpleExtractionResult(path, mime.name(), KFileMetaData::ExtractionResult::ExtractMetaData); - extractorList.first()->extract(&result); - - if (!result.types().contains(KFileMetaData::Type::Audio)) { continue; } - - auto track = KotoTrack::fromMetadata(result, info); - - this->i_tracks.append(track); - track->commit(); - Cartographer::instance().addTrack(track); - } else if (mime.name().startsWith("image/")) { - // This is an image, TODO add cover art to album - } - } -} - -void indexAllLibraries() { - for (auto library : KotoConfig::instance().getLibraries()) { - auto indexer = new FileIndexer(library); - indexer->index(); - } -} diff --git a/desktop/datalake/indexer.hpp b/desktop/datalake/indexer.hpp deleted file mode 100644 index 4ae06da..0000000 --- a/desktop/datalake/indexer.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include - -#include "cartographer.hpp" -#include "config/config.hpp" -#include "structs.hpp" - -class FileIndexer { - public: - FileIndexer(KotoLibraryConfig* config); - ~FileIndexer(); - - QList getArtists(); - QList getFiles(); - QString getRoot(); - - void index(); - - protected: - QList i_artists; - QList i_tracks; - QString i_root; -}; - -void indexAllLibraries(); diff --git a/desktop/datalake/models.cpp b/desktop/datalake/models.cpp deleted file mode 100644 index ade5f64..0000000 --- a/desktop/datalake/models.cpp +++ /dev/null @@ -1,49 +0,0 @@ -#include "structs.hpp" - -KotoArtistModel::KotoArtistModel(const QList& artists, QObject* parent) : QAbstractListModel(parent), m_artists(artists) {} - -KotoArtistModel::~KotoArtistModel() { - this->beginResetModel(); - this->m_artists.clear(); - this->endResetModel(); -} - -void KotoArtistModel::addArtist(KotoArtist* artist) { - this->beginInsertRows(QModelIndex(), this->m_artists.count(), this->m_artists.count()); - this->m_artists.append(artist); - this->endInsertRows(); -} - -int KotoArtistModel::rowCount(const QModelIndex& parent) const { - return this->m_artists.count(); -} - -QVariant KotoArtistModel::data(const QModelIndex& index, int role) const { - if (!index.isValid()) { return {}; } - - if (index.row() >= this->m_artists.size()) { return {}; } - - if (role == Qt::DisplayRole) { - return this->m_artists.at(index.row())->getName(); - } else if (role == KotoArtistRoles::NameRole) { - return this->m_artists.at(index.row())->getName(); - } else if (role == KotoArtistRoles::PathRole) { - return this->m_artists.at(index.row())->getPath(); - } else if (role == KotoArtistRoles::UuidRole) { - return this->m_artists.at(index.row())->uuid; - } else { - return {}; - } -} - -QList KotoArtistModel::getArtists() { - return this->m_artists; -} - -QHash KotoArtistModel::roleNames() const { - QHash roles; - roles[NameRole] = QByteArrayLiteral("name"); - roles[PathRole] = QByteArrayLiteral("path"); - roles[UuidRole] = QByteArrayLiteral("uuid"); - return roles; -} diff --git a/desktop/datalake/structs.hpp b/desktop/datalake/structs.hpp deleted file mode 100644 index 07d18ab..0000000 --- a/desktop/datalake/structs.hpp +++ /dev/null @@ -1,227 +0,0 @@ -#pragma once -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -class KotoArtist; -class KotoArtistModel; -class KotoAlbum; -class KotoAlbumModel; -class KotoTrack; -class KotoTrackModel; - -class KotoArtist : public QObject { - Q_OBJECT - QML_ELEMENT - Q_PROPERTY(QString name READ getName WRITE setName NOTIFY nameChanged) - Q_PROPERTY(QString path READ getPath WRITE setPath NOTIFY pathChanged) - Q_PROPERTY(QList albums READ getAlbums NOTIFY albumsChanged) - Q_PROPERTY(QList tracks READ getTracks NOTIFY tracksChanged) - Q_PROPERTY(QUuid uuid READ getUuid) - - public: - KotoArtist(); - static KotoArtist* fromDb(const QSqlQuery& query, const QSqlRecord& record); - virtual ~KotoArtist(); - - QUuid uuid; - - void addAlbum(KotoAlbum* album); - void addTrack(KotoTrack* track); - void commit(); - QList getAlbums(); - std::optional getAlbumByName(QString name); - QString getName(); - QString getPath(); - QList getTracks(); - QUuid getUuid(); - void removeAlbum(KotoAlbum* album); - void removeTrack(KotoTrack* track); - void setName(QString str); - void setPath(QString str); - - signals: - void albumsChanged(); - void nameChanged(); - void pathChanged(); - void tracksChanged(); - - private: - QString path; - QString name; - - QList albums; - QList tracks; -}; - -class KotoArtistModel : public QAbstractListModel { - Q_OBJECT - - public: - explicit KotoArtistModel(const QList& artists, QObject* parent = nullptr); - - void addArtist(KotoArtist* artist); - int rowCount(const QModelIndex& parent = QModelIndex()) const override; - QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - QHash roleNames() const override; - virtual ~KotoArtistModel(); - - QList getArtists(); - - enum KotoArtistRoles { - NameRole = Qt::UserRole + 1, - PathRole, - UuidRole, - }; - - private: - QList m_artists; -}; - -class KotoAlbum : public QObject { - Q_OBJECT - QML_ELEMENT - Q_PROPERTY(QString albumArtPath READ getAlbumArtPath WRITE setAlbumArtPath NOTIFY albumArtChanged) - Q_PROPERTY(QString description READ getDescription WRITE setDescription NOTIFY descriptionChanged) - Q_PROPERTY(QList genres READ getGenres WRITE setGenres NOTIFY genresChanged) - Q_PROPERTY(QString narrator READ getNarrator WRITE setNarrator NOTIFY narratorChanged) - Q_PROPERTY(QString path READ getPath WRITE setPath NOTIFY pathChanged) - Q_PROPERTY(QString title READ getTitle WRITE setTitle NOTIFY titleChanged) - Q_PROPERTY(QList tracks READ getTracks NOTIFY tracksChanged) - Q_PROPERTY(int year READ getYearQml WRITE setYear NOTIFY yearChanged) - - public: - KotoAlbum(); - static KotoAlbum* fromDb(const QSqlQuery& query, const QSqlRecord& record); - virtual ~KotoAlbum(); - - QUuid uuid; - QUuid artist_uuid; - - void commit(); - QString getAlbumArtPath(); - QString getDescription(); - QList getGenres(); - QString getNarrator(); - QString getPath(); - QString getTitle(); - QList getTracks(); - std::optional getYear(); - int getYearQml(); - - void addTrack(KotoTrack* track); - void removeTrack(KotoTrack* track); - void setAlbumArtPath(QString str); - void setDescription(QString str); - void setGenres(QList list); - void setNarrator(QString str); - void setPath(QString str); - void setTitle(QString str); - void setYear(int num); - - signals: - void albumArtChanged(); - void descriptionChanged(); - void genresChanged(); - void narratorChanged(); - void pathChanged(); - void titleChanged(); - void tracksChanged(); - void yearChanged(); - - private: - QString title; - QString description; - QString narrator; - std::optional year; - - QList genres; - QList tracks; - - QString path; - QString album_art_path; -}; - -class KotoTrack : public QObject { - Q_OBJECT - QML_ELEMENT - Q_PROPERTY(QString album_uuid READ getAlbumUuid NOTIFY albumChanged) - Q_PROPERTY(QUuid artist_uuid READ getArtistUuid NOTIFY artistChanged) - Q_PROPERTY(QUuid uuid READ getUuid) - Q_PROPERTY(int discNumber READ getDiscNumber WRITE setDiscNumber NOTIFY discNumberChanged) - Q_PROPERTY(int duration READ getDuration WRITE setDuration NOTIFY durationChanged) - Q_PROPERTY(QStringList genres READ getGenres WRITE setGenres NOTIFY genresChanged) - Q_PROPERTY(QString lyrics READ getLyrics WRITE setLyrics NOTIFY lyricsChanged) - Q_PROPERTY(QString narrator READ getNarrator WRITE setNarrator NOTIFY narratorChanged) - Q_PROPERTY(QString path READ getPath WRITE setPath NOTIFY pathChanged) - Q_PROPERTY(QString title READ getTitle WRITE setTitle NOTIFY titleChanged) - Q_PROPERTY(int trackNumber READ getTrackNumber WRITE setTrackNumber NOTIFY trackNumberChanged) - Q_PROPERTY(int year READ getYear WRITE setYear NOTIFY yearChanged) - - public: - KotoTrack(); // No-op constructor - static KotoTrack* fromDb(const QSqlQuery& query, const QSqlRecord& record); - static KotoTrack* fromMetadata(const KFileMetaData::SimpleExtractionResult& metadata, const QFileInfo& info); - virtual ~KotoTrack(); - - std::optional album_uuid; - QUuid artist_uuid; - QUuid uuid; - - void commit(); - QString getAlbumUuid(); - QUuid getArtistUuid(); - int getDiscNumber(); - int getDuration(); - QStringList getGenres(); - QString getLyrics(); - QString getNarrator(); - QString getPath(); - QString getTitle(); - int getTrackNumber(); - QUuid getUuid(); - int getYear(); - - void setAlbum(KotoAlbum* album); - void setArtist(KotoArtist* artist); - void setDiscNumber(int num); - void setDuration(int num); - void setGenres(QList list); - void setLyrics(QString str); - void setNarrator(QString str); - void setPath(QString path); - void setTitle(QString str); - void setTrackNumber(int num); - void setYear(int num); - - signals: - void albumChanged(); - void artistChanged(); - void discNumberChanged(); - void durationChanged(); - void genresChanged(); - void lyricsChanged(); - void narratorChanged(); - void pathChanged(); - void titleChanged(); - void trackNumberChanged(); - void yearChanged(); - - private: - int disc_number; - int duration; - QStringList genres; - QString lyrics; - QString narrator; - QString path; - QString title; - int track_number; - int year; -}; diff --git a/desktop/datalake/track.cpp b/desktop/datalake/track.cpp deleted file mode 100644 index 5e5ee29..0000000 --- a/desktop/datalake/track.cpp +++ /dev/null @@ -1,198 +0,0 @@ -#include -#include - -#include "cartographer.hpp" -#include "database.hpp" -#include "structs.hpp" - -KotoTrack::KotoTrack() { - this->uuid = QUuid::createUuid(); -} - -KotoTrack* KotoTrack::fromDb(const QSqlQuery& query, const QSqlRecord& record) { - KotoTrack* track = new KotoTrack(); - track->uuid = QUuid {query.value(record.indexOf("id")).toString()}; - auto artist_id = query.value(record.indexOf("artist_id")); - if (!artist_id.isNull()) { track->artist_uuid = QUuid {artist_id.toString()}; } - - auto album_id = query.value(record.indexOf("album_id")); - if (!album_id.isNull()) { track->album_uuid = QUuid {album_id.toString()}; } - - track->title = QString {query.value(record.indexOf("name")).toString()}; - track->disc_number = query.value(record.indexOf("disc")).toInt(); - track->track_number = query.value(record.indexOf("position")).toInt(); - track->duration = query.value(record.indexOf("duration")).toInt(); - track->genres = QList {query.value(record.indexOf("genres")).toString().split(", ")}; - return track; -} - -KotoTrack* KotoTrack::fromMetadata(const KFileMetaData::SimpleExtractionResult& metadata, const QFileInfo& info) { - auto props = metadata.properties(); - KotoTrack* track = new KotoTrack(); - track->disc_number = props.value(KFileMetaData::Property::DiscNumber, 0).toInt(); - track->duration = props.value(KFileMetaData::Property::Duration, 0).toInt(); - - QStringList genres; - for (auto v : props.values(KFileMetaData::Property::Genre)) { genres.append(v.toString()); } - - track->genres = genres; - - track->lyrics = props.value(KFileMetaData::Property::Lyrics).toString(); - track->narrator = props.value(KFileMetaData::Property::Performer).toString(); - track->path = info.absolutePath(); - track->track_number = props.value(KFileMetaData::Property::TrackNumber, 0).toInt(); - track->year = props.value(KFileMetaData::Property::ReleaseYear, 0).toInt(); - - auto titleResult = props.value(KFileMetaData::Property::Title); - if (titleResult.isValid() && !titleResult.isNull()) { - track->title = titleResult.toString(); - } else { - // TODO: mirror the same logic we had for cleaning up file name to determine track name, position, chapter, artist, etc. - track->title = info.fileName(); - } - - auto artistResult = props.value(KFileMetaData::Property::Artist); - auto artistOptional = std::optional(); - if (artistResult.isValid()) { - artistOptional = Cartographer::instance().getArtist(artistResult.toString()); - - if (artistOptional.has_value()) { - auto artist = artistOptional.value(); - track->artist_uuid = QUuid(artist->uuid); - artist->addTrack(track); - } - } - - auto albumResult = props.value(KFileMetaData::Property::Album); - if (albumResult.isValid() && artistOptional.has_value()) { - auto artist = artistOptional.value(); - - auto albumMetaName = albumResult.toString(); - auto albumOptional = artist->getAlbumByName(albumMetaName); - - if (albumOptional.has_value()) { - auto album = albumOptional.value(); - track->album_uuid = QUuid(album->uuid); - album->addTrack(track); - if (album->getTitle() != albumMetaName) album->setTitle(albumMetaName); - } - } - - return track; -} - -KotoTrack::~KotoTrack() {} - -void KotoTrack::commit() { - QSqlQuery query(KotoDatabase::instance().getDatabase()); - query.prepare( - "INSERT INTO tracks(id, artist_id, album_id, name, disc, position, duration, genres) " - "VALUES (:id, :artist_id, :album_id, :name, :disc, :position, :duration, :genres) " - "ON CONFLICT(id) DO UPDATE SET artist_id = :artist_id, album_id = :album_id, name = :name, disc = :disc, position = :position, duration = :duration, " - "genres = :genres"); - query.bindValue(":id", this->uuid.toString()); - query.bindValue(":artist_id", !this->artist_uuid.isNull() ? this->artist_uuid.toString() : NULL); - query.bindValue(":album_id", this->album_uuid.has_value() ? this->album_uuid.value().toString() : NULL); - query.bindValue(":name", this->title); - query.bindValue(":disc", this->disc_number); - query.bindValue(":position", this->track_number); - query.bindValue(":duration", this->duration); - query.bindValue(":genres", this->genres.join(", ")); - - query.exec(); -} - -QString KotoTrack::getAlbumUuid() { - if (!this->album_uuid.has_value()) return this->album_uuid.value().toString(); - return {}; -} - -QUuid KotoTrack::getArtistUuid() { - return this->artist_uuid; -} - -int KotoTrack::getDiscNumber() { - return this->disc_number; -} - -int KotoTrack::getDuration() { - return this->duration; -} - -QList KotoTrack::getGenres() { - return QList {this->genres}; -} - -QString KotoTrack::getLyrics() { - return QString {this->lyrics}; -} - -QString KotoTrack::getNarrator() { - return QString {this->narrator}; -} - -QString KotoTrack::getPath() { - return QString {this->path}; -} - -QString KotoTrack::getTitle() { - return QString {this->title}; -} - -int KotoTrack::getTrackNumber() { - return this->track_number; -} - -QUuid KotoTrack::getUuid() { - return this->uuid; -} - -int KotoTrack::getYear() { - return this->year; -} - -void KotoTrack::setAlbum(KotoAlbum* album) { - this->album_uuid = QUuid(album->uuid); - - if (this->artist_uuid.isNull()) QUuid(album->artist_uuid); -} - -void KotoTrack::setArtist(KotoArtist* artist) { - this->artist_uuid = QUuid(artist->uuid); -} - -void KotoTrack::setDiscNumber(int num) { - this->disc_number = num; -} - -void KotoTrack::setDuration(int num) { - this->duration = num; -} - -void KotoTrack::setGenres(QList list) { - this->genres = QList {list}; -} - -void KotoTrack::setLyrics(QString str) { - this->lyrics = QString {str}; -} - -void KotoTrack::setNarrator(QString str) { - this->narrator = QString {str}; -} - -void KotoTrack::setPath(QString str) { - this->path = QString {str}; -} - -void KotoTrack::setTitle(QString str) { - this->title = QString {str}; -} - -void KotoTrack::setTrackNumber(int num) { - this->track_number = num; -} - -void KotoTrack::setYear(int num) { - this->year = num; -} diff --git a/desktop/example-config.toml b/desktop/example-config.toml deleted file mode 100644 index 84bdd49..0000000 --- a/desktop/example-config.toml +++ /dev/null @@ -1,11 +0,0 @@ -["preferences.ui"] -album_info_show_description = true -album_info_show_genre = true -album_info_show_narrator = true -album_info_show_year = true -last_used_volume = 0.5 - -[[libraries]] -name = "Music" -path = "/home/joshua/Music" -type = "music" \ No newline at end of file diff --git a/desktop/includes/toml.hpp b/desktop/includes/toml.hpp deleted file mode 100644 index fe5695c..0000000 --- a/desktop/includes/toml.hpp +++ /dev/null @@ -1,17240 +0,0 @@ -#ifndef TOML11_VERSION_HPP -#define TOML11_VERSION_HPP - -#define TOML11_VERSION_MAJOR 4 -#define TOML11_VERSION_MINOR 2 -#define TOML11_VERSION_PATCH 0 - -#ifndef __cplusplus -# error "__cplusplus is not defined" -#endif - -// Since MSVC does not define `__cplusplus` correctly unless you pass -// `/Zc:__cplusplus` when compiling, the workaround macros are added. -// -// The value of `__cplusplus` macro is defined in the C++ standard spec, but -// MSVC ignores the value, maybe because of backward compatibility. Instead, -// MSVC defines _MSVC_LANG that has the same value as __cplusplus defined in -// the C++ standard. So we check if _MSVC_LANG is defined before using `__cplusplus`. -// -// FYI: https://docs.microsoft.com/en-us/cpp/build/reference/zc-cplusplus?view=msvc-170 -// https://docs.microsoft.com/en-us/cpp/preprocessor/predefined-macros?view=msvc-170 -// - -#if defined(_MSVC_LANG) && defined(_MSC_VER) && 190024210 <= _MSC_FULL_VER -# define TOML11_CPLUSPLUS_STANDARD_VERSION _MSVC_LANG -#else -# define TOML11_CPLUSPLUS_STANDARD_VERSION __cplusplus -#endif - -#if TOML11_CPLUSPLUS_STANDARD_VERSION < 201103L -# error "toml11 requires C++11 or later." -#endif - -#if ! defined(__has_include) -# define __has_include(x) 0 -#endif - -#if ! defined(__has_cpp_attribute) -# define __has_cpp_attribute(x) 0 -#endif - -#if ! defined(__has_builtin) -# define __has_builtin(x) 0 -#endif - -// hard to remember - -#ifndef TOML11_CXX14_VALUE -#define TOML11_CXX14_VALUE 201402L -#endif//TOML11_CXX14_VALUE - -#ifndef TOML11_CXX17_VALUE -#define TOML11_CXX17_VALUE 201703L -#endif//TOML11_CXX17_VALUE - -#ifndef TOML11_CXX20_VALUE -#define TOML11_CXX20_VALUE 202002L -#endif//TOML11_CXX20_VALUE - -#if defined(__cpp_char8_t) -# if __cpp_char8_t >= 201811L -# define TOML11_HAS_CHAR8_T 1 -# endif -#endif - -#if TOML11_CPLUSPLUS_STANDARD_VERSION >= TOML11_CXX17_VALUE -# if __has_include() -# define TOML11_HAS_STRING_VIEW 1 -# endif -#endif - -#ifndef TOML11_DISABLE_STD_FILESYSTEM -# if TOML11_CPLUSPLUS_STANDARD_VERSION >= TOML11_CXX17_VALUE -# if __has_include() -# define TOML11_HAS_FILESYSTEM 1 -# endif -# endif -#endif - -#if TOML11_CPLUSPLUS_STANDARD_VERSION >= TOML11_CXX17_VALUE -# if __has_include() -# define TOML11_HAS_OPTIONAL 1 -# endif -#endif - -#if defined(TOML11_COMPILE_SOURCES) -# define TOML11_INLINE -#else -# define TOML11_INLINE inline -#endif - -namespace toml -{ - -inline const char* license_notice() noexcept -{ - return R"(The MIT License (MIT) - -Copyright (c) 2017-now Toru Niina - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE.)"; -} - -} // toml -#endif // TOML11_VERSION_HPP -#ifndef TOML11_FORMAT_HPP -#define TOML11_FORMAT_HPP - -#ifndef TOML11_FORMAT_FWD_HPP -#define TOML11_FORMAT_FWD_HPP - -#include -#include -#include - -#include -#include - -namespace toml -{ - -// toml types with serialization info - -enum class indent_char : std::uint8_t -{ - space, // use space - tab, // use tab - none // no indent -}; - -std::ostream& operator<<(std::ostream& os, const indent_char& c); -std::string to_string(const indent_char c); - -// ---------------------------------------------------------------------------- -// boolean - -struct boolean_format_info -{ - // nothing, for now -}; - -inline bool operator==(const boolean_format_info&, const boolean_format_info&) noexcept -{ - return true; -} -inline bool operator!=(const boolean_format_info&, const boolean_format_info&) noexcept -{ - return false; -} - -// ---------------------------------------------------------------------------- -// integer - -enum class integer_format : std::uint8_t -{ - dec = 0, - bin = 1, - oct = 2, - hex = 3, -}; - -std::ostream& operator<<(std::ostream& os, const integer_format f); -std::string to_string(const integer_format); - -struct integer_format_info -{ - integer_format fmt = integer_format::dec; - bool uppercase = true; // hex with uppercase - std::size_t width = 0; // minimal width (may exceed) - std::size_t spacer = 0; // position of `_` (if 0, no spacer) - std::string suffix = ""; // _suffix (library extension) -}; - -bool operator==(const integer_format_info&, const integer_format_info&) noexcept; -bool operator!=(const integer_format_info&, const integer_format_info&) noexcept; - -// ---------------------------------------------------------------------------- -// floating - -enum class floating_format : std::uint8_t -{ - defaultfloat = 0, - fixed = 1, // does not include exponential part - scientific = 2, // always include exponential part - hex = 3 // hexfloat extension -}; - -std::ostream& operator<<(std::ostream& os, const floating_format f); -std::string to_string(const floating_format); - -struct floating_format_info -{ - floating_format fmt = floating_format::defaultfloat; - std::size_t prec = 0; // precision (if 0, use the default) - std::string suffix = ""; // 1.0e+2_suffix (library extension) -}; - -bool operator==(const floating_format_info&, const floating_format_info&) noexcept; -bool operator!=(const floating_format_info&, const floating_format_info&) noexcept; - -// ---------------------------------------------------------------------------- -// string - -enum class string_format : std::uint8_t -{ - basic = 0, - literal = 1, - multiline_basic = 2, - multiline_literal = 3 -}; - -std::ostream& operator<<(std::ostream& os, const string_format f); -std::string to_string(const string_format); - -struct string_format_info -{ - string_format fmt = string_format::basic; - bool start_with_newline = false; -}; - -bool operator==(const string_format_info&, const string_format_info&) noexcept; -bool operator!=(const string_format_info&, const string_format_info&) noexcept; - -// ---------------------------------------------------------------------------- -// datetime - -enum class datetime_delimiter_kind : std::uint8_t -{ - upper_T = 0, - lower_t = 1, - space = 2, -}; -std::ostream& operator<<(std::ostream& os, const datetime_delimiter_kind d); -std::string to_string(const datetime_delimiter_kind); - -struct offset_datetime_format_info -{ - datetime_delimiter_kind delimiter = datetime_delimiter_kind::upper_T; - bool has_seconds = true; - std::size_t subsecond_precision = 6; // [us] -}; - -bool operator==(const offset_datetime_format_info&, const offset_datetime_format_info&) noexcept; -bool operator!=(const offset_datetime_format_info&, const offset_datetime_format_info&) noexcept; - -struct local_datetime_format_info -{ - datetime_delimiter_kind delimiter = datetime_delimiter_kind::upper_T; - bool has_seconds = true; - std::size_t subsecond_precision = 6; // [us] -}; - -bool operator==(const local_datetime_format_info&, const local_datetime_format_info&) noexcept; -bool operator!=(const local_datetime_format_info&, const local_datetime_format_info&) noexcept; - -struct local_date_format_info -{ - // nothing, for now -}; - -bool operator==(const local_date_format_info&, const local_date_format_info&) noexcept; -bool operator!=(const local_date_format_info&, const local_date_format_info&) noexcept; - -struct local_time_format_info -{ - bool has_seconds = true; - std::size_t subsecond_precision = 6; // [us] -}; - -bool operator==(const local_time_format_info&, const local_time_format_info&) noexcept; -bool operator!=(const local_time_format_info&, const local_time_format_info&) noexcept; - -// ---------------------------------------------------------------------------- -// array - -enum class array_format : std::uint8_t -{ - default_format = 0, - oneline = 1, - multiline = 2, - array_of_tables = 3 // [[format.in.this.way]] -}; - -std::ostream& operator<<(std::ostream& os, const array_format f); -std::string to_string(const array_format); - -struct array_format_info -{ - array_format fmt = array_format::default_format; - indent_char indent_type = indent_char::space; - std::int32_t body_indent = 4; // indent in case of multiline - std::int32_t closing_indent = 0; // indent of `]` -}; - -bool operator==(const array_format_info&, const array_format_info&) noexcept; -bool operator!=(const array_format_info&, const array_format_info&) noexcept; - -// ---------------------------------------------------------------------------- -// table - -enum class table_format : std::uint8_t -{ - multiline = 0, // [foo] \n bar = "baz" - oneline = 1, // foo = {bar = "baz"} - dotted = 2, // foo.bar = "baz" - multiline_oneline = 3, // foo = { \n bar = "baz" \n } - implicit = 4 // [x] defined by [x.y.z]. skip in serializer. -}; - -std::ostream& operator<<(std::ostream& os, const table_format f); -std::string to_string(const table_format); - -struct table_format_info -{ - table_format fmt = table_format::multiline; - indent_char indent_type = indent_char::space; - std::int32_t body_indent = 0; // indent of values - std::int32_t name_indent = 0; // indent of [table] - std::int32_t closing_indent = 0; // in case of {inline-table} -}; - -bool operator==(const table_format_info&, const table_format_info&) noexcept; -bool operator!=(const table_format_info&, const table_format_info&) noexcept; - -// ---------------------------------------------------------------------------- -// wrapper - -namespace detail -{ -template -struct value_with_format -{ - using value_type = T; - using format_type = F; - - value_with_format() = default; - ~value_with_format() = default; - value_with_format(const value_with_format&) = default; - value_with_format(value_with_format&&) = default; - value_with_format& operator=(const value_with_format&) = default; - value_with_format& operator=(value_with_format&&) = default; - - value_with_format(value_type v, format_type f) - : value{std::move(v)}, format{std::move(f)} - {} - - template - value_with_format(value_with_format other) - : value{std::move(other.value)}, format{std::move(other.format)} - {} - - value_type value; - format_type format; -}; -} // detail - -} // namespace toml -#endif // TOML11_FORMAT_FWD_HPP - -#if ! defined(TOML11_COMPILE_SOURCES) -#ifndef TOML11_FORMAT_IMPL_HPP -#define TOML11_FORMAT_IMPL_HPP - - -#include -#include - -namespace toml -{ - -// toml types with serialization info - -TOML11_INLINE std::ostream& operator<<(std::ostream& os, const indent_char& c) -{ - switch(c) - { - case indent_char::space: {os << "space" ; break;} - case indent_char::tab: {os << "tab" ; break;} - case indent_char::none: {os << "none" ; break;} - default: - { - os << "unknown indent char: " << static_cast(c); - } - } - return os; -} - -TOML11_INLINE std::string to_string(const indent_char c) -{ - std::ostringstream oss; - oss << c; - return oss.str(); -} - -// ---------------------------------------------------------------------------- -// boolean - -// ---------------------------------------------------------------------------- -// integer - -TOML11_INLINE std::ostream& operator<<(std::ostream& os, const integer_format f) -{ - switch(f) - { - case integer_format::dec: {os << "dec"; break;} - case integer_format::bin: {os << "bin"; break;} - case integer_format::oct: {os << "oct"; break;} - case integer_format::hex: {os << "hex"; break;} - default: - { - os << "unknown integer_format: " << static_cast(f); - break; - } - } - return os; -} -TOML11_INLINE std::string to_string(const integer_format c) -{ - std::ostringstream oss; - oss << c; - return oss.str(); -} - - -TOML11_INLINE bool operator==(const integer_format_info& lhs, const integer_format_info& rhs) noexcept -{ - return lhs.fmt == rhs.fmt && - lhs.uppercase == rhs.uppercase && - lhs.width == rhs.width && - lhs.spacer == rhs.spacer && - lhs.suffix == rhs.suffix ; -} -TOML11_INLINE bool operator!=(const integer_format_info& lhs, const integer_format_info& rhs) noexcept -{ - return !(lhs == rhs); -} - -// ---------------------------------------------------------------------------- -// floating - -TOML11_INLINE std::ostream& operator<<(std::ostream& os, const floating_format f) -{ - switch(f) - { - case floating_format::defaultfloat: {os << "defaultfloat"; break;} - case floating_format::fixed : {os << "fixed" ; break;} - case floating_format::scientific : {os << "scientific" ; break;} - case floating_format::hex : {os << "hex" ; break;} - default: - { - os << "unknown floating_format: " << static_cast(f); - break; - } - } - return os; -} -TOML11_INLINE std::string to_string(const floating_format c) -{ - std::ostringstream oss; - oss << c; - return oss.str(); -} - -TOML11_INLINE bool operator==(const floating_format_info& lhs, const floating_format_info& rhs) noexcept -{ - return lhs.fmt == rhs.fmt && - lhs.prec == rhs.prec && - lhs.suffix == rhs.suffix ; -} -TOML11_INLINE bool operator!=(const floating_format_info& lhs, const floating_format_info& rhs) noexcept -{ - return !(lhs == rhs); -} - -// ---------------------------------------------------------------------------- -// string - -TOML11_INLINE std::ostream& operator<<(std::ostream& os, const string_format f) -{ - switch(f) - { - case string_format::basic : {os << "basic" ; break;} - case string_format::literal : {os << "literal" ; break;} - case string_format::multiline_basic : {os << "multiline_basic" ; break;} - case string_format::multiline_literal: {os << "multiline_literal"; break;} - default: - { - os << "unknown string_format: " << static_cast(f); - break; - } - } - return os; -} -TOML11_INLINE std::string to_string(const string_format c) -{ - std::ostringstream oss; - oss << c; - return oss.str(); -} - -TOML11_INLINE bool operator==(const string_format_info& lhs, const string_format_info& rhs) noexcept -{ - return lhs.fmt == rhs.fmt && - lhs.start_with_newline == rhs.start_with_newline ; -} -TOML11_INLINE bool operator!=(const string_format_info& lhs, const string_format_info& rhs) noexcept -{ - return !(lhs == rhs); -} -// ---------------------------------------------------------------------------- -// datetime - -TOML11_INLINE std::ostream& operator<<(std::ostream& os, const datetime_delimiter_kind d) -{ - switch(d) - { - case datetime_delimiter_kind::upper_T: { os << "upper_T, "; break; } - case datetime_delimiter_kind::lower_t: { os << "lower_t, "; break; } - case datetime_delimiter_kind::space: { os << "space, "; break; } - default: - { - os << "unknown datetime delimiter: " << static_cast(d); - break; - } - } - return os; -} -TOML11_INLINE std::string to_string(const datetime_delimiter_kind c) -{ - std::ostringstream oss; - oss << c; - return oss.str(); -} - -TOML11_INLINE bool operator==(const offset_datetime_format_info& lhs, const offset_datetime_format_info& rhs) noexcept -{ - return lhs.delimiter == rhs.delimiter && - lhs.has_seconds == rhs.has_seconds && - lhs.subsecond_precision == rhs.subsecond_precision ; -} -TOML11_INLINE bool operator!=(const offset_datetime_format_info& lhs, const offset_datetime_format_info& rhs) noexcept -{ - return !(lhs == rhs); -} - -TOML11_INLINE bool operator==(const local_datetime_format_info& lhs, const local_datetime_format_info& rhs) noexcept -{ - return lhs.delimiter == rhs.delimiter && - lhs.has_seconds == rhs.has_seconds && - lhs.subsecond_precision == rhs.subsecond_precision ; -} -TOML11_INLINE bool operator!=(const local_datetime_format_info& lhs, const local_datetime_format_info& rhs) noexcept -{ - return !(lhs == rhs); -} - -TOML11_INLINE bool operator==(const local_date_format_info&, const local_date_format_info&) noexcept -{ - return true; -} -TOML11_INLINE bool operator!=(const local_date_format_info& lhs, const local_date_format_info& rhs) noexcept -{ - return !(lhs == rhs); -} - -TOML11_INLINE bool operator==(const local_time_format_info& lhs, const local_time_format_info& rhs) noexcept -{ - return lhs.has_seconds == rhs.has_seconds && - lhs.subsecond_precision == rhs.subsecond_precision ; -} -TOML11_INLINE bool operator!=(const local_time_format_info& lhs, const local_time_format_info& rhs) noexcept -{ - return !(lhs == rhs); -} - -// ---------------------------------------------------------------------------- -// array - -TOML11_INLINE std::ostream& operator<<(std::ostream& os, const array_format f) -{ - switch(f) - { - case array_format::default_format : {os << "default_format" ; break;} - case array_format::oneline : {os << "oneline" ; break;} - case array_format::multiline : {os << "multiline" ; break;} - case array_format::array_of_tables: {os << "array_of_tables"; break;} - default: - { - os << "unknown array_format: " << static_cast(f); - break; - } - } - return os; -} -TOML11_INLINE std::string to_string(const array_format c) -{ - std::ostringstream oss; - oss << c; - return oss.str(); -} - -TOML11_INLINE bool operator==(const array_format_info& lhs, const array_format_info& rhs) noexcept -{ - return lhs.fmt == rhs.fmt && - lhs.indent_type == rhs.indent_type && - lhs.body_indent == rhs.body_indent && - lhs.closing_indent == rhs.closing_indent ; -} -TOML11_INLINE bool operator!=(const array_format_info& lhs, const array_format_info& rhs) noexcept -{ - return !(lhs == rhs); -} - -// ---------------------------------------------------------------------------- -// table - -TOML11_INLINE std::ostream& operator<<(std::ostream& os, const table_format f) -{ - switch(f) - { - case table_format::multiline : {os << "multiline" ; break;} - case table_format::oneline : {os << "oneline" ; break;} - case table_format::dotted : {os << "dotted" ; break;} - case table_format::multiline_oneline: {os << "multiline_oneline"; break;} - case table_format::implicit : {os << "implicit" ; break;} - default: - { - os << "unknown table_format: " << static_cast(f); - break; - } - } - return os; -} -TOML11_INLINE std::string to_string(const table_format c) -{ - std::ostringstream oss; - oss << c; - return oss.str(); -} - -TOML11_INLINE bool operator==(const table_format_info& lhs, const table_format_info& rhs) noexcept -{ - return lhs.fmt == rhs.fmt && - lhs.indent_type == rhs.indent_type && - lhs.body_indent == rhs.body_indent && - lhs.name_indent == rhs.name_indent && - lhs.closing_indent == rhs.closing_indent ; -} -TOML11_INLINE bool operator!=(const table_format_info& lhs, const table_format_info& rhs) noexcept -{ - return !(lhs == rhs); -} - -} // namespace toml -#endif // TOML11_FORMAT_IMPL_HPP -#endif - -#endif// TOML11_FORMAT_HPP -#ifndef TOML11_DATETIME_HPP -#define TOML11_DATETIME_HPP - -#ifndef TOML11_DATETIME_FWD_HPP -#define TOML11_DATETIME_FWD_HPP - -#include -#include -#include - -#include -#include -#include - -namespace toml -{ - -enum class month_t : std::uint8_t -{ - Jan = 0, - Feb = 1, - Mar = 2, - Apr = 3, - May = 4, - Jun = 5, - Jul = 6, - Aug = 7, - Sep = 8, - Oct = 9, - Nov = 10, - Dec = 11 -}; - -// ---------------------------------------------------------------------------- - -struct local_date -{ - std::int16_t year{0}; // A.D. (like, 2018) - std::uint8_t month{0}; // [0, 11] - std::uint8_t day{0}; // [1, 31] - - local_date(int y, month_t m, int d) - : year {static_cast(y)}, - month{static_cast(m)}, - day {static_cast(d)} - {} - - explicit local_date(const std::tm& t) - : year {static_cast(t.tm_year + 1900)}, - month{static_cast(t.tm_mon)}, - day {static_cast(t.tm_mday)} - {} - - explicit local_date(const std::chrono::system_clock::time_point& tp); - explicit local_date(const std::time_t t); - - operator std::chrono::system_clock::time_point() const; - operator std::time_t() const; - - local_date() = default; - ~local_date() = default; - local_date(local_date const&) = default; - local_date(local_date&&) = default; - local_date& operator=(local_date const&) = default; - local_date& operator=(local_date&&) = default; -}; -bool operator==(const local_date& lhs, const local_date& rhs); -bool operator!=(const local_date& lhs, const local_date& rhs); -bool operator< (const local_date& lhs, const local_date& rhs); -bool operator<=(const local_date& lhs, const local_date& rhs); -bool operator> (const local_date& lhs, const local_date& rhs); -bool operator>=(const local_date& lhs, const local_date& rhs); - -std::ostream& operator<<(std::ostream& os, const local_date& date); -std::string to_string(const local_date& date); - -// ----------------------------------------------------------------------------- - -struct local_time -{ - std::uint8_t hour{0}; // [0, 23] - std::uint8_t minute{0}; // [0, 59] - std::uint8_t second{0}; // [0, 60] - std::uint16_t millisecond{0}; // [0, 999] - std::uint16_t microsecond{0}; // [0, 999] - std::uint16_t nanosecond{0}; // [0, 999] - - local_time(int h, int m, int s, - int ms = 0, int us = 0, int ns = 0) - : hour {static_cast(h)}, - minute{static_cast(m)}, - second{static_cast(s)}, - millisecond{static_cast(ms)}, - microsecond{static_cast(us)}, - nanosecond {static_cast(ns)} - {} - - explicit local_time(const std::tm& t) - : hour {static_cast(t.tm_hour)}, - minute{static_cast(t.tm_min )}, - second{static_cast(t.tm_sec )}, - millisecond{0}, microsecond{0}, nanosecond{0} - {} - - template - explicit local_time(const std::chrono::duration& t) - { - const auto h = std::chrono::duration_cast(t); - this->hour = static_cast(h.count()); - const auto t2 = t - h; - const auto m = std::chrono::duration_cast(t2); - this->minute = static_cast(m.count()); - const auto t3 = t2 - m; - const auto s = std::chrono::duration_cast(t3); - this->second = static_cast(s.count()); - const auto t4 = t3 - s; - const auto ms = std::chrono::duration_cast(t4); - this->millisecond = static_cast(ms.count()); - const auto t5 = t4 - ms; - const auto us = std::chrono::duration_cast(t5); - this->microsecond = static_cast(us.count()); - const auto t6 = t5 - us; - const auto ns = std::chrono::duration_cast(t6); - this->nanosecond = static_cast(ns.count()); - } - - operator std::chrono::nanoseconds() const; - - local_time() = default; - ~local_time() = default; - local_time(local_time const&) = default; - local_time(local_time&&) = default; - local_time& operator=(local_time const&) = default; - local_time& operator=(local_time&&) = default; -}; - -bool operator==(const local_time& lhs, const local_time& rhs); -bool operator!=(const local_time& lhs, const local_time& rhs); -bool operator< (const local_time& lhs, const local_time& rhs); -bool operator<=(const local_time& lhs, const local_time& rhs); -bool operator> (const local_time& lhs, const local_time& rhs); -bool operator>=(const local_time& lhs, const local_time& rhs); - -std::ostream& operator<<(std::ostream& os, const local_time& time); -std::string to_string(const local_time& time); - -// ---------------------------------------------------------------------------- - -struct time_offset -{ - std::int8_t hour{0}; // [-12, 12] - std::int8_t minute{0}; // [-59, 59] - - time_offset(int h, int m) - : hour {static_cast(h)}, - minute{static_cast(m)} - {} - - operator std::chrono::minutes() const; - - time_offset() = default; - ~time_offset() = default; - time_offset(time_offset const&) = default; - time_offset(time_offset&&) = default; - time_offset& operator=(time_offset const&) = default; - time_offset& operator=(time_offset&&) = default; -}; - -bool operator==(const time_offset& lhs, const time_offset& rhs); -bool operator!=(const time_offset& lhs, const time_offset& rhs); -bool operator< (const time_offset& lhs, const time_offset& rhs); -bool operator<=(const time_offset& lhs, const time_offset& rhs); -bool operator> (const time_offset& lhs, const time_offset& rhs); -bool operator>=(const time_offset& lhs, const time_offset& rhs); - -std::ostream& operator<<(std::ostream& os, const time_offset& offset); - -std::string to_string(const time_offset& offset); - -// ----------------------------------------------------------------------------- - -struct local_datetime -{ - local_date date{}; - local_time time{}; - - local_datetime(local_date d, local_time t): date{d}, time{t} {} - - explicit local_datetime(const std::tm& t): date{t}, time{t}{} - - explicit local_datetime(const std::chrono::system_clock::time_point& tp); - explicit local_datetime(const std::time_t t); - - operator std::chrono::system_clock::time_point() const; - operator std::time_t() const; - - local_datetime() = default; - ~local_datetime() = default; - local_datetime(local_datetime const&) = default; - local_datetime(local_datetime&&) = default; - local_datetime& operator=(local_datetime const&) = default; - local_datetime& operator=(local_datetime&&) = default; -}; - -bool operator==(const local_datetime& lhs, const local_datetime& rhs); -bool operator!=(const local_datetime& lhs, const local_datetime& rhs); -bool operator< (const local_datetime& lhs, const local_datetime& rhs); -bool operator<=(const local_datetime& lhs, const local_datetime& rhs); -bool operator> (const local_datetime& lhs, const local_datetime& rhs); -bool operator>=(const local_datetime& lhs, const local_datetime& rhs); - -std::ostream& operator<<(std::ostream& os, const local_datetime& dt); - -std::string to_string(const local_datetime& dt); - -// ----------------------------------------------------------------------------- - -struct offset_datetime -{ - local_date date{}; - local_time time{}; - time_offset offset{}; - - offset_datetime(local_date d, local_time t, time_offset o) - : date{d}, time{t}, offset{o} - {} - offset_datetime(const local_datetime& dt, time_offset o) - : date{dt.date}, time{dt.time}, offset{o} - {} - // use the current local timezone offset - explicit offset_datetime(const local_datetime& ld); - explicit offset_datetime(const std::chrono::system_clock::time_point& tp); - explicit offset_datetime(const std::time_t& t); - explicit offset_datetime(const std::tm& t); - - operator std::chrono::system_clock::time_point() const; - - operator std::time_t() const; - - offset_datetime() = default; - ~offset_datetime() = default; - offset_datetime(offset_datetime const&) = default; - offset_datetime(offset_datetime&&) = default; - offset_datetime& operator=(offset_datetime const&) = default; - offset_datetime& operator=(offset_datetime&&) = default; - - private: - - static time_offset get_local_offset(const std::time_t* tp); -}; - -bool operator==(const offset_datetime& lhs, const offset_datetime& rhs); -bool operator!=(const offset_datetime& lhs, const offset_datetime& rhs); -bool operator< (const offset_datetime& lhs, const offset_datetime& rhs); -bool operator<=(const offset_datetime& lhs, const offset_datetime& rhs); -bool operator> (const offset_datetime& lhs, const offset_datetime& rhs); -bool operator>=(const offset_datetime& lhs, const offset_datetime& rhs); - -std::ostream& operator<<(std::ostream& os, const offset_datetime& dt); - -std::string to_string(const offset_datetime& dt); - -}//toml -#endif // TOML11_DATETIME_FWD_HPP - -#if ! defined(TOML11_COMPILE_SOURCES) -#ifndef TOML11_DATETIME_IMPL_HPP -#define TOML11_DATETIME_IMPL_HPP - - -#include -#include -#include -#include -#include - -#include -#include - -namespace toml -{ - -// To avoid non-threadsafe std::localtime. In C11 (not C++11!), localtime_s is -// provided in the absolutely same purpose, but C++11 is actually not compatible -// with C11. We need to dispatch the function depending on the OS. -namespace detail -{ -// TODO: find more sophisticated way to handle this -#if defined(_MSC_VER) -TOML11_INLINE std::tm localtime_s(const std::time_t* src) -{ - std::tm dst; - const auto result = ::localtime_s(&dst, src); - if (result) { throw std::runtime_error("localtime_s failed."); } - return dst; -} -TOML11_INLINE std::tm gmtime_s(const std::time_t* src) -{ - std::tm dst; - const auto result = ::gmtime_s(&dst, src); - if (result) { throw std::runtime_error("gmtime_s failed."); } - return dst; -} -#elif (defined(_POSIX_C_SOURCE) && _POSIX_C_SOURCE >= 1) || defined(_XOPEN_SOURCE) || defined(_BSD_SOURCE) || defined(_SVID_SOURCE) || defined(_POSIX_SOURCE) -TOML11_INLINE std::tm localtime_s(const std::time_t* src) -{ - std::tm dst; - const auto result = ::localtime_r(src, &dst); - if (!result) { throw std::runtime_error("localtime_r failed."); } - return dst; -} -TOML11_INLINE std::tm gmtime_s(const std::time_t* src) -{ - std::tm dst; - const auto result = ::gmtime_r(src, &dst); - if (!result) { throw std::runtime_error("gmtime_r failed."); } - return dst; -} -#else // fallback. not threadsafe -TOML11_INLINE std::tm localtime_s(const std::time_t* src) -{ - const auto result = std::localtime(src); - if (!result) { throw std::runtime_error("localtime failed."); } - return *result; -} -TOML11_INLINE std::tm gmtime_s(const std::time_t* src) -{ - const auto result = std::gmtime(src); - if (!result) { throw std::runtime_error("gmtime failed."); } - return *result; -} -#endif -} // detail - -// ---------------------------------------------------------------------------- - -TOML11_INLINE local_date::local_date(const std::chrono::system_clock::time_point& tp) -{ - const auto t = std::chrono::system_clock::to_time_t(tp); - const auto time = detail::localtime_s(&t); - *this = local_date(time); -} - -TOML11_INLINE local_date::local_date(const std::time_t t) - : local_date{std::chrono::system_clock::from_time_t(t)} -{} - -TOML11_INLINE local_date::operator std::chrono::system_clock::time_point() const -{ - // std::mktime returns date as local time zone. no conversion needed - std::tm t; - t.tm_sec = 0; - t.tm_min = 0; - t.tm_hour = 0; - t.tm_mday = static_cast(this->day); - t.tm_mon = static_cast(this->month); - t.tm_year = static_cast(this->year) - 1900; - t.tm_wday = 0; // the value will be ignored - t.tm_yday = 0; // the value will be ignored - t.tm_isdst = -1; - return std::chrono::system_clock::from_time_t(std::mktime(&t)); -} - -TOML11_INLINE local_date::operator std::time_t() const -{ - return std::chrono::system_clock::to_time_t( - std::chrono::system_clock::time_point(*this)); -} - -TOML11_INLINE bool operator==(const local_date& lhs, const local_date& rhs) -{ - return std::make_tuple(lhs.year, lhs.month, lhs.day) == - std::make_tuple(rhs.year, rhs.month, rhs.day); -} -TOML11_INLINE bool operator!=(const local_date& lhs, const local_date& rhs) -{ - return !(lhs == rhs); -} -TOML11_INLINE bool operator< (const local_date& lhs, const local_date& rhs) -{ - return std::make_tuple(lhs.year, lhs.month, lhs.day) < - std::make_tuple(rhs.year, rhs.month, rhs.day); -} -TOML11_INLINE bool operator<=(const local_date& lhs, const local_date& rhs) -{ - return (lhs < rhs) || (lhs == rhs); -} -TOML11_INLINE bool operator> (const local_date& lhs, const local_date& rhs) -{ - return !(lhs <= rhs); -} -TOML11_INLINE bool operator>=(const local_date& lhs, const local_date& rhs) -{ - return !(lhs < rhs); -} - -TOML11_INLINE std::ostream& operator<<(std::ostream& os, const local_date& date) -{ - os << std::setfill('0') << std::setw(4) << static_cast(date.year ) << '-'; - os << std::setfill('0') << std::setw(2) << static_cast(date.month) + 1 << '-'; - os << std::setfill('0') << std::setw(2) << static_cast(date.day ) ; - return os; -} - -TOML11_INLINE std::string to_string(const local_date& date) -{ - std::ostringstream oss; - oss.imbue(std::locale::classic()); - oss << date; - return oss.str(); -} - -// ----------------------------------------------------------------------------- - -TOML11_INLINE local_time::operator std::chrono::nanoseconds() const -{ - return std::chrono::nanoseconds (this->nanosecond) + - std::chrono::microseconds(this->microsecond) + - std::chrono::milliseconds(this->millisecond) + - std::chrono::seconds(this->second) + - std::chrono::minutes(this->minute) + - std::chrono::hours(this->hour); -} - -TOML11_INLINE bool operator==(const local_time& lhs, const local_time& rhs) -{ - return std::make_tuple(lhs.hour, lhs.minute, lhs.second, lhs.millisecond, lhs.microsecond, lhs.nanosecond) == - std::make_tuple(rhs.hour, rhs.minute, rhs.second, rhs.millisecond, rhs.microsecond, rhs.nanosecond); -} -TOML11_INLINE bool operator!=(const local_time& lhs, const local_time& rhs) -{ - return !(lhs == rhs); -} -TOML11_INLINE bool operator< (const local_time& lhs, const local_time& rhs) -{ - return std::make_tuple(lhs.hour, lhs.minute, lhs.second, lhs.millisecond, lhs.microsecond, lhs.nanosecond) < - std::make_tuple(rhs.hour, rhs.minute, rhs.second, rhs.millisecond, rhs.microsecond, rhs.nanosecond); -} -TOML11_INLINE bool operator<=(const local_time& lhs, const local_time& rhs) -{ - return (lhs < rhs) || (lhs == rhs); -} -TOML11_INLINE bool operator> (const local_time& lhs, const local_time& rhs) -{ - return !(lhs <= rhs); -} -TOML11_INLINE bool operator>=(const local_time& lhs, const local_time& rhs) -{ - return !(lhs < rhs); -} - -TOML11_INLINE std::ostream& operator<<(std::ostream& os, const local_time& time) -{ - os << std::setfill('0') << std::setw(2) << static_cast(time.hour ) << ':'; - os << std::setfill('0') << std::setw(2) << static_cast(time.minute) << ':'; - os << std::setfill('0') << std::setw(2) << static_cast(time.second); - if(time.millisecond != 0 || time.microsecond != 0 || time.nanosecond != 0) - { - os << '.'; - os << std::setfill('0') << std::setw(3) << static_cast(time.millisecond); - if(time.microsecond != 0 || time.nanosecond != 0) - { - os << std::setfill('0') << std::setw(3) << static_cast(time.microsecond); - if(time.nanosecond != 0) - { - os << std::setfill('0') << std::setw(3) << static_cast(time.nanosecond); - } - } - } - return os; -} - -TOML11_INLINE std::string to_string(const local_time& time) -{ - std::ostringstream oss; - oss.imbue(std::locale::classic()); - oss << time; - return oss.str(); -} - -// ---------------------------------------------------------------------------- - -TOML11_INLINE time_offset::operator std::chrono::minutes() const -{ - return std::chrono::minutes(this->minute) + - std::chrono::hours(this->hour); -} - -TOML11_INLINE bool operator==(const time_offset& lhs, const time_offset& rhs) -{ - return std::make_tuple(lhs.hour, lhs.minute) == - std::make_tuple(rhs.hour, rhs.minute); -} -TOML11_INLINE bool operator!=(const time_offset& lhs, const time_offset& rhs) -{ - return !(lhs == rhs); -} -TOML11_INLINE bool operator< (const time_offset& lhs, const time_offset& rhs) -{ - return std::make_tuple(lhs.hour, lhs.minute) < - std::make_tuple(rhs.hour, rhs.minute); -} -TOML11_INLINE bool operator<=(const time_offset& lhs, const time_offset& rhs) -{ - return (lhs < rhs) || (lhs == rhs); -} -TOML11_INLINE bool operator> (const time_offset& lhs, const time_offset& rhs) -{ - return !(lhs <= rhs); -} -TOML11_INLINE bool operator>=(const time_offset& lhs, const time_offset& rhs) -{ - return !(lhs < rhs); -} - -TOML11_INLINE std::ostream& operator<<(std::ostream& os, const time_offset& offset) -{ - if(offset.hour == 0 && offset.minute == 0) - { - os << 'Z'; - return os; - } - int minute = static_cast(offset.hour) * 60 + offset.minute; - if(minute < 0){os << '-'; minute = std::abs(minute);} else {os << '+';} - os << std::setfill('0') << std::setw(2) << minute / 60 << ':'; - os << std::setfill('0') << std::setw(2) << minute % 60; - return os; -} - -TOML11_INLINE std::string to_string(const time_offset& offset) -{ - std::ostringstream oss; - oss.imbue(std::locale::classic()); - oss << offset; - return oss.str(); -} - -// ----------------------------------------------------------------------------- - -TOML11_INLINE local_datetime::local_datetime(const std::chrono::system_clock::time_point& tp) -{ - const auto t = std::chrono::system_clock::to_time_t(tp); - std::tm ltime = detail::localtime_s(&t); - - this->date = local_date(ltime); - this->time = local_time(ltime); - - // std::tm lacks subsecond information, so diff between tp and tm - // can be used to get millisecond & microsecond information. - const auto t_diff = tp - - std::chrono::system_clock::from_time_t(std::mktime(<ime)); - this->time.millisecond = static_cast( - std::chrono::duration_cast(t_diff).count()); - this->time.microsecond = static_cast( - std::chrono::duration_cast(t_diff).count()); - this->time.nanosecond = static_cast( - std::chrono::duration_cast(t_diff).count()); -} - -TOML11_INLINE local_datetime::local_datetime(const std::time_t t) - : local_datetime{std::chrono::system_clock::from_time_t(t)} -{} - -TOML11_INLINE local_datetime::operator std::chrono::system_clock::time_point() const -{ - using internal_duration = - typename std::chrono::system_clock::time_point::duration; - - // Normally DST begins at A.M. 3 or 4. If we re-use conversion operator - // of local_date and local_time independently, the conversion fails if - // it is the day when DST begins or ends. Since local_date considers the - // time is 00:00 A.M. and local_time does not consider DST because it - // does not have any date information. We need to consider both date and - // time information at the same time to convert it correctly. - - std::tm t; - t.tm_sec = static_cast(this->time.second); - t.tm_min = static_cast(this->time.minute); - t.tm_hour = static_cast(this->time.hour); - t.tm_mday = static_cast(this->date.day); - t.tm_mon = static_cast(this->date.month); - t.tm_year = static_cast(this->date.year) - 1900; - t.tm_wday = 0; // the value will be ignored - t.tm_yday = 0; // the value will be ignored - t.tm_isdst = -1; - - // std::mktime returns date as local time zone. no conversion needed - auto dt = std::chrono::system_clock::from_time_t(std::mktime(&t)); - dt += std::chrono::duration_cast( - std::chrono::milliseconds(this->time.millisecond) + - std::chrono::microseconds(this->time.microsecond) + - std::chrono::nanoseconds (this->time.nanosecond)); - return dt; -} - -TOML11_INLINE local_datetime::operator std::time_t() const -{ - return std::chrono::system_clock::to_time_t( - std::chrono::system_clock::time_point(*this)); -} - -TOML11_INLINE bool operator==(const local_datetime& lhs, const local_datetime& rhs) -{ - return std::make_tuple(lhs.date, lhs.time) == - std::make_tuple(rhs.date, rhs.time); -} -TOML11_INLINE bool operator!=(const local_datetime& lhs, const local_datetime& rhs) -{ - return !(lhs == rhs); -} -TOML11_INLINE bool operator< (const local_datetime& lhs, const local_datetime& rhs) -{ - return std::make_tuple(lhs.date, lhs.time) < - std::make_tuple(rhs.date, rhs.time); -} -TOML11_INLINE bool operator<=(const local_datetime& lhs, const local_datetime& rhs) -{ - return (lhs < rhs) || (lhs == rhs); -} -TOML11_INLINE bool operator> (const local_datetime& lhs, const local_datetime& rhs) -{ - return !(lhs <= rhs); -} -TOML11_INLINE bool operator>=(const local_datetime& lhs, const local_datetime& rhs) -{ - return !(lhs < rhs); -} - -TOML11_INLINE std::ostream& operator<<(std::ostream& os, const local_datetime& dt) -{ - os << dt.date << 'T' << dt.time; - return os; -} - -TOML11_INLINE std::string to_string(const local_datetime& dt) -{ - std::ostringstream oss; - oss.imbue(std::locale::classic()); - oss << dt; - return oss.str(); -} - -// ----------------------------------------------------------------------------- - - -TOML11_INLINE offset_datetime::offset_datetime(const local_datetime& ld) - : date{ld.date}, time{ld.time}, offset{get_local_offset(nullptr)} - // use the current local timezone offset -{} -TOML11_INLINE offset_datetime::offset_datetime(const std::chrono::system_clock::time_point& tp) - : offset{0, 0} // use gmtime -{ - const auto timet = std::chrono::system_clock::to_time_t(tp); - const auto tm = detail::gmtime_s(&timet); - this->date = local_date(tm); - this->time = local_time(tm); -} -TOML11_INLINE offset_datetime::offset_datetime(const std::time_t& t) - : offset{0, 0} // use gmtime -{ - const auto tm = detail::gmtime_s(&t); - this->date = local_date(tm); - this->time = local_time(tm); -} -TOML11_INLINE offset_datetime::offset_datetime(const std::tm& t) - : offset{0, 0} // assume gmtime -{ - this->date = local_date(t); - this->time = local_time(t); -} - -TOML11_INLINE offset_datetime::operator std::chrono::system_clock::time_point() const -{ - // get date-time - using internal_duration = - typename std::chrono::system_clock::time_point::duration; - - // first, convert it to local date-time information in the same way as - // local_datetime does. later we will use time_t to adjust time offset. - std::tm t; - t.tm_sec = static_cast(this->time.second); - t.tm_min = static_cast(this->time.minute); - t.tm_hour = static_cast(this->time.hour); - t.tm_mday = static_cast(this->date.day); - t.tm_mon = static_cast(this->date.month); - t.tm_year = static_cast(this->date.year) - 1900; - t.tm_wday = 0; // the value will be ignored - t.tm_yday = 0; // the value will be ignored - t.tm_isdst = -1; - const std::time_t tp_loc = std::mktime(std::addressof(t)); - - auto tp = std::chrono::system_clock::from_time_t(tp_loc); - tp += std::chrono::duration_cast( - std::chrono::milliseconds(this->time.millisecond) + - std::chrono::microseconds(this->time.microsecond) + - std::chrono::nanoseconds (this->time.nanosecond)); - - // Since mktime uses local time zone, it should be corrected. - // `12:00:00+09:00` means `03:00:00Z`. So mktime returns `03:00:00Z` if - // we are in `+09:00` timezone. To represent `12:00:00Z` there, we need - // to add `+09:00` to `03:00:00Z`. - // Here, it uses the time_t converted from date-time info to handle - // daylight saving time. - const auto ofs = get_local_offset(std::addressof(tp_loc)); - tp += std::chrono::hours (ofs.hour); - tp += std::chrono::minutes(ofs.minute); - - // We got `12:00:00Z` by correcting local timezone applied by mktime. - // Then we will apply the offset. Let's say `12:00:00-08:00` is given. - // And now, we have `12:00:00Z`. `12:00:00-08:00` means `20:00:00Z`. - // So we need to subtract the offset. - tp -= std::chrono::minutes(this->offset); - return tp; -} - -TOML11_INLINE offset_datetime::operator std::time_t() const -{ - return std::chrono::system_clock::to_time_t( - std::chrono::system_clock::time_point(*this)); -} - -TOML11_INLINE time_offset offset_datetime::get_local_offset(const std::time_t* tp) -{ - // get local timezone with the same date-time information as mktime - const auto t = detail::localtime_s(tp); - - std::array buf; - const auto result = std::strftime(buf.data(), 6, "%z", &t); // +hhmm\0 - if(result != 5) - { - throw std::runtime_error("toml::offset_datetime: cannot obtain " - "timezone information of current env"); - } - const int ofs = std::atoi(buf.data()); - const int ofs_h = ofs / 100; - const int ofs_m = ofs - (ofs_h * 100); - return time_offset(ofs_h, ofs_m); -} - -TOML11_INLINE bool operator==(const offset_datetime& lhs, const offset_datetime& rhs) -{ - return std::make_tuple(lhs.date, lhs.time, lhs.offset) == - std::make_tuple(rhs.date, rhs.time, rhs.offset); -} -TOML11_INLINE bool operator!=(const offset_datetime& lhs, const offset_datetime& rhs) -{ - return !(lhs == rhs); -} -TOML11_INLINE bool operator< (const offset_datetime& lhs, const offset_datetime& rhs) -{ - return std::make_tuple(lhs.date, lhs.time, lhs.offset) < - std::make_tuple(rhs.date, rhs.time, rhs.offset); -} -TOML11_INLINE bool operator<=(const offset_datetime& lhs, const offset_datetime& rhs) -{ - return (lhs < rhs) || (lhs == rhs); -} -TOML11_INLINE bool operator> (const offset_datetime& lhs, const offset_datetime& rhs) -{ - return !(lhs <= rhs); -} -TOML11_INLINE bool operator>=(const offset_datetime& lhs, const offset_datetime& rhs) -{ - return !(lhs < rhs); -} - -TOML11_INLINE std::ostream& operator<<(std::ostream& os, const offset_datetime& dt) -{ - os << dt.date << 'T' << dt.time << dt.offset; - return os; -} - -TOML11_INLINE std::string to_string(const offset_datetime& dt) -{ - std::ostringstream oss; - oss.imbue(std::locale::classic()); - oss << dt; - return oss.str(); -} - -}//toml -#endif // TOML11_DATETIME_IMPL_HPP -#endif - -#endif // TOML11_DATETIME_HPP -#ifndef TOML11_COMPAT_HPP -#define TOML11_COMPAT_HPP - - -#include -#include -#include -#include -#include - -#include - -#if TOML11_CPLUSPLUS_STANDARD_VERSION >= TOML11_CXX20_VALUE -# if __has_include() -# include -# endif -#endif - -#include - -// ---------------------------------------------------------------------------- - -#if TOML11_CPLUSPLUS_STANDARD_VERSION >= TOML11_CXX14_VALUE -# if __has_cpp_attribute(deprecated) -# define TOML11_HAS_ATTR_DEPRECATED 1 -# endif -#endif - -#if defined(TOML11_HAS_ATTR_DEPRECATED) -# define TOML11_DEPRECATED(msg) [[deprecated(msg)]] -#elif defined(__GNUC__) -# define TOML11_DEPRECATED(msg) __attribute__((deprecated(msg))) -#elif defined(_MSC_VER) -# define TOML11_DEPRECATED(msg) __declspec(deprecated(msg)) -#else -# define TOML11_DEPRECATED(msg) -#endif - -// ---------------------------------------------------------------------------- - -#if defined(__cpp_if_constexpr) -# if __cpp_if_constexpr >= 201606L -# define TOML11_HAS_CONSTEXPR_IF 1 -# endif -#endif - -#if defined(TOML11_HAS_CONSTEXPR_IF) -# define TOML11_CONSTEXPR_IF if constexpr -#else -# define TOML11_CONSTEXPR_IF if -#endif - -// ---------------------------------------------------------------------------- - -#if TOML11_CPLUSPLUS_STANDARD_VERSION >= TOML11_CXX14_VALUE -# if defined(__cpp_lib_make_unique) -# if __cpp_lib_make_unique >= 201304L -# define TOML11_HAS_STD_MAKE_UNIQUE 1 -# endif -# endif -#endif - -namespace toml -{ -namespace cxx -{ - -#if defined(TOML11_HAS_STD_MAKE_UNIQUE) - -using std::make_unique; - -#else - -template -std::unique_ptr make_unique(Ts&& ... args) -{ - return std::unique_ptr(new T(std::forward(args)...)); -} - -#endif // TOML11_HAS_STD_MAKE_UNIQUE - -} // cxx -} // toml - -// --------------------------------------------------------------------------- - -#if TOML11_CPLUSPLUS_STANDARD_VERSION >= TOML11_CXX14_VALUE -# if defined(__cpp_lib_make_reverse_iterator) -# if __cpp_lib_make_reverse_iterator >= 201402L -# define TOML11_HAS_STD_MAKE_REVERSE_ITERATOR 1 -# endif -# endif -#endif - -namespace toml -{ -namespace cxx -{ -# if defined(TOML11_HAS_STD_MAKE_REVERSE_ITERATOR) - -using std::make_reverse_iterator; - -#else - -template -std::reverse_iterator make_reverse_iterator(Iterator iter) -{ - return std::reverse_iterator(iter); -} - -#endif // TOML11_HAS_STD_MAKE_REVERSE_ITERATOR - -} // cxx -} // toml - -// --------------------------------------------------------------------------- - -#if TOML11_CPLUSPLUS_STANDARD_VERSION >= TOML11_CXX20_VALUE -# if defined(__cpp_lib_clamp) -# if __cpp_lib_clamp >= 201603L -# define TOML11_HAS_STD_CLAMP 1 -# endif -# endif -#endif - -namespace toml -{ -namespace cxx -{ -#if defined(TOML11_HAS_STD_CLAMP) - -using std::clamp; - -#else - -template -T clamp(const T& x, const T& low, const T& high) noexcept -{ - assert(low <= high); - return (std::min)((std::max)(x, low), high); -} - -#endif // TOML11_HAS_STD_CLAMP - -} // cxx -} // toml - -// --------------------------------------------------------------------------- - -#if TOML11_CPLUSPLUS_STANDARD_VERSION >= TOML11_CXX20_VALUE -# if defined(__cpp_lib_bit_cast) -# if __cpp_lib_bit_cast >= 201806L -# define TOML11_HAS_STD_BIT_CAST 1 -# endif -# endif -#endif - -namespace toml -{ -namespace cxx -{ -#if defined(TOML11_HAS_STD_BIT_CAST) - -using std::bit_cast; - -#else - -template -U bit_cast(const T& x) noexcept -{ - static_assert(sizeof(T) == sizeof(U), ""); - static_assert(std::is_default_constructible::value, ""); - - U z; - std::memcpy(reinterpret_cast(std::addressof(z)), - reinterpret_cast(std::addressof(x)), - sizeof(T)); - - return z; -} - -#endif // TOML11_HAS_STD_BIT_CAST - -} // cxx -} // toml - -// --------------------------------------------------------------------------- -// C++20 remove_cvref_t - -#if TOML11_CPLUSPLUS_STANDARD_VERSION >= TOML11_CXX20_VALUE -# if defined(__cpp_lib_remove_cvref) -# if __cpp_lib_remove_cvref >= 201711L -# define TOML11_HAS_STD_REMOVE_CVREF 1 -# endif -# endif -#endif - -namespace toml -{ -namespace cxx -{ -#if defined(TOML11_HAS_STD_REMOVE_CVREF) - -using std::remove_cvref; -using std::remove_cvref_t; - -#else - -template -struct remove_cvref -{ - using type = typename std::remove_cv< - typename std::remove_reference::type>::type; -}; - -template -using remove_cvref_t = typename remove_cvref::type; - -#endif // TOML11_HAS_STD_REMOVE_CVREF - -} // cxx -} // toml - -// --------------------------------------------------------------------------- -// C++17 and/or/not - -#if TOML11_CPLUSPLUS_STANDARD_VERSION >= TOML11_CXX17_VALUE -# if defined(__cpp_lib_logical_traits) -# if __cpp_lib_logical_traits >= 201510L -# define TOML11_HAS_STD_CONJUNCTION 1 -# endif -# endif -#endif - -namespace toml -{ -namespace cxx -{ -#if defined(TOML11_HAS_STD_CONJUNCTION) - -using std::conjunction; -using std::disjunction; -using std::negation; - -#else - -template struct conjunction : std::true_type{}; -template struct conjunction : T{}; -template -struct conjunction : - std::conditional(T::value), conjunction, T>::type -{}; - -template struct disjunction : std::false_type{}; -template struct disjunction : T {}; -template -struct disjunction : - std::conditional(T::value), T, disjunction>::type -{}; - -template -struct negation : std::integral_constant(T::value)>{}; - -#endif // TOML11_HAS_STD_CONJUNCTION - -} // cxx -} // toml - -// --------------------------------------------------------------------------- -// C++14 index_sequence - -#if TOML11_CPLUSPLUS_STANDARD_VERSION >= TOML11_CXX14_VALUE -# if defined(__cpp_lib_integer_sequence) -# if __cpp_lib_integer_sequence >= 201304L -# define TOML11_HAS_STD_INTEGER_SEQUENCE 1 -# endif -# endif -#endif - -namespace toml -{ -namespace cxx -{ -#if defined(TOML11_HAS_STD_INTEGER_SEQUENCE) - -using std::index_sequence; -using std::make_index_sequence; - -#else - -template struct index_sequence{}; - -template -struct double_index_sequence; - -template -struct double_index_sequence> -{ - using type = index_sequence; -}; -template -struct double_index_sequence> -{ - using type = index_sequence; -}; - -template -struct index_sequence_maker -{ - using type = typename double_index_sequence< - N % 2 == 1, N/2, typename index_sequence_maker::type - >::type; -}; -template<> -struct index_sequence_maker<0> -{ - using type = index_sequence<>; -}; - -template -using make_index_sequence = typename index_sequence_maker::type; - -#endif // TOML11_HAS_STD_INTEGER_SEQUENCE - -} // cxx -} // toml - -// --------------------------------------------------------------------------- -// C++14 enable_if_t - -#if TOML11_CPLUSPLUS_STANDARD_VERSION >= TOML11_CXX14_VALUE -# if defined(__cpp_lib_transformation_trait_aliases) -# if __cpp_lib_transformation_trait_aliases >= 201304L -# define TOML11_HAS_STD_ENABLE_IF_T 1 -# endif -# endif -#endif - -namespace toml -{ -namespace cxx -{ -#if defined(TOML11_HAS_STD_ENABLE_IF_T) - -using std::enable_if_t; - -#else - -template -using enable_if_t = typename std::enable_if::type; - -#endif // TOML11_HAS_STD_ENABLE_IF_T - -} // cxx -} // toml - -// --------------------------------------------------------------------------- -// return_type_of_t - -#if TOML11_CPLUSPLUS_STANDARD_VERSION >= TOML11_CXX17_VALUE -# if defined(__cpp_lib_is_invocable) -# if __cpp_lib_is_invocable >= 201703 -# define TOML11_HAS_STD_INVOKE_RESULT 1 -# endif -# endif -#endif - -namespace toml -{ -namespace cxx -{ -#if defined(TOML11_HAS_STD_INVOKE_RESULT) - -template -using return_type_of_t = std::invoke_result_t; - -#else - -// result_of is deprecated after C++17 -template -using return_type_of_t = typename std::result_of::type; - -#endif // TOML11_HAS_STD_INVOKE_RESULT - -} // cxx -} // toml - -// ---------------------------------------------------------------------------- -// (subset of) source_location - -#if TOML11_CPLUSPLUS_STANDARD_VERSION >= 202002L -# if __has_include() -# define TOML11_HAS_STD_SOURCE_LOCATION -# endif // has_include -#endif // c++20 - -#if ! defined(TOML11_HAS_STD_SOURCE_LOCATION) -# if defined(__GNUC__) && ! defined(__clang__) -# if TOML11_CPLUSPLUS_STANDARD_VERSION >= TOML11_CXX14_VALUE -# if __has_include() -# define TOML11_HAS_EXPERIMENTAL_SOURCE_LOCATION -# endif -# endif -# endif // GNU g++ -#endif // not TOML11_HAS_STD_SOURCE_LOCATION - -#if ! defined(TOML11_HAS_STD_SOURCE_LOCATION) && ! defined(TOML11_HAS_EXPERIMENTAL_SOURCE_LOCATION) -# if defined(__GNUC__) && ! defined(__clang__) -# if (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 9)) -# define TOML11_HAS_BUILTIN_FILE_LINE 1 -# define TOML11_BUILTIN_LINE_TYPE int -# endif -# elif defined(__clang__) // clang 9.0.0 implements builtin_FILE/LINE -# if __has_builtin(__builtin_FILE) && __has_builtin(__builtin_LINE) -# define TOML11_HAS_BUILTIN_FILE_LINE 1 -# define TOML11_BUILTIN_LINE_TYPE unsigned int -# endif -# elif defined(_MSVC_LANG) && defined(_MSC_VER) -# if _MSC_VER > 1926 -# define TOML11_HAS_BUILTIN_FILE_LINE 1 -# define TOML11_BUILTIN_LINE_TYPE int -# endif -# endif -#endif - -#if defined(TOML11_HAS_STD_SOURCE_LOCATION) -#include -namespace toml -{ -namespace cxx -{ -using source_location = std::source_location; - -inline std::string to_string(const source_location& loc) -{ - return std::string(" at line ") + std::to_string(loc.line()) + - std::string(" in file ") + std::string(loc.file_name()); -} -} // cxx -} // toml -#elif defined(TOML11_HAS_EXPERIMENTAL_SOURCE_LOCATION) -#include -namespace toml -{ -namespace cxx -{ -using source_location = std::experimental::source_location; - -inline std::string to_string(const source_location& loc) -{ - return std::string(" at line ") + std::to_string(loc.line()) + - std::string(" in file ") + std::string(loc.file_name()); -} -} // cxx -} // toml -#elif defined(TOML11_HAS_BUILTIN_FILE_LINE) -namespace toml -{ -namespace cxx -{ -struct source_location -{ - using line_type = TOML11_BUILTIN_LINE_TYPE; - static source_location current(const line_type line = __builtin_LINE(), - const char* file = __builtin_FILE()) - { - return source_location(line, file); - } - - source_location(const line_type line, const char* file) - : line_(line), file_name_(file) - {} - - line_type line() const noexcept {return line_;} - const char* file_name() const noexcept {return file_name_;} - - private: - - line_type line_; - const char* file_name_; -}; - -inline std::string to_string(const source_location& loc) -{ - return std::string(" at line ") + std::to_string(loc.line()) + - std::string(" in file ") + std::string(loc.file_name()); -} -} // cxx -} // toml -#else // no builtin -namespace toml -{ -namespace cxx -{ -struct source_location -{ - static source_location current() { return source_location{}; } -}; - -inline std::string to_string(const source_location&) -{ - return std::string(""); -} -} // cxx -} // toml -#endif // TOML11_HAS_STD_SOURCE_LOCATION - -// ---------------------------------------------------------------------------- -// (subset of) optional - -#if TOML11_CPLUSPLUS_STANDARD_VERSION >= TOML11_CXX17_VALUE -# if __has_include() -# include -# endif // has_include(optional) -#endif // C++17 - -#if TOML11_CPLUSPLUS_STANDARD_VERSION >= TOML11_CXX17_VALUE -# if defined(__cpp_lib_optional) -# if __cpp_lib_optional >= 201606L -# define TOML11_HAS_STD_OPTIONAL 1 -# endif -# endif -#endif - -#if defined(TOML11_HAS_STD_OPTIONAL) - -namespace toml -{ -namespace cxx -{ -using std::optional; - -inline std::nullopt_t make_nullopt() {return std::nullopt;} - -template -std::basic_ostream& -operator<<(std::basic_ostream& os, const std::nullopt_t&) -{ - os << "nullopt"; - return os; -} - -} // cxx -} // toml - -#else // TOML11_HAS_STD_OPTIONAL - -namespace toml -{ -namespace cxx -{ - -struct nullopt_t{}; -inline nullopt_t make_nullopt() {return nullopt_t{};} - -inline bool operator==(const nullopt_t&, const nullopt_t&) noexcept {return true;} -inline bool operator!=(const nullopt_t&, const nullopt_t&) noexcept {return false;} -inline bool operator< (const nullopt_t&, const nullopt_t&) noexcept {return false;} -inline bool operator<=(const nullopt_t&, const nullopt_t&) noexcept {return true;} -inline bool operator> (const nullopt_t&, const nullopt_t&) noexcept {return false;} -inline bool operator>=(const nullopt_t&, const nullopt_t&) noexcept {return true;} - -template -std::basic_ostream& -operator<<(std::basic_ostream& os, const nullopt_t&) -{ - os << "nullopt"; - return os; -} - -template -class optional -{ - public: - - using value_type = T; - - public: - - optional() noexcept : has_value_(false), null_('\0') {} - optional(nullopt_t) noexcept : has_value_(false), null_('\0') {} - - optional(const T& x): has_value_(true), value_(x) {} - optional(T&& x): has_value_(true), value_(std::move(x)) {} - - template::value, std::nullptr_t> = nullptr> - explicit optional(U&& x): has_value_(true), value_(std::forward(x)) {} - - optional(const optional& rhs): has_value_(rhs.has_value_) - { - if(rhs.has_value_) - { - this->assigner(rhs.value_); - } - } - optional(optional&& rhs): has_value_(rhs.has_value_) - { - if(this->has_value_) - { - this->assigner(std::move(rhs.value_)); - } - } - - optional& operator=(const optional& rhs) - { - if(this == std::addressof(rhs)) {return *this;} - - this->cleanup(); - this->has_value_ = rhs.has_value_; - if(this->has_value_) - { - this->assigner(rhs.value_); - } - return *this; - } - optional& operator=(optional&& rhs) - { - if(this == std::addressof(rhs)) {return *this;} - - this->cleanup(); - this->has_value_ = rhs.has_value_; - if(this->has_value_) - { - this->assigner(std::move(rhs.value_)); - } - return *this; - } - - template>, std::is_constructible - >::value, std::nullptr_t> = nullptr> - explicit optional(const optional& rhs): has_value_(rhs.has_value_), null_('\0') - { - if(rhs.has_value_) - { - this->assigner(rhs.value_); - } - } - template>, std::is_constructible - >::value, std::nullptr_t> = nullptr> - explicit optional(optional&& rhs): has_value_(rhs.has_value_), null_('\0') - { - if(this->has_value_) - { - this->assigner(std::move(rhs.value_)); - } - } - - template>, std::is_constructible - >::value, std::nullptr_t> = nullptr> - optional& operator=(const optional& rhs) - { - if(this == std::addressof(rhs)) {return *this;} - - this->cleanup(); - this->has_value_ = rhs.has_value_; - if(this->has_value_) - { - this->assigner(rhs.value_); - } - return *this; - } - - template>, std::is_constructible - >::value, std::nullptr_t> = nullptr> - optional& operator=(optional&& rhs) - { - if(this == std::addressof(rhs)) {return *this;} - - this->cleanup(); - this->has_value_ = rhs.has_value_; - if(this->has_value_) - { - this->assigner(std::move(rhs.value_)); - } - return *this; - } - ~optional() noexcept - { - this->cleanup(); - } - - explicit operator bool() const noexcept - { - return has_value_; - } - - bool has_value() const noexcept {return has_value_;} - - value_type const& value(source_location loc = source_location::current()) const - { - if( ! this->has_value_) - { - throw std::runtime_error("optional::value(): bad_unwrap" + to_string(loc)); - } - return this->value_; - } - value_type& value(source_location loc = source_location::current()) - { - if( ! this->has_value_) - { - throw std::runtime_error("optional::value(): bad_unwrap" + to_string(loc)); - } - return this->value_; - } - - value_type const& value_or(const value_type& opt) const - { - if(this->has_value_) {return this->value_;} else {return opt;} - } - value_type& value_or(value_type& opt) - { - if(this->has_value_) {return this->value_;} else {return opt;} - } - - private: - - void cleanup() noexcept - { - if(this->has_value_) - { - value_.~T(); - } - } - - template - void assigner(U&& x) - { - const auto tmp = ::new(std::addressof(this->value_)) value_type(std::forward(x)); - assert(tmp == std::addressof(this->value_)); - (void)tmp; - } - - private: - - bool has_value_; - union - { - char null_; - T value_; - }; -}; -} // cxx -} // toml -#endif // TOML11_HAS_STD_OPTIONAL - -#endif // TOML11_COMPAT_HPP -#ifndef TOML11_VALUE_T_HPP -#define TOML11_VALUE_T_HPP - -#ifndef TOML11_VALUE_T_FWD_HPP -#define TOML11_VALUE_T_FWD_HPP - - -#include -#include -#include - -#include - -namespace toml -{ - -// forward decl -template -class basic_value; - -// ---------------------------------------------------------------------------- -// enum representing toml types - -enum class value_t : std::uint8_t -{ - empty = 0, - boolean = 1, - integer = 2, - floating = 3, - string = 4, - offset_datetime = 5, - local_datetime = 6, - local_date = 7, - local_time = 8, - array = 9, - table = 10 -}; - -std::ostream& operator<<(std::ostream& os, value_t t); -std::string to_string(value_t t); - - -// ---------------------------------------------------------------------------- -// meta functions for internal use - -namespace detail -{ - -template -using value_t_constant = std::integral_constant; - -template -struct type_to_enum : value_t_constant {}; - -template struct type_to_enum : value_t_constant {}; -template struct type_to_enum : value_t_constant {}; -template struct type_to_enum : value_t_constant {}; -template struct type_to_enum : value_t_constant {}; -template struct type_to_enum : value_t_constant {}; -template struct type_to_enum : value_t_constant {}; -template struct type_to_enum : value_t_constant {}; -template struct type_to_enum : value_t_constant {}; -template struct type_to_enum : value_t_constant {}; -template struct type_to_enum : value_t_constant {}; - -template -struct enum_to_type { using type = void; }; - -template struct enum_to_type { using type = typename V::boolean_type ; }; -template struct enum_to_type { using type = typename V::integer_type ; }; -template struct enum_to_type { using type = typename V::floating_type ; }; -template struct enum_to_type { using type = typename V::string_type ; }; -template struct enum_to_type { using type = typename V::offset_datetime_type; }; -template struct enum_to_type { using type = typename V::local_datetime_type ; }; -template struct enum_to_type { using type = typename V::local_date_type ; }; -template struct enum_to_type { using type = typename V::local_time_type ; }; -template struct enum_to_type { using type = typename V::array_type ; }; -template struct enum_to_type { using type = typename V::table_type ; }; - -template -using enum_to_type_t = typename enum_to_type::type; - -template -struct enum_to_fmt_type { using type = void; }; - -template<> struct enum_to_fmt_type { using type = boolean_format_info ; }; -template<> struct enum_to_fmt_type { using type = integer_format_info ; }; -template<> struct enum_to_fmt_type { using type = floating_format_info ; }; -template<> struct enum_to_fmt_type { using type = string_format_info ; }; -template<> struct enum_to_fmt_type { using type = offset_datetime_format_info; }; -template<> struct enum_to_fmt_type { using type = local_datetime_format_info ; }; -template<> struct enum_to_fmt_type { using type = local_date_format_info ; }; -template<> struct enum_to_fmt_type { using type = local_time_format_info ; }; -template<> struct enum_to_fmt_type { using type = array_format_info ; }; -template<> struct enum_to_fmt_type { using type = table_format_info ; }; - -template -using enum_to_fmt_type_t = typename enum_to_fmt_type::type; - -template -struct is_exact_toml_type0 : cxx::disjunction< - std::is_same, - std::is_same, - std::is_same, - std::is_same, - std::is_same, - std::is_same, - std::is_same, - std::is_same, - std::is_same, - std::is_same - >{}; -template struct is_exact_toml_type: is_exact_toml_type0, V> {}; -template struct is_not_toml_type : cxx::negation> {}; - -} // namespace detail -} // namespace toml -#endif // TOML11_VALUE_T_FWD_HPP - -#if ! defined(TOML11_COMPILE_SOURCES) -#ifndef TOML11_VALUE_T_IMPL_HPP -#define TOML11_VALUE_T_IMPL_HPP - - -#include -#include -#include - -namespace toml -{ - -TOML11_INLINE std::ostream& operator<<(std::ostream& os, value_t t) -{ - switch(t) - { - case value_t::boolean : os << "boolean"; return os; - case value_t::integer : os << "integer"; return os; - case value_t::floating : os << "floating"; return os; - case value_t::string : os << "string"; return os; - case value_t::offset_datetime : os << "offset_datetime"; return os; - case value_t::local_datetime : os << "local_datetime"; return os; - case value_t::local_date : os << "local_date"; return os; - case value_t::local_time : os << "local_time"; return os; - case value_t::array : os << "array"; return os; - case value_t::table : os << "table"; return os; - case value_t::empty : os << "empty"; return os; - default : os << "unknown"; return os; - } -} - -TOML11_INLINE std::string to_string(value_t t) -{ - std::ostringstream oss; - oss << t; - return oss.str(); -} - -} // namespace toml -#endif // TOML11_VALUE_T_IMPL_HPP -#endif - -#endif // TOML11_VALUE_T_HPP -#ifndef TOML11_STORAGE_HPP -#define TOML11_STORAGE_HPP - - -namespace toml -{ -namespace detail -{ - -// It owns a pointer to T. It does deep-copy when copied. -// This struct is introduced to implement a recursive type. -// -// `toml::value` contains `std::vector` to represent a toml array. -// But, in the definition of `toml::value`, `toml::value` is still incomplete. -// `std::vector` of an incomplete type is not allowed in C++11 (it is allowed -// after C++17). To avoid this, we need to use a pointer to `toml::value`, like -// `std::vector>`. Although `std::unique_ptr` is -// noncopyable, we want to make `toml::value` copyable. `storage` is introduced -// to resolve those problems. -template -struct storage -{ - using value_type = T; - - explicit storage(value_type v): ptr_(cxx::make_unique(std::move(v))) {} - ~storage() = default; - - storage(const storage& rhs): ptr_(cxx::make_unique(*rhs.ptr_)) {} - storage& operator=(const storage& rhs) - { - this->ptr_ = cxx::make_unique(*rhs.ptr_); - return *this; - } - - storage(storage&&) = default; - storage& operator=(storage&&) = default; - - bool is_ok() const noexcept {return static_cast(ptr_);} - - value_type& get() const noexcept {return *ptr_;} - - private: - std::unique_ptr ptr_; -}; - -} // detail -} // toml -#endif // TOML11_STORAGE_HPP -#ifndef TOML11_COMMENTS_HPP -#define TOML11_COMMENTS_HPP - -#ifndef TOML11_COMMENTS_FWD_HPP -#define TOML11_COMMENTS_FWD_HPP - -// to use __has_builtin - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// This file provides mainly two classes, `preserve_comments` and `discard_comments`. -// Those two are a container that have the same interface as `std::vector` -// but bahaves in the opposite way. `preserve_comments` is just the same as -// `std::vector` and each `std::string` corresponds to a comment line. -// Conversely, `discard_comments` discards all the strings and ignores everything -// assigned in it. `discard_comments` is always empty and you will encounter an -// error whenever you access to the element. -namespace toml -{ -class discard_comments; // forward decl - -class preserve_comments -{ - public: - // `container_type` is not provided in discard_comments. - // do not use this inner-type in a generic code. - using container_type = std::vector; - - using size_type = container_type::size_type; - using difference_type = container_type::difference_type; - using value_type = container_type::value_type; - using reference = container_type::reference; - using const_reference = container_type::const_reference; - using pointer = container_type::pointer; - using const_pointer = container_type::const_pointer; - using iterator = container_type::iterator; - using const_iterator = container_type::const_iterator; - using reverse_iterator = container_type::reverse_iterator; - using const_reverse_iterator = container_type::const_reverse_iterator; - - public: - - preserve_comments() = default; - ~preserve_comments() = default; - preserve_comments(preserve_comments const&) = default; - preserve_comments(preserve_comments &&) = default; - preserve_comments& operator=(preserve_comments const&) = default; - preserve_comments& operator=(preserve_comments &&) = default; - - explicit preserve_comments(const std::vector& c): comments(c){} - explicit preserve_comments(std::vector&& c) - : comments(std::move(c)) - {} - preserve_comments& operator=(const std::vector& c) - { - comments = c; - return *this; - } - preserve_comments& operator=(std::vector&& c) - { - comments = std::move(c); - return *this; - } - - explicit preserve_comments(const discard_comments&) {} - - explicit preserve_comments(size_type n): comments(n) {} - preserve_comments(size_type n, const std::string& x): comments(n, x) {} - preserve_comments(std::initializer_list x): comments(x) {} - template - preserve_comments(InputIterator first, InputIterator last) - : comments(first, last) - {} - - template - void assign(InputIterator first, InputIterator last) {comments.assign(first, last);} - void assign(std::initializer_list ini) {comments.assign(ini);} - void assign(size_type n, const std::string& val) {comments.assign(n, val);} - - // Related to the issue #97. - // - // `std::vector::insert` and `std::vector::erase` in the STL implementation - // included in GCC 4.8.5 takes `std::vector::iterator` instead of - // `std::vector::const_iterator`. It causes compilation error in GCC 4.8.5. -#if defined(__GNUC__) && defined(__GNUC_MINOR__) && defined(__GNUC_PATCHLEVEL__) && !defined(__clang__) -# if (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) <= 40805 -# define TOML11_WORKAROUND_GCC_4_8_X_STANDARD_LIBRARY_IMPLEMENTATION -# endif -#endif - -#ifdef TOML11_WORKAROUND_GCC_4_8_X_STANDARD_LIBRARY_IMPLEMENTATION - iterator insert(iterator p, const std::string& x) - { - return comments.insert(p, x); - } - iterator insert(iterator p, std::string&& x) - { - return comments.insert(p, std::move(x)); - } - void insert(iterator p, size_type n, const std::string& x) - { - return comments.insert(p, n, x); - } - template - void insert(iterator p, InputIterator first, InputIterator last) - { - return comments.insert(p, first, last); - } - void insert(iterator p, std::initializer_list ini) - { - return comments.insert(p, ini); - } - - template - iterator emplace(iterator p, Ts&& ... args) - { - return comments.emplace(p, std::forward(args)...); - } - - iterator erase(iterator pos) {return comments.erase(pos);} - iterator erase(iterator first, iterator last) - { - return comments.erase(first, last); - } -#else - iterator insert(const_iterator p, const std::string& x) - { - return comments.insert(p, x); - } - iterator insert(const_iterator p, std::string&& x) - { - return comments.insert(p, std::move(x)); - } - iterator insert(const_iterator p, size_type n, const std::string& x) - { - return comments.insert(p, n, x); - } - template - iterator insert(const_iterator p, InputIterator first, InputIterator last) - { - return comments.insert(p, first, last); - } - iterator insert(const_iterator p, std::initializer_list ini) - { - return comments.insert(p, ini); - } - - template - iterator emplace(const_iterator p, Ts&& ... args) - { - return comments.emplace(p, std::forward(args)...); - } - - iterator erase(const_iterator pos) {return comments.erase(pos);} - iterator erase(const_iterator first, const_iterator last) - { - return comments.erase(first, last); - } -#endif - - void swap(preserve_comments& other) {comments.swap(other.comments);} - - void push_back(const std::string& v) {comments.push_back(v);} - void push_back(std::string&& v) {comments.push_back(std::move(v));} - void pop_back() {comments.pop_back();} - - template - void emplace_back(Ts&& ... args) {comments.emplace_back(std::forward(args)...);} - - void clear() {comments.clear();} - - size_type size() const noexcept {return comments.size();} - size_type max_size() const noexcept {return comments.max_size();} - size_type capacity() const noexcept {return comments.capacity();} - bool empty() const noexcept {return comments.empty();} - - void reserve(size_type n) {comments.reserve(n);} - void resize(size_type n) {comments.resize(n);} - void resize(size_type n, const std::string& c) {comments.resize(n, c);} - void shrink_to_fit() {comments.shrink_to_fit();} - - reference operator[](const size_type n) noexcept {return comments[n];} - const_reference operator[](const size_type n) const noexcept {return comments[n];} - reference at(const size_type n) {return comments.at(n);} - const_reference at(const size_type n) const {return comments.at(n);} - reference front() noexcept {return comments.front();} - const_reference front() const noexcept {return comments.front();} - reference back() noexcept {return comments.back();} - const_reference back() const noexcept {return comments.back();} - - pointer data() noexcept {return comments.data();} - const_pointer data() const noexcept {return comments.data();} - - iterator begin() noexcept {return comments.begin();} - iterator end() noexcept {return comments.end();} - const_iterator begin() const noexcept {return comments.begin();} - const_iterator end() const noexcept {return comments.end();} - const_iterator cbegin() const noexcept {return comments.cbegin();} - const_iterator cend() const noexcept {return comments.cend();} - - reverse_iterator rbegin() noexcept {return comments.rbegin();} - reverse_iterator rend() noexcept {return comments.rend();} - const_reverse_iterator rbegin() const noexcept {return comments.rbegin();} - const_reverse_iterator rend() const noexcept {return comments.rend();} - const_reverse_iterator crbegin() const noexcept {return comments.crbegin();} - const_reverse_iterator crend() const noexcept {return comments.crend();} - - friend bool operator==(const preserve_comments&, const preserve_comments&); - friend bool operator!=(const preserve_comments&, const preserve_comments&); - friend bool operator< (const preserve_comments&, const preserve_comments&); - friend bool operator<=(const preserve_comments&, const preserve_comments&); - friend bool operator> (const preserve_comments&, const preserve_comments&); - friend bool operator>=(const preserve_comments&, const preserve_comments&); - - friend void swap(preserve_comments&, std::vector&); - friend void swap(std::vector&, preserve_comments&); - - private: - - container_type comments; -}; - -bool operator==(const preserve_comments& lhs, const preserve_comments& rhs); -bool operator!=(const preserve_comments& lhs, const preserve_comments& rhs); -bool operator< (const preserve_comments& lhs, const preserve_comments& rhs); -bool operator<=(const preserve_comments& lhs, const preserve_comments& rhs); -bool operator> (const preserve_comments& lhs, const preserve_comments& rhs); -bool operator>=(const preserve_comments& lhs, const preserve_comments& rhs); - -void swap(preserve_comments& lhs, preserve_comments& rhs); -void swap(preserve_comments& lhs, std::vector& rhs); -void swap(std::vector& lhs, preserve_comments& rhs); - -std::ostream& operator<<(std::ostream& os, const preserve_comments& com); - -namespace detail -{ - -// To provide the same interface with `preserve_comments`, `discard_comments` -// should have an iterator. But it does not contain anything, so we need to -// add an iterator that points nothing. -// -// It always points null, so DO NOT unwrap this iterator. It always crashes -// your program. -template -struct empty_iterator -{ - using value_type = T; - using reference_type = typename std::conditional::type; - using pointer_type = typename std::conditional::type; - using difference_type = std::ptrdiff_t; - using iterator_category = std::random_access_iterator_tag; - - empty_iterator() = default; - ~empty_iterator() = default; - empty_iterator(empty_iterator const&) = default; - empty_iterator(empty_iterator &&) = default; - empty_iterator& operator=(empty_iterator const&) = default; - empty_iterator& operator=(empty_iterator &&) = default; - - // DO NOT call these operators. - reference_type operator*() const noexcept {std::terminate();} - pointer_type operator->() const noexcept {return nullptr;} - reference_type operator[](difference_type) const noexcept {return this->operator*();} - - // These operators do nothing. - empty_iterator& operator++() noexcept {return *this;} - empty_iterator operator++(int) noexcept {return *this;} - empty_iterator& operator--() noexcept {return *this;} - empty_iterator operator--(int) noexcept {return *this;} - - empty_iterator& operator+=(difference_type) noexcept {return *this;} - empty_iterator& operator-=(difference_type) noexcept {return *this;} - - empty_iterator operator+(difference_type) const noexcept {return *this;} - empty_iterator operator-(difference_type) const noexcept {return *this;} -}; - -template -bool operator==(const empty_iterator&, const empty_iterator&) noexcept {return true;} -template -bool operator!=(const empty_iterator&, const empty_iterator&) noexcept {return false;} -template -bool operator< (const empty_iterator&, const empty_iterator&) noexcept {return false;} -template -bool operator<=(const empty_iterator&, const empty_iterator&) noexcept {return true;} -template -bool operator> (const empty_iterator&, const empty_iterator&) noexcept {return false;} -template -bool operator>=(const empty_iterator&, const empty_iterator&) noexcept {return true;} - -template -typename empty_iterator::difference_type -operator-(const empty_iterator&, const empty_iterator&) noexcept {return 0;} - -template -empty_iterator -operator+(typename empty_iterator::difference_type, const empty_iterator& rhs) noexcept {return rhs;} -template -empty_iterator -operator+(const empty_iterator& lhs, typename empty_iterator::difference_type) noexcept {return lhs;} - -} // detail - -// The default comment type. It discards all the comments. It requires only one -// byte to contain, so the memory footprint is smaller than preserve_comments. -// -// It just ignores `push_back`, `insert`, `erase`, and any other modifications. -// IT always returns size() == 0, the iterator taken by `begin()` is always the -// same as that of `end()`, and accessing through `operator[]` or iterators -// always causes a segmentation fault. DO NOT access to the element of this. -// -// Why this is chose as the default type is because the last version (2.x.y) -// does not contain any comments in a value. To minimize the impact on the -// efficiency, this is chosen as a default. -// -// To reduce the memory footprint, later we can try empty base optimization (EBO). -class discard_comments -{ - public: - using size_type = std::size_t; - using difference_type = std::ptrdiff_t; - using value_type = std::string; - using reference = std::string&; - using const_reference = std::string const&; - using pointer = std::string*; - using const_pointer = std::string const*; - using iterator = detail::empty_iterator; - using const_iterator = detail::empty_iterator; - using reverse_iterator = detail::empty_iterator; - using const_reverse_iterator = detail::empty_iterator; - - public: - discard_comments() = default; - ~discard_comments() = default; - discard_comments(discard_comments const&) = default; - discard_comments(discard_comments &&) = default; - discard_comments& operator=(discard_comments const&) = default; - discard_comments& operator=(discard_comments &&) = default; - - explicit discard_comments(const std::vector&) noexcept {} - explicit discard_comments(std::vector&&) noexcept {} - discard_comments& operator=(const std::vector&) noexcept {return *this;} - discard_comments& operator=(std::vector&&) noexcept {return *this;} - - explicit discard_comments(const preserve_comments&) noexcept {} - - explicit discard_comments(size_type) noexcept {} - discard_comments(size_type, const std::string&) noexcept {} - discard_comments(std::initializer_list) noexcept {} - template - discard_comments(InputIterator, InputIterator) noexcept {} - - template - void assign(InputIterator, InputIterator) noexcept {} - void assign(std::initializer_list) noexcept {} - void assign(size_type, const std::string&) noexcept {} - - iterator insert(const_iterator, const std::string&) {return iterator{};} - iterator insert(const_iterator, std::string&&) {return iterator{};} - iterator insert(const_iterator, size_type, const std::string&) {return iterator{};} - template - iterator insert(const_iterator, InputIterator, InputIterator) {return iterator{};} - iterator insert(const_iterator, std::initializer_list) {return iterator{};} - - template - iterator emplace(const_iterator, Ts&& ...) {return iterator{};} - iterator erase(const_iterator) {return iterator{};} - iterator erase(const_iterator, const_iterator) {return iterator{};} - - void swap(discard_comments&) {return;} - - void push_back(const std::string&) {return;} - void push_back(std::string&& ) {return;} - void pop_back() {return;} - - template - void emplace_back(Ts&& ...) {return;} - - void clear() {return;} - - size_type size() const noexcept {return 0;} - size_type max_size() const noexcept {return 0;} - size_type capacity() const noexcept {return 0;} - bool empty() const noexcept {return true;} - - void reserve(size_type) {return;} - void resize(size_type) {return;} - void resize(size_type, const std::string&) {return;} - void shrink_to_fit() {return;} - - // DO NOT access to the element of this container. This container is always - // empty, so accessing through operator[], front/back, data causes address - // error. - - reference operator[](const size_type) noexcept {never_call("toml::discard_comment::operator[]");} - const_reference operator[](const size_type) const noexcept {never_call("toml::discard_comment::operator[]");} - reference at(const size_type) {throw std::out_of_range("toml::discard_comment is always empty.");} - const_reference at(const size_type) const {throw std::out_of_range("toml::discard_comment is always empty.");} - reference front() noexcept {never_call("toml::discard_comment::front");} - const_reference front() const noexcept {never_call("toml::discard_comment::front");} - reference back() noexcept {never_call("toml::discard_comment::back");} - const_reference back() const noexcept {never_call("toml::discard_comment::back");} - - pointer data() noexcept {return nullptr;} - const_pointer data() const noexcept {return nullptr;} - - iterator begin() noexcept {return iterator{};} - iterator end() noexcept {return iterator{};} - const_iterator begin() const noexcept {return const_iterator{};} - const_iterator end() const noexcept {return const_iterator{};} - const_iterator cbegin() const noexcept {return const_iterator{};} - const_iterator cend() const noexcept {return const_iterator{};} - - reverse_iterator rbegin() noexcept {return iterator{};} - reverse_iterator rend() noexcept {return iterator{};} - const_reverse_iterator rbegin() const noexcept {return const_iterator{};} - const_reverse_iterator rend() const noexcept {return const_iterator{};} - const_reverse_iterator crbegin() const noexcept {return const_iterator{};} - const_reverse_iterator crend() const noexcept {return const_iterator{};} - - private: - - [[noreturn]] static void never_call(const char *const this_function) - { -#if __has_builtin(__builtin_unreachable) - __builtin_unreachable(); -#endif - throw std::logic_error{this_function}; - } -}; - -inline bool operator==(const discard_comments&, const discard_comments&) noexcept {return true;} -inline bool operator!=(const discard_comments&, const discard_comments&) noexcept {return false;} -inline bool operator< (const discard_comments&, const discard_comments&) noexcept {return false;} -inline bool operator<=(const discard_comments&, const discard_comments&) noexcept {return true;} -inline bool operator> (const discard_comments&, const discard_comments&) noexcept {return false;} -inline bool operator>=(const discard_comments&, const discard_comments&) noexcept {return true;} - -inline void swap(const discard_comments&, const discard_comments&) noexcept {return;} - -inline std::ostream& operator<<(std::ostream& os, const discard_comments&) {return os;} - -} // toml11 -#endif // TOML11_COMMENTS_FWD_HPP - -#if ! defined(TOML11_COMPILE_SOURCES) -#ifndef TOML11_COMMENTS_IMPL_HPP -#define TOML11_COMMENTS_IMPL_HPP - - -namespace toml -{ - -TOML11_INLINE bool operator==(const preserve_comments& lhs, const preserve_comments& rhs) {return lhs.comments == rhs.comments;} -TOML11_INLINE bool operator!=(const preserve_comments& lhs, const preserve_comments& rhs) {return lhs.comments != rhs.comments;} -TOML11_INLINE bool operator< (const preserve_comments& lhs, const preserve_comments& rhs) {return lhs.comments < rhs.comments;} -TOML11_INLINE bool operator<=(const preserve_comments& lhs, const preserve_comments& rhs) {return lhs.comments <= rhs.comments;} -TOML11_INLINE bool operator> (const preserve_comments& lhs, const preserve_comments& rhs) {return lhs.comments > rhs.comments;} -TOML11_INLINE bool operator>=(const preserve_comments& lhs, const preserve_comments& rhs) {return lhs.comments >= rhs.comments;} - -TOML11_INLINE void swap(preserve_comments& lhs, preserve_comments& rhs) -{ - lhs.swap(rhs); - return; -} -TOML11_INLINE void swap(preserve_comments& lhs, std::vector& rhs) -{ - lhs.comments.swap(rhs); - return; -} -TOML11_INLINE void swap(std::vector& lhs, preserve_comments& rhs) -{ - lhs.swap(rhs.comments); - return; -} - -TOML11_INLINE std::ostream& operator<<(std::ostream& os, const preserve_comments& com) -{ - for(const auto& c : com) - { - if(c.front() != '#') - { - os << '#'; - } - os << c << '\n'; - } - return os; -} - -} // toml11 -#endif // TOML11_COMMENTS_IMPL_HPP -#endif - -#endif // TOML11_COMMENTS_HPP -#ifndef TOML11_COLOR_HPP -#define TOML11_COLOR_HPP - -#ifndef TOML11_COLOR_FWD_HPP -#define TOML11_COLOR_FWD_HPP - -#include - -#ifdef TOML11_COLORIZE_ERROR_MESSAGE -#define TOML11_ERROR_MESSAGE_COLORIZED true -#else -#define TOML11_ERROR_MESSAGE_COLORIZED false -#endif - -#ifdef TOML11_USE_THREAD_LOCAL_COLORIZATION -#define TOML11_THREAD_LOCAL_COLORIZATION thread_local -#else -#define TOML11_THREAD_LOCAL_COLORIZATION -#endif - -namespace toml -{ -namespace color -{ -// put ANSI escape sequence to ostream -inline namespace ansi -{ -namespace detail -{ - -// Control color mode globally -class color_mode -{ - public: - - void enable() noexcept - { - should_color_ = true; - } - void disable() noexcept - { - should_color_ = false; - } - bool should_color() const noexcept - { - return should_color_; - } - - private: - - bool should_color_ = TOML11_ERROR_MESSAGE_COLORIZED; -}; - -inline color_mode& color_status() noexcept -{ - static TOML11_THREAD_LOCAL_COLORIZATION color_mode status; - return status; -} - -} // detail - -std::ostream& reset (std::ostream& os); -std::ostream& bold (std::ostream& os); -std::ostream& grey (std::ostream& os); -std::ostream& gray (std::ostream& os); -std::ostream& red (std::ostream& os); -std::ostream& green (std::ostream& os); -std::ostream& yellow (std::ostream& os); -std::ostream& blue (std::ostream& os); -std::ostream& magenta(std::ostream& os); -std::ostream& cyan (std::ostream& os); -std::ostream& white (std::ostream& os); - -} // ansi - -inline void enable() -{ - return detail::color_status().enable(); -} -inline void disable() -{ - return detail::color_status().disable(); -} -inline bool should_color() -{ - return detail::color_status().should_color(); -} - -} // color -} // toml -#endif // TOML11_COLOR_FWD_HPP - -#if ! defined(TOML11_COMPILE_SOURCES) -#ifndef TOML11_COLOR_IMPL_HPP -#define TOML11_COLOR_IMPL_HPP - - -#include - -namespace toml -{ -namespace color -{ -// put ANSI escape sequence to ostream -inline namespace ansi -{ - -TOML11_INLINE std::ostream& reset(std::ostream& os) -{ - if(detail::color_status().should_color()) {os << "\033[00m";} - return os; -} -TOML11_INLINE std::ostream& bold(std::ostream& os) -{ - if(detail::color_status().should_color()) {os << "\033[01m";} - return os; -} -TOML11_INLINE std::ostream& grey(std::ostream& os) -{ - if(detail::color_status().should_color()) {os << "\033[30m";} - return os; -} -TOML11_INLINE std::ostream& gray(std::ostream& os) -{ - if(detail::color_status().should_color()) {os << "\033[30m";} - return os; -} -TOML11_INLINE std::ostream& red(std::ostream& os) -{ - if(detail::color_status().should_color()) {os << "\033[31m";} - return os; -} -TOML11_INLINE std::ostream& green(std::ostream& os) -{ - if(detail::color_status().should_color()) {os << "\033[32m";} - return os; -} -TOML11_INLINE std::ostream& yellow(std::ostream& os) -{ - if(detail::color_status().should_color()) {os << "\033[33m";} - return os; -} -TOML11_INLINE std::ostream& blue(std::ostream& os) -{ - if(detail::color_status().should_color()) {os << "\033[34m";} - return os; -} -TOML11_INLINE std::ostream& magenta(std::ostream& os) -{ - if(detail::color_status().should_color()) {os << "\033[35m";} - return os; -} -TOML11_INLINE std::ostream& cyan (std::ostream& os) -{ - if(detail::color_status().should_color()) {os << "\033[36m";} - return os; -} -TOML11_INLINE std::ostream& white (std::ostream& os) -{ - if(detail::color_status().should_color()) {os << "\033[37m";} - return os; -} - -} // ansi -} // color -} // toml -#endif // TOML11_COLOR_IMPL_HPP -#endif - -#endif // TOML11_COLOR_HPP -#ifndef TOML11_SPEC_HPP -#define TOML11_SPEC_HPP - -#include -#include - -#include - -namespace toml -{ - -struct semantic_version -{ - constexpr semantic_version(std::uint32_t mjr, std::uint32_t mnr, std::uint32_t p) noexcept - : major{mjr}, minor{mnr}, patch{p} - {} - - std::uint32_t major; - std::uint32_t minor; - std::uint32_t patch; -}; - -constexpr inline semantic_version -make_semver(std::uint32_t mjr, std::uint32_t mnr, std::uint32_t p) noexcept -{ - return semantic_version(mjr, mnr, p); -} - -constexpr inline bool -operator==(const semantic_version& lhs, const semantic_version& rhs) noexcept -{ - return lhs.major == rhs.major && - lhs.minor == rhs.minor && - lhs.patch == rhs.patch; -} -constexpr inline bool -operator!=(const semantic_version& lhs, const semantic_version& rhs) noexcept -{ - return !(lhs == rhs); -} -constexpr inline bool -operator<(const semantic_version& lhs, const semantic_version& rhs) noexcept -{ - return lhs.major < rhs.major || - (lhs.major == rhs.major && lhs.minor < rhs.minor) || - (lhs.major == rhs.major && lhs.minor == rhs.minor && lhs.patch < rhs.patch); -} -constexpr inline bool -operator>(const semantic_version& lhs, const semantic_version& rhs) noexcept -{ - return rhs < lhs; -} -constexpr inline bool -operator<=(const semantic_version& lhs, const semantic_version& rhs) noexcept -{ - return !(lhs > rhs); -} -constexpr inline bool -operator>=(const semantic_version& lhs, const semantic_version& rhs) noexcept -{ - return !(lhs < rhs); -} - -inline std::ostream& operator<<(std::ostream& os, const semantic_version& v) -{ - os << v.major << '.' << v.minor << '.' << v.patch; - return os; -} - -inline std::string to_string(const semantic_version& v) -{ - std::ostringstream oss; - oss << v; - return oss.str(); -} - -struct spec -{ - constexpr static spec default_version() noexcept - { - return spec::v(1, 0, 0); - } - - constexpr static spec v(std::uint32_t mjr, std::uint32_t mnr, std::uint32_t p) noexcept - { - return spec(make_semver(mjr, mnr, p)); - } - - constexpr explicit spec(const semantic_version& semver) noexcept - : version{semver}, - v1_1_0_allow_control_characters_in_comments {semantic_version{1, 1, 0} <= semver}, - v1_1_0_allow_newlines_in_inline_tables {semantic_version{1, 1, 0} <= semver}, - v1_1_0_allow_trailing_comma_in_inline_tables{semantic_version{1, 1, 0} <= semver}, - v1_1_0_allow_non_english_in_bare_keys {semantic_version{1, 1, 0} <= semver}, - v1_1_0_add_escape_sequence_e {semantic_version{1, 1, 0} <= semver}, - v1_1_0_add_escape_sequence_x {semantic_version{1, 1, 0} <= semver}, - v1_1_0_make_seconds_optional {semantic_version{1, 1, 0} <= semver}, - ext_hex_float {false}, - ext_num_suffix{false}, - ext_null_value{false} - {} - - semantic_version version; // toml version - - // diff from v1.0.0 -> v1.1.0 - bool v1_1_0_allow_control_characters_in_comments; - bool v1_1_0_allow_newlines_in_inline_tables; - bool v1_1_0_allow_trailing_comma_in_inline_tables; - bool v1_1_0_allow_non_english_in_bare_keys; - bool v1_1_0_add_escape_sequence_e; - bool v1_1_0_add_escape_sequence_x; - bool v1_1_0_make_seconds_optional; - - // library extensions - bool ext_hex_float; // allow hex float (in C++ style) - bool ext_num_suffix; // allow number suffix (in C++ style) - bool ext_null_value; // allow `null` as a value -}; - -} // namespace toml -#endif // TOML11_SPEC_HPP -#ifndef TOML11_ORDERED_MAP_HPP -#define TOML11_ORDERED_MAP_HPP - -#include -#include -#include -#include - -namespace toml -{ - -namespace detail -{ -template -struct ordered_map_ebo_container -{ - Cmp cmp_; // empty base optimization for empty Cmp type -}; -} // detail - -template, - typename Allocator = std::allocator>> -class ordered_map : detail::ordered_map_ebo_container -{ - public: - using key_type = Key; - using mapped_type = Val; - using value_type = std::pair; - - using key_compare = Cmp; - using allocator_type = Allocator; - - using container_type = std::vector; - using reference = typename container_type::reference; - using pointer = typename container_type::pointer; - using const_reference = typename container_type::const_reference; - using const_pointer = typename container_type::const_pointer; - using iterator = typename container_type::iterator; - using const_iterator = typename container_type::const_iterator; - using size_type = typename container_type::size_type; - using difference_type = typename container_type::difference_type; - - private: - - using ebo_base = detail::ordered_map_ebo_container; - - public: - - ordered_map() = default; - ~ordered_map() = default; - ordered_map(const ordered_map&) = default; - ordered_map(ordered_map&&) = default; - ordered_map& operator=(const ordered_map&) = default; - ordered_map& operator=(ordered_map&&) = default; - - ordered_map(const ordered_map& other, const Allocator& alloc) - : container_(other.container_, alloc) - {} - ordered_map(ordered_map&& other, const Allocator& alloc) - : container_(std::move(other.container_), alloc) - {} - - explicit ordered_map(const Cmp& cmp, const Allocator& alloc = Allocator()) - : ebo_base{cmp}, container_(alloc) - {} - explicit ordered_map(const Allocator& alloc) - : container_(alloc) - {} - - template - ordered_map(InputIterator first, InputIterator last, const Cmp& cmp = Cmp(), const Allocator& alloc = Allocator()) - : ebo_base{cmp}, container_(first, last, alloc) - {} - template - ordered_map(InputIterator first, InputIterator last, const Allocator& alloc) - : container_(first, last, alloc) - {} - - ordered_map(std::initializer_list v, const Cmp& cmp = Cmp(), const Allocator& alloc = Allocator()) - : ebo_base{cmp}, container_(std::move(v), alloc) - {} - ordered_map(std::initializer_list v, const Allocator& alloc) - : container_(std::move(v), alloc) - {} - ordered_map& operator=(std::initializer_list v) - { - this->container_ = std::move(v); - return *this; - } - - iterator begin() noexcept {return container_.begin();} - iterator end() noexcept {return container_.end();} - const_iterator begin() const noexcept {return container_.begin();} - const_iterator end() const noexcept {return container_.end();} - const_iterator cbegin() const noexcept {return container_.cbegin();} - const_iterator cend() const noexcept {return container_.cend();} - - bool empty() const noexcept {return container_.empty();} - std::size_t size() const noexcept {return container_.size();} - std::size_t max_size() const noexcept {return container_.max_size();} - - void clear() {container_.clear();} - - void push_back(const value_type& v) - { - if(this->contains(v.first)) - { - throw std::out_of_range("ordered_map: value already exists"); - } - container_.push_back(v); - } - void push_back(value_type&& v) - { - if(this->contains(v.first)) - { - throw std::out_of_range("ordered_map: value already exists"); - } - container_.push_back(std::move(v)); - } - void emplace_back(key_type k, mapped_type v) - { - if(this->contains(k)) - { - throw std::out_of_range("ordered_map: value already exists"); - } - container_.emplace_back(std::move(k), std::move(v)); - } - void pop_back() {container_.pop_back();} - - void insert(value_type kv) - { - if(this->contains(kv.first)) - { - throw std::out_of_range("ordered_map: value already exists"); - } - container_.push_back(std::move(kv)); - } - void emplace(key_type k, mapped_type v) - { - if(this->contains(k)) - { - throw std::out_of_range("ordered_map: value already exists"); - } - container_.emplace_back(std::move(k), std::move(v)); - } - - std::size_t count(const key_type& key) const - { - if(this->find(key) != this->end()) - { - return 1; - } - else - { - return 0; - } - } - bool contains(const key_type& key) const - { - return this->find(key) != this->end(); - } - iterator find(const key_type& key) noexcept - { - return std::find_if(this->begin(), this->end(), - [&key, this](const value_type& v) {return this->cmp_(v.first, key);}); - } - const_iterator find(const key_type& key) const noexcept - { - return std::find_if(this->begin(), this->end(), - [&key, this](const value_type& v) {return this->cmp_(v.first, key);}); - } - - mapped_type& at(const key_type& k) - { - const auto iter = this->find(k); - if(iter == this->end()) - { - throw std::out_of_range("ordered_map: no such element"); - } - return iter->second; - } - mapped_type const& at(const key_type& k) const - { - const auto iter = this->find(k); - if(iter == this->end()) - { - throw std::out_of_range("ordered_map: no such element"); - } - return iter->second; - } - - mapped_type& operator[](const key_type& k) - { - const auto iter = this->find(k); - if(iter == this->end()) - { - this->container_.emplace_back(k, mapped_type{}); - return this->container_.back().second; - } - return iter->second; - } - - mapped_type const& operator[](const key_type& k) const - { - const auto iter = this->find(k); - if(iter == this->end()) - { - throw std::out_of_range("ordered_map: no such element"); - } - return iter->second; - } - - key_compare key_comp() const {return this->cmp_;} - - void swap(ordered_map& other) - { - container_.swap(other.container_); - } - - private: - - container_type container_; -}; - -template -bool operator==(const ordered_map& lhs, const ordered_map& rhs) -{ - return lhs.size() == rhs.size() && std::equal(lhs.begin(), lhs.end(), rhs.begin()); -} -template -bool operator!=(const ordered_map& lhs, const ordered_map& rhs) -{ - return !(lhs == rhs); -} -template -bool operator<(const ordered_map& lhs, const ordered_map& rhs) -{ - return std::lexicographical_compare(lhs.begin(), lhs.end(), rhs.begin(), rhs.end()); -} -template -bool operator>(const ordered_map& lhs, const ordered_map& rhs) -{ - return rhs < lhs; -} -template -bool operator<=(const ordered_map& lhs, const ordered_map& rhs) -{ - return !(lhs > rhs); -} -template -bool operator>=(const ordered_map& lhs, const ordered_map& rhs) -{ - return !(lhs < rhs); -} - -template -void swap(ordered_map& lhs, ordered_map& rhs) -{ - lhs.swap(rhs); - return; -} - - -} // toml -#endif // TOML11_ORDERED_MAP_HPP -#ifndef TOML11_INTO_HPP -#define TOML11_INTO_HPP - -namespace toml -{ - -template -struct into; -// { -// static toml::value into_toml(const T& user_defined_type) -// { -// // User-defined conversions ... -// } -// }; - -} // toml -#endif // TOML11_INTO_HPP -#ifndef TOML11_FROM_HPP -#define TOML11_FROM_HPP - -namespace toml -{ - -template -struct from; -// { -// static T from_toml(const toml::value& v) -// { -// // User-defined conversions ... -// } -// }; - -} // toml -#endif // TOML11_FROM_HPP -#ifndef TOML11_TRAITS_HPP -#define TOML11_TRAITS_HPP - - -#include -#include -#include -#include -#include -#include -#include - -#if defined(TOML11_HAS_STRING_VIEW) -#include -#endif - -namespace toml -{ -template -class basic_value; - -namespace detail -{ -// --------------------------------------------------------------------------- -// check whether type T is a kind of container/map class - -struct has_iterator_impl -{ - template static std::true_type check(typename T::iterator*); - template static std::false_type check(...); -}; -struct has_value_type_impl -{ - template static std::true_type check(typename T::value_type*); - template static std::false_type check(...); -}; -struct has_key_type_impl -{ - template static std::true_type check(typename T::key_type*); - template static std::false_type check(...); -}; -struct has_mapped_type_impl -{ - template static std::true_type check(typename T::mapped_type*); - template static std::false_type check(...); -}; -struct has_reserve_method_impl -{ - template static std::false_type check(...); - template static std::true_type check( - decltype(std::declval().reserve(std::declval()))*); -}; -struct has_push_back_method_impl -{ - template static std::false_type check(...); - template static std::true_type check( - decltype(std::declval().push_back(std::declval()))*); -}; -struct is_comparable_impl -{ - template static std::false_type check(...); - template static std::true_type check( - decltype(std::declval() < std::declval())*); -}; - -struct has_from_toml_method_impl -{ - template - static std::true_type check( - decltype(std::declval().from_toml(std::declval<::toml::basic_value>()))*); - - template - static std::false_type check(...); -}; -struct has_into_toml_method_impl -{ - template - static std::true_type check(decltype(std::declval().into_toml())*); - template - static std::false_type check(...); -}; - -struct has_template_into_toml_method_impl -{ - template - static std::true_type check(decltype(std::declval().template into_toml())*); - template - static std::false_type check(...); -}; - -struct has_specialized_from_impl -{ - template - static std::false_type check(...); - template)> - static std::true_type check(::toml::from*); -}; -struct has_specialized_into_impl -{ - template - static std::false_type check(...); - template)> - static std::true_type check(::toml::into*); -}; - - -/// Intel C++ compiler can not use decltype in parent class declaration, here -/// is a hack to work around it. https://stackoverflow.com/a/23953090/4692076 -#ifdef __INTEL_COMPILER -#define decltype(...) std::enable_if::type -#endif - -template -struct has_iterator: decltype(has_iterator_impl::check(nullptr)){}; -template -struct has_value_type: decltype(has_value_type_impl::check(nullptr)){}; -template -struct has_key_type: decltype(has_key_type_impl::check(nullptr)){}; -template -struct has_mapped_type: decltype(has_mapped_type_impl::check(nullptr)){}; -template -struct has_reserve_method: decltype(has_reserve_method_impl::check(nullptr)){}; -template -struct has_push_back_method: decltype(has_push_back_method_impl::check(nullptr)){}; -template -struct is_comparable: decltype(is_comparable_impl::check(nullptr)){}; - -template -struct has_from_toml_method: decltype(has_from_toml_method_impl::check(nullptr)){}; - -template -struct has_into_toml_method: decltype(has_into_toml_method_impl::check(nullptr)){}; - -template -struct has_template_into_toml_method: decltype(has_template_into_toml_method_impl::check(nullptr)){}; - -template -struct has_specialized_from: decltype(has_specialized_from_impl::check(nullptr)){}; -template -struct has_specialized_into: decltype(has_specialized_into_impl::check(nullptr)){}; - -#ifdef __INTEL_COMPILER -#undef decltype -#endif - -// --------------------------------------------------------------------------- -// type checkers - -template struct is_std_pair_impl : std::false_type{}; -template -struct is_std_pair_impl> : std::true_type{}; -template -using is_std_pair = is_std_pair_impl>; - -template struct is_std_tuple_impl : std::false_type{}; -template -struct is_std_tuple_impl> : std::true_type{}; -template -using is_std_tuple = is_std_tuple_impl>; - -template struct is_std_array_impl : std::false_type{}; -template -struct is_std_array_impl> : std::true_type{}; -template -using is_std_array = is_std_array_impl>; - -template struct is_std_forward_list_impl : std::false_type{}; -template -struct is_std_forward_list_impl> : std::true_type{}; -template -using is_std_forward_list = is_std_forward_list_impl>; - -template struct is_std_basic_string_impl : std::false_type{}; -template -struct is_std_basic_string_impl> : std::true_type{}; -template -using is_std_basic_string = is_std_basic_string_impl>; - -template struct is_1byte_std_basic_string_impl : std::false_type{}; -template -struct is_1byte_std_basic_string_impl> - : std::integral_constant {}; -template -using is_1byte_std_basic_string = is_std_basic_string_impl>; - -#if defined(TOML11_HAS_STRING_VIEW) -template struct is_std_basic_string_view_impl : std::false_type{}; -template -struct is_std_basic_string_view_impl> : std::true_type{}; -template -using is_std_basic_string_view = is_std_basic_string_view_impl>; - -template -struct is_string_view_of : std::false_type {}; -template -struct is_string_view_of, std::basic_string> : std::true_type {}; -#endif - -template struct is_chrono_duration_impl: std::false_type{}; -template -struct is_chrono_duration_impl>: std::true_type{}; -template -using is_chrono_duration = is_chrono_duration_impl>; - -template -struct is_map_impl : cxx::conjunction< // map satisfies all the following conditions - has_iterator, // has T::iterator - has_value_type, // has T::value_type - has_key_type, // has T::key_type - has_mapped_type // has T::mapped_type - >{}; -template -using is_map = is_map_impl>; - -template -struct is_container_impl : cxx::conjunction< - cxx::negation>, // not a map - cxx::negation>, // not a std::string -#ifdef TOML11_HAS_STRING_VIEW - cxx::negation>, // not a std::string_view -#endif - has_iterator, // has T::iterator - has_value_type // has T::value_type - >{}; -template -using is_container = is_container_impl>; - -template -struct is_basic_value_impl: std::false_type{}; -template -struct is_basic_value_impl<::toml::basic_value>: std::true_type{}; -template -using is_basic_value = is_basic_value_impl>; - -}// detail -}//toml -#endif // TOML11_TRAITS_HPP -#ifndef TOML11_EXCEPTION_HPP -#define TOML11_EXCEPTION_HPP - -#include - -namespace toml -{ - -struct exception : public std::exception -{ - public: - virtual ~exception() noexcept override = default; - virtual const char* what() const noexcept override {return "";} -}; - -} // toml -#endif // TOMl11_EXCEPTION_HPP -#ifndef TOML11_RESULT_HPP -#define TOML11_RESULT_HPP - - -#include -#include -#include -#include - -#include - -namespace toml -{ - -struct bad_result_access final : public ::toml::exception -{ - public: - explicit bad_result_access(std::string what_arg) - : what_(std::move(what_arg)) - {} - ~bad_result_access() noexcept override = default; - const char* what() const noexcept override {return what_.c_str();} - - private: - std::string what_; -}; - -// ----------------------------------------------------------------------------- - -template -struct success -{ - static_assert( ! std::is_same::value, ""); - - using value_type = T; - - explicit success(value_type v) - noexcept(std::is_nothrow_move_constructible::value) - : value(std::move(v)) - {} - - template, T>::value, - std::nullptr_t> = nullptr> - explicit success(U&& v): value(std::forward(v)) {} - - template - explicit success(success v): value(std::move(v.value)) {} - - ~success() = default; - success(const success&) = default; - success(success&&) = default; - success& operator=(const success&) = default; - success& operator=(success&&) = default; - - value_type& get() noexcept {return value;} - value_type const& get() const noexcept {return value;} - - private: - - value_type value; -}; - -template -struct success> -{ - static_assert( ! std::is_same::value, ""); - - using value_type = T; - - explicit success(std::reference_wrapper v) noexcept - : value(std::move(v)) - {} - - ~success() = default; - success(const success&) = default; - success(success&&) = default; - success& operator=(const success&) = default; - success& operator=(success&&) = default; - - value_type& get() noexcept {return value.get();} - value_type const& get() const noexcept {return value.get();} - - private: - - std::reference_wrapper value; -}; - -template -success::type> ok(T&& v) -{ - return success::type>(std::forward(v)); -} -template -success ok(const char (&literal)[N]) -{ - return success(std::string(literal)); -} - -// ----------------------------------------------------------------------------- - -template -struct failure -{ - using value_type = T; - - explicit failure(value_type v) - noexcept(std::is_nothrow_move_constructible::value) - : value(std::move(v)) - {} - - template, T>::value, - std::nullptr_t> = nullptr> - explicit failure(U&& v): value(std::forward(v)) {} - - template - explicit failure(failure v): value(std::move(v.value)) {} - - ~failure() = default; - failure(const failure&) = default; - failure(failure&&) = default; - failure& operator=(const failure&) = default; - failure& operator=(failure&&) = default; - - value_type& get() noexcept {return value;} - value_type const& get() const noexcept {return value;} - - private: - - value_type value; -}; - -template -struct failure> -{ - using value_type = T; - - explicit failure(std::reference_wrapper v) noexcept - : value(std::move(v)) - {} - - ~failure() = default; - failure(const failure&) = default; - failure(failure&&) = default; - failure& operator=(const failure&) = default; - failure& operator=(failure&&) = default; - - value_type& get() noexcept {return value.get();} - value_type const& get() const noexcept {return value.get();} - - private: - - std::reference_wrapper value; -}; - -template -failure::type> err(T&& v) -{ - return failure::type>(std::forward(v)); -} - -template -failure err(const char (&literal)[N]) -{ - return failure(std::string(literal)); -} - -/* ============================================================================ - * _ _ - * _ _ ___ ____ _| | |_ - * | '_/ -_|_-< || | | _| - * |_| \___/__/\_,_|_|\__| - */ - -template -struct result -{ - using success_type = success; - using failure_type = failure; - using value_type = typename success_type::value_type; - using error_type = typename failure_type::value_type; - - result(success_type s): is_ok_(true), succ_(std::move(s)) {} - result(failure_type f): is_ok_(false), fail_(std::move(f)) {} - - template, value_type>>, - std::is_convertible, value_type> - >::value, std::nullptr_t> = nullptr> - result(success s): is_ok_(true), succ_(std::move(s.value)) {} - - template, error_type>>, - std::is_convertible, error_type> - >::value, std::nullptr_t> = nullptr> - result(failure f): is_ok_(false), fail_(std::move(f.value)) {} - - result& operator=(success_type s) - { - this->cleanup(); - this->is_ok_ = true; - auto tmp = ::new(std::addressof(this->succ_)) success_type(std::move(s)); - assert(tmp == std::addressof(this->succ_)); - (void)tmp; - return *this; - } - result& operator=(failure_type f) - { - this->cleanup(); - this->is_ok_ = false; - auto tmp = ::new(std::addressof(this->fail_)) failure_type(std::move(f)); - assert(tmp == std::addressof(this->fail_)); - (void)tmp; - return *this; - } - - template - result& operator=(success s) - { - this->cleanup(); - this->is_ok_ = true; - auto tmp = ::new(std::addressof(this->succ_)) success_type(std::move(s.value)); - assert(tmp == std::addressof(this->succ_)); - (void)tmp; - return *this; - } - template - result& operator=(failure f) - { - this->cleanup(); - this->is_ok_ = false; - auto tmp = ::new(std::addressof(this->fail_)) failure_type(std::move(f.value)); - assert(tmp == std::addressof(this->fail_)); - (void)tmp; - return *this; - } - - ~result() noexcept {this->cleanup();} - - result(const result& other): is_ok_(other.is_ok()) - { - if(other.is_ok()) - { - auto tmp = ::new(std::addressof(this->succ_)) success_type(other.succ_); - assert(tmp == std::addressof(this->succ_)); - (void)tmp; - } - else - { - auto tmp = ::new(std::addressof(this->fail_)) failure_type(other.fail_); - assert(tmp == std::addressof(this->fail_)); - (void)tmp; - } - } - result(result&& other): is_ok_(other.is_ok()) - { - if(other.is_ok()) - { - auto tmp = ::new(std::addressof(this->succ_)) success_type(std::move(other.succ_)); - assert(tmp == std::addressof(this->succ_)); - (void)tmp; - } - else - { - auto tmp = ::new(std::addressof(this->fail_)) failure_type(std::move(other.fail_)); - assert(tmp == std::addressof(this->fail_)); - (void)tmp; - } - } - - result& operator=(const result& other) - { - this->cleanup(); - if(other.is_ok()) - { - auto tmp = ::new(std::addressof(this->succ_)) success_type(other.succ_); - assert(tmp == std::addressof(this->succ_)); - (void)tmp; - } - else - { - auto tmp = ::new(std::addressof(this->fail_)) failure_type(other.fail_); - assert(tmp == std::addressof(this->fail_)); - (void)tmp; - } - is_ok_ = other.is_ok(); - return *this; - } - result& operator=(result&& other) - { - this->cleanup(); - if(other.is_ok()) - { - auto tmp = ::new(std::addressof(this->succ_)) success_type(std::move(other.succ_)); - assert(tmp == std::addressof(this->succ_)); - (void)tmp; - } - else - { - auto tmp = ::new(std::addressof(this->fail_)) failure_type(std::move(other.fail_)); - assert(tmp == std::addressof(this->fail_)); - (void)tmp; - } - is_ok_ = other.is_ok(); - return *this; - } - - template, value_type>>, - cxx::negation, error_type>>, - std::is_convertible, value_type>, - std::is_convertible, error_type> - >::value, std::nullptr_t> = nullptr> - result(result other): is_ok_(other.is_ok()) - { - if(other.is_ok()) - { - auto tmp = ::new(std::addressof(this->succ_)) success_type(std::move(other.as_ok())); - assert(tmp == std::addressof(this->succ_)); - (void)tmp; - } - else - { - auto tmp = ::new(std::addressof(this->fail_)) failure_type(std::move(other.as_err())); - assert(tmp == std::addressof(this->fail_)); - (void)tmp; - } - } - - template, value_type>>, - cxx::negation, error_type>>, - std::is_convertible, value_type>, - std::is_convertible, error_type> - >::value, std::nullptr_t> = nullptr> - result& operator=(result other) - { - this->cleanup(); - if(other.is_ok()) - { - auto tmp = ::new(std::addressof(this->succ_)) success_type(std::move(other.as_ok())); - assert(tmp == std::addressof(this->succ_)); - (void)tmp; - } - else - { - auto tmp = ::new(std::addressof(this->fail_)) failure_type(std::move(other.as_err())); - assert(tmp == std::addressof(this->fail_)); - (void)tmp; - } - is_ok_ = other.is_ok(); - return *this; - } - - bool is_ok() const noexcept {return is_ok_;} - bool is_err() const noexcept {return !is_ok_;} - - explicit operator bool() const noexcept {return is_ok_;} - - value_type& unwrap(cxx::source_location loc = cxx::source_location::current()) - { - if(this->is_err()) - { - throw bad_result_access("toml::result: bad unwrap" + cxx::to_string(loc)); - } - return this->succ_.get(); - } - value_type const& unwrap(cxx::source_location loc = cxx::source_location::current()) const - { - if(this->is_err()) - { - throw bad_result_access("toml::result: bad unwrap" + cxx::to_string(loc)); - } - return this->succ_.get(); - } - - value_type& unwrap_or(value_type& opt) noexcept - { - if(this->is_err()) {return opt;} - return this->succ_.get(); - } - value_type const& unwrap_or(value_type const& opt) const noexcept - { - if(this->is_err()) {return opt;} - return this->succ_.get(); - } - - error_type& unwrap_err(cxx::source_location loc = cxx::source_location::current()) - { - if(this->is_ok()) - { - throw bad_result_access("toml::result: bad unwrap_err" + cxx::to_string(loc)); - } - return this->fail_.get(); - } - error_type const& unwrap_err(cxx::source_location loc = cxx::source_location::current()) const - { - if(this->is_ok()) - { - throw bad_result_access("toml::result: bad unwrap_err" + cxx::to_string(loc)); - } - return this->fail_.get(); - } - - value_type& as_ok() noexcept - { - assert(this->is_ok()); - return this->succ_.get(); - } - value_type const& as_ok() const noexcept - { - assert(this->is_ok()); - return this->succ_.get(); - } - - error_type& as_err() noexcept - { - assert(this->is_err()); - return this->fail_.get(); - } - error_type const& as_err() const noexcept - { - assert(this->is_err()); - return this->fail_.get(); - } - - private: - - void cleanup() noexcept - { -#if defined(__GNUC__) && ! defined(__clang__) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wduplicated-branches" -#endif - - if(this->is_ok_) {this->succ_.~success_type();} - else {this->fail_.~failure_type();} - -#if defined(__GNUC__) && ! defined(__clang__) -#pragma GCC diagnostic pop -#endif - return; - } - - private: - - bool is_ok_; - union - { - success_type succ_; - failure_type fail_; - }; -}; - -// ---------------------------------------------------------------------------- - -namespace detail -{ -struct none_t {}; -inline bool operator==(const none_t&, const none_t&) noexcept {return true;} -inline bool operator!=(const none_t&, const none_t&) noexcept {return false;} -inline bool operator< (const none_t&, const none_t&) noexcept {return false;} -inline bool operator<=(const none_t&, const none_t&) noexcept {return true;} -inline bool operator> (const none_t&, const none_t&) noexcept {return false;} -inline bool operator>=(const none_t&, const none_t&) noexcept {return true;} -inline std::ostream& operator<<(std::ostream& os, const none_t&) -{ - os << "none"; - return os; -} -} // detail - -inline success ok() noexcept -{ - return success(detail::none_t{}); -} -inline failure err() noexcept -{ - return failure(detail::none_t{}); -} - -} // toml -#endif // TOML11_RESULT_HPP -#ifndef TOML11_UTILITY_HPP -#define TOML11_UTILITY_HPP - - -#include -#include - -#include -#include -#include - -namespace toml -{ -namespace detail -{ - -// to output character in an error message. -inline std::string show_char(const int c) -{ - using char_type = unsigned char; - if(std::isgraph(c)) - { - return std::string(1, static_cast(c)); - } - else - { - std::array buf; - buf.fill('\0'); - const auto r = std::snprintf(buf.data(), buf.size(), "0x%02x", c & 0xFF); - assert(r == static_cast(buf.size()) - 1); - (void) r; // Unused variable warning - auto in_hex = std::string(buf.data()); - switch(c) - { - case char_type('\0'): {in_hex += "(NUL)"; break;} - case char_type(' ') : {in_hex += "(SPACE)"; break;} - case char_type('\n'): {in_hex += "(LINE FEED)"; break;} - case char_type('\r'): {in_hex += "(CARRIAGE RETURN)"; break;} - case char_type('\t'): {in_hex += "(TAB)"; break;} - case char_type('\v'): {in_hex += "(VERTICAL TAB)"; break;} - case char_type('\f'): {in_hex += "(FORM FEED)"; break;} - case char_type('\x1B'): {in_hex += "(ESCAPE)"; break;} - default: break; - } - return in_hex; - } -} - -// --------------------------------------------------------------------------- - -template -void try_reserve_impl(Container& container, std::size_t N, std::true_type) -{ - container.reserve(N); - return; -} -template -void try_reserve_impl(Container&, std::size_t, std::false_type) noexcept -{ - return; -} - -template -void try_reserve(Container& container, std::size_t N) -{ - try_reserve_impl(container, N, has_reserve_method{}); - return; -} - -// --------------------------------------------------------------------------- - -template -result from_string(const std::string& str) -{ - T v; - std::istringstream iss(str); - iss >> v; - if(iss.fail()) - { - return err(); - } - return ok(v); -} - -// --------------------------------------------------------------------------- - -// helper function to avoid std::string(0, 'c') or std::string(iter, iter) -template -std::string make_string(Iterator first, Iterator last) -{ - if(first == last) {return "";} - return std::string(first, last); -} -inline std::string make_string(std::size_t len, char c) -{ - if(len == 0) {return "";} - return std::string(len, c); -} - -// --------------------------------------------------------------------------- - -template -struct string_conv_impl -{ - static_assert(sizeof(Char) == sizeof(char), ""); - static_assert(sizeof(Char2) == sizeof(char), ""); - - static std::basic_string invoke(std::basic_string s) - { - std::basic_string retval; - std::transform(s.begin(), s.end(), std::back_inserter(retval), - [](const Char2 c) {return static_cast(c);}); - return retval; - } - template - static std::basic_string invoke(const Char2 (&s)[N]) - { - std::basic_string retval; - // "string literal" has null-char at the end. to skip it, we use prev. - std::transform(std::begin(s), std::prev(std::end(s)), std::back_inserter(retval), - [](const Char2 c) {return static_cast(c);}); - return retval; - } -}; - -template -struct string_conv_impl -{ - static_assert(sizeof(Char) == sizeof(char), ""); - - static std::basic_string invoke(std::basic_string s) - { - return s; - } - template - static std::basic_string invoke(const Char (&s)[N]) - { - return std::basic_string(s); - } -}; - -template -cxx::enable_if_t::value, S> -string_conv(std::basic_string s) -{ - using C = typename S::value_type; - using T = typename S::traits_type; - using A = typename S::allocator_type; - return string_conv_impl::invoke(std::move(s)); -} -template -cxx::enable_if_t::value, S> -string_conv(const char (&s)[N]) -{ - using C = typename S::value_type; - using T = typename S::traits_type; - using A = typename S::allocator_type; - using C2 = char; - using T2 = std::char_traits; - using A2 = std::allocator; - - return string_conv_impl::template invoke(s); -} - -} // namespace detail -} // namespace toml -#endif // TOML11_UTILITY_HPP -#ifndef TOML11_LOCATION_HPP -#define TOML11_LOCATION_HPP - -#ifndef TOML11_LOCATION_FWD_HPP -#define TOML11_LOCATION_FWD_HPP - - -#include -#include -#include - -namespace toml -{ -namespace detail -{ - -class region; // fwd decl - -// -// To represent where we are reading in the parse functions. -// Since it "points" somewhere in the input stream, the length is always 1. -// -class location -{ - public: - - using char_type = unsigned char; // must be unsigned - using container_type = std::vector; - using difference_type = typename container_type::difference_type; // to suppress sign-conversion warning - using source_ptr = std::shared_ptr; - - public: - - location(source_ptr src, std::string src_name) - : source_(std::move(src)), source_name_(std::move(src_name)), - location_(0), line_number_(1) - {} - - location(const location&) = default; - location(location&&) = default; - location& operator=(const location&) = default; - location& operator=(location&&) = default; - ~location() = default; - - void advance(std::size_t n = 1) noexcept; - void retrace(std::size_t n = 1) noexcept; - - bool is_ok() const noexcept { return static_cast(this->source_); } - - bool eof() const noexcept; - char_type current() const; - - char_type peek(); - - std::size_t get_location() const noexcept - { - return this->location_; - } - void set_location(const std::size_t loc) noexcept; - - std::size_t line_number() const noexcept - { - return this->line_number_; - } - std::string get_line() const; - std::size_t column_number() const noexcept; - - source_ptr const& source() const noexcept {return this->source_;} - std::string const& source_name() const noexcept {return this->source_name_;} - - private: - - void advance_line_number(const std::size_t n); - void retrace_line_number(const std::size_t n); - - private: - - friend region; - - private: - - source_ptr source_; - std::string source_name_; - std::size_t location_; // std::vector<>::difference_type is signed - std::size_t line_number_; -}; - -bool operator==(const location& lhs, const location& rhs) noexcept; -bool operator!=(const location& lhs, const location& rhs); - -location prev(const location& loc); -location next(const location& loc); -location make_temporary_location(const std::string& str) noexcept; - -template -result -find_if(const location& first, const location& last, const F& func) noexcept -{ - if(first.source() != last.source()) { return err(); } - if(first.get_location() >= last.get_location()) { return err(); } - - auto loc = first; - while(loc.get_location() != last.get_location()) - { - if(func(loc.current())) - { - return ok(loc); - } - loc.advance(); - } - return err(); -} - -template -result -rfind_if(location first, const location& last, const F& func) -{ - if(first.source() != last.source()) { return err(); } - if(first.get_location() >= last.get_location()) { return err(); } - - auto loc = last; - while(loc.get_location() != first.get_location()) - { - if(func(loc.current())) - { - return ok(loc); - } - loc.retrace(); - } - if(func(first.current())) - { - return ok(first); - } - return err(); -} - -result find(const location& first, const location& last, - const location::char_type val); -result rfind(const location& first, const location& last, - const location::char_type val); - -std::size_t count(const location& first, const location& last, - const location::char_type& c); - -} // detail -} // toml -#endif // TOML11_LOCATION_FWD_HPP - -#if ! defined(TOML11_COMPILE_SOURCES) -#ifndef TOML11_LOCATION_IMPL_HPP -#define TOML11_LOCATION_IMPL_HPP - - -namespace toml -{ -namespace detail -{ - -TOML11_INLINE void location::advance(std::size_t n) noexcept -{ - assert(this->is_ok()); - if(this->location_ + n < this->source_->size()) - { - this->advance_line_number(n); - this->location_ += n; - } - else - { - this->advance_line_number(this->source_->size() - this->location_); - this->location_ = this->source_->size(); - } -} -TOML11_INLINE void location::retrace(std::size_t n) noexcept -{ - assert(this->is_ok()); - if(this->location_ < n) - { - this->location_ = 0; - this->line_number_ = 1; - } - else - { - this->retrace_line_number(n); - this->location_ -= n; - } -} - -TOML11_INLINE bool location::eof() const noexcept -{ - assert(this->is_ok()); - return this->location_ >= this->source_->size(); -} -TOML11_INLINE location::char_type location::current() const -{ - assert(this->is_ok()); - if(this->eof()) {return '\0';} - - assert(this->location_ < this->source_->size()); - return this->source_->at(this->location_); -} - -TOML11_INLINE location::char_type location::peek() -{ - assert(this->is_ok()); - if(this->location_ >= this->source_->size()) - { - return '\0'; - } - else - { - return this->source_->at(this->location_ + 1); - } -} - -TOML11_INLINE void location::set_location(const std::size_t loc) noexcept -{ - if(this->location_ == loc) - { - return ; - } - - if(loc == 0) - { - this->line_number_ = 1; - } - else if(this->location_ < loc) - { - const auto d = loc - this->location_; - this->advance_line_number(d); - } - else - { - const auto d = this->location_ - loc; - this->retrace_line_number(d); - } - this->location_ = loc; -} - -TOML11_INLINE std::string location::get_line() const -{ - assert(this->is_ok()); - const auto iter = std::next(this->source_->cbegin(), static_cast(this->location_)); - const auto riter = cxx::make_reverse_iterator(iter); - - const auto prev = std::find(riter, this->source_->crend(), char_type('\n')); - const auto next = std::find(iter, this->source_->cend(), char_type('\n')); - - return make_string(std::next(prev.base()), next); -} -TOML11_INLINE std::size_t location::column_number() const noexcept -{ - assert(this->is_ok()); - const auto iter = std::next(this->source_->cbegin(), static_cast(this->location_)); - const auto riter = cxx::make_reverse_iterator(iter); - const auto prev = std::find(riter, this->source_->crend(), char_type('\n')); - - assert(prev.base() <= iter); - return static_cast(std::distance(prev.base(), iter) + 1); // 1-origin -} - - -TOML11_INLINE void location::advance_line_number(const std::size_t n) -{ - assert(this->is_ok()); - assert(this->location_ + n <= this->source_->size()); - - const auto iter = this->source_->cbegin(); - this->line_number_ += static_cast(std::count( - std::next(iter, static_cast(this->location_)), - std::next(iter, static_cast(this->location_ + n)), - char_type('\n'))); - - return; -} -TOML11_INLINE void location::retrace_line_number(const std::size_t n) -{ - assert(this->is_ok()); - assert(n <= this->location_); // loc - n >= 0 - - const auto iter = this->source_->cbegin(); - const auto dline_num = static_cast(std::count( - std::next(iter, static_cast(this->location_ - n)), - std::next(iter, static_cast(this->location_)), - char_type('\n'))); - - if(this->line_number_ <= dline_num) - { - this->line_number_ = 1; - } - else - { - this->line_number_ -= dline_num; - } - return; -} - -TOML11_INLINE bool operator==(const location& lhs, const location& rhs) noexcept -{ - if( ! lhs.is_ok() || ! rhs.is_ok()) - { - return (!lhs.is_ok()) && (!rhs.is_ok()); - } - return lhs.source() == rhs.source() && - lhs.source_name() == rhs.source_name() && - lhs.get_location() == rhs.get_location(); -} -TOML11_INLINE bool operator!=(const location& lhs, const location& rhs) -{ - return !(lhs == rhs); -} - -TOML11_INLINE location prev(const location& loc) -{ - location p(loc); - p.retrace(1); - return p; -} -TOML11_INLINE location next(const location& loc) -{ - location p(loc); - p.advance(1); - return p; -} - -TOML11_INLINE location make_temporary_location(const std::string& str) noexcept -{ - location::container_type cont(str.size()); - std::transform(str.begin(), str.end(), cont.begin(), - [](const std::string::value_type& c) { - return cxx::bit_cast(c); - }); - return location(std::make_shared( - std::move(cont)), "internal temporary"); -} - -TOML11_INLINE result -find(const location& first, const location& last, const location::char_type val) -{ - return find_if(first, last, [val](const location::char_type c) { - return c == val; - }); -} -TOML11_INLINE result -rfind(const location& first, const location& last, const location::char_type val) -{ - return rfind_if(first, last, [val](const location::char_type c) { - return c == val; - }); -} - -TOML11_INLINE std::size_t -count(const location& first, const location& last, const location::char_type& c) -{ - if(first.source() != last.source()) { return 0; } - if(first.get_location() >= last.get_location()) { return 0; } - - auto loc = first; - std::size_t num = 0; - while(loc.get_location() != last.get_location()) - { - if(loc.current() == c) - { - num += 1; - } - loc.advance(); - } - return num; -} - -} // detail -} // toml -#endif // TOML11_LOCATION_HPP -#endif - -#endif // TOML11_LOCATION_HPP -#ifndef TOML11_REGION_HPP -#define TOML11_REGION_HPP - -#ifndef TOML11_REGION_FWD_HPP -#define TOML11_REGION_FWD_HPP - - -#include -#include - -#include - -namespace toml -{ -namespace detail -{ - -// -// To represent where is a toml::value defined, or where does an error occur. -// Stored in toml::value. source_location will be constructed based on this. -// -class region -{ - public: - - using char_type = location::char_type; - using container_type = location::container_type; - using difference_type = location::difference_type; - using source_ptr = location::source_ptr; - - using iterator = typename container_type::iterator; - using const_iterator = typename container_type::const_iterator; - - public: - - // a value that is constructed manually does not have input stream info - region() - : source_(nullptr), source_name_(""), length_(0), - first_line_(0), first_column_(0), last_line_(0), last_column_(0) - {} - - // a value defined in [first, last). - // Those source must be the same. Instread, `region` does not make sense. - region(const location& first, const location& last); - - // shorthand of [loc, loc+1) - explicit region(const location& loc); - - ~region() = default; - region(const region&) = default; - region(region&&) = default; - region& operator=(const region&) = default; - region& operator=(region&&) = default; - - bool is_ok() const noexcept { return static_cast(this->source_); } - - operator bool() const noexcept { return this->is_ok(); } - - std::size_t length() const noexcept {return this->length_;} - - std::size_t first_line_number() const noexcept - { - return this->first_line_; - } - std::size_t first_column_number() const noexcept - { - return this->first_column_; - } - std::size_t last_line_number() const noexcept - { - return this->last_line_; - } - std::size_t last_column_number() const noexcept - { - return this->last_column_; - } - - char_type at(std::size_t i) const; - - const_iterator begin() const noexcept; - const_iterator end() const noexcept; - const_iterator cbegin() const noexcept; - const_iterator cend() const noexcept; - - std::string as_string() const; - std::vector as_lines() const; - - source_ptr const& source() const noexcept {return this->source_;} - std::string const& source_name() const noexcept {return this->source_name_;} - - private: - - source_ptr source_; - std::string source_name_; - std::size_t length_; - std::size_t first_; - std::size_t first_line_; - std::size_t first_column_; - std::size_t last_; - std::size_t last_line_; - std::size_t last_column_; -}; - -} // namespace detail -} // namespace toml -#endif // TOML11_REGION_FWD_HPP - -#if ! defined(TOML11_COMPILE_SOURCES) -#ifndef TOML11_REGION_IMPL_HPP -#define TOML11_REGION_IMPL_HPP - - -#include -#include -#include -#include -#include -#include - -namespace toml -{ -namespace detail -{ - -// a value defined in [first, last). -// Those source must be the same. Instread, `region` does not make sense. -TOML11_INLINE region::region(const location& first, const location& last) - : source_(first.source()), source_name_(first.source_name()), - length_(last.get_location() - first.get_location()), - first_(first.get_location()), - first_line_(first.line_number()), - first_column_(first.column_number()), - last_(last.get_location()), - last_line_(last.line_number()), - last_column_(last.column_number()) -{ - assert(first.source() == last.source()); - assert(first.source_name() == last.source_name()); -} - - // shorthand of [loc, loc+1) -TOML11_INLINE region::region(const location& loc) - : source_(loc.source()), source_name_(loc.source_name()), length_(0), - first_line_(0), first_column_(0), last_line_(0), last_column_(0) -{ - // if the file ends with LF, the resulting region points no char. - if(loc.eof()) - { - if(loc.get_location() == 0) - { - this->length_ = 0; - this->first_ = 0; - this->first_line_ = 0; - this->first_column_ = 0; - this->last_ = 0; - this->last_line_ = 0; - this->last_column_ = 0; - } - else - { - const auto first = prev(loc); - this->first_ = first.get_location(); - this->first_line_ = first.line_number(); - this->first_column_ = first.column_number(); - this->last_ = loc.get_location(); - this->last_line_ = loc.line_number(); - this->last_column_ = loc.column_number(); - this->length_ = 1; - } - } - else - { - this->first_ = loc.get_location(); - this->first_line_ = loc.line_number(); - this->first_column_ = loc.column_number(); - this->last_ = loc.get_location() + 1; - this->last_line_ = loc.line_number(); - this->last_column_ = loc.column_number() + 1; - this->length_ = 1; - } -} - -TOML11_INLINE region::char_type region::at(std::size_t i) const -{ - if(this->last_ <= this->first_ + i) - { - throw std::out_of_range("range::at: index " + std::to_string(i) + - " exceeds length " + std::to_string(this->length_)); - } - const auto iter = std::next(this->source_->cbegin(), - static_cast(this->first_ + i)); - return *iter; -} - -TOML11_INLINE region::const_iterator region::begin() const noexcept -{ - return std::next(this->source_->cbegin(), - static_cast(this->first_)); -} -TOML11_INLINE region::const_iterator region::end() const noexcept -{ - return std::next(this->source_->cbegin(), - static_cast(this->last_)); -} -TOML11_INLINE region::const_iterator region::cbegin() const noexcept -{ - return std::next(this->source_->cbegin(), - static_cast(this->first_)); -} -TOML11_INLINE region::const_iterator region::cend() const noexcept -{ - return std::next(this->source_->cbegin(), - static_cast(this->last_)); -} - -TOML11_INLINE std::string region::as_string() const -{ - if(this->is_ok()) - { - const auto begin = std::next(this->source_->cbegin(), static_cast(this->first_)); - const auto end = std::next(this->source_->cbegin(), static_cast(this->last_ )); - return ::toml::detail::make_string(begin, end); - } - else - { - return std::string(""); - } -} - -TOML11_INLINE std::vector region::as_lines() const -{ - assert(this->is_ok()); - if(this->length_ == 0) - { - return std::vector{""}; - } - - // Consider the following toml file - // ``` - // array = [ - // ] # comment - // ``` - // and the region represnets - // ``` - // [ - // ] - // ``` - // but we want to show the following. - // ``` - // array = [ - // ] # comment - // ``` - // So we need to find LFs before `begin` and after `end`. - // - // But, if region ends with LF, it should not include the next line. - // ``` - // a = 42 - // ^^^- with the last LF - // ``` - // So we start from `end-1` when looking for LF. - - const auto begin_idx = static_cast(this->first_); - const auto end_idx = static_cast(this->last_) - 1; - - // length_ != 0, so begin < end. then begin <= end-1 - assert(begin_idx <= end_idx); - - const auto begin = std::next(this->source_->cbegin(), begin_idx); - const auto end = std::next(this->source_->cbegin(), end_idx); - - const auto line_begin = std::find(cxx::make_reverse_iterator(begin), this->source_->crend(), char_type('\n')).base(); - const auto line_end = std::find(end, this->source_->cend(), char_type('\n')); - - const auto reg_lines = make_string(line_begin, line_end); - - if(reg_lines == "") // the region is an empty line that only contains LF - { - return std::vector{""}; - } - - std::istringstream iss(reg_lines); - - std::vector lines; - std::string line; - while(std::getline(iss, line)) - { - lines.push_back(line); - } - return lines; -} - -} // namespace detail -} // namespace toml -#endif // TOML11_REGION_IMPL_HPP -#endif - -#endif // TOML11_REGION_HPP -#ifndef TOML11_SOURCE_LOCATION_HPP -#define TOML11_SOURCE_LOCATION_HPP - -#ifndef TOML11_SOURCE_LOCATION_FWD_HPP -#define TOML11_SOURCE_LOCATION_FWD_HPP - - -#include -#include -#include - -namespace toml -{ - -// A struct to contain location in a toml file. -struct source_location -{ - public: - - explicit source_location(const detail::region& r); - ~source_location() = default; - source_location(source_location const&) = default; - source_location(source_location &&) = default; - source_location& operator=(source_location const&) = default; - source_location& operator=(source_location &&) = default; - - bool is_ok() const noexcept {return this->is_ok_;} - std::size_t length() const noexcept {return this->length_;} - - std::size_t first_line_number() const noexcept {return this->first_line_;} - std::size_t first_column_number() const noexcept {return this->first_column_;} - std::size_t last_line_number() const noexcept {return this->last_line_;} - std::size_t last_column_number() const noexcept {return this->last_column_;} - - std::string const& file_name() const noexcept {return this->file_name_;} - - std::size_t num_lines() const noexcept {return this->line_str_.size();} - - std::string const& first_line() const; - std::string const& last_line() const; - - std::vector const& lines() const noexcept {return line_str_;} - - private: - - bool is_ok_; - std::size_t first_line_; - std::size_t first_column_; - std::size_t last_line_; - std::size_t last_column_; - std::size_t length_; - std::string file_name_; - std::vector line_str_; -}; - -namespace detail -{ - -std::size_t integer_width_base10(std::size_t i) noexcept; - -inline std::size_t line_width() noexcept {return 0;} - -template -std::size_t line_width(const source_location& loc, const std::string& /*msg*/, - const Ts& ... tail) noexcept -{ - return (std::max)( - integer_width_base10(loc.last_line_number()), line_width(tail...)); -} - -std::ostringstream& -format_filename(std::ostringstream& oss, const source_location& loc); - -std::ostringstream& -format_empty_line(std::ostringstream& oss, const std::size_t lnw); - -std::ostringstream& format_line(std::ostringstream& oss, - const std::size_t lnw, const std::size_t linenum, const std::string& line); - -std::ostringstream& format_underline(std::ostringstream& oss, - const std::size_t lnw, const std::size_t col, const std::size_t len, - const std::string& msg); - -std::string format_location_impl(const std::size_t lnw, - const std::string& prev_fname, - const source_location& loc, const std::string& msg); - -inline std::string format_location_rec(const std::size_t, const std::string&) -{ - return ""; -} - -template -std::string format_location_rec(const std::size_t lnw, - const std::string& prev_fname, - const source_location& loc, const std::string& msg, - const Ts& ... tail) -{ - return format_location_impl(lnw, prev_fname, loc, msg) + - format_location_rec(lnw, loc.file_name(), tail...); -} - -} // namespace detail - -// format a location info without title -template -std::string format_location( - const source_location& loc, const std::string& msg, const Ts& ... tail) -{ - const auto lnw = detail::line_width(loc, msg, tail...); - - const std::string f(""); // at the 1st iteration, no prev_filename is given - return detail::format_location_rec(lnw, f, loc, msg, tail...); -} - -} // toml -#endif // TOML11_SOURCE_LOCATION_FWD_HPP - -#if ! defined(TOML11_COMPILE_SOURCES) -#ifndef TOML11_SOURCE_LOCATION_IMPL_HPP -#define TOML11_SOURCE_LOCATION_IMPL_HPP - - - -#include -#include -#include -#include - -#include - -namespace toml -{ - -TOML11_INLINE source_location::source_location(const detail::region& r) - : is_ok_(false), - first_line_(1), - first_column_(1), - last_line_(1), - last_column_(1), - length_(0), - file_name_("unknown file") -{ - if(r.is_ok()) - { - this->is_ok_ = true; - this->file_name_ = r.source_name(); - this->first_line_ = r.first_line_number(); - this->first_column_ = r.first_column_number(); - this->last_line_ = r.last_line_number(); - this->last_column_ = r.last_column_number(); - this->length_ = r.length(); - this->line_str_ = r.as_lines(); - } -} - -TOML11_INLINE std::string const& source_location::first_line() const -{ - if(this->line_str_.size() == 0) - { - throw std::out_of_range("toml::source_location::first_line: `lines` is empty"); - } - return this->line_str_.front(); -} -TOML11_INLINE std::string const& source_location::last_line() const -{ - if(this->line_str_.size() == 0) - { - throw std::out_of_range("toml::source_location::first_line: `lines` is empty"); - } - return this->line_str_.back(); -} - -namespace detail -{ - -TOML11_INLINE std::size_t integer_width_base10(std::size_t i) noexcept -{ - std::size_t width = 0; - while(i != 0) - { - i /= 10; - width += 1; - } - return width; -} - -TOML11_INLINE std::ostringstream& -format_filename(std::ostringstream& oss, const source_location& loc) -{ - // --> example.toml - oss << color::bold << color::blue << " --> " << color::reset - << color::bold << loc.file_name() << '\n' << color::reset; - return oss; -} - -TOML11_INLINE std::ostringstream& format_empty_line(std::ostringstream& oss, - const std::size_t lnw) -{ - // | - oss << detail::make_string(lnw + 1, ' ') - << color::bold << color::blue << " |\n" << color::reset; - return oss; -} - -TOML11_INLINE std::ostringstream& format_line(std::ostringstream& oss, - const std::size_t lnw, const std::size_t linenum, const std::string& line) -{ - // 10 | key = "value" - oss << ' ' << color::bold << color::blue - << std::setw(static_cast(lnw)) - << std::right << linenum << " | " << color::reset; - for(const char c : line) - { - if(std::isgraph(c) || c == ' ') - { - oss << c; - } - else - { - oss << show_char(c); - } - } - oss << '\n'; - return oss; -} -TOML11_INLINE std::ostringstream& format_underline(std::ostringstream& oss, - const std::size_t lnw, const std::size_t col, const std::size_t len, - const std::string& msg) -{ - // | ^^^^^^^-- this part - oss << make_string(lnw + 1, ' ') - << color::bold << color::blue << " | " << color::reset; - - oss << make_string(col-1 /*1-origin*/, ' ') - << color::bold << color::red - << make_string(len, '^') << "-- " - << color::reset << msg << '\n'; - - return oss; -} - -TOML11_INLINE std::string format_location_impl(const std::size_t lnw, - const std::string& prev_fname, - const source_location& loc, const std::string& msg) -{ - std::ostringstream oss; - - if(loc.file_name() != prev_fname) - { - format_filename(oss, loc); - if( ! loc.lines().empty()) - { - format_empty_line(oss, lnw); - } - } - - if(loc.lines().size() == 1) - { - // when column points LF, it exceeds the size of the first line. - std::size_t underline_limit = 1; - if(loc.first_line().size() < loc.first_column_number()) - { - underline_limit = 1; - } - else - { - underline_limit = loc.first_line().size() - loc.first_column_number() + 1; - } - const auto underline_len = (std::min)(underline_limit, loc.length()); - - format_line(oss, lnw, loc.first_line_number(), loc.first_line()); - format_underline(oss, lnw, loc.first_column_number(), underline_len, msg); - } - else if(loc.lines().size() == 2) - { - const auto first_underline_len = - loc.first_line().size() - loc.first_column_number() + 1; - format_line(oss, lnw, loc.first_line_number(), loc.first_line()); - format_underline(oss, lnw, loc.first_column_number(), - first_underline_len, ""); - - format_line(oss, lnw, loc.last_line_number(), loc.last_line()); - format_underline(oss, lnw, 1, loc.last_column_number(), msg); - } - else if(loc.lines().size() > 2) - { - const auto first_underline_len = - loc.first_line().size() - loc.first_column_number() + 1; - format_line(oss, lnw, loc.first_line_number(), loc.first_line()); - format_underline(oss, lnw, loc.first_column_number(), - first_underline_len, "and"); - - if(loc.lines().size() == 3) - { - format_line(oss, lnw, loc.first_line_number()+1, loc.lines().at(1)); - format_underline(oss, lnw, 1, loc.lines().at(1).size(), "and"); - } - else - { - format_line(oss, lnw, loc.first_line_number()+1, " ..."); - format_empty_line(oss, lnw); - } - format_line(oss, lnw, loc.last_line_number(), loc.last_line()); - format_underline(oss, lnw, 1, loc.last_column_number(), msg); - } - // if loc is empty, do nothing. - return oss.str(); -} - -} // namespace detail -} // toml -#endif // TOML11_SOURCE_LOCATION_IMPL_HPP -#endif - -#endif // TOML11_SOURCE_LOCATION_HPP -#ifndef TOML11_ERROR_INFO_HPP -#define TOML11_ERROR_INFO_HPP - -#ifndef TOML11_ERROR_INFO_FWD_HPP -#define TOML11_ERROR_INFO_FWD_HPP - - -namespace toml -{ - -// error info returned from parser. -struct error_info -{ - error_info(std::string t, source_location l, std::string m, std::string s = "") - : title_(std::move(t)), locations_{std::make_pair(std::move(l), std::move(m))}, - suffix_(std::move(s)) - {} - - error_info(std::string t, std::vector> l, - std::string s = "") - : title_(std::move(t)), locations_(std::move(l)), suffix_(std::move(s)) - {} - - std::string const& title() const noexcept {return title_;} - std::string & title() noexcept {return title_;} - - std::vector> const& - locations() const noexcept {return locations_;} - - void add_locations(source_location loc, std::string msg) noexcept - { - locations_.emplace_back(std::move(loc), std::move(msg)); - } - - std::string const& suffix() const noexcept {return suffix_;} - std::string & suffix() noexcept {return suffix_;} - - private: - - std::string title_; - std::vector> locations_; - std::string suffix_; // hint or something like that -}; - -// forward decl -template -class basic_value; - -namespace detail -{ -inline error_info make_error_info_rec(error_info e) -{ - return e; -} -inline error_info make_error_info_rec(error_info e, std::string s) -{ - e.suffix() = s; - return e; -} - -template -error_info make_error_info_rec(error_info e, - const basic_value& v, std::string msg, Ts&& ... tail); - -template -error_info make_error_info_rec(error_info e, - source_location loc, std::string msg, Ts&& ... tail) -{ - e.add_locations(std::move(loc), std::move(msg)); - return make_error_info_rec(std::move(e), std::forward(tail)...); -} - -} // detail - -template -error_info make_error_info( - std::string title, source_location loc, std::string msg, Ts&& ... tail) -{ - error_info ei(std::move(title), std::move(loc), std::move(msg)); - return detail::make_error_info_rec(ei, std::forward(tail) ... ); -} - -std::string format_error(const std::string& errkind, const error_info& err); -std::string format_error(const error_info& err); - -// for custom error message -template -std::string format_error(std::string title, - source_location loc, std::string msg, Ts&& ... tail) -{ - return format_error("", make_error_info(std::move(title), - std::move(loc), std::move(msg), std::forward(tail)...)); -} - -std::ostream& operator<<(std::ostream& os, const error_info& e); - -} // toml -#endif // TOML11_ERROR_INFO_FWD_HPP - -#if ! defined(TOML11_COMPILE_SOURCES) -#ifndef TOML11_ERROR_INFO_IMPL_HPP -#define TOML11_ERROR_INFO_IMPL_HPP - - -#include - -namespace toml -{ - -TOML11_INLINE std::string format_error(const std::string& errkind, const error_info& err) -{ - std::string errmsg; - if( ! errkind.empty()) - { - errmsg = errkind; - errmsg += ' '; - } - errmsg += err.title(); - errmsg += '\n'; - - const auto lnw = [&err]() { - std::size_t width = 0; - for(const auto& l : err.locations()) - { - width = (std::max)(detail::integer_width_base10(l.first.last_line_number()), width); - } - return width; - }(); - - bool first = true; - std::string prev_fname; - for(const auto& lm : err.locations()) - { - if( ! first) - { - std::ostringstream oss; - oss << detail::make_string(lnw + 1, ' ') - << color::bold << color::blue << " |" << color::reset - << color::bold << " ...\n" << color::reset; - oss << detail::make_string(lnw + 1, ' ') - << color::bold << color::blue << " |\n" << color::reset; - errmsg += oss.str(); - } - - const auto& l = lm.first; - const auto& m = lm.second; - - errmsg += detail::format_location_impl(lnw, prev_fname, l, m); - - prev_fname = l.file_name(); - first = false; - } - - errmsg += err.suffix(); - - return errmsg; -} - -TOML11_INLINE std::string format_error(const error_info& err) -{ - std::ostringstream oss; - oss << color::red << color::bold << "[error]" << color::reset; - return format_error(oss.str(), err); -} - -TOML11_INLINE std::ostream& operator<<(std::ostream& os, const error_info& e) -{ - os << format_error(e); - return os; -} - -} // toml -#endif // TOML11_ERROR_INFO_IMPL_HPP -#endif - -#endif // TOML11_ERROR_INFO_HPP -#ifndef TOML11_VALUE_HPP -#define TOML11_VALUE_HPP - - -#ifdef TOML11_HAS_STRING_VIEW -#include -#endif - -#include - -namespace toml -{ -template -class basic_value; - -struct type_error final : public ::toml::exception -{ - public: - type_error(std::string what_arg, source_location loc) - : what_(std::move(what_arg)), loc_(std::move(loc)) - {} - ~type_error() noexcept override = default; - - const char* what() const noexcept override {return what_.c_str();} - - source_location const& location() const noexcept {return loc_;} - - private: - std::string what_; - source_location loc_; -}; - -// only for internal use -namespace detail -{ -template -error_info make_type_error(const basic_value&, const std::string&, const value_t); - -template -error_info make_not_found_error(const basic_value&, const std::string&, const typename basic_value::key_type&); - -template -void change_region_of_value(basic_value&, const basic_value&); - -template -struct getter; -} // detail - -template -class basic_value -{ - public: - - using config_type = TypeConfig; - using key_type = typename config_type::string_type; - using value_type = basic_value; - using boolean_type = typename config_type::boolean_type; - using integer_type = typename config_type::integer_type; - using floating_type = typename config_type::floating_type; - using string_type = typename config_type::string_type; - using local_time_type = ::toml::local_time; - using local_date_type = ::toml::local_date; - using local_datetime_type = ::toml::local_datetime; - using offset_datetime_type = ::toml::offset_datetime; - using array_type = typename config_type::template array_type; - using table_type = typename config_type::template table_type; - using comment_type = typename config_type::comment_type; - using char_type = typename string_type::value_type; - - private: - - using region_type = detail::region; - - public: - - basic_value() noexcept - : type_(value_t::empty), empty_('\0'), region_{}, comments_{} - {} - ~basic_value() noexcept {this->cleanup();} - - // copy/move constructor/assigner ===================================== {{{ - - basic_value(const basic_value& v) - : type_(v.type_), region_(v.region_), comments_(v.comments_) - { - switch(this->type_) - { - case value_t::boolean : assigner(boolean_ , v.boolean_ ); break; - case value_t::integer : assigner(integer_ , v.integer_ ); break; - case value_t::floating : assigner(floating_ , v.floating_ ); break; - case value_t::string : assigner(string_ , v.string_ ); break; - case value_t::offset_datetime: assigner(offset_datetime_, v.offset_datetime_); break; - case value_t::local_datetime : assigner(local_datetime_ , v.local_datetime_ ); break; - case value_t::local_date : assigner(local_date_ , v.local_date_ ); break; - case value_t::local_time : assigner(local_time_ , v.local_time_ ); break; - case value_t::array : assigner(array_ , v.array_ ); break; - case value_t::table : assigner(table_ , v.table_ ); break; - default : assigner(empty_ , '\0' ); break; - } - } - basic_value(basic_value&& v) - : type_(v.type()), region_(std::move(v.region_)), - comments_(std::move(v.comments_)) - { - switch(this->type_) - { - case value_t::boolean : assigner(boolean_ , std::move(v.boolean_ )); break; - case value_t::integer : assigner(integer_ , std::move(v.integer_ )); break; - case value_t::floating : assigner(floating_ , std::move(v.floating_ )); break; - case value_t::string : assigner(string_ , std::move(v.string_ )); break; - case value_t::offset_datetime: assigner(offset_datetime_, std::move(v.offset_datetime_)); break; - case value_t::local_datetime : assigner(local_datetime_ , std::move(v.local_datetime_ )); break; - case value_t::local_date : assigner(local_date_ , std::move(v.local_date_ )); break; - case value_t::local_time : assigner(local_time_ , std::move(v.local_time_ )); break; - case value_t::array : assigner(array_ , std::move(v.array_ )); break; - case value_t::table : assigner(table_ , std::move(v.table_ )); break; - default : assigner(empty_ , '\0' ); break; - } - } - - basic_value& operator=(const basic_value& v) - { - if(this == std::addressof(v)) {return *this;} - - this->cleanup(); - this->type_ = v.type_; - this->region_ = v.region_; - this->comments_ = v.comments_; - switch(this->type_) - { - case value_t::boolean : assigner(boolean_ , v.boolean_ ); break; - case value_t::integer : assigner(integer_ , v.integer_ ); break; - case value_t::floating : assigner(floating_ , v.floating_ ); break; - case value_t::string : assigner(string_ , v.string_ ); break; - case value_t::offset_datetime: assigner(offset_datetime_, v.offset_datetime_); break; - case value_t::local_datetime : assigner(local_datetime_ , v.local_datetime_ ); break; - case value_t::local_date : assigner(local_date_ , v.local_date_ ); break; - case value_t::local_time : assigner(local_time_ , v.local_time_ ); break; - case value_t::array : assigner(array_ , v.array_ ); break; - case value_t::table : assigner(table_ , v.table_ ); break; - default : assigner(empty_ , '\0' ); break; - } - return *this; - } - basic_value& operator=(basic_value&& v) - { - if(this == std::addressof(v)) {return *this;} - - this->cleanup(); - this->type_ = v.type_; - this->region_ = std::move(v.region_); - this->comments_ = std::move(v.comments_); - switch(this->type_) - { - case value_t::boolean : assigner(boolean_ , std::move(v.boolean_ )); break; - case value_t::integer : assigner(integer_ , std::move(v.integer_ )); break; - case value_t::floating : assigner(floating_ , std::move(v.floating_ )); break; - case value_t::string : assigner(string_ , std::move(v.string_ )); break; - case value_t::offset_datetime: assigner(offset_datetime_, std::move(v.offset_datetime_)); break; - case value_t::local_datetime : assigner(local_datetime_ , std::move(v.local_datetime_ )); break; - case value_t::local_date : assigner(local_date_ , std::move(v.local_date_ )); break; - case value_t::local_time : assigner(local_time_ , std::move(v.local_time_ )); break; - case value_t::array : assigner(array_ , std::move(v.array_ )); break; - case value_t::table : assigner(table_ , std::move(v.table_ )); break; - default : assigner(empty_ , '\0' ); break; - } - return *this; - } - // }}} - - // constructor to overwrite commnets ================================== {{{ - - basic_value(basic_value v, std::vector com) - : type_(v.type()), region_(std::move(v.region_)), - comments_(std::move(com)) - { - switch(this->type_) - { - case value_t::boolean : assigner(boolean_ , std::move(v.boolean_ )); break; - case value_t::integer : assigner(integer_ , std::move(v.integer_ )); break; - case value_t::floating : assigner(floating_ , std::move(v.floating_ )); break; - case value_t::string : assigner(string_ , std::move(v.string_ )); break; - case value_t::offset_datetime: assigner(offset_datetime_, std::move(v.offset_datetime_)); break; - case value_t::local_datetime : assigner(local_datetime_ , std::move(v.local_datetime_ )); break; - case value_t::local_date : assigner(local_date_ , std::move(v.local_date_ )); break; - case value_t::local_time : assigner(local_time_ , std::move(v.local_time_ )); break; - case value_t::array : assigner(array_ , std::move(v.array_ )); break; - case value_t::table : assigner(table_ , std::move(v.table_ )); break; - default : assigner(empty_ , '\0' ); break; - } - } - // }}} - - // conversion between different basic_values ========================== {{{ - - template - basic_value(basic_value other) - : type_(other.type_), - region_(std::move(other.region_)), - comments_(std::move(other.comments_)) - { - switch(other.type_) - { - // use auto-convert in constructor - case value_t::boolean : assigner(boolean_ , std::move(other.boolean_ )); break; - case value_t::integer : assigner(integer_ , std::move(other.integer_ )); break; - case value_t::floating : assigner(floating_ , std::move(other.floating_ )); break; - case value_t::string : assigner(string_ , std::move(other.string_ )); break; - case value_t::offset_datetime: assigner(offset_datetime_, std::move(other.offset_datetime_)); break; - case value_t::local_datetime : assigner(local_datetime_ , std::move(other.local_datetime_ )); break; - case value_t::local_date : assigner(local_date_ , std::move(other.local_date_ )); break; - case value_t::local_time : assigner(local_time_ , std::move(other.local_time_ )); break; - - // may have different container type - case value_t::array : - { - array_type tmp( - std::make_move_iterator(other.array_.value.get().begin()), - std::make_move_iterator(other.array_.value.get().end())); - assigner(array_, array_storage( - detail::storage(std::move(tmp)), - other.array_.format - )); - break; - } - case value_t::table : - { - table_type tmp( - std::make_move_iterator(other.table_.value.get().begin()), - std::make_move_iterator(other.table_.value.get().end())); - assigner(table_, table_storage( - detail::storage(std::move(tmp)), - other.table_.format - )); - break; - } - default: break; - } - } - - template - basic_value(basic_value other, std::vector com) - : type_(other.type_), - region_(std::move(other.region_)), - comments_(std::move(com)) - { - switch(other.type_) - { - // use auto-convert in constructor - case value_t::boolean : assigner(boolean_ , std::move(other.boolean_ )); break; - case value_t::integer : assigner(integer_ , std::move(other.integer_ )); break; - case value_t::floating : assigner(floating_ , std::move(other.floating_ )); break; - case value_t::string : assigner(string_ , std::move(other.string_ )); break; - case value_t::offset_datetime: assigner(offset_datetime_, std::move(other.offset_datetime_)); break; - case value_t::local_datetime : assigner(local_datetime_ , std::move(other.local_datetime_ )); break; - case value_t::local_date : assigner(local_date_ , std::move(other.local_date_ )); break; - case value_t::local_time : assigner(local_time_ , std::move(other.local_time_ )); break; - - // may have different container type - case value_t::array : - { - array_type tmp( - std::make_move_iterator(other.array_.value.get().begin()), - std::make_move_iterator(other.array_.value.get().end())); - assigner(array_, array_storage( - detail::storage(std::move(tmp)), - other.array_.format - )); - break; - } - case value_t::table : - { - table_type tmp( - std::make_move_iterator(other.table_.value.get().begin()), - std::make_move_iterator(other.table_.value.get().end())); - assigner(table_, table_storage( - detail::storage(std::move(tmp)), - other.table_.format - )); - break; - } - default: break; - } - } - template - basic_value& operator=(basic_value other) - { - this->cleanup(); - this->region_ = other.region_; - this->comments_ = comment_type(other.comments_); - this->type_ = other.type_; - switch(other.type_) - { - // use auto-convert in constructor - case value_t::boolean : assigner(boolean_ , std::move(other.boolean_ )); break; - case value_t::integer : assigner(integer_ , std::move(other.integer_ )); break; - case value_t::floating : assigner(floating_ , std::move(other.floating_ )); break; - case value_t::string : assigner(string_ , std::move(other.string_ )); break; - case value_t::offset_datetime: assigner(offset_datetime_, std::move(other.offset_datetime_)); break; - case value_t::local_datetime : assigner(local_datetime_ , std::move(other.local_datetime_ )); break; - case value_t::local_date : assigner(local_date_ , std::move(other.local_date_ )); break; - case value_t::local_time : assigner(local_time_ , std::move(other.local_time_ )); break; - - // may have different container type - case value_t::array : - { - array_type tmp( - std::make_move_iterator(other.array_.value.get().begin()), - std::make_move_iterator(other.array_.value.get().end())); - assigner(array_, array_storage( - detail::storage(std::move(tmp)), - other.array_.format - )); - break; - } - case value_t::table : - { - table_type tmp( - std::make_move_iterator(other.table_.value.get().begin()), - std::make_move_iterator(other.table_.value.get().end())); - assigner(table_, table_storage( - detail::storage(std::move(tmp)), - other.table_.format - )); - break; - } - default: break; - } - return *this; - } - // }}} - - // constructor (boolean) ============================================== {{{ - - basic_value(boolean_type x) - : basic_value(x, boolean_format_info{}, std::vector{}, region_type{}) - {} - basic_value(boolean_type x, boolean_format_info fmt) - : basic_value(x, fmt, std::vector{}, region_type{}) - {} - basic_value(boolean_type x, std::vector com) - : basic_value(x, boolean_format_info{}, std::move(com), region_type{}) - {} - basic_value(boolean_type x, boolean_format_info fmt, std::vector com) - : basic_value(x, fmt, std::move(com), region_type{}) - {} - basic_value(boolean_type x, boolean_format_info fmt, - std::vector com, region_type reg) - : type_(value_t::boolean), boolean_(boolean_storage(x, fmt)), - region_(std::move(reg)), comments_(std::move(com)) - {} - basic_value& operator=(boolean_type x) - { - boolean_format_info fmt; - if(this->is_boolean()) - { - fmt = this->as_boolean_fmt(); - } - this->cleanup(); - this->type_ = value_t::boolean; - this->region_ = region_type{}; - assigner(this->boolean_, boolean_storage(x, fmt)); - return *this; - } - - // }}} - - // constructor (integer) ============================================== {{{ - - basic_value(integer_type x) - : basic_value(std::move(x), integer_format_info{}, std::vector{}, region_type{}) - {} - basic_value(integer_type x, integer_format_info fmt) - : basic_value(std::move(x), std::move(fmt), std::vector{}, region_type{}) - {} - basic_value(integer_type x, std::vector com) - : basic_value(std::move(x), integer_format_info{}, std::move(com), region_type{}) - {} - basic_value(integer_type x, integer_format_info fmt, std::vector com) - : basic_value(std::move(x), std::move(fmt), std::move(com), region_type{}) - {} - basic_value(integer_type x, integer_format_info fmt, std::vector com, region_type reg) - : type_(value_t::integer), integer_(integer_storage(std::move(x), std::move(fmt))), - region_(std::move(reg)), comments_(std::move(com)) - {} - basic_value& operator=(integer_type x) - { - integer_format_info fmt; - if(this->is_integer()) - { - fmt = this->as_integer_fmt(); - } - this->cleanup(); - this->type_ = value_t::integer; - this->region_ = region_type{}; - assigner(this->integer_, integer_storage(std::move(x), std::move(fmt))); - return *this; - } - - private: - - template - using enable_if_integer_like_t = cxx::enable_if_t, boolean_type>>, - cxx::negation, integer_type>>, - std::is_integral> - >::value, std::nullptr_t>; - - public: - - template = nullptr> - basic_value(T x) - : basic_value(std::move(x), integer_format_info{}, std::vector{}, region_type{}) - {} - template = nullptr> - basic_value(T x, integer_format_info fmt) - : basic_value(std::move(x), std::move(fmt), std::vector{}, region_type{}) - {} - template = nullptr> - basic_value(T x, std::vector com) - : basic_value(std::move(x), integer_format_info{}, std::move(com), region_type{}) - {} - template = nullptr> - basic_value(T x, integer_format_info fmt, std::vector com) - : basic_value(std::move(x), std::move(fmt), std::move(com), region_type{}) - {} - template = nullptr> - basic_value(T x, integer_format_info fmt, std::vector com, region_type reg) - : type_(value_t::integer), integer_(integer_storage(std::move(x), std::move(fmt))), - region_(std::move(reg)), comments_(std::move(com)) - {} - template = nullptr> - basic_value& operator=(T x) - { - integer_format_info fmt; - if(this->is_integer()) - { - fmt = this->as_integer_fmt(); - } - this->cleanup(); - this->type_ = value_t::integer; - this->region_ = region_type{}; - assigner(this->integer_, integer_storage(x, std::move(fmt))); - return *this; - } - - // }}} - - // constructor (floating) ============================================= {{{ - - basic_value(floating_type x) - : basic_value(std::move(x), floating_format_info{}, std::vector{}, region_type{}) - {} - basic_value(floating_type x, floating_format_info fmt) - : basic_value(std::move(x), std::move(fmt), std::vector{}, region_type{}) - {} - basic_value(floating_type x, std::vector com) - : basic_value(std::move(x), floating_format_info{}, std::move(com), region_type{}) - {} - basic_value(floating_type x, floating_format_info fmt, std::vector com) - : basic_value(std::move(x), std::move(fmt), std::move(com), region_type{}) - {} - basic_value(floating_type x, floating_format_info fmt, std::vector com, region_type reg) - : type_(value_t::floating), floating_(floating_storage(std::move(x), std::move(fmt))), - region_(std::move(reg)), comments_(std::move(com)) - {} - basic_value& operator=(floating_type x) - { - floating_format_info fmt; - if(this->is_floating()) - { - fmt = this->as_floating_fmt(); - } - this->cleanup(); - this->type_ = value_t::floating; - this->region_ = region_type{}; - assigner(this->floating_, floating_storage(std::move(x), std::move(fmt))); - return *this; - } - - private: - - template - using enable_if_floating_like_t = cxx::enable_if_t, floating_type>>, - std::is_floating_point> - >::value, std::nullptr_t>; - - public: - - template = nullptr> - basic_value(T x) - : basic_value(x, floating_format_info{}, std::vector{}, region_type{}) - {} - - template = nullptr> - basic_value(T x, floating_format_info fmt) - : basic_value(x, std::move(fmt), std::vector{}, region_type{}) - {} - - template = nullptr> - basic_value(T x, std::vector com) - : basic_value(x, floating_format_info{}, std::move(com), region_type{}) - {} - - template = nullptr> - basic_value(T x, floating_format_info fmt, std::vector com) - : basic_value(x, std::move(fmt), std::move(com), region_type{}) - {} - - template = nullptr> - basic_value(T x, floating_format_info fmt, std::vector com, region_type reg) - : type_(value_t::floating), floating_(floating_storage(x, std::move(fmt))), - region_(std::move(reg)), comments_(std::move(com)) - {} - - template = nullptr> - basic_value& operator=(T x) - { - floating_format_info fmt; - if(this->is_floating()) - { - fmt = this->as_floating_fmt(); - } - this->cleanup(); - this->type_ = value_t::floating; - this->region_ = region_type{}; - assigner(this->floating_, floating_storage(x, std::move(fmt))); - return *this; - } - - // }}} - - // constructor (string) =============================================== {{{ - - basic_value(string_type x) - : basic_value(std::move(x), string_format_info{}, std::vector{}, region_type{}) - {} - basic_value(string_type x, string_format_info fmt) - : basic_value(std::move(x), std::move(fmt), std::vector{}, region_type{}) - {} - basic_value(string_type x, std::vector com) - : basic_value(std::move(x), string_format_info{}, std::move(com), region_type{}) - {} - basic_value(string_type x, string_format_info fmt, std::vector com) - : basic_value(std::move(x), std::move(fmt), std::move(com), region_type{}) - {} - basic_value(string_type x, string_format_info fmt, - std::vector com, region_type reg) - : type_(value_t::string), string_(string_storage(std::move(x), std::move(fmt))), - region_(std::move(reg)), comments_(std::move(com)) - {} - basic_value& operator=(string_type x) - { - string_format_info fmt; - if(this->is_string()) - { - fmt = this->as_string_fmt(); - } - this->cleanup(); - this->type_ = value_t::string; - this->region_ = region_type{}; - assigner(this->string_, string_storage(x, std::move(fmt))); - return *this; - } - - // "string literal" - - basic_value(const typename string_type::value_type* x) - : basic_value(x, string_format_info{}, std::vector{}, region_type{}) - {} - basic_value(const typename string_type::value_type* x, string_format_info fmt) - : basic_value(x, std::move(fmt), std::vector{}, region_type{}) - {} - basic_value(const typename string_type::value_type* x, std::vector com) - : basic_value(x, string_format_info{}, std::move(com), region_type{}) - {} - basic_value(const typename string_type::value_type* x, string_format_info fmt, std::vector com) - : basic_value(x, std::move(fmt), std::move(com), region_type{}) - {} - basic_value(const typename string_type::value_type* x, string_format_info fmt, - std::vector com, region_type reg) - : type_(value_t::string), string_(string_storage(string_type(x), std::move(fmt))), - region_(std::move(reg)), comments_(std::move(com)) - {} - basic_value& operator=(const typename string_type::value_type* x) - { - string_format_info fmt; - if(this->is_string()) - { - fmt = this->as_string_fmt(); - } - this->cleanup(); - this->type_ = value_t::string; - this->region_ = region_type{}; - assigner(this->string_, string_storage(string_type(x), std::move(fmt))); - return *this; - } - -#if defined(TOML11_HAS_STRING_VIEW) - using string_view_type = std::basic_string_view< - typename string_type::value_type, typename string_type::traits_type>; - - basic_value(string_view_type x) - : basic_value(x, string_format_info{}, std::vector{}, region_type{}) - {} - basic_value(string_view_type x, string_format_info fmt) - : basic_value(x, std::move(fmt), std::vector{}, region_type{}) - {} - basic_value(string_view_type x, std::vector com) - : basic_value(x, string_format_info{}, std::move(com), region_type{}) - {} - basic_value(string_view_type x, string_format_info fmt, std::vector com) - : basic_value(x, std::move(fmt), std::move(com), region_type{}) - {} - basic_value(string_view_type x, string_format_info fmt, - std::vector com, region_type reg) - : type_(value_t::string), string_(string_storage(string_type(x), std::move(fmt))), - region_(std::move(reg)), comments_(std::move(com)) - {} - basic_value& operator=(string_view_type x) - { - string_format_info fmt; - if(this->is_string()) - { - fmt = this->as_string_fmt(); - } - this->cleanup(); - this->type_ = value_t::string; - this->region_ = region_type{}; - assigner(this->string_, string_storage(string_type(x), std::move(fmt))); - return *this; - } - -#endif // TOML11_HAS_STRING_VIEW - - template, string_type>>, - detail::is_1byte_std_basic_string - >::value, std::nullptr_t> = nullptr> - basic_value(const T& x) - : basic_value(x, string_format_info{}, std::vector{}, region_type{}) - {} - template, string_type>>, - detail::is_1byte_std_basic_string - >::value, std::nullptr_t> = nullptr> - basic_value(const T& x, string_format_info fmt) - : basic_value(x, std::move(fmt), std::vector{}, region_type{}) - {} - template, string_type>>, - detail::is_1byte_std_basic_string - >::value, std::nullptr_t> = nullptr> - basic_value(const T& x, std::vector com) - : basic_value(x, string_format_info{}, std::move(com), region_type{}) - {} - template, string_type>>, - detail::is_1byte_std_basic_string - >::value, std::nullptr_t> = nullptr> - basic_value(const T& x, string_format_info fmt, std::vector com) - : basic_value(x, std::move(fmt), std::move(com), region_type{}) - {} - template, string_type>>, - detail::is_1byte_std_basic_string - >::value, std::nullptr_t> = nullptr> - basic_value(const T& x, string_format_info fmt, - std::vector com, region_type reg) - : type_(value_t::string), - string_(string_storage(detail::string_conv(x), std::move(fmt))), - region_(std::move(reg)), comments_(std::move(com)) - {} - template, string_type>>, - detail::is_1byte_std_basic_string - >::value, std::nullptr_t> = nullptr> - basic_value& operator=(const T& x) - { - string_format_info fmt; - if(this->is_string()) - { - fmt = this->as_string_fmt(); - } - this->cleanup(); - this->type_ = value_t::string; - this->region_ = region_type{}; - assigner(this->string_, string_storage(detail::string_conv(x), std::move(fmt))); - return *this; - } - - // }}} - - // constructor (local_date) =========================================== {{{ - - basic_value(local_date_type x) - : basic_value(x, local_date_format_info{}, std::vector{}, region_type{}) - {} - basic_value(local_date_type x, local_date_format_info fmt) - : basic_value(x, fmt, std::vector{}, region_type{}) - {} - basic_value(local_date_type x, std::vector com) - : basic_value(x, local_date_format_info{}, std::move(com), region_type{}) - {} - basic_value(local_date_type x, local_date_format_info fmt, std::vector com) - : basic_value(x, fmt, std::move(com), region_type{}) - {} - basic_value(local_date_type x, local_date_format_info fmt, - std::vector com, region_type reg) - : type_(value_t::local_date), local_date_(local_date_storage(x, fmt)), - region_(std::move(reg)), comments_(std::move(com)) - {} - basic_value& operator=(local_date_type x) - { - local_date_format_info fmt; - if(this->is_local_date()) - { - fmt = this->as_local_date_fmt(); - } - this->cleanup(); - this->type_ = value_t::local_date; - this->region_ = region_type{}; - assigner(this->local_date_, local_date_storage(x, fmt)); - return *this; - } - - // }}} - - // constructor (local_time) =========================================== {{{ - - basic_value(local_time_type x) - : basic_value(x, local_time_format_info{}, std::vector{}, region_type{}) - {} - basic_value(local_time_type x, local_time_format_info fmt) - : basic_value(x, fmt, std::vector{}, region_type{}) - {} - basic_value(local_time_type x, std::vector com) - : basic_value(x, local_time_format_info{}, std::move(com), region_type{}) - {} - basic_value(local_time_type x, local_time_format_info fmt, std::vector com) - : basic_value(x, fmt, std::move(com), region_type{}) - {} - basic_value(local_time_type x, local_time_format_info fmt, - std::vector com, region_type reg) - : type_(value_t::local_time), local_time_(local_time_storage(x, fmt)), - region_(std::move(reg)), comments_(std::move(com)) - {} - basic_value& operator=(local_time_type x) - { - local_time_format_info fmt; - if(this->is_local_time()) - { - fmt = this->as_local_time_fmt(); - } - this->cleanup(); - this->type_ = value_t::local_time; - this->region_ = region_type{}; - assigner(this->local_time_, local_time_storage(x, fmt)); - return *this; - } - - template - basic_value(const std::chrono::duration& x) - : basic_value(local_time_type(x), local_time_format_info{}, std::vector{}, region_type{}) - {} - template - basic_value(const std::chrono::duration& x, local_time_format_info fmt) - : basic_value(local_time_type(x), std::move(fmt), std::vector{}, region_type{}) - {} - template - basic_value(const std::chrono::duration& x, std::vector com) - : basic_value(local_time_type(x), local_time_format_info{}, std::move(com), region_type{}) - {} - template - basic_value(const std::chrono::duration& x, local_time_format_info fmt, std::vector com) - : basic_value(local_time_type(x), std::move(fmt), std::move(com), region_type{}) - {} - template - basic_value(const std::chrono::duration& x, - local_time_format_info fmt, - std::vector com, region_type reg) - : basic_value(local_time_type(x), std::move(fmt), std::move(com), std::move(reg)) - {} - template - basic_value& operator=(const std::chrono::duration& x) - { - local_time_format_info fmt; - if(this->is_local_time()) - { - fmt = this->as_local_time_fmt(); - } - this->cleanup(); - this->type_ = value_t::local_time; - this->region_ = region_type{}; - assigner(this->local_time_, local_time_storage(local_time_type(x), std::move(fmt))); - return *this; - } - - // }}} - - // constructor (local_datetime) =========================================== {{{ - - basic_value(local_datetime_type x) - : basic_value(x, local_datetime_format_info{}, std::vector{}, region_type{}) - {} - basic_value(local_datetime_type x, local_datetime_format_info fmt) - : basic_value(x, fmt, std::vector{}, region_type{}) - {} - basic_value(local_datetime_type x, std::vector com) - : basic_value(x, local_datetime_format_info{}, std::move(com), region_type{}) - {} - basic_value(local_datetime_type x, local_datetime_format_info fmt, std::vector com) - : basic_value(x, fmt, std::move(com), region_type{}) - {} - basic_value(local_datetime_type x, local_datetime_format_info fmt, - std::vector com, region_type reg) - : type_(value_t::local_datetime), local_datetime_(local_datetime_storage(x, fmt)), - region_(std::move(reg)), comments_(std::move(com)) - {} - basic_value& operator=(local_datetime_type x) - { - local_datetime_format_info fmt; - if(this->is_local_datetime()) - { - fmt = this->as_local_datetime_fmt(); - } - this->cleanup(); - this->type_ = value_t::local_datetime; - this->region_ = region_type{}; - assigner(this->local_datetime_, local_datetime_storage(x, fmt)); - return *this; - } - - // }}} - - // constructor (offset_datetime) =========================================== {{{ - - basic_value(offset_datetime_type x) - : basic_value(x, offset_datetime_format_info{}, std::vector{}, region_type{}) - {} - basic_value(offset_datetime_type x, offset_datetime_format_info fmt) - : basic_value(x, fmt, std::vector{}, region_type{}) - {} - basic_value(offset_datetime_type x, std::vector com) - : basic_value(x, offset_datetime_format_info{}, std::move(com), region_type{}) - {} - basic_value(offset_datetime_type x, offset_datetime_format_info fmt, std::vector com) - : basic_value(x, fmt, std::move(com), region_type{}) - {} - basic_value(offset_datetime_type x, offset_datetime_format_info fmt, - std::vector com, region_type reg) - : type_(value_t::offset_datetime), offset_datetime_(offset_datetime_storage(x, fmt)), - region_(std::move(reg)), comments_(std::move(com)) - {} - basic_value& operator=(offset_datetime_type x) - { - offset_datetime_format_info fmt; - if(this->is_offset_datetime()) - { - fmt = this->as_offset_datetime_fmt(); - } - this->cleanup(); - this->type_ = value_t::offset_datetime; - this->region_ = region_type{}; - assigner(this->offset_datetime_, offset_datetime_storage(x, fmt)); - return *this; - } - - // system_clock::time_point - - basic_value(std::chrono::system_clock::time_point x) - : basic_value(offset_datetime_type(x), offset_datetime_format_info{}, std::vector{}, region_type{}) - {} - basic_value(std::chrono::system_clock::time_point x, offset_datetime_format_info fmt) - : basic_value(offset_datetime_type(x), fmt, std::vector{}, region_type{}) - {} - basic_value(std::chrono::system_clock::time_point x, std::vector com) - : basic_value(offset_datetime_type(x), offset_datetime_format_info{}, std::move(com), region_type{}) - {} - basic_value(std::chrono::system_clock::time_point x, offset_datetime_format_info fmt, std::vector com) - : basic_value(offset_datetime_type(x), fmt, std::move(com), region_type{}) - {} - basic_value(std::chrono::system_clock::time_point x, offset_datetime_format_info fmt, - std::vector com, region_type reg) - : basic_value(offset_datetime_type(x), std::move(fmt), std::move(com), std::move(reg)) - {} - basic_value& operator=(std::chrono::system_clock::time_point x) - { - offset_datetime_format_info fmt; - if(this->is_offset_datetime()) - { - fmt = this->as_offset_datetime_fmt(); - } - this->cleanup(); - this->type_ = value_t::offset_datetime; - this->region_ = region_type{}; - assigner(this->offset_datetime_, offset_datetime_storage(offset_datetime_type(x), fmt)); - return *this; - } - - // }}} - - // constructor (array) ================================================ {{{ - - basic_value(array_type x) - : basic_value(std::move(x), array_format_info{}, std::vector{}, region_type{}) - {} - basic_value(array_type x, array_format_info fmt) - : basic_value(std::move(x), std::move(fmt), std::vector{}, region_type{}) - {} - basic_value(array_type x, std::vector com) - : basic_value(std::move(x), array_format_info{}, std::move(com), region_type{}) - {} - basic_value(array_type x, array_format_info fmt, std::vector com) - : basic_value(std::move(x), fmt, std::move(com), region_type{}) - {} - basic_value(array_type x, array_format_info fmt, - std::vector com, region_type reg) - : type_(value_t::array), array_(array_storage( - detail::storage(std::move(x)), std::move(fmt) - )), region_(std::move(reg)), comments_(std::move(com)) - {} - basic_value& operator=(array_type x) - { - array_format_info fmt; - if(this->is_array()) - { - fmt = this->as_array_fmt(); - } - this->cleanup(); - this->type_ = value_t::array; - this->region_ = region_type{}; - assigner(this->array_, array_storage( - detail::storage(std::move(x)), std::move(fmt))); - return *this; - } - - private: - - template - using enable_if_array_like_t = cxx::enable_if_t, - cxx::negation>, - cxx::negation>, -#if defined(TOML11_HAS_STRING_VIEW) - cxx::negation>, -#endif - cxx::negation>, - cxx::negation> - >::value, std::nullptr_t>; - - public: - - template = nullptr> - basic_value(T x) - : basic_value(std::move(x), array_format_info{}, std::vector{}, region_type{}) - {} - template = nullptr> - basic_value(T x, array_format_info fmt) - : basic_value(std::move(x), std::move(fmt), std::vector{}, region_type{}) - {} - template = nullptr> - basic_value(T x, std::vector com) - : basic_value(std::move(x), array_format_info{}, std::move(com), region_type{}) - {} - template = nullptr> - basic_value(T x, array_format_info fmt, std::vector com) - : basic_value(std::move(x), fmt, std::move(com), region_type{}) - {} - template = nullptr> - basic_value(T x, array_format_info fmt, - std::vector com, region_type reg) - : type_(value_t::array), array_(array_storage( - detail::storage(array_type( - std::make_move_iterator(x.begin()), - std::make_move_iterator(x.end())) - ), std::move(fmt) - )), region_(std::move(reg)), comments_(std::move(com)) - {} - template = nullptr> - basic_value& operator=(T x) - { - array_format_info fmt; - if(this->is_array()) - { - fmt = this->as_array_fmt(); - } - this->cleanup(); - this->type_ = value_t::array; - this->region_ = region_type{}; - - array_type a(std::make_move_iterator(x.begin()), - std::make_move_iterator(x.end())); - assigner(this->array_, array_storage( - detail::storage(std::move(a)), std::move(fmt))); - return *this; - } - - // }}} - - // constructor (table) ================================================ {{{ - - basic_value(table_type x) - : basic_value(std::move(x), table_format_info{}, std::vector{}, region_type{}) - {} - basic_value(table_type x, table_format_info fmt) - : basic_value(std::move(x), std::move(fmt), std::vector{}, region_type{}) - {} - basic_value(table_type x, std::vector com) - : basic_value(std::move(x), table_format_info{}, std::move(com), region_type{}) - {} - basic_value(table_type x, table_format_info fmt, std::vector com) - : basic_value(std::move(x), fmt, std::move(com), region_type{}) - {} - basic_value(table_type x, table_format_info fmt, - std::vector com, region_type reg) - : type_(value_t::table), table_(table_storage( - detail::storage(std::move(x)), std::move(fmt) - )), region_(std::move(reg)), comments_(std::move(com)) - {} - basic_value& operator=(table_type x) - { - table_format_info fmt; - if(this->is_table()) - { - fmt = this->as_table_fmt(); - } - this->cleanup(); - this->type_ = value_t::table; - this->region_ = region_type{}; - assigner(this->table_, table_storage( - detail::storage(std::move(x)), std::move(fmt))); - return *this; - } - - // table-like - - private: - - template - using enable_if_table_like_t = cxx::enable_if_t>, - detail::is_map, - cxx::negation>, - cxx::negation> - >::value, std::nullptr_t>; - - public: - - template = nullptr> - basic_value(T x) - : basic_value(std::move(x), table_format_info{}, std::vector{}, region_type{}) - {} - template = nullptr> - basic_value(T x, table_format_info fmt) - : basic_value(std::move(x), std::move(fmt), std::vector{}, region_type{}) - {} - template = nullptr> - basic_value(T x, std::vector com) - : basic_value(std::move(x), table_format_info{}, std::move(com), region_type{}) - {} - template = nullptr> - basic_value(T x, table_format_info fmt, std::vector com) - : basic_value(std::move(x), fmt, std::move(com), region_type{}) - {} - template = nullptr> - basic_value(T x, table_format_info fmt, - std::vector com, region_type reg) - : type_(value_t::table), table_(table_storage( - detail::storage(table_type( - std::make_move_iterator(x.begin()), - std::make_move_iterator(x.end()) - )), std::move(fmt) - )), region_(std::move(reg)), comments_(std::move(com)) - {} - template = nullptr> - basic_value& operator=(T x) - { - table_format_info fmt; - if(this->is_table()) - { - fmt = this->as_table_fmt(); - } - this->cleanup(); - this->type_ = value_t::table; - this->region_ = region_type{}; - - table_type t(std::make_move_iterator(x.begin()), - std::make_move_iterator(x.end())); - assigner(this->table_, table_storage( - detail::storage(std::move(t)), std::move(fmt))); - return *this; - } - - // }}} - - // constructor (user_defined) ========================================= {{{ - - template::value, std::nullptr_t> = nullptr> - basic_value(const T& ud) - : basic_value( - into>::template into_toml(ud)) - {} - template::value, std::nullptr_t> = nullptr> - basic_value(const T& ud, std::vector com) - : basic_value( - into>::template into_toml(ud), - std::move(com)) - {} - template::value, std::nullptr_t> = nullptr> - basic_value& operator=(const T& ud) - { - *this = into>::template into_toml(ud); - return *this; - } - - template, - cxx::negation> - >::value, std::nullptr_t> = nullptr> - basic_value(const T& ud): basic_value(ud.into_toml()) {} - - template, - cxx::negation> - >::value, std::nullptr_t> = nullptr> - basic_value(const T& ud, std::vector com) - : basic_value(ud.into_toml(), std::move(com)) - {} - template, - cxx::negation> - >::value, std::nullptr_t> = nullptr> - basic_value& operator=(const T& ud) - { - *this = ud.into_toml(); - return *this; - } - - template, - cxx::negation> - >::value, std::nullptr_t> = nullptr> - basic_value(const T& ud): basic_value(ud.template into_toml()) {} - - template, - cxx::negation> - >::value, std::nullptr_t> = nullptr> - basic_value(const T& ud, std::vector com) - : basic_value(ud.template into_toml(), std::move(com)) - {} - template, - cxx::negation> - >::value, std::nullptr_t> = nullptr> - basic_value& operator=(const T& ud) - { - *this = ud.template into_toml(); - return *this; - } - // }}} - - // empty value with region info ======================================= {{{ - - // mainly for `null` extension - basic_value(detail::none_t, region_type reg) noexcept - : type_(value_t::empty), empty_('\0'), region_(std::move(reg)), comments_{} - {} - - // }}} - - // type checking ====================================================== {{{ - - template, value_type>::value, - std::nullptr_t> = nullptr> - bool is() const noexcept - { - return detail::type_to_enum::value == this->type_; - } - bool is(value_t t) const noexcept {return t == this->type_;} - - bool is_empty() const noexcept {return this->is(value_t::empty );} - bool is_boolean() const noexcept {return this->is(value_t::boolean );} - bool is_integer() const noexcept {return this->is(value_t::integer );} - bool is_floating() const noexcept {return this->is(value_t::floating );} - bool is_string() const noexcept {return this->is(value_t::string );} - bool is_offset_datetime() const noexcept {return this->is(value_t::offset_datetime);} - bool is_local_datetime() const noexcept {return this->is(value_t::local_datetime );} - bool is_local_date() const noexcept {return this->is(value_t::local_date );} - bool is_local_time() const noexcept {return this->is(value_t::local_time );} - bool is_array() const noexcept {return this->is(value_t::array );} - bool is_table() const noexcept {return this->is(value_t::table );} - - bool is_array_of_tables() const noexcept - { - if( ! this->is_array()) {return false;} - const auto& a = this->as_array(std::nothrow); // already checked. - - // when you define [[array.of.tables]], at least one empty table will be - // assigned. In case of array of inline tables, `array_of_tables = []`, - // there is no reason to consider this as an array of *tables*. - // So empty array is not an array-of-tables. - if(a.empty()) {return false;} - - // since toml v1.0.0 allows array of heterogeneous types, we need to - // check all the elements. if any of the elements is not a table, it - // is a heterogeneous array and cannot be expressed by `[[aot]]` form. - for(const auto& e : a) - { - if( ! e.is_table()) {return false;} - } - return true; - } - - value_t type() const noexcept {return type_;} - - // }}} - - // as_xxx (noexcept) version ========================================== {{{ - - template - detail::enum_to_type_t> const& - as(const std::nothrow_t&) const noexcept - { - return detail::getter::get_nothrow(*this); - } - template - detail::enum_to_type_t>& - as(const std::nothrow_t&) noexcept - { - return detail::getter::get_nothrow(*this); - } - - boolean_type const& as_boolean (const std::nothrow_t&) const noexcept {return this->boolean_.value;} - integer_type const& as_integer (const std::nothrow_t&) const noexcept {return this->integer_.value;} - floating_type const& as_floating (const std::nothrow_t&) const noexcept {return this->floating_.value;} - string_type const& as_string (const std::nothrow_t&) const noexcept {return this->string_.value;} - offset_datetime_type const& as_offset_datetime(const std::nothrow_t&) const noexcept {return this->offset_datetime_.value;} - local_datetime_type const& as_local_datetime (const std::nothrow_t&) const noexcept {return this->local_datetime_.value;} - local_date_type const& as_local_date (const std::nothrow_t&) const noexcept {return this->local_date_.value;} - local_time_type const& as_local_time (const std::nothrow_t&) const noexcept {return this->local_time_.value;} - array_type const& as_array (const std::nothrow_t&) const noexcept {return this->array_.value.get();} - table_type const& as_table (const std::nothrow_t&) const noexcept {return this->table_.value.get();} - - boolean_type & as_boolean (const std::nothrow_t&) noexcept {return this->boolean_.value;} - integer_type & as_integer (const std::nothrow_t&) noexcept {return this->integer_.value;} - floating_type & as_floating (const std::nothrow_t&) noexcept {return this->floating_.value;} - string_type & as_string (const std::nothrow_t&) noexcept {return this->string_.value;} - offset_datetime_type& as_offset_datetime(const std::nothrow_t&) noexcept {return this->offset_datetime_.value;} - local_datetime_type & as_local_datetime (const std::nothrow_t&) noexcept {return this->local_datetime_.value;} - local_date_type & as_local_date (const std::nothrow_t&) noexcept {return this->local_date_.value;} - local_time_type & as_local_time (const std::nothrow_t&) noexcept {return this->local_time_.value;} - array_type & as_array (const std::nothrow_t&) noexcept {return this->array_.value.get();} - table_type & as_table (const std::nothrow_t&) noexcept {return this->table_.value.get();} - - // }}} - - // as_xxx (throw) ===================================================== {{{ - - template - detail::enum_to_type_t> const& as() const - { - return detail::getter::get(*this); - } - template - detail::enum_to_type_t>& as() - { - return detail::getter::get(*this); - } - - boolean_type const& as_boolean() const - { - if(this->type_ != value_t::boolean) - { - this->throw_bad_cast("toml::value::as_boolean()", value_t::boolean); - } - return this->boolean_.value; - } - integer_type const& as_integer() const - { - if(this->type_ != value_t::integer) - { - this->throw_bad_cast("toml::value::as_integer()", value_t::integer); - } - return this->integer_.value; - } - floating_type const& as_floating() const - { - if(this->type_ != value_t::floating) - { - this->throw_bad_cast("toml::value::as_floating()", value_t::floating); - } - return this->floating_.value; - } - string_type const& as_string() const - { - if(this->type_ != value_t::string) - { - this->throw_bad_cast("toml::value::as_string()", value_t::string); - } - return this->string_.value; - } - offset_datetime_type const& as_offset_datetime() const - { - if(this->type_ != value_t::offset_datetime) - { - this->throw_bad_cast("toml::value::as_offset_datetime()", value_t::offset_datetime); - } - return this->offset_datetime_.value; - } - local_datetime_type const& as_local_datetime() const - { - if(this->type_ != value_t::local_datetime) - { - this->throw_bad_cast("toml::value::as_local_datetime()", value_t::local_datetime); - } - return this->local_datetime_.value; - } - local_date_type const& as_local_date() const - { - if(this->type_ != value_t::local_date) - { - this->throw_bad_cast("toml::value::as_local_date()", value_t::local_date); - } - return this->local_date_.value; - } - local_time_type const& as_local_time() const - { - if(this->type_ != value_t::local_time) - { - this->throw_bad_cast("toml::value::as_local_time()", value_t::local_time); - } - return this->local_time_.value; - } - array_type const& as_array() const - { - if(this->type_ != value_t::array) - { - this->throw_bad_cast("toml::value::as_array()", value_t::array); - } - return this->array_.value.get(); - } - table_type const& as_table() const - { - if(this->type_ != value_t::table) - { - this->throw_bad_cast("toml::value::as_table()", value_t::table); - } - return this->table_.value.get(); - } - - // ------------------------------------------------------------------------ - // nonconst reference - - boolean_type& as_boolean() - { - if(this->type_ != value_t::boolean) - { - this->throw_bad_cast("toml::value::as_boolean()", value_t::boolean); - } - return this->boolean_.value; - } - integer_type& as_integer() - { - if(this->type_ != value_t::integer) - { - this->throw_bad_cast("toml::value::as_integer()", value_t::integer); - } - return this->integer_.value; - } - floating_type& as_floating() - { - if(this->type_ != value_t::floating) - { - this->throw_bad_cast("toml::value::as_floating()", value_t::floating); - } - return this->floating_.value; - } - string_type& as_string() - { - if(this->type_ != value_t::string) - { - this->throw_bad_cast("toml::value::as_string()", value_t::string); - } - return this->string_.value; - } - offset_datetime_type& as_offset_datetime() - { - if(this->type_ != value_t::offset_datetime) - { - this->throw_bad_cast("toml::value::as_offset_datetime()", value_t::offset_datetime); - } - return this->offset_datetime_.value; - } - local_datetime_type& as_local_datetime() - { - if(this->type_ != value_t::local_datetime) - { - this->throw_bad_cast("toml::value::as_local_datetime()", value_t::local_datetime); - } - return this->local_datetime_.value; - } - local_date_type& as_local_date() - { - if(this->type_ != value_t::local_date) - { - this->throw_bad_cast("toml::value::as_local_date()", value_t::local_date); - } - return this->local_date_.value; - } - local_time_type& as_local_time() - { - if(this->type_ != value_t::local_time) - { - this->throw_bad_cast("toml::value::as_local_time()", value_t::local_time); - } - return this->local_time_.value; - } - array_type& as_array() - { - if(this->type_ != value_t::array) - { - this->throw_bad_cast("toml::value::as_array()", value_t::array); - } - return this->array_.value.get(); - } - table_type& as_table() - { - if(this->type_ != value_t::table) - { - this->throw_bad_cast("toml::value::as_table()", value_t::table); - } - return this->table_.value.get(); - } - - // }}} - - // format accessors (noexcept) ======================================== {{{ - - template - detail::enum_to_fmt_type_t const& - as_fmt(const std::nothrow_t&) const noexcept - { - return detail::getter::get_fmt_nothrow(*this); - } - template - detail::enum_to_fmt_type_t& - as_fmt(const std::nothrow_t&) noexcept - { - return detail::getter::get_fmt_nothrow(*this); - } - - boolean_format_info & as_boolean_fmt (const std::nothrow_t&) noexcept {return this->boolean_.format;} - integer_format_info & as_integer_fmt (const std::nothrow_t&) noexcept {return this->integer_.format;} - floating_format_info & as_floating_fmt (const std::nothrow_t&) noexcept {return this->floating_.format;} - string_format_info & as_string_fmt (const std::nothrow_t&) noexcept {return this->string_.format;} - offset_datetime_format_info& as_offset_datetime_fmt(const std::nothrow_t&) noexcept {return this->offset_datetime_.format;} - local_datetime_format_info & as_local_datetime_fmt (const std::nothrow_t&) noexcept {return this->local_datetime_.format;} - local_date_format_info & as_local_date_fmt (const std::nothrow_t&) noexcept {return this->local_date_.format;} - local_time_format_info & as_local_time_fmt (const std::nothrow_t&) noexcept {return this->local_time_.format;} - array_format_info & as_array_fmt (const std::nothrow_t&) noexcept {return this->array_.format;} - table_format_info & as_table_fmt (const std::nothrow_t&) noexcept {return this->table_.format;} - - boolean_format_info const& as_boolean_fmt (const std::nothrow_t&) const noexcept {return this->boolean_.format;} - integer_format_info const& as_integer_fmt (const std::nothrow_t&) const noexcept {return this->integer_.format;} - floating_format_info const& as_floating_fmt (const std::nothrow_t&) const noexcept {return this->floating_.format;} - string_format_info const& as_string_fmt (const std::nothrow_t&) const noexcept {return this->string_.format;} - offset_datetime_format_info const& as_offset_datetime_fmt(const std::nothrow_t&) const noexcept {return this->offset_datetime_.format;} - local_datetime_format_info const& as_local_datetime_fmt (const std::nothrow_t&) const noexcept {return this->local_datetime_.format;} - local_date_format_info const& as_local_date_fmt (const std::nothrow_t&) const noexcept {return this->local_date_.format;} - local_time_format_info const& as_local_time_fmt (const std::nothrow_t&) const noexcept {return this->local_time_.format;} - array_format_info const& as_array_fmt (const std::nothrow_t&) const noexcept {return this->array_.format;} - table_format_info const& as_table_fmt (const std::nothrow_t&) const noexcept {return this->table_.format;} - - // }}} - - // format accessors (throw) =========================================== {{{ - - template - detail::enum_to_fmt_type_t const& as_fmt() const - { - return detail::getter::get_fmt(*this); - } - template - detail::enum_to_fmt_type_t& as_fmt() - { - return detail::getter::get_fmt(*this); - } - - boolean_format_info const& as_boolean_fmt() const - { - if(this->type_ != value_t::boolean) - { - this->throw_bad_cast("toml::value::as_boolean_fmt()", value_t::boolean); - } - return this->boolean_.format; - } - integer_format_info const& as_integer_fmt() const - { - if(this->type_ != value_t::integer) - { - this->throw_bad_cast("toml::value::as_integer_fmt()", value_t::integer); - } - return this->integer_.format; - } - floating_format_info const& as_floating_fmt() const - { - if(this->type_ != value_t::floating) - { - this->throw_bad_cast("toml::value::as_floating_fmt()", value_t::floating); - } - return this->floating_.format; - } - string_format_info const& as_string_fmt() const - { - if(this->type_ != value_t::string) - { - this->throw_bad_cast("toml::value::as_string_fmt()", value_t::string); - } - return this->string_.format; - } - offset_datetime_format_info const& as_offset_datetime_fmt() const - { - if(this->type_ != value_t::offset_datetime) - { - this->throw_bad_cast("toml::value::as_offset_datetime_fmt()", value_t::offset_datetime); - } - return this->offset_datetime_.format; - } - local_datetime_format_info const& as_local_datetime_fmt() const - { - if(this->type_ != value_t::local_datetime) - { - this->throw_bad_cast("toml::value::as_local_datetime_fmt()", value_t::local_datetime); - } - return this->local_datetime_.format; - } - local_date_format_info const& as_local_date_fmt() const - { - if(this->type_ != value_t::local_date) - { - this->throw_bad_cast("toml::value::as_local_date_fmt()", value_t::local_date); - } - return this->local_date_.format; - } - local_time_format_info const& as_local_time_fmt() const - { - if(this->type_ != value_t::local_time) - { - this->throw_bad_cast("toml::value::as_local_time_fmt()", value_t::local_time); - } - return this->local_time_.format; - } - array_format_info const& as_array_fmt() const - { - if(this->type_ != value_t::array) - { - this->throw_bad_cast("toml::value::as_array_fmt()", value_t::array); - } - return this->array_.format; - } - table_format_info const& as_table_fmt() const - { - if(this->type_ != value_t::table) - { - this->throw_bad_cast("toml::value::as_table_fmt()", value_t::table); - } - return this->table_.format; - } - - // ------------------------------------------------------------------------ - // nonconst reference - - boolean_format_info& as_boolean_fmt() - { - if(this->type_ != value_t::boolean) - { - this->throw_bad_cast("toml::value::as_boolean_fmt()", value_t::boolean); - } - return this->boolean_.format; - } - integer_format_info& as_integer_fmt() - { - if(this->type_ != value_t::integer) - { - this->throw_bad_cast("toml::value::as_integer_fmt()", value_t::integer); - } - return this->integer_.format; - } - floating_format_info& as_floating_fmt() - { - if(this->type_ != value_t::floating) - { - this->throw_bad_cast("toml::value::as_floating_fmt()", value_t::floating); - } - return this->floating_.format; - } - string_format_info& as_string_fmt() - { - if(this->type_ != value_t::string) - { - this->throw_bad_cast("toml::value::as_string_fmt()", value_t::string); - } - return this->string_.format; - } - offset_datetime_format_info& as_offset_datetime_fmt() - { - if(this->type_ != value_t::offset_datetime) - { - this->throw_bad_cast("toml::value::as_offset_datetime_fmt()", value_t::offset_datetime); - } - return this->offset_datetime_.format; - } - local_datetime_format_info& as_local_datetime_fmt() - { - if(this->type_ != value_t::local_datetime) - { - this->throw_bad_cast("toml::value::as_local_datetime_fmt()", value_t::local_datetime); - } - return this->local_datetime_.format; - } - local_date_format_info& as_local_date_fmt() - { - if(this->type_ != value_t::local_date) - { - this->throw_bad_cast("toml::value::as_local_date_fmt()", value_t::local_date); - } - return this->local_date_.format; - } - local_time_format_info& as_local_time_fmt() - { - if(this->type_ != value_t::local_time) - { - this->throw_bad_cast("toml::value::as_local_time_fmt()", value_t::local_time); - } - return this->local_time_.format; - } - array_format_info& as_array_fmt() - { - if(this->type_ != value_t::array) - { - this->throw_bad_cast("toml::value::as_array_fmt()", value_t::array); - } - return this->array_.format; - } - table_format_info& as_table_fmt() - { - if(this->type_ != value_t::table) - { - this->throw_bad_cast("toml::value::as_table_fmt()", value_t::table); - } - return this->table_.format; - } - // }}} - - // table accessors ==================================================== {{{ - - value_type& at(const key_type& k) - { - if(!this->is_table()) - { - this->throw_bad_cast("toml::value::at(key_type)", value_t::table); - } - auto& table = this->as_table(std::nothrow); - const auto found = table.find(k); - if(found == table.end()) - { - this->throw_key_not_found_error("toml::value::at", k); - } - assert(found->first == k); - return found->second; - } - value_type const& at(const key_type& k) const - { - if(!this->is_table()) - { - this->throw_bad_cast("toml::value::at(key_type)", value_t::table); - } - const auto& table = this->as_table(std::nothrow); - const auto found = table.find(k); - if(found == table.end()) - { - this->throw_key_not_found_error("toml::value::at", k); - } - assert(found->first == k); - return found->second; - } - value_type& operator[](const key_type& k) - { - if(this->is_empty()) - { - (*this) = table_type{}; - } - else if( ! this->is_table()) // initialized, but not a table - { - this->throw_bad_cast("toml::value::operator[](key_type)", value_t::table); - } - return (this->as_table(std::nothrow))[k]; - } - std::size_t count(const key_type& k) const - { - if(!this->is_table()) - { - this->throw_bad_cast("toml::value::count(key_type)", value_t::table); - } - return this->as_table(std::nothrow).count(k); - } - bool contains(const key_type& k) const - { - if(!this->is_table()) - { - this->throw_bad_cast("toml::value::contains(key_type)", value_t::table); - } - const auto& table = this->as_table(std::nothrow); - return table.find(k) != table.end(); - } - // }}} - - // array accessors ==================================================== {{{ - - value_type& at(const std::size_t idx) - { - if(!this->is_array()) - { - this->throw_bad_cast("toml::value::at(idx)", value_t::array); - } - auto& ar = this->as_array(std::nothrow); - - if(ar.size() <= idx) - { - std::ostringstream oss; - oss << "actual length (" << ar.size() - << ") is shorter than the specified index (" << idx << ")."; - throw std::out_of_range(format_error( - "toml::value::at(idx): no element corresponding to the index", - this->location(), oss.str() - )); - } - return ar.at(idx); - } - value_type const& at(const std::size_t idx) const - { - if(!this->is_array()) - { - this->throw_bad_cast("toml::value::at(idx)", value_t::array); - } - const auto& ar = this->as_array(std::nothrow); - - if(ar.size() <= idx) - { - std::ostringstream oss; - oss << "actual length (" << ar.size() - << ") is shorter than the specified index (" << idx << ")."; - - throw std::out_of_range(format_error( - "toml::value::at(idx): no element corresponding to the index", - this->location(), oss.str() - )); - } - return ar.at(idx); - } - - value_type& operator[](const std::size_t idx) noexcept - { - // no check... - return this->as_array(std::nothrow)[idx]; - } - value_type const& operator[](const std::size_t idx) const noexcept - { - // no check... - return this->as_array(std::nothrow)[idx]; - } - - void push_back(const value_type& x) - { - if(!this->is_array()) - { - this->throw_bad_cast("toml::value::push_back(idx)", value_t::array); - } - this->as_array(std::nothrow).push_back(x); - return; - } - void push_back(value_type&& x) - { - if(!this->is_array()) - { - this->throw_bad_cast("toml::value::push_back(idx)", value_t::array); - } - this->as_array(std::nothrow).push_back(std::move(x)); - return; - } - - template - value_type& emplace_back(Ts&& ... args) - { - if(!this->is_array()) - { - this->throw_bad_cast("toml::value::emplace_back(idx)", value_t::array); - } - auto& ar = this->as_array(std::nothrow); - ar.emplace_back(std::forward(args) ...); - return ar.back(); - } - - std::size_t size() const - { - switch(this->type_) - { - case value_t::array: - { - return this->as_array(std::nothrow).size(); - } - case value_t::table: - { - return this->as_table(std::nothrow).size(); - } - case value_t::string: - { - return this->as_string(std::nothrow).size(); - } - default: - { - throw type_error(format_error( - "toml::value::size(): bad_cast to container types", - this->location(), - "the actual type is " + to_string(this->type_) - ), this->location()); - } - } - } - - // }}} - - source_location location() const - { - return source_location(this->region_); - } - - comment_type const& comments() const noexcept {return this->comments_;} - comment_type& comments() noexcept {return this->comments_;} - - private: - - // private helper functions =========================================== {{{ - - void cleanup() noexcept - { - switch(this->type_) - { - case value_t::boolean : { boolean_ .~boolean_storage (); break; } - case value_t::integer : { integer_ .~integer_storage (); break; } - case value_t::floating : { floating_ .~floating_storage (); break; } - case value_t::string : { string_ .~string_storage (); break; } - case value_t::offset_datetime : { offset_datetime_.~offset_datetime_storage (); break; } - case value_t::local_datetime : { local_datetime_ .~local_datetime_storage (); break; } - case value_t::local_date : { local_date_ .~local_date_storage (); break; } - case value_t::local_time : { local_time_ .~local_time_storage (); break; } - case value_t::array : { array_ .~array_storage (); break; } - case value_t::table : { table_ .~table_storage (); break; } - default : { break; } - } - this->type_ = value_t::empty; - return; - } - - template - static void assigner(T& dst, U&& v) - { - const auto tmp = ::new(std::addressof(dst)) T(std::forward(v)); - assert(tmp == std::addressof(dst)); - (void)tmp; - } - - [[noreturn]] - void throw_bad_cast(const std::string& funcname, const value_t ty) const - { - throw type_error(format_error(detail::make_type_error(*this, funcname, ty)), - this->location()); - } - - [[noreturn]] - void throw_key_not_found_error(const std::string& funcname, const key_type& key) const - { - throw std::out_of_range(format_error( - detail::make_not_found_error(*this, funcname, key))); - } - - template - friend void detail::change_region_of_value(basic_value&, const basic_value&); - - template - friend class basic_value; - - // }}} - - private: - - using boolean_storage = detail::value_with_format; - using integer_storage = detail::value_with_format; - using floating_storage = detail::value_with_format; - using string_storage = detail::value_with_format; - using offset_datetime_storage = detail::value_with_format; - using local_datetime_storage = detail::value_with_format; - using local_date_storage = detail::value_with_format; - using local_time_storage = detail::value_with_format; - using array_storage = detail::value_with_format, array_format_info >; - using table_storage = detail::value_with_format, table_format_info >; - - private: - - value_t type_; - union - { - char empty_; // the smallest type - boolean_storage boolean_; - integer_storage integer_; - floating_storage floating_; - string_storage string_; - offset_datetime_storage offset_datetime_; - local_datetime_storage local_datetime_; - local_date_storage local_date_; - local_time_storage local_time_; - array_storage array_; - table_storage table_; - }; - region_type region_; - comment_type comments_; -}; - -template -bool operator==(const basic_value& lhs, const basic_value& rhs) -{ - if(lhs.type() != rhs.type()) {return false;} - if(lhs.comments() != rhs.comments()) {return false;} - - switch(lhs.type()) - { - case value_t::boolean : - { - return lhs.as_boolean() == rhs.as_boolean(); - } - case value_t::integer : - { - return lhs.as_integer() == rhs.as_integer(); - } - case value_t::floating : - { - return lhs.as_floating() == rhs.as_floating(); - } - case value_t::string : - { - return lhs.as_string() == rhs.as_string(); - } - case value_t::offset_datetime: - { - return lhs.as_offset_datetime() == rhs.as_offset_datetime(); - } - case value_t::local_datetime: - { - return lhs.as_local_datetime() == rhs.as_local_datetime(); - } - case value_t::local_date: - { - return lhs.as_local_date() == rhs.as_local_date(); - } - case value_t::local_time: - { - return lhs.as_local_time() == rhs.as_local_time(); - } - case value_t::array : - { - return lhs.as_array() == rhs.as_array(); - } - case value_t::table : - { - return lhs.as_table() == rhs.as_table(); - } - case value_t::empty : {return true; } - default: {return false;} - } -} - -template -bool operator!=(const basic_value& lhs, const basic_value& rhs) -{ - return !(lhs == rhs); -} - -template -cxx::enable_if_t::array_type>, - detail::is_comparable::table_type> - >::value, bool> -operator<(const basic_value& lhs, const basic_value& rhs) -{ - if(lhs.type() != rhs.type()) - { - return (lhs.type() < rhs.type()); - } - switch(lhs.type()) - { - case value_t::boolean : - { - return lhs.as_boolean() < rhs.as_boolean() || - (lhs.as_boolean() == rhs.as_boolean() && - lhs.comments() < rhs.comments()); - } - case value_t::integer : - { - return lhs.as_integer() < rhs.as_integer() || - (lhs.as_integer() == rhs.as_integer() && - lhs.comments() < rhs.comments()); - } - case value_t::floating : - { - return lhs.as_floating() < rhs.as_floating() || - (lhs.as_floating() == rhs.as_floating() && - lhs.comments() < rhs.comments()); - } - case value_t::string : - { - return lhs.as_string() < rhs.as_string() || - (lhs.as_string() == rhs.as_string() && - lhs.comments() < rhs.comments()); - } - case value_t::offset_datetime: - { - return lhs.as_offset_datetime() < rhs.as_offset_datetime() || - (lhs.as_offset_datetime() == rhs.as_offset_datetime() && - lhs.comments() < rhs.comments()); - } - case value_t::local_datetime: - { - return lhs.as_local_datetime() < rhs.as_local_datetime() || - (lhs.as_local_datetime() == rhs.as_local_datetime() && - lhs.comments() < rhs.comments()); - } - case value_t::local_date: - { - return lhs.as_local_date() < rhs.as_local_date() || - (lhs.as_local_date() == rhs.as_local_date() && - lhs.comments() < rhs.comments()); - } - case value_t::local_time: - { - return lhs.as_local_time() < rhs.as_local_time() || - (lhs.as_local_time() == rhs.as_local_time() && - lhs.comments() < rhs.comments()); - } - case value_t::array : - { - return lhs.as_array() < rhs.as_array() || - (lhs.as_array() == rhs.as_array() && - lhs.comments() < rhs.comments()); - } - case value_t::table : - { - return lhs.as_table() < rhs.as_table() || - (lhs.as_table() == rhs.as_table() && - lhs.comments() < rhs.comments()); - } - case value_t::empty : - { - return lhs.comments() < rhs.comments(); - } - default: - { - return lhs.comments() < rhs.comments(); - } - } -} - -template -cxx::enable_if_t::array_type>, - detail::is_comparable::table_type> - >::value, bool> -operator<=(const basic_value& lhs, const basic_value& rhs) -{ - return (lhs < rhs) || (lhs == rhs); -} -template -cxx::enable_if_t::array_type>, - detail::is_comparable::table_type> - >::value, bool> -operator>(const basic_value& lhs, const basic_value& rhs) -{ - return !(lhs <= rhs); -} -template -cxx::enable_if_t::array_type>, - detail::is_comparable::table_type> - >::value, bool> -operator>=(const basic_value& lhs, const basic_value& rhs) -{ - return !(lhs < rhs); -} - -// error_info helper -namespace detail -{ -template -error_info make_error_info_rec(error_info e, - const basic_value& v, std::string msg, Ts&& ... tail) -{ - return make_error_info_rec(std::move(e), v.location(), std::move(msg), std::forward(tail)...); -} -} // detail - -template -error_info make_error_info( - std::string title, const basic_value& v, std::string msg, Ts&& ... tail) -{ - return make_error_info(std::move(title), - v.location(), std::move(msg), std::forward(tail)...); -} -template -std::string format_error(std::string title, - const basic_value& v, std::string msg, Ts&& ... tail) -{ - return format_error(std::move(title), - v.location(), std::move(msg), std::forward(tail)...); -} - -namespace detail -{ - -template -error_info make_type_error(const basic_value& v, const std::string& fname, const value_t ty) -{ - return make_error_info(fname + ": bad_cast to " + to_string(ty), - v.location(), "the actual type is " + to_string(v.type())); -} -template -error_info make_not_found_error(const basic_value& v, const std::string& fname, const typename basic_value::key_type& key) -{ - const auto loc = v.location(); - const std::string title = fname + ": key \"" + string_conv(key) + "\" not found"; - - std::vector> locs; - if( ! loc.is_ok()) - { - return error_info(title, locs); - } - - if(loc.first_line_number() == 1 && loc.first_column_number() == 1 && loc.length() == 1) - { - // The top-level table has its region at the 0th character of the file. - // That means that, in the case when a key is not found in the top-level - // table, the error message points to the first character. If the file has - // the first table at the first line, the error message would be like this. - // ```console - // [error] key "a" not found - // --> example.toml - // | - // 1 | [table] - // | ^------ in this table - // ``` - // It actually points to the top-level table at the first character, not - // `[table]`. But it is too confusing. To avoid the confusion, the error - // message should explicitly say "key not found in the top-level table". - locs.emplace_back(v.location(), "at the top-level table"); - } - else - { - locs.emplace_back(v.location(), "in this table"); - } - return error_info(title, locs); -} - -#define TOML11_DETAIL_GENERATE_COMPTIME_GETTER(ty) \ - template \ - struct getter \ - { \ - using value_type = basic_value; \ - using result_type = enum_to_type_t; \ - using format_type = enum_to_fmt_type_t; \ - \ - static result_type& get(value_type& v) \ - { \ - return v.as_ ## ty(); \ - } \ - static result_type const& get(const value_type& v) \ - { \ - return v.as_ ## ty(); \ - } \ - \ - static result_type& get_nothrow(value_type& v) noexcept \ - { \ - return v.as_ ## ty(std::nothrow); \ - } \ - static result_type const& get_nothrow(const value_type& v) noexcept \ - { \ - return v.as_ ## ty(std::nothrow); \ - } \ - \ - static format_type& get_fmt(value_type& v) \ - { \ - return v.as_ ## ty ## _fmt(); \ - } \ - static format_type const& get_fmt(const value_type& v) \ - { \ - return v.as_ ## ty ## _fmt(); \ - } \ - \ - static format_type& get_fmt_nothrow(value_type& v) noexcept \ - { \ - return v.as_ ## ty ## _fmt(std::nothrow); \ - } \ - static format_type const& get_fmt_nothrow(const value_type& v) noexcept \ - { \ - return v.as_ ## ty ## _fmt(std::nothrow); \ - } \ - }; - -TOML11_DETAIL_GENERATE_COMPTIME_GETTER(boolean ) -TOML11_DETAIL_GENERATE_COMPTIME_GETTER(integer ) -TOML11_DETAIL_GENERATE_COMPTIME_GETTER(floating ) -TOML11_DETAIL_GENERATE_COMPTIME_GETTER(string ) -TOML11_DETAIL_GENERATE_COMPTIME_GETTER(offset_datetime) -TOML11_DETAIL_GENERATE_COMPTIME_GETTER(local_datetime ) -TOML11_DETAIL_GENERATE_COMPTIME_GETTER(local_date ) -TOML11_DETAIL_GENERATE_COMPTIME_GETTER(local_time ) -TOML11_DETAIL_GENERATE_COMPTIME_GETTER(array ) -TOML11_DETAIL_GENERATE_COMPTIME_GETTER(table ) - -#undef TOML11_DETAIL_GENERATE_COMPTIME_GETTER - -template -void change_region_of_value(basic_value& dst, const basic_value& src) -{ - dst.region_ = std::move(src.region_); - return; -} - -} // namespace detail -} // namespace toml -#endif // TOML11_VALUE_HPP -#ifndef TOML11_VISIT_HPP -#define TOML11_VISIT_HPP - - -namespace toml -{ - -template -cxx::return_type_of_t::boolean_type&> -visit(Visitor&& visitor, const basic_value& v) -{ - switch(v.type()) - { - case value_t::boolean : {return visitor(v.as_boolean ());} - case value_t::integer : {return visitor(v.as_integer ());} - case value_t::floating : {return visitor(v.as_floating ());} - case value_t::string : {return visitor(v.as_string ());} - case value_t::offset_datetime: {return visitor(v.as_offset_datetime());} - case value_t::local_datetime : {return visitor(v.as_local_datetime ());} - case value_t::local_date : {return visitor(v.as_local_date ());} - case value_t::local_time : {return visitor(v.as_local_time ());} - case value_t::array : {return visitor(v.as_array ());} - case value_t::table : {return visitor(v.as_table ());} - case value_t::empty : break; - default: break; - } - throw type_error(format_error("[error] toml::visit: toml::basic_value " - "does not have any valid type.", v.location(), "here"), v.location()); -} - -template -cxx::return_type_of_t::boolean_type&> -visit(Visitor&& visitor, basic_value& v) -{ - switch(v.type()) - { - case value_t::boolean : {return visitor(v.as_boolean ());} - case value_t::integer : {return visitor(v.as_integer ());} - case value_t::floating : {return visitor(v.as_floating ());} - case value_t::string : {return visitor(v.as_string ());} - case value_t::offset_datetime: {return visitor(v.as_offset_datetime());} - case value_t::local_datetime : {return visitor(v.as_local_datetime ());} - case value_t::local_date : {return visitor(v.as_local_date ());} - case value_t::local_time : {return visitor(v.as_local_time ());} - case value_t::array : {return visitor(v.as_array ());} - case value_t::table : {return visitor(v.as_table ());} - case value_t::empty : break; - default: break; - } - throw type_error(format_error("[error] toml::visit: toml::basic_value " - "does not have any valid type.", v.location(), "here"), v.location()); -} - -template -cxx::return_type_of_t::boolean_type&&> -visit(Visitor&& visitor, basic_value&& v) -{ - switch(v.type()) - { - case value_t::boolean : {return visitor(std::move(v.as_boolean ()));} - case value_t::integer : {return visitor(std::move(v.as_integer ()));} - case value_t::floating : {return visitor(std::move(v.as_floating ()));} - case value_t::string : {return visitor(std::move(v.as_string ()));} - case value_t::offset_datetime: {return visitor(std::move(v.as_offset_datetime()));} - case value_t::local_datetime : {return visitor(std::move(v.as_local_datetime ()));} - case value_t::local_date : {return visitor(std::move(v.as_local_date ()));} - case value_t::local_time : {return visitor(std::move(v.as_local_time ()));} - case value_t::array : {return visitor(std::move(v.as_array ()));} - case value_t::table : {return visitor(std::move(v.as_table ()));} - case value_t::empty : break; - default: break; - } - throw type_error(format_error("[error] toml::visit: toml::basic_value " - "does not have any valid type.", v.location(), "here"), v.location()); -} - -} // toml -#endif // TOML11_VISIT_HPP -#ifndef TOML11_TYPES_HPP -#define TOML11_TYPES_HPP - - -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace toml -{ - -// forward decl -template -class basic_value; - -// when you use a special integer type as toml::value::integer_type, parse must -// be able to read it. So, type_config has static member functions that read the -// integer_type as {dec, hex, oct, bin}-integer. But, in most cases, operator<< -// is enough. To make config easy, we provide the default read functions. -// -// Before this functions is called, syntax is checked and prefix(`0x` etc) and -// spacer(`_`) are removed. - -template -result -read_dec_int(const std::string& str, const source_location src) -{ - constexpr auto max_digits = std::numeric_limits::digits; - assert( ! str.empty()); - - T val{0}; - std::istringstream iss(str); - iss >> val; - if(iss.fail()) - { - return err(make_error_info("toml::parse_dec_integer: " - "too large integer: current max digits = 2^" + std::to_string(max_digits), - std::move(src), "must be < 2^" + std::to_string(max_digits))); - } - return ok(val); -} - -template -result -read_hex_int(const std::string& str, const source_location src) -{ - constexpr auto max_digits = std::numeric_limits::digits; - assert( ! str.empty()); - - T val{0}; - std::istringstream iss(str); - iss >> std::hex >> val; - if(iss.fail()) - { - return err(make_error_info("toml::parse_hex_integer: " - "too large integer: current max value = 2^" + std::to_string(max_digits), - std::move(src), "must be < 2^" + std::to_string(max_digits))); - } - return ok(val); -} - -template -result -read_oct_int(const std::string& str, const source_location src) -{ - constexpr auto max_digits = std::numeric_limits::digits; - assert( ! str.empty()); - - T val{0}; - std::istringstream iss(str); - iss >> std::oct >> val; - if(iss.fail()) - { - return err(make_error_info("toml::parse_oct_integer: " - "too large integer: current max value = 2^" + std::to_string(max_digits), - std::move(src), "must be < 2^" + std::to_string(max_digits))); - } - return ok(val); -} - -template -result -read_bin_int(const std::string& str, const source_location src) -{ - constexpr auto is_bounded = std::numeric_limits::is_bounded; - constexpr auto max_digits = std::numeric_limits::digits; - const auto max_value = (std::numeric_limits::max)(); - - T val{0}; - T base{1}; - for(auto i = str.rbegin(); i != str.rend(); ++i) - { - const auto c = *i; - if(c == '1') - { - val += base; - // prevent `base` from overflow - if(is_bounded && max_value / 2 < base && std::next(i) != str.rend()) - { - base = 0; - } - else - { - base *= 2; - } - } - else - { - assert(c == '0'); - - if(is_bounded && max_value / 2 < base && std::next(i) != str.rend()) - { - base = 0; - } - else - { - base *= 2; - } - } - } - if(base == 0) - { - return err(make_error_info("toml::parse_bin_integer: " - "too large integer: current max value = 2^" + std::to_string(max_digits), - std::move(src), "must be < 2^" + std::to_string(max_digits))); - } - return ok(val); -} - -template -result -read_int(const std::string& str, const source_location src, const std::uint8_t base) -{ - assert(base == 10 || base == 16 || base == 8 || base == 2); - switch(base) - { - case 2: { return read_bin_int(str, src); } - case 8: { return read_oct_int(str, src); } - case 16: { return read_hex_int(str, src); } - default: - { - assert(base == 10); - return read_dec_int(str, src); - } - } -} - -inline result -read_hex_float(const std::string& str, const source_location src, float val) -{ -#if defined(_MSC_VER) && ! defined(__clang__) - const auto res = ::sscanf_s(str.c_str(), "%a", std::addressof(val)); -#else - const auto res = std::sscanf(str.c_str(), "%a", std::addressof(val)); -#endif - if(res != 1) - { - return err(make_error_info("toml::parse_floating: " - "failed to read hexadecimal floating point value ", - std::move(src), "here")); - } - return ok(val); -} -inline result -read_hex_float(const std::string& str, const source_location src, double val) -{ -#if defined(_MSC_VER) && ! defined(__clang__) - const auto res = ::sscanf_s(str.c_str(), "%la", std::addressof(val)); -#else - const auto res = std::sscanf(str.c_str(), "%la", std::addressof(val)); -#endif - if(res != 1) - { - return err(make_error_info("toml::parse_floating: " - "failed to read hexadecimal floating point value ", - std::move(src), "here")); - } - return ok(val); -} -template -cxx::enable_if_t, double>>, - cxx::negation, float>> - >::value, result> -read_hex_float(const std::string&, const source_location src, T) -{ - return err(make_error_info("toml::parse_floating: failed to read " - "floating point value because of unknown type in type_config", - std::move(src), "here")); -} - -template -result -read_dec_float(const std::string& str, const source_location src) -{ - T val; - std::istringstream iss(str); - iss >> val; - if(iss.fail()) - { - return err(make_error_info("toml::parse_floating: " - "failed to read floating point value from stream", - std::move(src), "here")); - } - return ok(val); -} - -template -result -read_float(const std::string& str, const source_location src, const bool is_hex) -{ - if(is_hex) - { - return read_hex_float(str, src, T{}); - } - else - { - return read_dec_float(str, src); - } -} - -struct type_config -{ - using comment_type = preserve_comments; - - using boolean_type = bool; - using integer_type = std::int64_t; - using floating_type = double; - using string_type = std::string; - - template - using array_type = std::vector; - template - using table_type = std::unordered_map; - - static result - parse_int(const std::string& str, const source_location src, const std::uint8_t base) - { - return read_int(str, src, base); - } - static result - parse_float(const std::string& str, const source_location src, const bool is_hex) - { - return read_float(str, src, is_hex); - } -}; - -using value = basic_value; -using table = typename value::table_type; -using array = typename value::array_type; - -struct ordered_type_config -{ - using comment_type = preserve_comments; - - using boolean_type = bool; - using integer_type = std::int64_t; - using floating_type = double; - using string_type = std::string; - - template - using array_type = std::vector; - template - using table_type = ordered_map; - - static result - parse_int(const std::string& str, const source_location src, const std::uint8_t base) - { - return read_int(str, src, base); - } - static result - parse_float(const std::string& str, const source_location src, const bool is_hex) - { - return read_float(str, src, is_hex); - } -}; - -using ordered_value = basic_value; -using ordered_table = typename ordered_value::table_type; -using ordered_array = typename ordered_value::array_type; - -// ---------------------------------------------------------------------------- -// meta functions for internal use - -namespace detail -{ - -// ---------------------------------------------------------------------------- -// check if type T has all the needed member types - -struct has_comment_type_impl -{ - template static std::true_type check(typename T::comment_type*); - template static std::false_type check(...); -}; -template -using has_comment_type = decltype(has_comment_type_impl::check(nullptr)); - -struct has_integer_type_impl -{ - template static std::true_type check(typename T::integer_type*); - template static std::false_type check(...); -}; -template -using has_integer_type = decltype(has_integer_type_impl::check(nullptr)); - -struct has_floating_type_impl -{ - template static std::true_type check(typename T::floating_type*); - template static std::false_type check(...); -}; -template -using has_floating_type = decltype(has_floating_type_impl::check(nullptr)); - -struct has_string_type_impl -{ - template static std::true_type check(typename T::string_type*); - template static std::false_type check(...); -}; -template -using has_string_type = decltype(has_string_type_impl::check(nullptr)); - -struct has_array_type_impl -{ - template static std::true_type check(typename T::template array_type*); - template static std::false_type check(...); -}; -template -using has_array_type = decltype(has_array_type_impl::check(nullptr)); - -struct has_table_type_impl -{ - template static std::true_type check(typename T::template table_type*); - template static std::false_type check(...); -}; -template -using has_table_type = decltype(has_table_type_impl::check(nullptr)); - -struct has_parse_int_impl -{ - template static std::true_type check(decltype(std::declval().parse_int( - std::declval(), - std::declval(), - std::declval() - ))*); - template static std::false_type check(...); -}; -template -using has_parse_int = decltype(has_parse_int_impl::check(nullptr)); - -struct has_parse_float_impl -{ - template static std::true_type check(decltype(std::declval().parse_float( - std::declval(), - std::declval(), - std::declval() - ))*); - template static std::false_type check(...); -}; -template -using has_parse_float = decltype(has_parse_float_impl::check(nullptr)); - -template -using is_type_config = cxx::conjunction< - has_comment_type, - has_integer_type, - has_floating_type, - has_string_type, - has_array_type, - has_table_type, - has_parse_int, - has_parse_float - >; - -} // namespace detail -} // namespace toml - -#if defined(TOML11_COMPILE_SOURCES) -namespace toml -{ -extern template class basic_value; -extern template class basic_value; -} // toml -#endif // TOML11_COMPILE_SOURCES - -#endif // TOML11_TYPES_HPP -#ifndef TOML11_GET_HPP -#define TOML11_GET_HPP - -#include - - -#if defined(TOML11_HAS_STRING_VIEW) -#include -#endif // string_view - -namespace toml -{ - -// ============================================================================ -// T is toml::value; identity transformation. - -template -cxx::enable_if_t>::value, T>& -get(basic_value& v) -{ - return v; -} - -template -cxx::enable_if_t>::value, T> const& -get(const basic_value& v) -{ - return v; -} - -template -cxx::enable_if_t>::value, T> -get(basic_value&& v) -{ - return basic_value(std::move(v)); -} - -// ============================================================================ -// exact toml::* type - -template -cxx::enable_if_t>::value, T> & -get(basic_value& v) -{ - constexpr auto ty = detail::type_to_enum>::value; - return detail::getter::get(v); -} - -template -cxx::enable_if_t>::value, T> const& -get(const basic_value& v) -{ - constexpr auto ty = detail::type_to_enum>::value; - return detail::getter::get(v); -} - -template -cxx::enable_if_t>::value, T> -get(basic_value&& v) -{ - constexpr auto ty = detail::type_to_enum>::value; - return detail::getter::get(std::move(v)); -} - -// ============================================================================ -// T is toml::basic_value - -template -cxx::enable_if_t, - cxx::negation>> - >::value, T> -get(basic_value v) -{ - return T(std::move(v)); -} - -// ============================================================================ -// integer convertible from toml::value::integer_type - -template -cxx::enable_if_t, - cxx::negation>, - detail::is_not_toml_type>, - cxx::negation>, - cxx::negation> - >::value, T> -get(const basic_value& v) -{ - return static_cast(v.as_integer()); -} - -// ============================================================================ -// floating point convertible from toml::value::floating_type - -template -cxx::enable_if_t, - detail::is_not_toml_type>, - cxx::negation>, - cxx::negation> - >::value, T> -get(const basic_value& v) -{ - return static_cast(v.as_floating()); -} - -// ============================================================================ -// std::string with different char/trait/allocator - -template -cxx::enable_if_t>, - detail::is_1byte_std_basic_string - >::value, T> -get(const basic_value& v) -{ - return detail::string_conv>(v.as_string()); -} - -// ============================================================================ -// std::string_view - -#if defined(TOML11_HAS_STRING_VIEW) - -template -cxx::enable_if_t::string_type>::value, T> -get(const basic_value& v) -{ - return T(v.as_string()); -} - -#endif // string_view - -// ============================================================================ -// std::chrono::duration from toml::local_time - -template -cxx::enable_if_t::value, T> -get(const basic_value& v) -{ - return std::chrono::duration_cast( - std::chrono::nanoseconds(v.as_local_time())); -} - -// ============================================================================ -// std::chrono::system_clock::time_point from toml::datetime variants - -template -cxx::enable_if_t< - std::is_same::value, T> -get(const basic_value& v) -{ - switch(v.type()) - { - case value_t::local_date: - { - return std::chrono::system_clock::time_point(v.as_local_date()); - } - case value_t::local_datetime: - { - return std::chrono::system_clock::time_point(v.as_local_datetime()); - } - case value_t::offset_datetime: - { - return std::chrono::system_clock::time_point(v.as_offset_datetime()); - } - default: - { - const auto loc = v.location(); - throw type_error(format_error("toml::get: " - "bad_cast to std::chrono::system_clock::time_point", loc, - "the actual type is " + to_string(v.type())), loc); - } - } -} - -// ============================================================================ -// forward declaration to use this recursively. ignore this and go ahead. - -// array-like (w/ push_back) -template -cxx::enable_if_t, // T is a container - detail::has_push_back_method, // .push_back() works - detail::is_not_toml_type>, // but not toml::array - cxx::negation>, // but not std::basic_string -#if defined(TOML11_HAS_STRING_VIEW) - cxx::negation>, // but not std::basic_string_view -#endif - cxx::negation>, // no T.from_toml() - cxx::negation>, // no toml::from - cxx::negation&>> - >::value, T> -get(const basic_value&); - -// std::array -template -cxx::enable_if_t::value, T> -get(const basic_value&); - -// std::forward_list -template -cxx::enable_if_t::value, T> -get(const basic_value&); - -// std::pair -template -cxx::enable_if_t::value, T> -get(const basic_value&); - -// std::tuple -template -cxx::enable_if_t::value, T> -get(const basic_value&); - -// std::map (key is convertible from toml::value::key_type) -template -cxx::enable_if_t, // T is map - detail::is_not_toml_type>, // but not toml::table - std::is_convertible::key_type, - typename T::key_type>, // keys are convertible - cxx::negation>, // no T.from_toml() - cxx::negation>, // no toml::from - cxx::negation&>> - >::value, T> -get(const basic_value& v); - -// std::map (key is not convertible from toml::value::key_type, but -// is a std::basic_string) -template -cxx::enable_if_t, // T is map - detail::is_not_toml_type>, // but not toml::table - cxx::negation::key_type, - typename T::key_type>>, // keys are NOT convertible - detail::is_1byte_std_basic_string, // is std::basic_string - cxx::negation>, // no T.from_toml() - cxx::negation>, // no toml::from - cxx::negation&>> - >::value, T> -get(const basic_value& v); - -// toml::from::from_toml(v) -template -cxx::enable_if_t::value, T> -get(const basic_value&); - -// has T.from_toml(v) but no from -template -cxx::enable_if_t, // has T.from_toml() - cxx::negation>, // no toml::from - std::is_default_constructible // T{} works - >::value, T> -get(const basic_value&); - -// T(const toml::value&) and T is not toml::basic_value, -// and it does not have `from` nor `from_toml`. -template -cxx::enable_if_t&>, // has T(const basic_value&) - cxx::negation>, // but not basic_value itself - cxx::negation>, // no .from_toml() - cxx::negation> // no toml::from - >::value, T> -get(const basic_value&); - -// ============================================================================ -// array-like types; most likely STL container, like std::vector, etc. - -template -cxx::enable_if_t, // T is a container - detail::has_push_back_method, // .push_back() works - detail::is_not_toml_type>, // but not toml::array - cxx::negation>, // but not std::basic_string -#if defined(TOML11_HAS_STRING_VIEW) - cxx::negation>, // but not std::basic_string_view -#endif - cxx::negation>, // no T.from_toml() - cxx::negation>, // no toml::from - cxx::negation&>> - >::value, T> -get(const basic_value& v) -{ - using value_type = typename T::value_type; - const auto& a = v.as_array(); - - T container; - detail::try_reserve(container, a.size()); // if T has .reserve(), call it - - for(const auto& elem : a) - { - container.push_back(get(elem)); - } - return container; -} - -// ============================================================================ -// std::array - -template -cxx::enable_if_t::value, T> -get(const basic_value& v) -{ - using value_type = typename T::value_type; - const auto& a = v.as_array(); - - T container; - if(a.size() != container.size()) - { - const auto loc = v.location(); - throw std::out_of_range(format_error("toml::get: while converting to an array: " - " array size is " + std::to_string(container.size()) + - " but there are " + std::to_string(a.size()) + " elements in toml array.", - loc, "here")); - } - for(std::size_t i=0; i(a.at(i)); - } - return container; -} - -// ============================================================================ -// std::forward_list - -template -cxx::enable_if_t::value, T> -get(const basic_value& v) -{ - using value_type = typename T::value_type; - - T container; - for(const auto& elem : v.as_array()) - { - container.push_front(get(elem)); - } - container.reverse(); - return container; -} - -// ============================================================================ -// std::pair - -template -cxx::enable_if_t::value, T> -get(const basic_value& v) -{ - using first_type = typename T::first_type; - using second_type = typename T::second_type; - - const auto& ar = v.as_array(); - if(ar.size() != 2) - { - const auto loc = v.location(); - throw std::out_of_range(format_error("toml::get: while converting std::pair: " - " but there are " + std::to_string(ar.size()) + " > 2 elements in toml array.", - loc, "here")); - } - return std::make_pair(::toml::get(ar.at(0)), - ::toml::get(ar.at(1))); -} - -// ============================================================================ -// std::tuple. - -namespace detail -{ -template -T get_tuple_impl(const Array& a, cxx::index_sequence) -{ - return std::make_tuple( - ::toml::get::type>(a.at(I))...); -} -} // detail - -template -cxx::enable_if_t::value, T> -get(const basic_value& v) -{ - const auto& ar = v.as_array(); - if(ar.size() != std::tuple_size::value) - { - const auto loc = v.location(); - throw std::out_of_range(format_error("toml::get: while converting std::tuple: " - " there are " + std::to_string(ar.size()) + " > " + - std::to_string(std::tuple_size::value) + " elements in toml array.", - loc, "here")); - } - return detail::get_tuple_impl(ar, - cxx::make_index_sequence::value>{}); -} - -// ============================================================================ -// map-like types; most likely STL map, like std::map or std::unordered_map. - -// key is convertible from toml::value::key_type -template -cxx::enable_if_t, // T is map - detail::is_not_toml_type>, // but not toml::table - std::is_convertible::key_type, - typename T::key_type>, // keys are convertible - cxx::negation>, // no T.from_toml() - cxx::negation>, // no toml::from - cxx::negation&>> - >::value, T> -get(const basic_value& v) -{ - using key_type = typename T::key_type; - using mapped_type = typename T::mapped_type; - static_assert( - std::is_convertible::key_type, key_type>::value, - "toml::get only supports map type of which key_type is " - "convertible from toml::basic_value::key_type."); - - T m; - for(const auto& kv : v.as_table()) - { - m.emplace(key_type(kv.first), get(kv.second)); - } - return m; -} - -// key is NOT convertible from toml::value::key_type but std::basic_string -template -cxx::enable_if_t, // T is map - detail::is_not_toml_type>, // but not toml::table - cxx::negation::key_type, - typename T::key_type>>, // keys are NOT convertible - detail::is_1byte_std_basic_string, // is std::basic_string - cxx::negation>, // no T.from_toml() - cxx::negation>, // no toml::from - cxx::negation&>> - >::value, T> -get(const basic_value& v) -{ - using key_type = typename T::key_type; - using mapped_type = typename T::mapped_type; - - T m; - for(const auto& kv : v.as_table()) - { - m.emplace(detail::string_conv(kv.first), get(kv.second)); - } - return m; -} - -// ============================================================================ -// user-defined, but convertible types. - -// toml::from -template -cxx::enable_if_t::value, T> -get(const basic_value& v) -{ - return ::toml::from::from_toml(v); -} - -// has T.from_toml(v) but no from -template -cxx::enable_if_t, // has T.from_toml() - cxx::negation>, // no toml::from - std::is_default_constructible // T{} works - >::value, T> -get(const basic_value& v) -{ - T ud; - ud.from_toml(v); - return ud; -} - -// T(const toml::value&) and T is not toml::basic_value, -// and it does not have `from` nor `from_toml`. -template -cxx::enable_if_t&>, // has T(const basic_value&) - cxx::negation>, // but not basic_value itself - cxx::negation>, // no .from_toml() - cxx::negation> // no toml::from - >::value, T> -get(const basic_value& v) -{ - return T(v); -} - -// ============================================================================ -// get_or(value, fallback) - -template -cxx::enable_if_t::value, basic_value> const& -get_or(const basic_value& v, const basic_value&) -{ - return v; -} - -template -cxx::enable_if_t::value, basic_value>& -get_or(basic_value& v, basic_value&) -{ - return v; -} - -template -cxx::enable_if_t::value, basic_value> -get_or(basic_value&& v, basic_value&&) -{ - return v; -} - -// ---------------------------------------------------------------------------- -// specialization for the exact toml types (return type becomes lvalue ref) - -template -cxx::enable_if_t< - detail::is_exact_toml_type>::value, T> const& -get_or(const basic_value& v, const T& opt) noexcept -{ - try - { - return get>(v); - } - catch(...) - { - return opt; - } -} -template -cxx::enable_if_t>, - detail::is_exact_toml_type> - >::value, T>& -get_or(basic_value& v, T& opt) noexcept -{ - try - { - return get>(v); - } - catch(...) - { - return opt; - } -} -template -cxx::enable_if_t, - basic_value>::value, cxx::remove_cvref_t> -get_or(basic_value&& v, T&& opt) noexcept -{ - try - { - return get>(std::move(v)); - } - catch(...) - { - return cxx::remove_cvref_t(std::forward(opt)); - } -} - -// ---------------------------------------------------------------------------- -// specialization for string literal - -// template -// typename basic_value::string_type -// get_or(const basic_value& v, -// const typename basic_value::string_type::value_type (&opt)[N]) -// { -// try -// { -// return v.as_string(); -// } -// catch(...) -// { -// return typename basic_value::string_type(opt); -// } -// } -// -// The above only matches to the literal, like `get_or(v, "foo");` but not -// ```cpp -// const auto opt = "foo"; -// const auto str = get_or(v, opt); -// ``` -// . And the latter causes an error. -// To match to both `"foo"` and `const auto opt = "foo"`, we take a pointer to -// a character here. - -template -typename basic_value::string_type -get_or(const basic_value& v, - const typename basic_value::string_type::value_type* opt) -{ - try - { - return v.as_string(); - } - catch(...) - { - return typename basic_value::string_type(opt); - } -} - -// ---------------------------------------------------------------------------- -// others (require type conversion and return type cannot be lvalue reference) - -template -cxx::enable_if_t>, - cxx::negation>>, - cxx::negation, typename basic_value::string_type::value_type const*>> - >::value, cxx::remove_cvref_t> -get_or(const basic_value& v, T&& opt) -{ - try - { - return get>(v); - } - catch(...) - { - return cxx::remove_cvref_t(std::forward(opt)); - } -} - -} // toml -#endif // TOML11_GET_HPP -#ifndef TOML11_FIND_HPP -#define TOML11_FIND_HPP - -#include - - -#if defined(TOML11_HAS_STRING_VIEW) -#include -#endif - -namespace toml -{ - -// ---------------------------------------------------------------------------- -// find(value, key); - -template -decltype(::toml::get(std::declval const&>())) -find(const basic_value& v, const typename basic_value::key_type& ky) -{ - return ::toml::get(v.at(ky)); -} - -template -decltype(::toml::get(std::declval&>())) -find(basic_value& v, const typename basic_value::key_type& ky) -{ - return ::toml::get(v.at(ky)); -} - -template -decltype(::toml::get(std::declval&&>())) -find(basic_value&& v, const typename basic_value::key_type& ky) -{ - return ::toml::get(std::move(v.at(ky))); -} - -// ---------------------------------------------------------------------------- -// find(value, idx) - -template -decltype(::toml::get(std::declval const&>())) -find(const basic_value& v, const std::size_t idx) -{ - return ::toml::get(v.at(idx)); -} -template -decltype(::toml::get(std::declval&>())) -find(basic_value& v, const std::size_t idx) -{ - return ::toml::get(v.at(idx)); -} -template -decltype(::toml::get(std::declval&&>())) -find(basic_value&& v, const std::size_t idx) -{ - return ::toml::get(std::move(v.at(idx))); -} - -// ---------------------------------------------------------------------------- -// find(value, key/idx), w/o conversion - -template -cxx::enable_if_t::value, basic_value>& -find(basic_value& v, const typename basic_value::key_type& ky) -{ - return v.at(ky); -} -template -cxx::enable_if_t::value, basic_value> const& -find(basic_value const& v, const typename basic_value::key_type& ky) -{ - return v.at(ky); -} -template -cxx::enable_if_t::value, basic_value> -find(basic_value&& v, const typename basic_value::key_type& ky) -{ - return basic_value(std::move(v.at(ky))); -} - -template -cxx::enable_if_t::value, basic_value>& -find(basic_value& v, const std::size_t idx) -{ - return v.at(idx); -} -template -cxx::enable_if_t::value, basic_value> const& -find(basic_value const& v, const std::size_t idx) -{ - return v.at(idx); -} -template -cxx::enable_if_t::value, basic_value> -find(basic_value&& v, const std::size_t idx) -{ - return basic_value(std::move(v.at(idx))); -} - -// -------------------------------------------------------------------------- -// toml::find(toml::value, toml::key, Ts&& ... keys) - -namespace detail -{ - -// It suppresses warnings by -Wsign-conversion when we pass integer literal -// to toml::find. integer literal `0` is deduced as an int, and will be -// converted to std::size_t. This causes sign-conversion. - -template -std::size_t key_cast(const std::size_t& v) noexcept -{ - return v; -} -template -cxx::enable_if_t>::value, std::size_t> -key_cast(const T& v) noexcept -{ - return static_cast(v); -} - -// for string-like (string, string literal, string_view) - -template -typename basic_value::key_type const& -key_cast(const typename basic_value::key_type& v) noexcept -{ - return v; -} -template -typename basic_value::key_type -key_cast(const typename basic_value::key_type::value_type* v) -{ - return typename basic_value::key_type(v); -} -#if defined(TOML11_HAS_STRING_VIEW) -template -typename basic_value::key_type -key_cast(const std::string_view v) -{ - return typename basic_value::key_type(v); -} -#endif // string_view - -} // detail - -// ---------------------------------------------------------------------------- -// find(v, keys...) - -template -cxx::enable_if_t::value, basic_value> const& -find(const basic_value& v, const K1& k1, const K2& k2, const Ks& ... ks) -{ - return find(v.at(detail::key_cast(k1)), detail::key_cast(k2), ks...); -} -template -cxx::enable_if_t::value, basic_value>& -find(basic_value& v, const K1& k1, const K2& k2, const Ks& ... ks) -{ - return find(v.at(detail::key_cast(k1)), detail::key_cast(k2), ks...); -} -template -cxx::enable_if_t::value, basic_value> -find(basic_value&& v, const K1& k1, const K2& k2, const Ks& ... ks) -{ - return find(std::move(v.at(detail::key_cast(k1))), detail::key_cast(k2), ks...); -} - -// ---------------------------------------------------------------------------- -// find(v, keys...) - -template -decltype(::toml::get(std::declval&>())) -find(const basic_value& v, const K1& k1, const K2& k2, const Ks& ... ks) -{ - return find(v.at(detail::key_cast(k1)), detail::key_cast(k2), ks...); -} -template -decltype(::toml::get(std::declval&>())) -find(basic_value& v, const K1& k1, const K2& k2, const Ks& ... ks) -{ - return find(v.at(detail::key_cast(k1)), detail::key_cast(k2), ks...); -} -template -decltype(::toml::get(std::declval&&>())) -find(basic_value&& v, const K1& k1, const K2& k2, const Ks& ... ks) -{ - return find(std::move(v.at(detail::key_cast(k1))), detail::key_cast(k2), ks...); -} - -// =========================================================================== -// find_or(value, key, fallback) - -// --------------------------------------------------------------------------- -// find_or(v, key, other_v) - -template -cxx::enable_if_t::value, basic_value>& -find_or(basic_value& v, const K& k, basic_value& opt) noexcept -{ - try - { - return ::toml::find(v, detail::key_cast(k)); - } - catch(...) - { - return opt; - } -} -template -cxx::enable_if_t::value, basic_value> const& -find_or(const basic_value& v, const K& k, const basic_value& opt) noexcept -{ - try - { - return ::toml::find(v, detail::key_cast(k)); - } - catch(...) - { - return opt; - } -} -template -cxx::enable_if_t::value, basic_value> -find_or(basic_value&& v, const K& k, basic_value&& opt) noexcept -{ - try - { - return ::toml::find(v, detail::key_cast(k)); - } - catch(...) - { - return opt; - } -} - -// --------------------------------------------------------------------------- -// toml types (return type can be a reference) - -template -cxx::enable_if_t>::value, - cxx::remove_cvref_t const&> -find_or(const basic_value& v, const K& k, const T& opt) -{ - try - { - return ::toml::get(v.at(detail::key_cast(k))); - } - catch(...) - { - return opt; - } -} - -template -cxx::enable_if_t>, - detail::is_exact_toml_type> - >::value, cxx::remove_cvref_t&> -find_or(basic_value& v, const K& k, T& opt) -{ - try - { - return ::toml::get(v.at(detail::key_cast(k))); - } - catch(...) - { - return opt; - } -} - -template -cxx::enable_if_t>::value, - cxx::remove_cvref_t> -find_or(basic_value&& v, const K& k, T opt) -{ - try - { - return ::toml::get(std::move(v.at(detail::key_cast(k)))); - } - catch(...) - { - return T(std::move(opt)); - } -} - -// --------------------------------------------------------------------------- -// string literal (deduced as std::string) - -// XXX to avoid confusion when T is explicitly specified in find_or(), -// we restrict the string type as std::string. -template -cxx::enable_if_t::value, std::string> -find_or(const basic_value& v, const K& k, const char* opt) -{ - try - { - return ::toml::get(v.at(detail::key_cast(k))); - } - catch(...) - { - return std::string(opt); - } -} - -// --------------------------------------------------------------------------- -// other types (requires type conversion and return type cannot be a reference) - -template -cxx::enable_if_t>>, - detail::is_not_toml_type, basic_value>, - cxx::negation, - const typename basic_value::string_type::value_type*>> - >::value, cxx::remove_cvref_t> -find_or(const basic_value& v, const K& ky, T opt) -{ - try - { - return ::toml::get>(v.at(detail::key_cast(ky))); - } - catch(...) - { - return cxx::remove_cvref_t(std::move(opt)); - } -} - -// ---------------------------------------------------------------------------- -// recursive - -namespace detail -{ - -template -auto last_one(Ts&&... args) - -> decltype(std::get(std::forward_as_tuple(std::forward(args)...))) -{ - return std::get(std::forward_as_tuple(std::forward(args)...)); -} - -} // detail - -template -auto find_or(Value&& v, const K1& k1, const K2& k2, K3&& k3, Ks&& ... keys) noexcept - -> cxx::enable_if_t< - detail::is_basic_value>::value, - decltype(find_or(v, k2, std::forward(k3), std::forward(keys)...)) - > -{ - try - { - return find_or(v.at(k1), k2, std::forward(k3), std::forward(keys)...); - } - catch(...) - { - return detail::last_one(k3, keys...); - } -} - -template -T find_or(const basic_value& v, const K1& k1, const K2& k2, const K3& k3, const Ks& ... keys) noexcept -{ - try - { - return find_or(v.at(k1), k2, k3, keys...); - } - catch(...) - { - return static_cast(detail::last_one(k3, keys...)); - } -} - -} // toml -#endif // TOML11_FIND_HPP -#ifndef TOML11_CONVERSION_HPP -#define TOML11_CONVERSION_HPP - - -#if defined(TOML11_HAS_OPTIONAL) - -#include - -namespace toml -{ -namespace detail -{ - -template -inline constexpr bool is_optional_v = false; - -template -inline constexpr bool is_optional_v> = true; - -template -void find_member_variable_from_value(T& obj, const basic_value& v, const char* var_name) -{ - if constexpr(is_optional_v) - { - if(v.contains(var_name)) - { - obj = toml::find(v, var_name); - } - else - { - obj = std::nullopt; - } - } - else - { - obj = toml::find(v, var_name); - } -} - -template -void assign_member_variable_to_value(const T& obj, basic_value& v, const char* var_name) -{ - if constexpr(is_optional_v) - { - if(obj.has_value()) - { - v[var_name] = obj.value(); - } - } - else - { - v[var_name] = obj; - } -} - -} // detail -} // toml - -#else - -namespace toml -{ -namespace detail -{ - -template -void find_member_variable_from_value(T& obj, const basic_value& v, const char* var_name) -{ - obj = toml::find(v, var_name); -} - -template -void assign_member_variable_to_value(const T& obj, basic_value& v, const char* var_name) -{ - v[var_name] = obj; -} - -} // detail -} // toml - -#endif // optional - -// use it in the following way. -// ```cpp -// namespace foo -// { -// struct Foo -// { -// std::string s; -// double d; -// int i; -// }; -// } // foo -// -// TOML11_DEFINE_CONVERSION_NON_INTRUSIVE(foo::Foo, s, d, i) -// ``` -// -// And then you can use `toml::get(v)` and `toml::find(file, "foo");` -// - -#define TOML11_STRINGIZE_AUX(x) #x -#define TOML11_STRINGIZE(x) TOML11_STRINGIZE_AUX(x) - -#define TOML11_CONCATENATE_AUX(x, y) x##y -#define TOML11_CONCATENATE(x, y) TOML11_CONCATENATE_AUX(x, y) - -// ============================================================================ -// TOML11_DEFINE_CONVERSION_NON_INTRUSIVE - -#ifndef TOML11_WITHOUT_DEFINE_NON_INTRUSIVE - -// ---------------------------------------------------------------------------- -// TOML11_ARGS_SIZE - -#define TOML11_INDEX_RSEQ() \ - 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, \ - 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 -#define TOML11_ARGS_SIZE_IMPL(\ - ARG1, ARG2, ARG3, ARG4, ARG5, ARG6, ARG7, ARG8, ARG9, ARG10, \ - ARG11, ARG12, ARG13, ARG14, ARG15, ARG16, ARG17, ARG18, ARG19, ARG20, \ - ARG21, ARG22, ARG23, ARG24, ARG25, ARG26, ARG27, ARG28, ARG29, ARG30, \ - ARG31, ARG32, N, ...) N -#define TOML11_ARGS_SIZE_AUX(...) TOML11_ARGS_SIZE_IMPL(__VA_ARGS__) -#define TOML11_ARGS_SIZE(...) TOML11_ARGS_SIZE_AUX(__VA_ARGS__, TOML11_INDEX_RSEQ()) - -// ---------------------------------------------------------------------------- -// TOML11_FOR_EACH_VA_ARGS - -#define TOML11_FOR_EACH_VA_ARGS_AUX_1( FUNCTOR, ARG1 ) FUNCTOR(ARG1) -#define TOML11_FOR_EACH_VA_ARGS_AUX_2( FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_1( FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_3( FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_2( FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_4( FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_3( FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_5( FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_4( FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_6( FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_5( FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_7( FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_6( FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_8( FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_7( FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_9( FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_8( FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_10(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_9( FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_11(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_10(FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_12(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_11(FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_13(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_12(FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_14(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_13(FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_15(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_14(FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_16(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_15(FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_17(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_16(FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_18(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_17(FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_19(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_18(FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_20(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_19(FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_21(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_20(FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_22(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_21(FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_23(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_22(FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_24(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_23(FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_25(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_24(FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_26(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_25(FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_27(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_26(FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_28(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_27(FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_29(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_28(FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_30(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_29(FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_31(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_30(FUNCTOR, __VA_ARGS__) -#define TOML11_FOR_EACH_VA_ARGS_AUX_32(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) TOML11_FOR_EACH_VA_ARGS_AUX_31(FUNCTOR, __VA_ARGS__) - -#define TOML11_FOR_EACH_VA_ARGS(FUNCTOR, ...)\ - TOML11_CONCATENATE(TOML11_FOR_EACH_VA_ARGS_AUX_, TOML11_ARGS_SIZE(__VA_ARGS__))(FUNCTOR, __VA_ARGS__) - - -#define TOML11_FIND_MEMBER_VARIABLE_FROM_VALUE(VAR_NAME)\ - toml::detail::find_member_variable_from_value(obj.VAR_NAME, v, TOML11_STRINGIZE(VAR_NAME)); - -#define TOML11_ASSIGN_MEMBER_VARIABLE_TO_VALUE(VAR_NAME)\ - toml::detail::assign_member_variable_to_value(obj.VAR_NAME, v, TOML11_STRINGIZE(VAR_NAME)); - -#define TOML11_DEFINE_CONVERSION_NON_INTRUSIVE(NAME, ...)\ - namespace toml { \ - template<> \ - struct from \ - { \ - template \ - static NAME from_toml(const basic_value& v) \ - { \ - NAME obj; \ - TOML11_FOR_EACH_VA_ARGS(TOML11_FIND_MEMBER_VARIABLE_FROM_VALUE, __VA_ARGS__) \ - return obj; \ - } \ - }; \ - template<> \ - struct into \ - { \ - template \ - static basic_value into_toml(const NAME& obj) \ - { \ - ::toml::basic_value v = typename ::toml::basic_value::table_type{}; \ - TOML11_FOR_EACH_VA_ARGS(TOML11_ASSIGN_MEMBER_VARIABLE_TO_VALUE, __VA_ARGS__) \ - return v; \ - } \ - }; \ - } /* toml */ - -#endif// TOML11_WITHOUT_DEFINE_NON_INTRUSIVE - -#endif // TOML11_CONVERSION_HPP -#ifndef TOML11_CONTEXT_HPP -#define TOML11_CONTEXT_HPP - - -#include - -namespace toml -{ -namespace detail -{ - -template -class context -{ - public: - - explicit context(const spec& toml_spec) - : toml_spec_(toml_spec), errors_{} - {} - - bool has_error() const noexcept {return !errors_.empty();} - - std::vector const& errors() const noexcept {return errors_;} - - semantic_version& toml_version() noexcept {return toml_spec_.version;} - semantic_version const& toml_version() const noexcept {return toml_spec_.version;} - - spec& toml_spec() noexcept {return toml_spec_;} - spec const& toml_spec() const noexcept {return toml_spec_;} - - void report_error(error_info err) - { - this->errors_.push_back(std::move(err)); - } - - error_info pop_last_error() - { - assert( ! errors_.empty()); - auto e = std::move(errors_.back()); - errors_.pop_back(); - return e; - } - - private: - - spec toml_spec_; - std::vector errors_; -}; - -} // detail -} // toml - -#if defined(TOML11_COMPILE_SOURCES) -namespace toml -{ -struct type_config; -struct ordered_type_config; -namespace detail -{ -extern template class context<::toml::type_config>; -extern template class context<::toml::ordered_type_config>; -} // detail -} // toml -#endif // TOML11_COMPILE_SOURCES - -#endif // TOML11_CONTEXT_HPP -#ifndef TOML11_SCANNER_HPP -#define TOML11_SCANNER_HPP - -#ifndef TOML11_SCANNER_FWD_HPP -#define TOML11_SCANNER_FWD_HPP - - -#include -#include -#include -#include - -#include -#include -#include - -namespace toml -{ -namespace detail -{ - -class scanner_base -{ - public: - virtual ~scanner_base() = default; - virtual region scan(location& loc) const = 0; - virtual scanner_base* clone() const = 0; - - // returns expected character or set of characters or literal. - // to show the error location, it changes loc (in `sequence`, especially). - virtual std::string expected_chars(location& loc) const = 0; - virtual std::string name() const = 0; -}; - -// make `scanner*` copyable -struct scanner_storage -{ - template>::value, - std::nullptr_t> = nullptr> - explicit scanner_storage(Scanner&& s) - : scanner_(cxx::make_unique>(std::forward(s))) - {} - ~scanner_storage() = default; - - scanner_storage(const scanner_storage& other); - scanner_storage& operator=(const scanner_storage& other); - scanner_storage(scanner_storage&&) = default; - scanner_storage& operator=(scanner_storage&&) = default; - - bool is_ok() const noexcept {return static_cast(scanner_);} - - region scan(location& loc) const; - - std::string expected_chars(location& loc) const; - - scanner_base& get() const noexcept; - - std::string name() const; - - private: - - std::unique_ptr scanner_; -}; - -// ---------------------------------------------------------------------------- - -class character final : public scanner_base -{ - public: - - using char_type = location::char_type; - - public: - - explicit character(const char_type c) noexcept - : value_(c) - {} - ~character() override = default; - - region scan(location& loc) const override; - - std::string expected_chars(location&) const override; - - scanner_base* clone() const override; - - std::string name() const override; - - private: - char_type value_; -}; - -// ---------------------------------------------------------------------------- - -class character_either final : public scanner_base -{ - public: - - using char_type = location::char_type; - - public: - - explicit character_either(std::initializer_list cs) noexcept - : chars_(std::move(cs)) - { - assert(! this->chars_.empty()); - } - - template - explicit character_either(const char (&cs)[N]) noexcept - : chars_(N-1, '\0') - { - static_assert(N >= 1, ""); - for(std::size_t i=0; i+1 chars_; -}; - -// ---------------------------------------------------------------------------- - -class character_in_range final : public scanner_base -{ - public: - - using char_type = location::char_type; - - public: - - explicit character_in_range(const char_type from, const char_type to) noexcept - : from_(from), to_(to) - {} - ~character_in_range() override = default; - - region scan(location& loc) const override; - - std::string expected_chars(location&) const override; - - scanner_base* clone() const override; - - std::string name() const override; - - private: - char_type from_; - char_type to_; -}; - -// ---------------------------------------------------------------------------- - -class literal final : public scanner_base -{ - public: - - using char_type = location::char_type; - - public: - - template - explicit literal(const char (&cs)[N]) noexcept - : value_(cs), size_(N-1) // remove null character at the end - {} - ~literal() override = default; - - region scan(location& loc) const override; - - std::string expected_chars(location&) const override; - - scanner_base* clone() const override; - - std::string name() const override; - - private: - const char* value_; - std::size_t size_; -}; - -// ---------------------------------------------------------------------------- - -class sequence final: public scanner_base -{ - public: - using char_type = location::char_type; - - public: - - template - explicit sequence(Ts&& ... args) - { - push_back_all(std::forward(args)...); - } - sequence(const sequence&) = default; - sequence(sequence&&) = default; - sequence& operator=(const sequence&) = default; - sequence& operator=(sequence&&) = default; - ~sequence() override = default; - - region scan(location& loc) const override; - - std::string expected_chars(location& loc) const override; - - scanner_base* clone() const override; - - template - void push_back(Scanner&& other_scanner) - { - this->others_.emplace_back(std::forward(other_scanner)); - } - - std::string name() const override; - - private: - - void push_back_all() - { - return; - } - template - void push_back_all(T&& head, Ts&& ... args) - { - others_.emplace_back(std::forward(head)); - push_back_all(std::forward(args)...); - return; - } - - private: - std::vector others_; -}; - -// ---------------------------------------------------------------------------- - -class either final: public scanner_base -{ - public: - using char_type = location::char_type; - - public: - - template - explicit either(Ts&& ... args) - { - push_back_all(std::forward(args)...); - } - either(const either&) = default; - either(either&&) = default; - either& operator=(const either&) = default; - either& operator=(either&&) = default; - ~either() override = default; - - region scan(location& loc) const override; - - std::string expected_chars(location& loc) const override; - - scanner_base* clone() const override; - - template - void push_back(Scanner&& other_scanner) - { - this->others_.emplace_back(std::forward(other_scanner)); - } - - std::string name() const override; - - private: - - void push_back_all() - { - return; - } - template - void push_back_all(T&& head, Ts&& ... args) - { - others_.emplace_back(std::forward(head)); - push_back_all(std::forward(args)...); - return; - } - - private: - std::vector others_; -}; - -// ---------------------------------------------------------------------------- - -class repeat_exact final: public scanner_base -{ - public: - using char_type = location::char_type; - - public: - - template - repeat_exact(const std::size_t length, Scanner&& other) - : length_(length), other_(std::forward(other)) - {} - repeat_exact(const repeat_exact&) = default; - repeat_exact(repeat_exact&&) = default; - repeat_exact& operator=(const repeat_exact&) = default; - repeat_exact& operator=(repeat_exact&&) = default; - ~repeat_exact() override = default; - - region scan(location& loc) const override; - - std::string expected_chars(location& loc) const override; - - scanner_base* clone() const override; - - std::string name() const override; - - private: - std::size_t length_; - scanner_storage other_; -}; - -// ---------------------------------------------------------------------------- - -class repeat_at_least final: public scanner_base -{ - public: - using char_type = location::char_type; - - public: - - template - repeat_at_least(const std::size_t length, Scanner&& s) - : length_(length), other_(std::forward(s)) - {} - repeat_at_least(const repeat_at_least&) = default; - repeat_at_least(repeat_at_least&&) = default; - repeat_at_least& operator=(const repeat_at_least&) = default; - repeat_at_least& operator=(repeat_at_least&&) = default; - ~repeat_at_least() override = default; - - region scan(location& loc) const override; - - std::string expected_chars(location& loc) const override; - - scanner_base* clone() const override; - - std::string name() const override; - - private: - std::size_t length_; - scanner_storage other_; -}; - -// ---------------------------------------------------------------------------- - -class maybe final: public scanner_base -{ - public: - using char_type = location::char_type; - - public: - - template - explicit maybe(Scanner&& s) - : other_(std::forward(s)) - {} - maybe(const maybe&) = default; - maybe(maybe&&) = default; - maybe& operator=(const maybe&) = default; - maybe& operator=(maybe&&) = default; - ~maybe() override = default; - - region scan(location& loc) const override; - - std::string expected_chars(location&) const override; - - scanner_base* clone() const override; - - std::string name() const override; - - private: - scanner_storage other_; -}; - -} // detail -} // toml -#endif // TOML11_SCANNER_FWD_HPP - -#if ! defined(TOML11_COMPILE_SOURCES) -#ifndef TOML11_SCANNER_IMPL_HPP -#define TOML11_SCANNER_IMPL_HPP - - -namespace toml -{ -namespace detail -{ - -TOML11_INLINE scanner_storage::scanner_storage(const scanner_storage& other) - : scanner_(nullptr) -{ - if(other.is_ok()) - { - scanner_.reset(other.get().clone()); - } -} -TOML11_INLINE scanner_storage& scanner_storage::operator=(const scanner_storage& other) -{ - if(this == std::addressof(other)) {return *this;} - if(other.is_ok()) - { - scanner_.reset(other.get().clone()); - } - return *this; -} - -TOML11_INLINE region scanner_storage::scan(location& loc) const -{ - assert(this->is_ok()); - return this->scanner_->scan(loc); -} - -TOML11_INLINE std::string scanner_storage::expected_chars(location& loc) const -{ - assert(this->is_ok()); - return this->scanner_->expected_chars(loc); -} - -TOML11_INLINE scanner_base& scanner_storage::get() const noexcept -{ - assert(this->is_ok()); - return *scanner_; -} - -TOML11_INLINE std::string scanner_storage::name() const -{ - assert(this->is_ok()); - return this->scanner_->name(); -} - -// ---------------------------------------------------------------------------- - -TOML11_INLINE region character::scan(location& loc) const -{ - if(loc.eof()) {return region{};} - - if(loc.current() == this->value_) - { - const auto first = loc; - loc.advance(1); - return region(first, loc); - } - return region{}; -} - -TOML11_INLINE std::string character::expected_chars(location&) const -{ - return show_char(value_); -} - -TOML11_INLINE scanner_base* character::clone() const -{ - return new character(*this); -} - -TOML11_INLINE std::string character::name() const -{ - return "character{" + show_char(value_) + "}"; -} - -// ---------------------------------------------------------------------------- - -TOML11_INLINE region character_either::scan(location& loc) const -{ - if(loc.eof()) {return region{};} - - for(const auto c : this->chars_) - { - if(loc.current() == c) - { - const auto first = loc; - loc.advance(1); - return region(first, loc); - } - } - return region{}; -} - -TOML11_INLINE std::string character_either::expected_chars(location&) const -{ - assert( ! chars_.empty()); - - std::string expected; - if(chars_.size() == 1) - { - expected += show_char(chars_.at(0)); - } - else if(chars_.size() == 2) - { - expected += show_char(chars_.at(0)) + " or " + show_char(chars_.at(1)); - } - else - { - for(std::size_t i=0; ichars_) - { - n += show_char(c); - n += ", "; - } - if( ! this->chars_.empty()) - { - n.pop_back(); - n.pop_back(); - } - n += "}"; - return n; -} - -// ---------------------------------------------------------------------------- -// character_in_range - -TOML11_INLINE region character_in_range::scan(location& loc) const -{ - if(loc.eof()) {return region{};} - - const auto curr = loc.current(); - if(this->from_ <= curr && curr <= this->to_) - { - const auto first = loc; - loc.advance(1); - return region(first, loc); - } - return region{}; -} - -TOML11_INLINE std::string character_in_range::expected_chars(location&) const -{ - std::string expected("from `"); - expected += show_char(from_); - expected += "` to `"; - expected += show_char(to_); - expected += "`"; - return expected; -} - -TOML11_INLINE scanner_base* character_in_range::clone() const -{ - return new character_in_range(*this); -} - -TOML11_INLINE std::string character_in_range::name() const -{ - return "character_in_range{" + show_char(from_) + "," + show_char(to_) + "}"; -} - -// ---------------------------------------------------------------------------- -// literal - -TOML11_INLINE region literal::scan(location& loc) const -{ - const auto first = loc; - for(std::size_t i=0; iothers_.empty()) - { - n.pop_back(); - n.pop_back(); - } - n += "}"; - return n; -} - -// ---------------------------------------------------------------------------- -// either - -TOML11_INLINE region either::scan(location& loc) const -{ - for(const auto& other : others_) - { - const auto reg = other.scan(loc); - if(reg.is_ok()) - { - return reg; - } - } - return region{}; -} - -TOML11_INLINE std::string either::expected_chars(location& loc) const -{ - assert( ! others_.empty()); - - std::string expected = others_.at(0).expected_chars(loc); - if(others_.size() == 2) - { - expected += " or "; - expected += others_.at(1).expected_chars(loc); - } - else - { - for(std::size_t i=1; iothers_.empty()) - { - n.pop_back(); - n.pop_back(); - } - n += "}"; - return n; -} - -// ---------------------------------------------------------------------------- -// repeat_exact - -TOML11_INLINE region repeat_exact::scan(location& loc) const -{ - const auto first = loc; - for(std::size_t i=0; i(b1); - } - else if((b1 >> 5) == 6) // 0b110 == 6 - { - const auto b2 = loc.current(); loc.advance(1); - - const std::uint32_t c1 = b1 & ((1 << 5) - 1); - const std::uint32_t c2 = b2 & ((1 << 6) - 1); - const std::uint32_t codep = (c1 << 6) + c2; - - if(codep < 0x80) - { - return 0xFFFFFFFF; - } - return codep; - } - else if((b1 >> 4) == 14) // 0b1110 == 14 - { - const auto b2 = loc.current(); loc.advance(1); if(loc.eof()) {return 0xFFFFFFFF;} - const auto b3 = loc.current(); loc.advance(1); - - const std::uint32_t c1 = b1 & ((1 << 4) - 1); - const std::uint32_t c2 = b2 & ((1 << 6) - 1); - const std::uint32_t c3 = b3 & ((1 << 6) - 1); - - const std::uint32_t codep = (c1 << 12) + (c2 << 6) + c3; - if(codep < 0x800) - { - return 0xFFFFFFFF; - } - return codep; - } - else if((b1 >> 3) == 30) // 0b11110 == 30 - { - const auto b2 = loc.current(); loc.advance(1); if(loc.eof()) {return 0xFFFFFFFF;} - const auto b3 = loc.current(); loc.advance(1); if(loc.eof()) {return 0xFFFFFFFF;} - const auto b4 = loc.current(); loc.advance(1); - - const std::uint32_t c1 = b1 & ((1 << 3) - 1); - const std::uint32_t c2 = b2 & ((1 << 6) - 1); - const std::uint32_t c3 = b3 & ((1 << 6) - 1); - const std::uint32_t c4 = b4 & ((1 << 6) - 1); - const std::uint32_t codep = (c1 << 18) + (c2 << 12) + (c3 << 6) + c4; - - if(codep < 0x10000) - { - return 0xFFFFFFFF; - } - return codep; - } - else // not a Unicode codepoint in UTF-8 - { - return 0xFFFFFFFF; - } -} - -TOML11_INLINE region non_ascii_key_char::scan(location& loc) const -{ - if(loc.eof()) {return region{};} - - const auto first = loc; - - const auto cp = read_utf8(loc); - - if(cp == 0xFFFFFFFF) - { - return region{}; - } - - // ALPHA / DIGIT / %x2D / %x5F ; a-z A-Z 0-9 - _ - // / %xB2 / %xB3 / %xB9 / %xBC-BE ; superscript digits, fractions - // / %xC0-D6 / %xD8-F6 / %xF8-37D ; non-symbol chars in Latin block - // / %x37F-1FFF ; exclude GREEK QUESTION MARK, which is basically a semi-colon - // / %x200C-200D / %x203F-2040 ; from General Punctuation Block, include the two tie symbols and ZWNJ, ZWJ - // / %x2070-218F / %x2460-24FF ; include super-/subscripts, letterlike/numberlike forms, enclosed alphanumerics - // / %x2C00-2FEF / %x3001-D7FF ; skip arrows, math, box drawing etc, skip 2FF0-3000 ideographic up/down markers and spaces - // / %xF900-FDCF / %xFDF0-FFFD ; skip D800-DFFF surrogate block, E000-F8FF Private Use area, FDD0-FDEF intended for process-internal use (unicode) - // / %x10000-EFFFF ; all chars outside BMP range, excluding Private Use planes (F0000-10FFFF) - - if(cp == 0xB2 || cp == 0xB3 || cp == 0xB9 || (0xBC <= cp && cp <= 0xBE) || - (0xC0 <= cp && cp <= 0xD6 ) || (0xD8 <= cp && cp <= 0xF6) || (0xF8 <= cp && cp <= 0x37D) || - (0x37F <= cp && cp <= 0x1FFF) || - (0x200C <= cp && cp <= 0x200D) || (0x203F <= cp && cp <= 0x2040) || - (0x2070 <= cp && cp <= 0x218F) || (0x2460 <= cp && cp <= 0x24FF) || - (0x2C00 <= cp && cp <= 0x2FEF) || (0x3001 <= cp && cp <= 0xD7FF) || - (0xF900 <= cp && cp <= 0xFDCF) || (0xFDF0 <= cp && cp <= 0xFFFD) || - (0x10000 <= cp && cp <= 0xEFFFF) ) - { - return region(first, loc); - } - loc = first; - return region{}; -} - -TOML11_INLINE repeat_at_least unquoted_key(const spec& s) -{ - auto keychar = either( - alpha(s), digit(s), character{0x2D}, character{0x5F} - ); - - if(s.v1_1_0_allow_non_english_in_bare_keys) - { - keychar.push_back(non_ascii_key_char(s)); - } - - return repeat_at_least(1, std::move(keychar)); -} - -TOML11_INLINE either quoted_key(const spec& s) -{ - return either(basic_string(s), literal_string(s)); -} - -TOML11_INLINE either simple_key(const spec& s) -{ - return either(unquoted_key(s), quoted_key(s)); -} - -TOML11_INLINE sequence dot_sep(const spec& s) -{ - return sequence(ws(s), character('.'), ws(s)); -} - -TOML11_INLINE sequence dotted_key(const spec& s) -{ - return sequence( - simple_key(s), - repeat_at_least(1, sequence(dot_sep(s), simple_key(s))) - ); -} - -TOML11_INLINE key::key(const spec& s) noexcept - : scanner_(dotted_key(s), simple_key(s)) -{} - -TOML11_INLINE sequence keyval_sep(const spec& s) -{ - return sequence(ws(s), character('='), ws(s)); -} - -// =========================================================================== -// Table key - -TOML11_INLINE sequence std_table(const spec& s) -{ - return sequence(character('['), ws(s), key(s), ws(s), character(']')); -} - -TOML11_INLINE sequence array_table(const spec& s) -{ - return sequence(literal("[["), ws(s), key(s), ws(s), literal("]]")); -} - -// =========================================================================== -// extension: null - -TOML11_INLINE literal null_value(const spec&) -{ - return literal("null"); -} - -} // namespace syntax -} // namespace detail -} // namespace toml -#endif // TOML11_SYNTAX_IMPL_HPP -#endif - -#endif// TOML11_SYNTAX_HPP -#ifndef TOML11_SKIP_HPP -#define TOML11_SKIP_HPP - - -#include - -namespace toml -{ -namespace detail -{ - -template -bool skip_whitespace(location& loc, const context& ctx) -{ - return syntax::ws(ctx.toml_spec()).scan(loc).is_ok(); -} - -template -bool skip_empty_lines(location& loc, const context& ctx) -{ - return repeat_at_least(1, sequence( - syntax::ws(ctx.toml_spec()), - syntax::newline(ctx.toml_spec()) - )).scan(loc).is_ok(); -} - -// For error recovery. -// -// In case if a comment line contains an invalid character, we need to skip it -// to advance parsing. -template -void skip_comment_block(location& loc, const context& ctx) -{ - while( ! loc.eof()) - { - skip_whitespace(loc, ctx); - if(loc.current() == '#') - { - while( ! loc.eof()) - { - // both CRLF and LF ends with LF. - if(loc.current() == '\n') - { - loc.advance(); - break; - } - } - } - else if(syntax::newline(ctx.toml_spec()).scan(loc).is_ok()) - { - ; // an empty line. skip this also - } - else - { - // the next token is neither a comment nor empty line. - return ; - } - } - return ; -} - -template -void skip_empty_or_comment_lines(location& loc, const context& ctx) -{ - const auto& spec = ctx.toml_spec(); - repeat_at_least(0, sequence( - syntax::ws(spec), - maybe(syntax::comment(spec)), - syntax::newline(spec)) - ).scan(loc); - return ; -} - -// For error recovery. -// -// Sometimes we need to skip a value and find a delimiter, like `,`, `]`, or `}`. -// To find delimiter, we need to skip delimiters in a string. -// Since we are skipping invalid value while error recovery, we don't need -// to check the syntax. Here we just skip string-like region until closing quote -// is found. -template -void skip_string_like(location& loc, const context&) -{ - // if """ is found, skip until the closing """ is found. - if(literal("\"\"\"").scan(loc).is_ok()) - { - while( ! loc.eof()) - { - if(literal("\"\"\"").scan(loc).is_ok()) - { - return; - } - loc.advance(); - } - } - else if(literal("'''").scan(loc).is_ok()) - { - while( ! loc.eof()) - { - if(literal("'''").scan(loc).is_ok()) - { - return; - } - loc.advance(); - } - } - // if " is found, skip until the closing " or newline is found. - else if(loc.current() == '"') - { - while( ! loc.eof()) - { - loc.advance(); - if(loc.current() == '"' || loc.current() == '\n') - { - loc.advance(); - return; - } - } - } - else if(loc.current() == '\'') - { - while( ! loc.eof()) - { - loc.advance(); - if(loc.current() == '\'' || loc.current() == '\n') - { - loc.advance(); - return ; - } - } - } - return; -} - -template -void skip_value(location& loc, const context& ctx); -template -void skip_array_like(location& loc, const context& ctx); -template -void skip_inline_table_like(location& loc, const context& ctx); -template -void skip_key_value_pair(location& loc, const context& ctx); - -template -result -guess_value_type(const location& loc, const context& ctx); - -template -void skip_array_like(location& loc, const context& ctx) -{ - const auto& spec = ctx.toml_spec(); - assert(loc.current() == '['); - loc.advance(); - - while( ! loc.eof()) - { - if(loc.current() == '\"' || loc.current() == '\'') - { - skip_string_like(loc, ctx); - } - else if(loc.current() == '#') - { - skip_comment_block(loc, ctx); - } - else if(loc.current() == '{') - { - skip_inline_table_like(loc, ctx); - } - else if(loc.current() == '[') - { - const auto checkpoint = loc; - if(syntax::std_table(spec).scan(loc).is_ok() || - syntax::array_table(spec).scan(loc).is_ok()) - { - loc = checkpoint; - break; - } - // if it is not a table-definition, then it is an array. - skip_array_like(loc, ctx); - } - else if(loc.current() == '=') - { - // key-value pair cannot be inside the array. - // guessing the error is "missing closing bracket `]`". - // find the previous key just before `=`. - while(loc.get_location() != 0) - { - loc.retrace(); - if(loc.current() == '\n') - { - loc.advance(); - break; - } - } - break; - } - else if(loc.current() == ']') - { - break; // found closing bracket - } - else - { - loc.advance(); - } - } - return ; -} - -template -void skip_inline_table_like(location& loc, const context& ctx) -{ - assert(loc.current() == '{'); - loc.advance(); - - const auto& spec = ctx.toml_spec(); - - while( ! loc.eof()) - { - if(loc.current() == '\n' && ! spec.v1_1_0_allow_newlines_in_inline_tables) - { - break; // missing closing `}`. - } - else if(loc.current() == '\"' || loc.current() == '\'') - { - skip_string_like(loc, ctx); - } - else if(loc.current() == '#') - { - skip_comment_block(loc, ctx); - if( ! spec.v1_1_0_allow_newlines_in_inline_tables) - { - // comment must end with newline. - break; // missing closing `}`. - } - } - else if(loc.current() == '[') - { - const auto checkpoint = loc; - if(syntax::std_table(spec).scan(loc).is_ok() || - syntax::array_table(spec).scan(loc).is_ok()) - { - loc = checkpoint; - break; // missing closing `}`. - } - // if it is not a table-definition, then it is an array. - skip_array_like(loc, ctx); - } - else if(loc.current() == '{') - { - skip_inline_table_like(loc, ctx); - } - else if(loc.current() == '}') - { - // closing brace found. guessing the error is inside the table. - break; - } - else - { - // skip otherwise. - loc.advance(); - } - } - return ; -} - -template -void skip_value(location& loc, const context& ctx) -{ - value_t ty = guess_value_type(loc, ctx).unwrap_or(value_t::empty); - if(ty == value_t::string) - { - skip_string_like(loc, ctx); - } - else if(ty == value_t::array) - { - skip_array_like(loc, ctx); - } - else if(ty == value_t::table) - { - // In case of multiline tables, it may skip key-value pair but not the - // whole table. - skip_inline_table_like(loc, ctx); - } - else // others are an "in-line" values. skip until the next line - { - while( ! loc.eof()) - { - if(loc.current() == '\n') - { - break; - } - else if(loc.current() == ',' || loc.current() == ']' || loc.current() == '}') - { - break; - } - loc.advance(); - } - } - return; -} - -template -void skip_key_value_pair(location& loc, const context& ctx) -{ - while( ! loc.eof()) - { - if(loc.current() == '=') - { - skip_whitespace(loc, ctx); - skip_value(loc, ctx); - return; - } - else if(loc.current() == '\n') - { - // newline is found before finding `=`. assuming "missing `=`". - return; - } - loc.advance(); - } - return ; -} - -template -void skip_until_next_table(location& loc, const context& ctx) -{ - const auto& spec = ctx.toml_spec(); - while( ! loc.eof()) - { - if(loc.current() == '\n') - { - loc.advance(); - const auto line_begin = loc; - - skip_whitespace(loc, ctx); - if(syntax::std_table(spec).scan(loc).is_ok()) - { - loc = line_begin; - return ; - } - if(syntax::array_table(spec).scan(loc).is_ok()) - { - loc = line_begin; - return ; - } - } - loc.advance(); - } -} - -} // namespace detail -} // namespace toml - -#if defined(TOML11_COMPILE_SOURCES) -namespace toml -{ -struct type_config; -struct ordered_type_config; - -namespace detail -{ -extern template bool skip_whitespace (location& loc, const context&); -extern template bool skip_empty_lines (location& loc, const context&); -extern template void skip_comment_block (location& loc, const context&); -extern template void skip_empty_or_comment_lines(location& loc, const context&); -extern template void skip_string_like (location& loc, const context&); -extern template void skip_array_like (location& loc, const context&); -extern template void skip_inline_table_like (location& loc, const context&); -extern template void skip_value (location& loc, const context&); -extern template void skip_key_value_pair (location& loc, const context&); -extern template void skip_until_next_table (location& loc, const context&); - -extern template bool skip_whitespace (location& loc, const context&); -extern template bool skip_empty_lines (location& loc, const context&); -extern template void skip_comment_block (location& loc, const context&); -extern template void skip_empty_or_comment_lines(location& loc, const context&); -extern template void skip_string_like (location& loc, const context&); -extern template void skip_array_like (location& loc, const context&); -extern template void skip_inline_table_like (location& loc, const context&); -extern template void skip_value (location& loc, const context&); -extern template void skip_key_value_pair (location& loc, const context&); -extern template void skip_until_next_table (location& loc, const context&); - -} // detail -} // toml -#endif // TOML11_COMPILE_SOURCES - -#endif // TOML11_SKIP_HPP -#ifndef TOML11_PARSER_HPP -#define TOML11_PARSER_HPP - - -#include -#include - -#include -#include - -#if defined(TOML11_HAS_FILESYSTEM) && TOML11_HAS_FILESYSTEM -#include -#endif - -namespace toml -{ - -struct syntax_error final : public ::toml::exception -{ - public: - syntax_error(std::string what_arg, std::vector err) - : what_(std::move(what_arg)), err_(std::move(err)) - {} - ~syntax_error() noexcept override = default; - - const char* what() const noexcept override {return what_.c_str();} - - std::vector const& errors() const noexcept - { - return err_; - } - - private: - std::string what_; - std::vector err_; -}; - -struct file_io_error final : public ::toml::exception -{ - public: - - file_io_error(const std::string& msg, const std::string& fname) - : errno_(cxx::make_nullopt()), - what_(msg + " \"" + fname + "\"") - {} - file_io_error(int errnum, const std::string& msg, const std::string& fname) - : errno_(errnum), - what_(msg + " \"" + fname + "\": errno=" + std::to_string(errnum)) - {} - ~file_io_error() noexcept override = default; - - const char* what() const noexcept override {return what_.c_str();} - - bool has_errno() const noexcept {return errno_.has_value();} - int get_errno() const noexcept {return errno_.value_or(0);} - - private: - - cxx::optional errno_; - std::string what_; -}; - -namespace detail -{ - -/* ============================================================================ - * __ ___ _ __ _ __ ___ _ _ - * / _/ _ \ ' \| ' \/ _ \ ' \ - * \__\___/_|_|_|_|_|_\___/_||_| - */ - -template -error_info make_syntax_error(std::string title, - const S& scanner, location loc, std::string suffix = "") -{ - auto msg = std::string("expected ") + scanner.expected_chars(loc); - auto src = source_location(region(loc)); - return make_error_info( - std::move(title), std::move(src), std::move(msg), std::move(suffix)); -} - - -/* ============================================================================ - * _ - * __ ___ _ __ _ __ ___ _ _| |_ - * / _/ _ \ ' \| ' \/ -_) ' \ _| - * \__\___/_|_|_|_|_|_\___|_||_\__| - */ - -template -result, error_info> -parse_comment_line(location& loc, context& ctx) -{ - const auto& spec = ctx.toml_spec(); - const auto first = loc; - - skip_whitespace(loc, ctx); - - const auto com_reg = syntax::comment(spec).scan(loc); - if(com_reg.is_ok()) - { - // once comment started, newline must follow (or reach EOF). - if( ! loc.eof() && ! syntax::newline(spec).scan(loc).is_ok()) - { - while( ! loc.eof()) // skip until newline to continue parsing - { - loc.advance(); - if(loc.current() == '\n') { /*skip LF*/ loc.advance(); break; } - } - return err(make_error_info("toml::parse_comment_line: " - "newline (LF / CRLF) or EOF is expected", - source_location(region(loc)), "but got this", - "Hint: most of the control characters are not allowed in comments")); - } - return ok(cxx::optional(com_reg.as_string())); - } - else - { - loc = first; // rollback whitespace to parse indent - return ok(cxx::optional(cxx::make_nullopt())); - } -} - -/* ============================================================================ - * ___ _ - * | _ ) ___ ___| |___ __ _ _ _ - * | _ \/ _ \/ _ \ / -_) _` | ' \ - * |___/\___/\___/_\___\__,_|_||_| - */ - -template -result, error_info> -parse_boolean(location& loc, const context& ctx) -{ - const auto& spec = ctx.toml_spec(); - - // ---------------------------------------------------------------------- - // check syntax - auto reg = syntax::boolean(spec).scan(loc); - if( ! reg.is_ok()) - { - return err(make_syntax_error("toml::parse_boolean: " - "invalid boolean: boolean must be `true` or `false`, in lowercase. " - "string must be surrounded by `\"`", syntax::boolean(spec), loc)); - } - - // ---------------------------------------------------------------------- - // it matches. gen value - const auto str = reg.as_string(); - const auto val = [&str]() { - if(str == "true") - { - return true; - } - else - { - assert(str == "false"); - return false; - } - }(); - - // ---------------------------------------------------------------------- - // no format info for boolean - boolean_format_info fmt; - - return ok(basic_value(val, std::move(fmt), {}, std::move(reg))); -} - -/* ============================================================================ - * ___ _ - * |_ _|_ _| |_ ___ __ _ ___ _ _ - * | || ' \ _/ -_) _` / -_) '_| - * |___|_||_\__\___\__, \___|_| - * |___/ - */ - -template -result, error_info> -parse_bin_integer(location& loc, const context& ctx) -{ - const auto first = loc; - const auto& spec = ctx.toml_spec(); - auto reg = syntax::bin_int(spec).scan(loc); - if( ! reg.is_ok()) - { - return err(make_syntax_error("toml::parse_bin_integer: " - "invalid integer: bin_int must be like: 0b0101, 0b1111_0000", - syntax::bin_int(spec), loc)); - } - - auto str = reg.as_string(); - - integer_format_info fmt; - fmt.fmt = integer_format::bin; - fmt.width = str.size() - 2 - static_cast(std::count(str.begin(), str.end(), '_')); - - const auto first_underscore = std::find(str.rbegin(), str.rend(), '_'); - if(first_underscore != str.rend()) - { - fmt.spacer = static_cast(std::distance(str.rbegin(), first_underscore)); - } - - // skip prefix `0b` and zeros and underscores at the MSB - str.erase(str.begin(), std::find(std::next(str.begin(), 2), str.end(), '1')); - - // remove all `_` before calling TC::parse_int - str.erase(std::remove(str.begin(), str.end(), '_'), str.end()); - - // 0b0000_0000 becomes empty. - if(str.empty()) { str = "0"; } - - const auto val = TC::parse_int(str, source_location(region(loc)), 2); - if(val.is_ok()) - { - return ok(basic_value(val.as_ok(), std::move(fmt), {}, std::move(reg))); - } - else - { - loc = first; - return err(val.as_err()); - } -} - -// ---------------------------------------------------------------------------- - -template -result, error_info> -parse_oct_integer(location& loc, const context& ctx) -{ - const auto first = loc; - const auto& spec = ctx.toml_spec(); - auto reg = syntax::oct_int(spec).scan(loc); - if( ! reg.is_ok()) - { - return err(make_syntax_error("toml::parse_oct_integer: " - "invalid integer: oct_int must be like: 0o775, 0o04_44", - syntax::oct_int(spec), loc)); - } - - auto str = reg.as_string(); - - integer_format_info fmt; - fmt.fmt = integer_format::oct; - fmt.width = str.size() - 2 - static_cast(std::count(str.begin(), str.end(), '_')); - - const auto first_underscore = std::find(str.rbegin(), str.rend(), '_'); - if(first_underscore != str.rend()) - { - fmt.spacer = static_cast(std::distance(str.rbegin(), first_underscore)); - } - - // skip prefix `0o` and zeros and underscores at the MSB - str.erase(str.begin(), std::find_if( - std::next(str.begin(), 2), str.end(), [](const char c) { - return c != '0' && c != '_'; - })); - - // remove all `_` before calling TC::parse_int - str.erase(std::remove(str.begin(), str.end(), '_'), str.end()); - - // 0o0000_0000 becomes empty. - if(str.empty()) { str = "0"; } - - const auto val = TC::parse_int(str, source_location(region(loc)), 8); - if(val.is_ok()) - { - return ok(basic_value(val.as_ok(), std::move(fmt), {}, std::move(reg))); - } - else - { - loc = first; - return err(val.as_err()); - } -} - -template -result, error_info> -parse_hex_integer(location& loc, const context& ctx) -{ - const auto first = loc; - const auto& spec = ctx.toml_spec(); - auto reg = syntax::hex_int(spec).scan(loc); - if( ! reg.is_ok()) - { - return err(make_syntax_error("toml::parse_hex_integer: " - "invalid integer: hex_int must be like: 0xC0FFEE, 0xdead_beef", - syntax::hex_int(spec), loc)); - } - - auto str = reg.as_string(); - - integer_format_info fmt; - fmt.fmt = integer_format::hex; - fmt.width = str.size() - 2 - static_cast(std::count(str.begin(), str.end(), '_')); - - const auto first_underscore = std::find(str.rbegin(), str.rend(), '_'); - if(first_underscore != str.rend()) - { - fmt.spacer = static_cast(std::distance(str.rbegin(), first_underscore)); - } - - // skip prefix `0x` and zeros and underscores at the MSB - str.erase(str.begin(), std::find_if( - std::next(str.begin(), 2), str.end(), [](const char c) { - return c != '0' && c != '_'; - })); - - // remove all `_` before calling TC::parse_int - str.erase(std::remove(str.begin(), str.end(), '_'), str.end()); - - // 0x0000_0000 becomes empty. - if(str.empty()) { str = "0"; } - - // prefix zero and _ is removed. check if it uses upper/lower case. - // if both upper and lower case letters are found, set upper=true. - const auto lower_not_found = std::find_if(str.begin(), str.end(), - [](const char c) { return std::islower(static_cast(c)) != 0; }) == str.end(); - const auto upper_found = std::find_if(str.begin(), str.end(), - [](const char c) { return std::isupper(static_cast(c)) != 0; }) != str.end(); - fmt.uppercase = lower_not_found || upper_found; - - const auto val = TC::parse_int(str, source_location(region(loc)), 16); - if(val.is_ok()) - { - return ok(basic_value(val.as_ok(), std::move(fmt), {}, std::move(reg))); - } - else - { - loc = first; - return err(val.as_err()); - } -} - -template -result, error_info> -parse_dec_integer(location& loc, const context& ctx) -{ - const auto first = loc; - const auto& spec = ctx.toml_spec(); - - // ---------------------------------------------------------------------- - // check syntax - auto reg = syntax::dec_int(spec).scan(loc); - if( ! reg.is_ok()) - { - return err(make_syntax_error("toml::parse_dec_integer: " - "invalid integer: dec_int must be like: 42, 123_456_789", - syntax::dec_int(spec), loc)); - } - - // ---------------------------------------------------------------------- - // it matches. gen value - auto str = reg.as_string(); - - integer_format_info fmt; - fmt.fmt = integer_format::dec; - fmt.width = str.size() - static_cast(std::count(str.begin(), str.end(), '_')); - - const auto first_underscore = std::find(str.rbegin(), str.rend(), '_'); - if(first_underscore != str.rend()) - { - fmt.spacer = static_cast(std::distance(str.rbegin(), first_underscore)); - } - - // remove all `_` before calling TC::parse_int - str.erase(std::remove(str.begin(), str.end(), '_'), str.end()); - - auto src = source_location(region(loc)); - const auto val = TC::parse_int(str, src, 10); - if(val.is_err()) - { - loc = first; - return err(val.as_err()); - } - - // ---------------------------------------------------------------------- - // parse suffix (extension) - - if(spec.ext_num_suffix && loc.current() == '_') - { - const auto sfx_reg = syntax::num_suffix(spec).scan(loc); - if( ! sfx_reg.is_ok()) - { - loc = first; - return err(make_error_info("toml::parse_dec_integer: " - "invalid suffix: should be `_ non-digit-graph (graph | _graph)`", - source_location(region(loc)), "here")); - } - auto sfx = sfx_reg.as_string(); - assert( ! sfx.empty() && sfx.front() == '_'); - sfx.erase(sfx.begin()); // remove the first `_` - - fmt.suffix = sfx; - } - - return ok(basic_value(val.as_ok(), std::move(fmt), {}, std::move(reg))); -} - -template -result, error_info> -parse_integer(location& loc, const context& ctx) -{ - const auto first = loc; - - if( ! loc.eof() && (loc.current() == '+' || loc.current() == '-')) - { - // skip +/- to diagnose +0xDEADBEEF or -0b0011 (invalid). - // without this, +0xDEAD_BEEF will be parsed as a decimal int and - // unexpected "xDEAD_BEEF" will appear after integer "+0". - loc.advance(); - } - - if( ! loc.eof() && loc.current() == '0') - { - loc.advance(); - if(loc.eof()) - { - // `[+-]?0`. parse as an decimal integer. - loc = first; - return parse_dec_integer(loc, ctx); - } - - const auto prefix = loc.current(); - auto prefix_src = source_location(region(loc)); - - loc = first; - - if(prefix == 'b') {return parse_bin_integer(loc, ctx);} - if(prefix == 'o') {return parse_oct_integer(loc, ctx);} - if(prefix == 'x') {return parse_hex_integer(loc, ctx);} - - if(std::isdigit(prefix)) - { - auto src = source_location(region(loc)); - return err(make_error_info("toml::parse_integer: " - "leading zero in an decimal integer is not allowed", - std::move(src), "leading zero")); - } - } - - loc = first; - return parse_dec_integer(loc, ctx); -} - -/* ============================================================================ - * ___ _ _ _ - * | __| |___ __ _| |_(_)_ _ __ _ - * | _|| / _ \/ _` | _| | ' \/ _` | - * |_| |_\___/\__,_|\__|_|_||_\__, | - * |___/ - */ - -template -result, error_info> -parse_floating(location& loc, const context& ctx) -{ - using floating_type = typename basic_value::floating_type; - - const auto first = loc; - const auto& spec = ctx.toml_spec(); - - // ---------------------------------------------------------------------- - // check syntax - bool is_hex = false; - std::string str; - region reg; - if(spec.ext_hex_float && sequence(character('0'), character('x')).scan(loc).is_ok()) - { - loc = first; - is_hex = true; - - reg = syntax::hex_floating(spec).scan(loc); - if( ! reg.is_ok()) - { - return err(make_syntax_error("toml::parse_floating: " - "invalid hex floating: float must be like: 0xABCp-3f", - syntax::floating(spec), loc)); - } - str = reg.as_string(); - } - else - { - reg = syntax::floating(spec).scan(loc); - if( ! reg.is_ok()) - { - return err(make_syntax_error("toml::parse_floating: " - "invalid floating: float must be like: -3.14159_26535, 6.022e+23, " - "inf, or nan (lowercase).", syntax::floating(spec), loc)); - } - str = reg.as_string(); - } - - // ---------------------------------------------------------------------- - // it matches. gen value - - floating_format_info fmt; - - if(is_hex) - { - fmt.fmt = floating_format::hex; - } - else - { - // since we already checked that the string conforms the TOML standard. - if(std::find(str.begin(), str.end(), 'e') != str.end() || - std::find(str.begin(), str.end(), 'E') != str.end()) - { - fmt.fmt = floating_format::scientific; // use exponent part - } - else - { - fmt.fmt = floating_format::fixed; // do not use exponent part - } - } - - str.erase(std::remove(str.begin(), str.end(), '_'), str.end()); - - floating_type val{0}; - - if(str == "inf" || str == "+inf") - { - TOML11_CONSTEXPR_IF(std::numeric_limits::has_infinity) - { - val = std::numeric_limits::infinity(); - } - else - { - return err(make_error_info("toml::parse_floating: inf value found" - " but the current environment does not support inf. Please" - " make sure that the floating-point implementation conforms" - " IEEE 754/ISO 60559 international standard.", - source_location(region(loc)), - "floating_type: inf is not supported")); - } - } - else if(str == "-inf") - { - TOML11_CONSTEXPR_IF(std::numeric_limits::has_infinity) - { - val = -std::numeric_limits::infinity(); - } - else - { - return err(make_error_info("toml::parse_floating: inf value found" - " but the current environment does not support inf. Please" - " make sure that the floating-point implementation conforms" - " IEEE 754/ISO 60559 international standard.", - source_location(region(loc)), - "floating_type: inf is not supported")); - } - } - else if(str == "nan" || str == "+nan") - { - TOML11_CONSTEXPR_IF(std::numeric_limits::has_quiet_NaN) - { - val = std::numeric_limits::quiet_NaN(); - } - else TOML11_CONSTEXPR_IF(std::numeric_limits::has_signaling_NaN) - { - val = std::numeric_limits::signaling_NaN(); - } - else - { - return err(make_error_info("toml::parse_floating: NaN value found" - " but the current environment does not support NaN. Please" - " make sure that the floating-point implementation conforms" - " IEEE 754/ISO 60559 international standard.", - source_location(region(loc)), - "floating_type: NaN is not supported")); - } - } - else if(str == "-nan") - { - using std::copysign; - TOML11_CONSTEXPR_IF(std::numeric_limits::has_quiet_NaN) - { - val = copysign(std::numeric_limits::quiet_NaN(), floating_type(-1)); - } - else TOML11_CONSTEXPR_IF(std::numeric_limits::has_signaling_NaN) - { - val = copysign(std::numeric_limits::signaling_NaN(), floating_type(-1)); - } - else - { - return err(make_error_info("toml::parse_floating: NaN value found" - " but the current environment does not support NaN. Please" - " make sure that the floating-point implementation conforms" - " IEEE 754/ISO 60559 international standard.", - source_location(region(loc)), - "floating_type: NaN is not supported")); - } - } - else - { - // set precision - const auto has_sign = ! str.empty() && (str.front() == '+' || str.front() == '-'); - const auto decpoint = std::find(str.begin(), str.end(), '.'); - const auto exponent = std::find_if(str.begin(), str.end(), - [](const char c) { return c == 'e' || c == 'E'; }); - if(decpoint != str.end() && exponent != str.end()) - { - assert(decpoint < exponent); - } - - if(fmt.fmt == floating_format::scientific) - { - // total width - fmt.prec = static_cast(std::distance(str.begin(), exponent)); - if(has_sign) - { - fmt.prec -= 1; - } - if(decpoint != str.end()) - { - fmt.prec -= 1; - } - } - else if(fmt.fmt == floating_format::hex) - { - fmt.prec = std::numeric_limits::max_digits10; - } - else - { - // width after decimal point - fmt.prec = static_cast(std::distance(std::next(decpoint), exponent)); - } - - auto src = source_location(region(loc)); - const auto res = TC::parse_float(str, src, is_hex); - if(res.is_ok()) - { - val = res.as_ok(); - } - else - { - return err(res.as_err()); - } - } - - // ---------------------------------------------------------------------- - // parse suffix (extension) - - if(spec.ext_num_suffix && loc.current() == '_') - { - const auto sfx_reg = syntax::num_suffix(spec).scan(loc); - if( ! sfx_reg.is_ok()) - { - auto src = source_location(region(loc)); - loc = first; - return err(make_error_info("toml::parse_floating: " - "invalid suffix: should be `_ non-digit-graph (graph | _graph)`", - std::move(src), "here")); - } - auto sfx = sfx_reg.as_string(); - assert( ! sfx.empty() && sfx.front() == '_'); - sfx.erase(sfx.begin()); // remove the first `_` - - fmt.suffix = sfx; - } - - return ok(basic_value(val, std::move(fmt), {}, std::move(reg))); -} - -/* ============================================================================ - * ___ _ _ _ - * | \ __ _| |_ ___| |_(_)_ __ ___ - * | |) / _` | _/ -_) _| | ' \/ -_) - * |___/\__,_|\__\___|\__|_|_|_|_\___| - */ - -// all the offset_datetime, local_datetime, local_date parses date part. -template -result, error_info> -parse_local_date_only(location& loc, const context& ctx) -{ - const auto first = loc; - const auto& spec = ctx.toml_spec(); - - local_date_format_info fmt; - - // ---------------------------------------------------------------------- - // check syntax - auto reg = syntax::local_date(spec).scan(loc); - if( ! reg.is_ok()) - { - return err(make_syntax_error("toml::parse_local_date: " - "invalid date: date must be like: 1234-05-06, yyyy-mm-dd.", - syntax::local_date(spec), loc)); - } - - // ---------------------------------------------------------------------- - // it matches. gen value - const auto str = reg.as_string(); - - // 0123456789 - // yyyy-mm-dd - const auto year_r = from_string(str.substr(0, 4)); - const auto month_r = from_string(str.substr(5, 2)); - const auto day_r = from_string(str.substr(8, 2)); - - if(year_r.is_err()) - { - auto src = source_location(region(first)); - return err(make_error_info("toml::parse_local_date: " - "failed to read year `" + str.substr(0, 4) + "`", - std::move(src), "here")); - } - if(month_r.is_err()) - { - auto src = source_location(region(first)); - return err(make_error_info("toml::parse_local_date: " - "failed to read month `" + str.substr(5, 2) + "`", - std::move(src), "here")); - } - if(day_r.is_err()) - { - auto src = source_location(region(first)); - return err(make_error_info("toml::parse_local_date: " - "failed to read day `" + str.substr(8, 2) + "`", - std::move(src), "here")); - } - - const auto year = year_r.unwrap(); - const auto month = month_r.unwrap(); - const auto day = day_r.unwrap(); - - { - // We briefly check whether the input date is valid or not. - // Actually, because of the historical reasons, there are several - // edge cases, such as 1582/10/5-1582/10/14 (only in several countries). - // But here, we do not care about it. - // It makes the code complicated and there is only low probability - // that such a specific date is needed in practice. If someone need to - // validate date accurately, that means that the one need a specialized - // library for their purpose in another layer. - - const bool is_leap = (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0)); - const auto max_day = [month, is_leap]() { - if(month == 2) - { - return is_leap ? 29 : 28; - } - if(month == 4 || month == 6 || month == 9 || month == 11) - { - return 30; - } - return 31; - }(); - - if((month < 1 || 12 < month) || (day < 1 || max_day < day)) - { - auto src = source_location(region(first)); - return err(make_error_info("toml::parse_local_date: invalid date.", - std::move(src), "month must be 01-12, day must be any of " - "01-28,29,30,31 depending on the month/year.")); - } - } - - return ok(std::make_tuple( - local_date(year, static_cast(month - 1), day), - std::move(fmt), std::move(reg) - )); -} - -template -result, error_info> -parse_local_date(location& loc, const context& ctx) -{ - auto val_fmt_reg = parse_local_date_only(loc, ctx); - if(val_fmt_reg.is_err()) - { - return err(val_fmt_reg.unwrap_err()); - } - - auto val = std::move(std::get<0>(val_fmt_reg.unwrap())); - auto fmt = std::move(std::get<1>(val_fmt_reg.unwrap())); - auto reg = std::move(std::get<2>(val_fmt_reg.unwrap())); - - return ok(basic_value(std::move(val), std::move(fmt), {}, std::move(reg))); -} - -// all the offset_datetime, local_datetime, local_time parses date part. -template -result, error_info> -parse_local_time_only(location& loc, const context& ctx) -{ - const auto first = loc; - const auto& spec = ctx.toml_spec(); - - local_time_format_info fmt; - - // ---------------------------------------------------------------------- - // check syntax - auto reg = syntax::local_time(spec).scan(loc); - if( ! reg.is_ok()) - { - if(spec.v1_1_0_make_seconds_optional) - { - return err(make_syntax_error("toml::parse_local_time: " - "invalid time: time must be HH:MM(:SS.sss) (seconds are optional)", - syntax::local_time(spec), loc)); - } - else - { - return err(make_syntax_error("toml::parse_local_time: " - "invalid time: time must be HH:MM:SS(.sss) (subseconds are optional)", - syntax::local_time(spec), loc)); - } - } - - // ---------------------------------------------------------------------- - // it matches. gen value - const auto str = reg.as_string(); - - // at least we have HH:MM. - // 01234 - // HH:MM - const auto hour_r = from_string(str.substr(0, 2)); - const auto minute_r = from_string(str.substr(3, 2)); - - if(hour_r.is_err()) - { - auto src = source_location(region(first)); - return err(make_error_info("toml::parse_local_time: " - "failed to read hour `" + str.substr(0, 2) + "`", - std::move(src), "here")); - } - if(minute_r.is_err()) - { - auto src = source_location(region(first)); - return err(make_error_info("toml::parse_local_time: " - "failed to read minute `" + str.substr(3, 2) + "`", - std::move(src), "here")); - } - - const auto hour = hour_r.unwrap(); - const auto minute = minute_r.unwrap(); - - if((hour < 0 || 24 <= hour) || (minute < 0 || 60 <= minute)) - { - auto src = source_location(region(first)); - return err(make_error_info("toml::parse_local_time: invalid time.", - std::move(src), "hour must be 00-23, minute must be 00-59.")); - } - - // ----------------------------------------------------------------------- - // we have hour and minute. - // Since toml v1.1.0, second and subsecond part becomes optional. - // Check the version and return if second does not exist. - - if(str.size() == 5 && spec.v1_1_0_make_seconds_optional) - { - fmt.has_seconds = false; - fmt.subsecond_precision = 0; - return ok(std::make_tuple(local_time(hour, minute, 0), std::move(fmt), std::move(reg))); - } - assert(str.at(5) == ':'); - - // we have at least `:SS` part. `.subseconds` are optional. - - // 0 1 - // 012345678901234 - // HH:MM:SS.subsec - const auto sec_r = from_string(str.substr(6, 2)); - if(sec_r.is_err()) - { - auto src = source_location(region(first)); - return err(make_error_info("toml::parse_local_time: " - "failed to read second `" + str.substr(6, 2) + "`", - std::move(src), "here")); - } - const auto sec = sec_r.unwrap(); - - if(sec < 0 || 60 < sec) // :60 is allowed - { - auto src = source_location(region(first)); - return err(make_error_info("toml::parse_local_time: invalid time.", - std::move(src), "second must be 00-60.")); - } - - if(str.size() == 8) - { - fmt.has_seconds = true; - fmt.subsecond_precision = 0; - return ok(std::make_tuple(local_time(hour, minute, sec), std::move(fmt), std::move(reg))); - } - - assert(str.at(8) == '.'); - - auto secfrac = str.substr(9, str.size() - 9); - - fmt.has_seconds = true; - fmt.subsecond_precision = secfrac.size(); - - while(secfrac.size() < 9) - { - secfrac += '0'; - } - assert(9 <= secfrac.size()); - const auto ms_r = from_string(secfrac.substr(0, 3)); - const auto us_r = from_string(secfrac.substr(3, 3)); - const auto ns_r = from_string(secfrac.substr(6, 3)); - - if(ms_r.is_err()) - { - auto src = source_location(region(first)); - return err(make_error_info("toml::parse_local_time: " - "failed to read milliseconds `" + secfrac.substr(0, 3) + "`", - std::move(src), "here")); - } - if(us_r.is_err()) - { - auto src = source_location(region(first)); - return err(make_error_info("toml::parse_local_time: " - "failed to read microseconds`" + str.substr(3, 3) + "`", - std::move(src), "here")); - } - if(ns_r.is_err()) - { - auto src = source_location(region(first)); - return err(make_error_info("toml::parse_local_time: " - "failed to read nanoseconds`" + str.substr(6, 3) + "`", - std::move(src), "here")); - } - const auto ms = ms_r.unwrap(); - const auto us = us_r.unwrap(); - const auto ns = ns_r.unwrap(); - - return ok(std::make_tuple(local_time(hour, minute, sec, ms, us, ns), std::move(fmt), std::move(reg))); -} - -template -result, error_info> -parse_local_time(location& loc, const context& ctx) -{ - const auto first = loc; - - auto val_fmt_reg = parse_local_time_only(loc, ctx); - if(val_fmt_reg.is_err()) - { - return err(val_fmt_reg.unwrap_err()); - } - - auto val = std::move(std::get<0>(val_fmt_reg.unwrap())); - auto fmt = std::move(std::get<1>(val_fmt_reg.unwrap())); - auto reg = std::move(std::get<2>(val_fmt_reg.unwrap())); - - return ok(basic_value(std::move(val), std::move(fmt), {}, std::move(reg))); -} - -template -result, error_info> -parse_local_datetime(location& loc, const context& ctx) -{ - using char_type = location::char_type; - - const auto first = loc; - - local_datetime_format_info fmt; - - // ---------------------------------------------------------------------- - - auto date_fmt_reg = parse_local_date_only(loc, ctx); - if(date_fmt_reg.is_err()) - { - return err(date_fmt_reg.unwrap_err()); - } - - if(loc.current() == char_type('T')) - { - loc.advance(); - fmt.delimiter = datetime_delimiter_kind::upper_T; - } - else if(loc.current() == char_type('t')) - { - loc.advance(); - fmt.delimiter = datetime_delimiter_kind::lower_t; - } - else if(loc.current() == char_type(' ')) - { - loc.advance(); - fmt.delimiter = datetime_delimiter_kind::space; - } - else - { - auto src = source_location(region(loc)); - return err(make_error_info("toml::parse_local_datetime: " - "expect date-time delimiter `T`, `t` or ` `(space).", - std::move(src), "here")); - } - - auto time_fmt_reg = parse_local_time_only(loc, ctx); - if(time_fmt_reg.is_err()) - { - return err(time_fmt_reg.unwrap_err()); - } - - fmt.has_seconds = std::get<1>(time_fmt_reg.unwrap()).has_seconds; - fmt.subsecond_precision = std::get<1>(time_fmt_reg.unwrap()).subsecond_precision; - - // ---------------------------------------------------------------------- - - region reg(first, loc); - local_datetime val(std::get<0>(date_fmt_reg.unwrap()), - std::get<0>(time_fmt_reg.unwrap())); - - return ok(basic_value(val, std::move(fmt), {}, std::move(reg))); -} - -template -result, error_info> -parse_offset_datetime(location& loc, const context& ctx) -{ - using char_type = location::char_type; - - const auto first = loc; - const auto& spec = ctx.toml_spec(); - - offset_datetime_format_info fmt; - - // ---------------------------------------------------------------------- - // date part - - auto date_fmt_reg = parse_local_date_only(loc, ctx); - if(date_fmt_reg.is_err()) - { - return err(date_fmt_reg.unwrap_err()); - } - - // ---------------------------------------------------------------------- - // delimiter - - if(loc.current() == char_type('T')) - { - loc.advance(); - fmt.delimiter = datetime_delimiter_kind::upper_T; - } - else if(loc.current() == char_type('t')) - { - loc.advance(); - fmt.delimiter = datetime_delimiter_kind::lower_t; - } - else if(loc.current() == char_type(' ')) - { - loc.advance(); - fmt.delimiter = datetime_delimiter_kind::space; - } - else - { - auto src = source_location(region(loc)); - return err(make_error_info("toml::parse_offset_datetime: " - "expect date-time delimiter `T` or ` `(space).", std::move(src), "here" - )); - } - - // ---------------------------------------------------------------------- - // time part - - auto time_fmt_reg = parse_local_time_only(loc, ctx); - if(time_fmt_reg.is_err()) - { - return err(time_fmt_reg.unwrap_err()); - } - - fmt.has_seconds = std::get<1>(time_fmt_reg.unwrap()).has_seconds; - fmt.subsecond_precision = std::get<1>(time_fmt_reg.unwrap()).subsecond_precision; - - // ---------------------------------------------------------------------- - // offset part - - const auto ofs_reg = syntax::time_offset(spec).scan(loc); - if( ! ofs_reg.is_ok()) - { - return err(make_syntax_error("toml::parse_offset_datetime: " - "invalid offset: offset must be like: Z, +01:00, or -10:00.", - syntax::time_offset(spec), loc)); - } - - const auto ofs_str = ofs_reg.as_string(); - - time_offset offset(0, 0); - - assert(ofs_str.size() != 0); - - if(ofs_str.at(0) == char_type('+') || ofs_str.at(0) == char_type('-')) - { - const auto hour_r = from_string(ofs_str.substr(1, 2)); - const auto minute_r = from_string(ofs_str.substr(4, 2)); - if(hour_r.is_err()) - { - auto src = source_location(region(loc)); - return err(make_error_info("toml::parse_offset_datetime: " - "Failed to read offset hour part", std::move(src), "here")); - } - if(minute_r.is_err()) - { - auto src = source_location(region(loc)); - return err(make_error_info("toml::parse_offset_datetime: " - "Failed to read offset minute part", std::move(src), "here")); - } - const auto hour = hour_r.unwrap(); - const auto minute = minute_r.unwrap(); - - if(ofs_str.at(0) == '+') - { - offset = time_offset(hour, minute); - } - else - { - offset = time_offset(-hour, -minute); - } - } - else - { - assert(ofs_str.at(0) == char_type('Z') || ofs_str.at(0) == char_type('z')); - } - - if (offset.hour < -24 || 24 < offset.hour || - offset.minute < -60 || 60 < offset.minute) - { - return err(make_error_info("toml::parse_offset_datetime: " - "too large offset: |hour| <= 24, |minute| <= 60", - source_location(region(first, loc)), "here")); - } - - - // ---------------------------------------------------------------------- - - region reg(first, loc); - offset_datetime val(local_datetime(std::get<0>(date_fmt_reg.unwrap()), - std::get<0>(time_fmt_reg.unwrap())), - offset); - - return ok(basic_value(val, std::move(fmt), {}, std::move(reg))); -} - -/* ============================================================================ - * ___ _ _ - * / __| |_ _ _(_)_ _ __ _ - * \__ \ _| '_| | ' \/ _` | - * |___/\__|_| |_|_||_\__, | - * |___/ - */ - -template -result::string_type, error_info> -parse_utf8_codepoint(const region& reg) -{ - using string_type = typename basic_value::string_type; - using char_type = typename string_type::value_type; - - // assert(reg.as_lines().size() == 1); // XXX heavy check - - const auto str = reg.as_string(); - assert( ! str.empty()); - assert(str.front() == 'u' || str.front() == 'U' || str.front() == 'x'); - - std::uint_least32_t codepoint; - std::istringstream iss(str.substr(1)); - iss >> std::hex >> codepoint; - - const auto to_char = [](const std::uint_least32_t i) noexcept -> char_type { - const auto uc = static_cast(i & 0xFF); - return cxx::bit_cast(uc); - }; - - string_type character; - if(codepoint < 0x80) // U+0000 ... U+0079 ; just an ASCII. - { - character += static_cast(codepoint); - } - else if(codepoint < 0x800) //U+0080 ... U+07FF - { - // 110yyyyx 10xxxxxx; 0x3f == 0b0011'1111 - character += to_char(0xC0|(codepoint >> 6 )); - character += to_char(0x80|(codepoint & 0x3F)); - } - else if(codepoint < 0x10000) // U+0800...U+FFFF - { - if(0xD800 <= codepoint && codepoint <= 0xDFFF) - { - auto src = source_location(reg); - return err(make_error_info("toml::parse_utf8_codepoint: " - "[0xD800, 0xDFFF] is not a valid UTF-8", - std::move(src), "here")); - } - assert(codepoint < 0xD800 || 0xDFFF < codepoint); - // 1110yyyy 10yxxxxx 10xxxxxx - character += to_char(0xE0| (codepoint >> 12)); - character += to_char(0x80|((codepoint >> 6 ) & 0x3F)); - character += to_char(0x80|((codepoint ) & 0x3F)); - } - else if(codepoint < 0x110000) // U+010000 ... U+10FFFF - { - // 11110yyy 10yyxxxx 10xxxxxx 10xxxxxx - character += to_char(0xF0| (codepoint >> 18)); - character += to_char(0x80|((codepoint >> 12) & 0x3F)); - character += to_char(0x80|((codepoint >> 6 ) & 0x3F)); - character += to_char(0x80|((codepoint ) & 0x3F)); - } - else // out of UTF-8 region - { - auto src = source_location(reg); - return err(make_error_info("toml::parse_utf8_codepoint: " - "input codepoint is too large.", - std::move(src), "must be in range [0x00, 0x10FFFF]")); - } - return ok(character); -} - -template -result::string_type, error_info> -parse_escape_sequence(location& loc, const context& ctx) -{ - using string_type = typename basic_value::string_type; - using char_type = typename string_type::value_type; - - const auto& spec = ctx.toml_spec(); - - assert( ! loc.eof()); - assert(loc.current() == '\\'); - loc.advance(); // consume the first backslash - - string_type retval; - - if (loc.current() == '\\') { retval += char_type('\\'); loc.advance(); } - else if(loc.current() == '"') { retval += char_type('\"'); loc.advance(); } - else if(loc.current() == 'b') { retval += char_type('\b'); loc.advance(); } - else if(loc.current() == 'f') { retval += char_type('\f'); loc.advance(); } - else if(loc.current() == 'n') { retval += char_type('\n'); loc.advance(); } - else if(loc.current() == 'r') { retval += char_type('\r'); loc.advance(); } - else if(loc.current() == 't') { retval += char_type('\t'); loc.advance(); } - else if(spec.v1_1_0_add_escape_sequence_e && loc.current() == 'e') - { - retval += char_type('\x1b'); - loc.advance(); - } - else if(spec.v1_1_0_add_escape_sequence_x && loc.current() == 'x') - { - auto scanner = sequence(character('x'), repeat_exact(2, syntax::hexdig(spec))); - const auto reg = scanner.scan(loc); - if( ! reg.is_ok()) - { - auto src = source_location(region(loc)); - return err(make_error_info("toml::parse_escape_sequence: " - "invalid token found in UTF-8 codepoint \\xhh", - std::move(src), "here")); - } - const auto utf8 = parse_utf8_codepoint(reg); - if(utf8.is_err()) - { - return err(utf8.as_err()); - } - retval += utf8.unwrap(); - } - else if(loc.current() == 'u') - { - auto scanner = sequence(character('u'), repeat_exact(4, syntax::hexdig(spec))); - const auto reg = scanner.scan(loc); - if( ! reg.is_ok()) - { - auto src = source_location(region(loc)); - return err(make_error_info("toml::parse_escape_sequence: " - "invalid token found in UTF-8 codepoint \\uhhhh", - std::move(src), "here")); - } - const auto utf8 = parse_utf8_codepoint(reg); - if(utf8.is_err()) - { - return err(utf8.as_err()); - } - retval += utf8.unwrap(); - } - else if(loc.current() == 'U') - { - auto scanner = sequence(character('U'), repeat_exact(8, syntax::hexdig(spec))); - const auto reg = scanner.scan(loc); - if( ! reg.is_ok()) - { - auto src = source_location(region(loc)); - return err(make_error_info("toml::parse_escape_sequence: " - "invalid token found in UTF-8 codepoint \\Uhhhhhhhh", - std::move(src), "here")); - } - const auto utf8 = parse_utf8_codepoint(reg); - if(utf8.is_err()) - { - return err(utf8.as_err()); - } - retval += utf8.unwrap(); - } - else - { - auto src = source_location(region(loc)); - std::string escape_seqs = "allowed escape seqs: \\\\, \\\", \\b, \\f, \\n, \\r, \\t"; - if(spec.v1_1_0_add_escape_sequence_e) - { - escape_seqs += ", \\e"; - } - if(spec.v1_1_0_add_escape_sequence_x) - { - escape_seqs += ", \\xhh"; - } - escape_seqs += ", \\uhhhh, or \\Uhhhhhhhh"; - - return err(make_error_info("toml::parse_escape_sequence: " - "unknown escape sequence.", std::move(src), escape_seqs)); - } - return ok(retval); -} - -template -result, error_info> -parse_ml_basic_string(location& loc, const context& ctx) -{ - const auto first = loc; - const auto& spec = ctx.toml_spec(); - - string_format_info fmt; - fmt.fmt = string_format::multiline_basic; - - auto reg = syntax::ml_basic_string(spec).scan(loc); - if( ! reg.is_ok()) - { - return err(make_syntax_error("toml::parse_ml_basic_string: " - "invalid string format", - syntax::ml_basic_string(spec), loc)); - } - - // ---------------------------------------------------------------------- - // it matches. gen value - - auto str = reg.as_string(); - - // we already checked that it starts with """ and ends with """. - assert(str.substr(0, 3) == "\"\"\""); - str.erase(0, 3); - - assert(str.size() >= 3); - assert(str.substr(str.size()-3, 3) == "\"\"\""); - str.erase(str.size()-3, 3); - - // the first newline just after """ is trimmed - if(str.size() >= 1 && str.at(0) == '\n') - { - str.erase(0, 1); - fmt.start_with_newline = true; - } - else if(str.size() >= 2 && str.at(0) == '\r' && str.at(1) == '\n') - { - str.erase(0, 2); - fmt.start_with_newline = true; - } - - using string_type = typename basic_value::string_type; - string_type val; - { - auto iter = str.cbegin(); - while(iter != str.cend()) - { - if(*iter == '\\') // remove whitespaces around escaped-newline - { - // we assume that the string is not too long to copy - auto loc2 = make_temporary_location(make_string(iter, str.cend())); - if(syntax::escaped_newline(spec).scan(loc2).is_ok()) - { - std::advance(iter, loc2.get_location()); // skip escaped newline and indent - // now iter points non-WS char - assert(iter == str.end() || (*iter != ' ' && *iter != '\t')); - } - else // normal escape seq. - { - auto esc = parse_escape_sequence(loc2, ctx); - - // syntax does not check its value. the unicode codepoint may be - // invalid, e.g. out-of-bound, [0xD800, 0xDFFF] - if(esc.is_err()) - { - return err(esc.unwrap_err()); - } - - val += esc.unwrap(); - std::advance(iter, loc2.get_location()); - } - } - else // we already checked the syntax. we don't need to check it again. - { - val += static_cast(*iter); - ++iter; - } - } - } - - return ok(basic_value( - std::move(val), std::move(fmt), {}, std::move(reg) - )); -} - -template -result::string_type, region>, error_info> -parse_basic_string_only(location& loc, const context& ctx) -{ - const auto first = loc; - const auto& spec = ctx.toml_spec(); - - auto reg = syntax::basic_string(spec).scan(loc); - if( ! reg.is_ok()) - { - return err(make_syntax_error("toml::parse_basic_string: " - "invalid string format", - syntax::basic_string(spec), loc)); - } - - // ---------------------------------------------------------------------- - // it matches. gen value - - auto str = reg.as_string(); - - assert(str.back() == '\"'); - str.pop_back(); - assert(str.at(0) == '\"'); - str.erase(0, 1); - - using string_type = typename basic_value::string_type; - using char_type = typename string_type::value_type; - string_type val; - - { - auto iter = str.begin(); - while(iter != str.end()) - { - if(*iter == '\\') - { - auto loc2 = make_temporary_location(make_string(iter, str.end())); - - auto esc = parse_escape_sequence(loc2, ctx); - - // syntax does not check its value. the unicode codepoint may be - // invalid, e.g. out-of-bound, [0xD800, 0xDFFF] - if(esc.is_err()) - { - return err(esc.unwrap_err()); - } - - val += esc.unwrap(); - std::advance(iter, loc2.get_location()); - } - else - { - val += char_type(*iter); // we already checked the syntax. - ++iter; - } - } - } - return ok(std::make_pair(val, reg)); -} - -template -result, error_info> -parse_basic_string(location& loc, const context& ctx) -{ - const auto first = loc; - - string_format_info fmt; - fmt.fmt = string_format::basic; - - auto val_res = parse_basic_string_only(loc, ctx); - if(val_res.is_err()) - { - return err(std::move(val_res.unwrap_err())); - } - auto val = std::move(val_res.unwrap().first ); - auto reg = std::move(val_res.unwrap().second); - - return ok(basic_value(std::move(val), std::move(fmt), {}, std::move(reg))); -} - -template -result, error_info> -parse_ml_literal_string(location& loc, const context& ctx) -{ - const auto first = loc; - const auto& spec = ctx.toml_spec(); - - string_format_info fmt; - fmt.fmt = string_format::multiline_literal; - - auto reg = syntax::ml_literal_string(spec).scan(loc); - if( ! reg.is_ok()) - { - return err(make_syntax_error("toml::parse_ml_literal_string: " - "invalid string format", - syntax::ml_literal_string(spec), loc)); - } - - // ---------------------------------------------------------------------- - // it matches. gen value - - auto str = reg.as_string(); - - assert(str.substr(0, 3) == "'''"); - assert(str.substr(str.size()-3, 3) == "'''"); - str.erase(0, 3); - str.erase(str.size()-3, 3); - - // the first newline just after """ is trimmed - if(str.size() >= 1 && str.at(0) == '\n') - { - str.erase(0, 1); - fmt.start_with_newline = true; - } - else if(str.size() >= 2 && str.at(0) == '\r' && str.at(1) == '\n') - { - str.erase(0, 2); - fmt.start_with_newline = true; - } - - using string_type = typename basic_value::string_type; - string_type val(str.begin(), str.end()); - - return ok(basic_value( - std::move(val), std::move(fmt), {}, std::move(reg) - )); -} - -template -result::string_type, region>, error_info> -parse_literal_string_only(location& loc, const context& ctx) -{ - const auto first = loc; - const auto& spec = ctx.toml_spec(); - - auto reg = syntax::literal_string(spec).scan(loc); - if( ! reg.is_ok()) - { - return err(make_syntax_error("toml::parse_literal_string: " - "invalid string format", - syntax::literal_string(spec), loc)); - } - - // ---------------------------------------------------------------------- - // it matches. gen value - - auto str = reg.as_string(); - - assert(str.back() == '\''); - str.pop_back(); - assert(str.at(0) == '\''); - str.erase(0, 1); - - using string_type = typename basic_value::string_type; - string_type val(str.begin(), str.end()); - - return ok(std::make_pair(std::move(val), std::move(reg))); -} - -template -result, error_info> -parse_literal_string(location& loc, const context& ctx) -{ - const auto first = loc; - - string_format_info fmt; - fmt.fmt = string_format::literal; - - auto val_res = parse_literal_string_only(loc, ctx); - if(val_res.is_err()) - { - return err(std::move(val_res.unwrap_err())); - } - auto val = std::move(val_res.unwrap().first ); - auto reg = std::move(val_res.unwrap().second); - - return ok(basic_value( - std::move(val), std::move(fmt), {}, std::move(reg) - )); -} - -template -result, error_info> -parse_string(location& loc, const context& ctx) -{ - const auto first = loc; - - if( ! loc.eof() && loc.current() == '"') - { - if(literal("\"\"\"").scan(loc).is_ok()) - { - loc = first; - return parse_ml_basic_string(loc, ctx); - } - else - { - loc = first; - return parse_basic_string(loc, ctx); - } - } - else if( ! loc.eof() && loc.current() == '\'') - { - if(literal("'''").scan(loc).is_ok()) - { - loc = first; - return parse_ml_literal_string(loc, ctx); - } - else - { - loc = first; - return parse_literal_string(loc, ctx); - } - } - else - { - auto src = source_location(region(loc)); - return err(make_error_info("toml::parse_string: " - "not a string", std::move(src), "here")); - } -} - -template -result, error_info> -parse_null(location& loc, const context& ctx) -{ - const auto& spec = ctx.toml_spec(); - if( ! spec.ext_null_value) - { - return err(make_error_info("toml::parse_null: " - "invalid spec: spec.ext_null_value must be true.", - source_location(region(loc)), "here")); - } - - // ---------------------------------------------------------------------- - // check syntax - auto reg = syntax::null_value(spec).scan(loc); - if( ! reg.is_ok()) - { - return err(make_syntax_error("toml::parse_null: " - "invalid null: null must be lowercase. ", - syntax::null_value(spec), loc)); - } - - // ---------------------------------------------------------------------- - // it matches. gen value - - // ---------------------------------------------------------------------- - // no format info for boolean - - return ok(basic_value(detail::none_t{}, std::move(reg))); -} - -/* ============================================================================ - * _ __ - * | |/ /___ _ _ - * | ' -result::key_type, error_info> -parse_simple_key(location& loc, const context& ctx) -{ - using key_type = typename basic_value::key_type; - const auto& spec = ctx.toml_spec(); - - if(loc.current() == '\"') - { - auto str_res = parse_basic_string_only(loc, ctx); - if(str_res.is_ok()) - { - return ok(std::move(str_res.unwrap().first)); - } - else - { - return err(std::move(str_res.unwrap_err())); - } - } - else if(loc.current() == '\'') - { - auto str_res = parse_literal_string_only(loc, ctx); - if(str_res.is_ok()) - { - return ok(std::move(str_res.unwrap().first)); - } - else - { - return err(std::move(str_res.unwrap_err())); - } - } - - // bare key. - - if(const auto bare = syntax::unquoted_key(spec).scan(loc)) - { - return ok(string_conv(bare.as_string())); - } - else - { - std::string postfix; - if(spec.v1_1_0_allow_non_english_in_bare_keys) - { - postfix = "Hint: Not all Unicode characters are allowed as bare key.\n"; - } - else - { - postfix = "Hint: non-ASCII scripts are allowed in toml v1.1.0, but not in v1.0.0.\n"; - } - return err(make_syntax_error("toml::parse_simple_key: " - "invalid key: key must be \"quoted\", 'quoted-literal', or bare key.", - syntax::unquoted_key(spec), loc, postfix)); - } -} - -// dotted key become vector of keys -template -result::key_type>, region>, error_info> -parse_key(location& loc, const context& ctx) -{ - const auto first = loc; - const auto& spec = ctx.toml_spec(); - - using key_type = typename basic_value::key_type; - std::vector keys; - while( ! loc.eof()) - { - auto key = parse_simple_key(loc, ctx); - if( ! key.is_ok()) - { - return err(key.unwrap_err()); - } - keys.push_back(std::move(key.unwrap())); - - auto reg = syntax::dot_sep(spec).scan(loc); - if( ! reg.is_ok()) - { - break; - } - } - if(keys.empty()) - { - auto src = source_location(region(first)); - return err(make_error_info("toml::parse_key: expected a new key, " - "but got nothing", std::move(src), "reached EOF")); - } - - return ok(std::make_pair(std::move(keys), region(first, loc))); -} - -// ============================================================================ - -// forward-decl to implement parse_array and parse_table -template -result, error_info> -parse_value(location&, context& ctx); - -template -result::key_type>, region>, - basic_value - >, error_info> -parse_key_value_pair(location& loc, context& ctx) -{ - const auto first = loc; - const auto& spec = ctx.toml_spec(); - - auto key_res = parse_key(loc, ctx); - if(key_res.is_err()) - { - loc = first; - return err(key_res.unwrap_err()); - } - - if( ! syntax::keyval_sep(spec).scan(loc).is_ok()) - { - auto e = make_syntax_error("toml::parse_key_value_pair: " - "invalid key value separator `=`", syntax::keyval_sep(spec), loc); - loc = first; - return err(std::move(e)); - } - - auto v_res = parse_value(loc, ctx); - if(v_res.is_err()) - { - // loc = first; - return err(v_res.unwrap_err()); - } - return ok(std::make_pair(std::move(key_res.unwrap()), std::move(v_res.unwrap()))); -} - -/* ============================================================================ - * __ _ _ _ _ _ __ _ _ _ - * / _` | '_| '_/ _` | || | - * \__,_|_| |_| \__,_|\_, | - * |__/ - */ - -// array(and multiline inline table with `{` and `}`) has the following format. -// `[` -// (ws|newline|comment-line)? (value) (ws|newline|comment-line)? `,` -// (ws|newline|comment-line)? (value) (ws|newline|comment-line)? `,` -// ... -// (ws|newline|comment-line)? (value) (ws|newline|comment-line)? (`,`)? -// (ws|newline|comment-line)? `]` -// it skips (ws|newline|comment-line) and returns the token. -template -struct multiline_spacer -{ - using comment_type = typename TC::comment_type; - bool newline_found; - indent_char indent_type; - std::int32_t indent; - comment_type comments; -}; -template -std::ostream& operator<<(std::ostream& os, const multiline_spacer& sp) -{ - os << "{newline=" << sp.newline_found << ", "; - os << "indent_type=" << sp.indent_type << ", "; - os << "indent=" << sp.indent << ", "; - os << "comments=" << sp.comments.size() << "}"; - return os; -} - -template -cxx::optional> -skip_multiline_spacer(location& loc, context& ctx, const bool newline_found = false) -{ - const auto& spec = ctx.toml_spec(); - - multiline_spacer spacer; - spacer.newline_found = newline_found; - spacer.indent_type = indent_char::none; - spacer.indent = 0; - spacer.comments.clear(); - - bool spacer_found = false; - while( ! loc.eof()) - { - if(auto comm = sequence(syntax::comment(spec), syntax::newline(spec)).scan(loc)) - { - spacer.newline_found = true; - auto comment = comm.as_string(); - if( ! comment.empty() && comment.back() == '\n') - { - comment.pop_back(); - if (!comment.empty() && comment.back() == '\r') - { - comment.pop_back(); - } - } - - spacer.comments.push_back(std::move(comment)); - spacer.indent_type = indent_char::none; - spacer.indent = 0; - spacer_found = true; - } - else if(auto nl = syntax::newline(spec).scan(loc)) - { - spacer.newline_found = true; - spacer.comments.clear(); - spacer.indent_type = indent_char::none; - spacer.indent = 0; - spacer_found = true; - } - else if(auto sp = repeat_at_least(1, character(cxx::bit_cast(' '))).scan(loc)) - { - spacer.indent_type = indent_char::space; - spacer.indent = static_cast(sp.length()); - spacer_found = true; - } - else if(auto tabs = repeat_at_least(1, character(cxx::bit_cast('\t'))).scan(loc)) - { - spacer.indent_type = indent_char::tab; - spacer.indent = static_cast(tabs.length()); - spacer_found = true; - } - else - { - break; // done - } - } - if( ! spacer_found) - { - return cxx::make_nullopt(); - } - return spacer; -} - -// not an [[array.of.tables]]. It parses ["this", "type"] -template -result, error_info> -parse_array(location& loc, context& ctx) -{ - const auto num_errors = ctx.errors().size(); - - const auto first = loc; - - if(loc.eof() || loc.current() != '[') - { - auto src = source_location(region(loc)); - return err(make_error_info("toml::parse_array: " - "The next token is not an array", std::move(src), "here")); - } - loc.advance(); - - typename basic_value::array_type val; - - array_format_info fmt; - fmt.fmt = array_format::oneline; - fmt.indent_type = indent_char::none; - - auto spacer = skip_multiline_spacer(loc, ctx); - if(spacer.has_value() && spacer.value().newline_found) - { - fmt.fmt = array_format::multiline; - } - - bool comma_found = true; - while( ! loc.eof()) - { - if(loc.current() == location::char_type(']')) - { - if(spacer.has_value() && spacer.value().newline_found && - spacer.value().indent_type != indent_char::none) - { - fmt.indent_type = spacer.value().indent_type; - fmt.closing_indent = spacer.value().indent; - } - break; - } - - if( ! comma_found) - { - auto src = source_location(region(loc)); - return err(make_error_info("toml::parse_array: " - "expected value-separator `,` or closing `]`", - std::move(src), "here")); - } - - if(spacer.has_value() && spacer.value().newline_found && - spacer.value().indent_type != indent_char::none) - { - fmt.indent_type = spacer.value().indent_type; - fmt.body_indent = spacer.value().indent; - } - - if(auto elem_res = parse_value(loc, ctx)) - { - auto elem = std::move(elem_res.unwrap()); - - if(spacer.has_value()) // copy previous comments to value - { - elem.comments() = std::move(spacer.value().comments); - } - - // parse spaces between a value and a comma - // array = [ - // 42 , # the answer - // ^^^^ - // 3.14 # pi - // , 2.71 ^^^^ - // ^^ - spacer = skip_multiline_spacer(loc, ctx); - if(spacer.has_value()) - { - for(std::size_t i=0; i( - std::move(val), std::move(fmt), {}, region(first, loc) - )); -} - -/* ============================================================================ - * _ _ _ _ _ _ - * (_)_ _ | (_)_ _ ___ | |_ __ _| |__| |___ - * | | ' \| | | ' \/ -_) | _/ _` | '_ \ / -_) - * |_|_||_|_|_|_||_\___| \__\__,_|_.__/_\___| - */ - -// ---------------------------------------------------------------------------- -// insert_value is the most complicated part of the toml spec. -// -// To parse a toml file correctly, we sometimes need to check an exising value -// is appendable or not. -// -// For example while parsing an inline array of tables, -// -// ```toml -// aot = [ -// {a = "foo"}, -// {a = "bar", b = "baz"}, -// ] -// ``` -// -// this `aot` is appendable until parser reaches to `]`. After that, it becomes -// non-appendable. -// -// On the other hand, a normal array of tables, such as -// -// ```toml -// [[aot]] -// a = "foo" -// -// [[aot]] -// a = "bar" -// b = "baz" -// ``` -// This `[[aot]]` is appendable until the parser reaches to the EOF. -// -// -// It becomes a bit more difficult in case of dotted keys. -// In TOML, it is allowed to append a key-value pair to a table that is -// *implicitly* defined by a subtable definitino. -// -// ```toml -// [x.y.z] -// w = 123 -// -// [x] -// a = "foo" # OK. x is defined implicitly by `[x.y.z]`. -// ``` -// -// But if the table is defined by a dotted keys, it is not appendable. -// -// ```toml -// [x] -// y.z.w = 123 -// -// [x.y] -// # ERROR. x.y is already defined by a dotted table in the previous table. -// ``` -// -// Also, reopening a table using dotted keys is invalid. -// -// ```toml -// [x.y.z] -// w = 123 -// -// [x] -// y.z.v = 42 # ERROR. [x.y.z] is already defined. -// ``` -// -// -// ```toml -// [a] -// b.c = "foo" -// b.d = "bar" -// ``` -// -// -// ```toml -// a.b = "foo" -// [a] -// c = "bar" # ERROR -// ``` -// -// In summary, -// - a table must be defined only once. -// - assignment to an exising table is possible only when: -// - defining a subtable [x.y] to an existing table [x]. -// - defining supertable [x] explicitly after [x.y]. -// - adding dotted keys in the same table. - -enum class inserting_value_kind : std::uint8_t -{ - std_table, // insert [standard.table] - array_table, // insert [[array.of.tables]] - dotted_keys // insert a.b.c = "this" -}; - -template -result*, error_info> -insert_value(const inserting_value_kind kind, - typename basic_value::table_type* current_table_ptr, - const std::vector::key_type>& keys, region key_reg, - basic_value val) -{ - using value_type = basic_value; - using array_type = typename basic_value::array_type; - using table_type = typename basic_value::table_type; - - auto key_loc = source_location(key_reg); - - assert( ! keys.empty()); - - // dotted key can insert to dotted key tables defined at the same level. - // dotted key can NOT reopen a table even if it is implcitly-defined one. - // - // [x.y.z] # define x and x.y implicitly. - // a = 42 - // - // [x] # reopening implcitly defined table - // r.s.t = 3.14 # VALID r and r.s are new tables. - // r.s.u = 2.71 # VALID r and r.s are dotted-key tables. valid. - // - // y.z.b = "foo" # INVALID x.y.z are multiline table, not a dotted key. - // y.c = "bar" # INVALID x.y is implicit multiline table, not a dotted key. - - // a table cannot reopen dotted-key tables. - // - // [t1] - // t2.t3.v = 0 - // [t1.t2] # INVALID t1.t2 is defined as a dotted-key table. - - for(std::size_t i=0; i{}, key_reg)); - - assert(current_table.at(key).is_table()); - current_table_ptr = std::addressof(current_table.at(key).as_table()); - } - else if (found->second.is_table()) - { - const auto fmt = found->second.as_table_fmt().fmt; - if(fmt == table_format::oneline || fmt == table_format::multiline_oneline) - { - // foo = {bar = "baz"} or foo = { \n bar = "baz" \n } - return err(make_error_info("toml::insert_value: " - "failed to insert a value: inline table is immutable", - key_loc, "inserting this", - found->second.location(), "to this table")); - } - // dotted key cannot reopen a table. - if(kind ==inserting_value_kind::dotted_keys && fmt != table_format::dotted) - { - return err(make_error_info("toml::insert_value: " - "reopening a table using dotted keys", - key_loc, "dotted key cannot reopen a table", - found->second.location(), "this table is already closed")); - } - assert(found->second.is_table()); - current_table_ptr = std::addressof(found->second.as_table()); - } - else if(found->second.is_array_of_tables()) - { - // aot = [{this = "type", of = "aot"}] # cannot be reopened - if(found->second.as_array_fmt().fmt != array_format::array_of_tables) - { - return err(make_error_info("toml::insert_value:" - "inline array of tables are immutable", - key_loc, "inserting this", - found->second.location(), "inline array of tables")); - } - // appending to [[aot]] - - if(kind == inserting_value_kind::dotted_keys) - { - // [[array.of.tables]] - // [array.of] # reopening supertable is okay - // tables.x = "foo" # appending `x` to the first table - return err(make_error_info("toml::insert_value:" - "dotted key cannot reopen an array-of-tables", - key_loc, "inserting this", - found->second.location(), "to this array-of-tables.")); - } - - // insert_value_by_dotkeys::std_table - // [[array.of.tables]] - // [array.of.tables.subtable] # appending to the last aot - // - // insert_value_by_dotkeys::array_table - // [[array.of.tables]] - // [[array.of.tables.subtable]] # appending to the last aot - auto& current_array_table = found->second.as_array().back(); - - assert(current_array_table.is_table()); - current_table_ptr = std::addressof(current_array_table.as_table()); - } - else - { - return err(make_error_info("toml::insert_value: " - "failed to insert a value, value already exists", - key_loc, "while inserting this", - found->second.location(), "non-table value already exists")); - } - } - else // this is the last key. insert a new value. - { - switch(kind) - { - case inserting_value_kind::dotted_keys: - { - if(current_table.find(key) != current_table.end()) - { - return err(make_error_info("toml::insert_value: " - "failed to insert a value, value already exists", - key_loc, "inserting this", - current_table.at(key).location(), "but value already exists")); - } - current_table.emplace(key, std::move(val)); - return ok(std::addressof(current_table.at(key))); - } - case inserting_value_kind::std_table: - { - // defining a new table or reopening supertable - auto found = current_table.find(key); - if(found == current_table.end()) // define a new aot - { - current_table.emplace(key, std::move(val)); - return ok(std::addressof(current_table.at(key))); - } - else // the table is already defined, reopen it - { - // assigning a [std.table]. it must be an implicit table. - auto& target = found->second; - if( ! target.is_table() || // could be an array-of-tables - target.as_table_fmt().fmt != table_format::implicit) - { - return err(make_error_info("toml::insert_value: " - "failed to insert a table, table already defined", - key_loc, "inserting this", - target.location(), "this table is explicitly defined")); - } - - // merge table - for(const auto& kv : val.as_table()) - { - if(target.contains(kv.first)) - { - // [x.y.z] - // w = "foo" - // [x] - // y = "bar" - return err(make_error_info("toml::insert_value: " - "failed to insert a table, table keys conflict to each other", - key_loc, "inserting this table", - kv.second.location(), "having this value", - target.at(kv.first).location(), "already defined here")); - } - else - { - target[kv.first] = kv.second; - } - } - // change implicit -> explicit - target.as_table_fmt().fmt = table_format::multiline; - // change definition region - change_region_of_value(target, val); - - return ok(std::addressof(current_table.at(key))); - } - } - case inserting_value_kind::array_table: - { - auto found = current_table.find(key); - if(found == current_table.end()) // define a new aot - { - array_format_info fmt; - fmt.fmt = array_format::array_of_tables; - fmt.indent_type = indent_char::none; - - current_table.emplace(key, value_type( - array_type{ std::move(val) }, std::move(fmt), - std::vector{}, std::move(key_reg) - )); - - assert( ! current_table.at(key).as_array().empty()); - return ok(std::addressof(current_table.at(key).as_array().back())); - } - else // the array is already defined, append to it - { - if( ! found->second.is_array_of_tables()) - { - return err(make_error_info("toml::insert_value: " - "failed to insert an array of tables, value already exists", - key_loc, "while inserting this", - found->second.location(), "non-table value already exists")); - } - if(found->second.as_array_fmt().fmt != array_format::array_of_tables) - { - return err(make_error_info("toml::insert_value: " - "failed to insert a table, inline array of tables is immutable", - key_loc, "while inserting this", - found->second.location(), "this is inline array-of-tables")); - } - found->second.as_array().push_back(std::move(val)); - assert( ! current_table.at(key).as_array().empty()); - return ok(std::addressof(current_table.at(key).as_array().back())); - } - } - default: {assert(false);} - } - } - } - return err(make_error_info("toml::insert_key: no keys found", - std::move(key_loc), "here")); -} - -// ---------------------------------------------------------------------------- - -template -result, error_info> -parse_inline_table(location& loc, context& ctx) -{ - using table_type = typename basic_value::table_type; - - const auto num_errors = ctx.errors().size(); - - const auto first = loc; - const auto& spec = ctx.toml_spec(); - - if(loc.eof() || loc.current() != '{') - { - auto src = source_location(region(loc)); - return err(make_error_info("toml::parse_inline_table: " - "The next token is not an inline table", std::move(src), "here")); - } - loc.advance(); - - table_type table; - table_format_info fmt; - fmt.fmt = table_format::oneline; - fmt.indent_type = indent_char::none; - - cxx::optional> spacer(cxx::make_nullopt()); - - if(spec.v1_1_0_allow_newlines_in_inline_tables) - { - spacer = skip_multiline_spacer(loc, ctx); - if(spacer.has_value() && spacer.value().newline_found) - { - fmt.fmt = table_format::multiline_oneline; - } - } - else - { - skip_whitespace(loc, ctx); - } - - bool still_empty = true; - bool comma_found = false; - while( ! loc.eof()) - { - // closing! - if(loc.current() == '}') - { - if(comma_found && ! spec.v1_1_0_allow_trailing_comma_in_inline_tables) - { - auto src = source_location(region(loc)); - return err(make_error_info("toml::parse_inline_table: trailing " - "comma is not allowed in TOML-v1.0.0)", std::move(src), "here")); - } - - if(spec.v1_1_0_allow_newlines_in_inline_tables) - { - if(spacer.has_value() && spacer.value().newline_found && - spacer.value().indent_type != indent_char::none) - { - fmt.indent_type = spacer.value().indent_type; - fmt.closing_indent = spacer.value().indent; - } - } - break; - } - - // if we already found a value and didn't found `,` nor `}`, error. - if( ! comma_found && ! still_empty) - { - auto src = source_location(region(loc)); - return err(make_error_info("toml::parse_inline_table: " - "expected value-separator `,` or closing `}`", - std::move(src), "here")); - } - - // parse indent. - if(spacer.has_value() && spacer.value().newline_found && - spacer.value().indent_type != indent_char::none) - { - fmt.indent_type = spacer.value().indent_type; - fmt.body_indent = spacer.value().indent; - } - - still_empty = false; // parsing a value... - if(auto kv_res = parse_key_value_pair(loc, ctx)) - { - auto keys = std::move(kv_res.unwrap().first.first); - auto key_reg = std::move(kv_res.unwrap().first.second); - auto val = std::move(kv_res.unwrap().second); - - auto ins_res = insert_value(inserting_value_kind::dotted_keys, - std::addressof(table), keys, std::move(key_reg), std::move(val)); - if(ins_res.is_err()) - { - ctx.report_error(std::move(ins_res.unwrap_err())); - // we need to skip until the next value (or end of the table) - // because we don't have valid kv pair. - while( ! loc.eof()) - { - const auto c = loc.current(); - if(c == ',' || c == '\n' || c == '}') - { - comma_found = (c == ','); - break; - } - loc.advance(); - } - continue; - } - - // if comment line follows immediately(without newline) after `,`, then - // the comment is for the elem. we need to check if comment follows `,`. - // - // (key) = (val) (ws|newline|comment-line)? `,` (ws)? (comment)? - - if(spec.v1_1_0_allow_newlines_in_inline_tables) - { - if(spacer.has_value()) // copy previous comments to value - { - for(std::size_t i=0; icomments().push_back(spacer.value().comments.at(i)); - } - } - spacer = skip_multiline_spacer(loc, ctx); - if(spacer.has_value()) - { - for(std::size_t i=0; icomments().push_back(spacer.value().comments.at(i)); - } - if(spacer.value().newline_found) - { - fmt.fmt = table_format::multiline_oneline; - if(spacer.value().indent_type != indent_char::none) - { - fmt.indent_type = spacer.value().indent_type; - fmt.body_indent = spacer.value().indent; - } - } - } - } - else - { - skip_whitespace(loc, ctx); - } - - comma_found = character(',').scan(loc).is_ok(); - - if(spec.v1_1_0_allow_newlines_in_inline_tables) - { - auto com_res = parse_comment_line(loc, ctx); - if(com_res.is_err()) - { - ctx.report_error(com_res.unwrap_err()); - } - const bool comment_found = com_res.is_ok() && com_res.unwrap().has_value(); - if(comment_found) - { - fmt.fmt = table_format::multiline_oneline; - ins_res.unwrap()->comments().push_back(com_res.unwrap().value()); - } - if(comma_found) - { - spacer = skip_multiline_spacer(loc, ctx, comment_found); - if(spacer.has_value() && spacer.value().newline_found) - { - fmt.fmt = table_format::multiline_oneline; - } - } - } - else - { - skip_whitespace(loc, ctx); - } - } - else - { - ctx.report_error(std::move(kv_res.unwrap_err())); - while( ! loc.eof()) - { - if(loc.current() == '}') - { - break; - } - if( ! spec.v1_1_0_allow_newlines_in_inline_tables && loc.current() == '\n') - { - break; - } - loc.advance(); - } - break; - } - } - - if(loc.current() != '}') - { - auto src = source_location(region(loc)); - return err(make_error_info("toml::parse_inline_table: " - "missing closing bracket `}`", - std::move(src), "expected `}`, reached line end")); - } - else - { - loc.advance(); // skip } - } - - // any error reported from this function - if(num_errors < ctx.errors().size()) - { - assert(ctx.has_error()); // already reported - return err(ctx.pop_last_error()); - } - - basic_value retval( - std::move(table), std::move(fmt), {}, region(first, loc)); - - return ok(std::move(retval)); -} - -/* ============================================================================ - * _ - * __ ____ _| |_ _ ___ - * \ V / _` | | || / -_) - * \_/\__,_|_|\_,_\___| - */ - -template -result -guess_number_type(const location& first, const context& ctx) -{ - const auto& spec = ctx.toml_spec(); - location loc = first; - - if(syntax::offset_datetime(spec).scan(loc).is_ok()) - { - return ok(value_t::offset_datetime); - } - loc = first; - - if(syntax::local_datetime(spec).scan(loc).is_ok()) - { - const auto curr = loc.current(); - // if offset_datetime contains bad offset, it syntax::offset_datetime - // fails to scan it. - if(curr == '+' || curr == '-') - { - return err(make_syntax_error("bad offset: must be [+-]HH:MM or Z", - syntax::time_offset(spec), loc, std::string( - "Hint: valid : +09:00, -05:30\n" - "Hint: invalid: +9:00, -5:30\n"))); - } - return ok(value_t::local_datetime); - } - loc = first; - - if(syntax::local_date(spec).scan(loc).is_ok()) - { - // bad time may appear after this. - - if( ! loc.eof()) - { - const auto c = loc.current(); - if(c == 'T' || c == 't') - { - loc.advance(); - - return err(make_syntax_error("bad time: must be HH:MM:SS.subsec", - syntax::local_time(spec), loc, std::string( - "Hint: valid : 1979-05-27T07:32:00, 1979-05-27 07:32:00.999999\n" - "Hint: invalid: 1979-05-27T7:32:00, 1979-05-27 17:32\n"))); - } - if(c == ' ') - { - // A space is allowed as a delimiter between local time. - // But there is a case where bad time follows a space. - // - invalid: 2019-06-16 7:00:00 - // - valid : 2019-06-16 07:00:00 - loc.advance(); - if( ! loc.eof() && ('0' <= loc.current() && loc.current() <= '9')) - { - return err(make_syntax_error("bad time: must be HH:MM:SS.subsec", - syntax::local_time(spec), loc, std::string( - "Hint: valid : 1979-05-27T07:32:00, 1979-05-27 07:32:00.999999\n" - "Hint: invalid: 1979-05-27T7:32:00, 1979-05-27 17:32\n"))); - } - } - if('0' <= c && c <= '9') - { - return err(make_syntax_error("bad datetime: missing T or space", - character_either{'T', 't', ' '}, loc, std::string( - "Hint: valid : 1979-05-27T07:32:00, 1979-05-27 07:32:00.999999\n" - "Hint: invalid: 1979-05-27T7:32:00, 1979-05-27 17:32\n"))); - } - } - return ok(value_t::local_date); - } - loc = first; - - if(syntax::local_time(spec).scan(loc).is_ok()) - { - return ok(value_t::local_time); - } - loc = first; - - if(syntax::floating(spec).scan(loc).is_ok()) - { - if( ! loc.eof() && loc.current() == '_') - { - if(spec.ext_num_suffix && syntax::num_suffix(spec).scan(loc).is_ok()) - { - return ok(value_t::floating); - } - auto src = source_location(region(loc)); - return err(make_error_info( - "bad float: `_` must be surrounded by digits", - std::move(src), "invalid underscore", - "Hint: valid : +1.0, -2e-2, 3.141_592_653_589, inf, nan\n" - "Hint: invalid: .0, 1., _1.0, 1.0_, 1_.0, 1.0__0\n")); - } - return ok(value_t::floating); - } - loc = first; - - if(spec.ext_hex_float) - { - if(syntax::hex_floating(spec).scan(loc).is_ok()) - { - if( ! loc.eof() && loc.current() == '_') - { - if(spec.ext_num_suffix && syntax::num_suffix(spec).scan(loc).is_ok()) - { - return ok(value_t::floating); - } - auto src = source_location(region(loc)); - return err(make_error_info( - "bad float: `_` must be surrounded by digits", - std::move(src), "invalid underscore", - "Hint: valid : +1.0, -2e-2, 3.141_592_653_589, inf, nan\n" - "Hint: invalid: .0, 1., _1.0, 1.0_, 1_.0, 1.0__0\n")); - } - return ok(value_t::floating); - } - loc = first; - } - - if(auto int_reg = syntax::integer(spec).scan(loc)) - { - if( ! loc.eof()) - { - const auto c = loc.current(); - if(c == '_') - { - if(spec.ext_num_suffix && syntax::num_suffix(spec).scan(loc).is_ok()) - { - return ok(value_t::integer); - } - - if(int_reg.length() <= 2 && (int_reg.as_string() == "0" || - int_reg.as_string() == "-0" || int_reg.as_string() == "+0")) - { - auto src = source_location(region(loc)); - return err(make_error_info( - "bad integer: leading zero is not allowed in decimal int", - std::move(src), "leading zero", - "Hint: valid : -42, 1_000, 1_2_3_4_5, 0xC0FFEE, 0b0010, 0o755\n" - "Hint: invalid: _42, 1__000, 0123\n")); - } - else - { - auto src = source_location(region(loc)); - return err(make_error_info( - "bad integer: `_` must be surrounded by digits", - std::move(src), "invalid underscore", - "Hint: valid : -42, 1_000, 1_2_3_4_5, 0xC0FFEE, 0b0010, 0o755\n" - "Hint: invalid: _42, 1__000, 0123\n")); - } - } - if('0' <= c && c <= '9') - { - if(loc.current() == '0') - { - loc.retrace(); - return err(make_error_info( - "bad integer: leading zero", - source_location(region(loc)), "leading zero is not allowed", - std::string("Hint: valid : -42, 1_000, 1_2_3_4_5, 0xC0FFEE, 0b0010, 0o755\n" - "Hint: invalid: _42, 1__000, 0123\n") - )); - } - else // invalid digits, especially in oct/bin ints. - { - return err(make_error_info( - "bad integer: invalid digit after an integer", - source_location(region(loc)), "this digit is not allowed", - std::string("Hint: valid : -42, 1_000, 1_2_3_4_5, 0xC0FFEE, 0b0010, 0o755\n" - "Hint: invalid: _42, 1__000, 0123\n") - )); - } - } - if(c == ':' || c == '-') - { - auto src = source_location(region(loc)); - return err(make_error_info("bad datetime: invalid format", - std::move(src), "here", - std::string("Hint: valid : 1979-05-27T07:32:00-07:00, 1979-05-27 07:32:00.999999Z\n" - "Hint: invalid: 1979-05-27T7:32:00-7:00, 1979-05-27 7:32-00:30") - )); - } - if(c == '.' || c == 'e' || c == 'E') - { - auto src = source_location(region(loc)); - return err(make_error_info("bad float: invalid format", - std::move(src), "here", std::string( - "Hint: valid : +1.0, -2e-2, 3.141_592_653_589, inf, nan\n" - "Hint: invalid: .0, 1., _1.0, 1.0_, 1_.0, 1.0__0\n"))); - } - } - return ok(value_t::integer); - } - if( ! loc.eof() && loc.current() == '.') - { - auto src = source_location(region(loc)); - return err(make_error_info("bad float: integer part is required before decimal point", - std::move(src), "missing integer part", std::string( - "Hint: valid : +1.0, -2e-2, 3.141_592_653_589, inf, nan\n" - "Hint: invalid: .0, 1., _1.0, 1.0_, 1_.0, 1.0__0\n") - )); - } - if( ! loc.eof() && loc.current() == '_') - { - auto src = source_location(region(loc)); - return err(make_error_info("bad number: `_` must be surrounded by digits", - std::move(src), "digits required before `_`", std::string( - "Hint: valid : -42, 1_000, 1_2_3_4_5, 0xC0FFEE, 0b0010, 0o755\n" - "Hint: invalid: _42, 1__000, 0123\n") - )); - } - - auto src = source_location(region(loc)); - return err(make_error_info("bad format: unknown value appeared", - std::move(src), "here")); -} - -template -result -guess_value_type(const location& loc, const context& ctx) -{ - const auto& sp = ctx.toml_spec(); - location inner(loc); - - switch(loc.current()) - { - case '"' : {return ok(value_t::string); } - case '\'': {return ok(value_t::string); } - case '[' : {return ok(value_t::array); } - case '{' : {return ok(value_t::table); } - case 't' : - { - return ok(value_t::boolean); - } - case 'f' : - { - return ok(value_t::boolean); - } - case 'T' : // invalid boolean. - { - return err(make_syntax_error("toml::parse_value: " - "`true` must be in lowercase. " - "A string must be surrounded by quotes.", - syntax::boolean(sp), inner)); - } - case 'F' : - { - return err(make_syntax_error("toml::parse_value: " - "`false` must be in lowercase. " - "A string must be surrounded by quotes.", - syntax::boolean(sp), inner)); - } - case 'i' : // inf or string without quotes(syntax error). - { - if(literal("inf").scan(inner).is_ok()) - { - return ok(value_t::floating); - } - else - { - return err(make_syntax_error("toml::parse_value: " - "`inf` must be in lowercase. " - "A string must be surrounded by quotes.", - syntax::floating(sp), inner)); - } - } - case 'I' : // Inf or string without quotes(syntax error). - { - return err(make_syntax_error("toml::parse_value: " - "`inf` must be in lowercase. " - "A string must be surrounded by quotes.", - syntax::floating(sp), inner)); - } - case 'n' : // nan or null-extension - { - if(sp.ext_null_value) - { - if(literal("nan").scan(inner).is_ok()) - { - return ok(value_t::floating); - } - else if(literal("null").scan(inner).is_ok()) - { - return ok(value_t::empty); - } - else - { - return err(make_syntax_error("toml::parse_value: " - "Both `nan` and `null` must be in lowercase. " - "A string must be surrounded by quotes.", - syntax::floating(sp), inner)); - } - } - else // must be nan. - { - if(literal("nan").scan(inner).is_ok()) - { - return ok(value_t::floating); - } - else - { - return err(make_syntax_error("toml::parse_value: " - "`nan` must be in lowercase. " - "A string must be surrounded by quotes.", - syntax::floating(sp), inner)); - } - } - } - case 'N' : // nan or null-extension - { - if(sp.ext_null_value) - { - return err(make_syntax_error("toml::parse_value: " - "Both `nan` and `null` must be in lowercase. " - "A string must be surrounded by quotes.", - syntax::floating(sp), inner)); - } - else - { - return err(make_syntax_error("toml::parse_value: " - "`nan` must be in lowercase. " - "A string must be surrounded by quotes.", - syntax::floating(sp), inner)); - } - } - default : - { - return guess_number_type(loc, ctx); - } - } -} - -template -result, error_info> -parse_value(location& loc, context& ctx) -{ - const auto ty_res = guess_value_type(loc, ctx); - if(ty_res.is_err()) - { - return err(ty_res.unwrap_err()); - } - - switch(ty_res.unwrap()) - { - case value_t::empty: - { - if(ctx.toml_spec().ext_null_value) - { - return parse_null(loc, ctx); - } - else - { - auto src = source_location(region(loc)); - return err(make_error_info("toml::parse_value: unknown value appeared", - std::move(src), "here")); - } - } - case value_t::boolean : {return parse_boolean (loc, ctx);} - case value_t::integer : {return parse_integer (loc, ctx);} - case value_t::floating : {return parse_floating (loc, ctx);} - case value_t::string : {return parse_string (loc, ctx);} - case value_t::offset_datetime: {return parse_offset_datetime(loc, ctx);} - case value_t::local_datetime : {return parse_local_datetime (loc, ctx);} - case value_t::local_date : {return parse_local_date (loc, ctx);} - case value_t::local_time : {return parse_local_time (loc, ctx);} - case value_t::array : {return parse_array (loc, ctx);} - case value_t::table : {return parse_inline_table (loc, ctx);} - default: - { - auto src = source_location(region(loc)); - return err(make_error_info("toml::parse_value: unknown value appeared", - std::move(src), "here")); - } - } -} - -/* ============================================================================ - * _____ _ _ - * |_ _|_ _| |__| |___ - * | |/ _` | '_ \ / -_) - * |_|\__,_|_.__/_\___| - */ - -template -result::key_type>, region>, error_info> -parse_table_key(location& loc, context& ctx) -{ - const auto first = loc; - const auto& spec = ctx.toml_spec(); - - auto reg = syntax::std_table(spec).scan(loc); - if(!reg.is_ok()) - { - return err(make_syntax_error("toml::parse_table_key: invalid table key", - syntax::std_table(spec), loc)); - } - - loc = first; - loc.advance(); // skip [ - skip_whitespace(loc, ctx); - - auto keys_res = parse_key(loc, ctx); - if(keys_res.is_err()) - { - return err(std::move(keys_res.unwrap_err())); - } - - skip_whitespace(loc, ctx); - loc.advance(); // ] - - return ok(std::make_pair(std::move(keys_res.unwrap().first), std::move(reg))); -} - -template -result::key_type>, region>, error_info> -parse_array_table_key(location& loc, context& ctx) -{ - const auto first = loc; - const auto& spec = ctx.toml_spec(); - - auto reg = syntax::array_table(spec).scan(loc); - if(!reg.is_ok()) - { - return err(make_syntax_error("toml::parse_array_table_key: invalid array-of-tables key", - syntax::array_table(spec), loc)); - } - - loc = first; - loc.advance(); // [ - loc.advance(); // [ - skip_whitespace(loc, ctx); - - auto keys_res = parse_key(loc, ctx); - if(keys_res.is_err()) - { - return err(std::move(keys_res.unwrap_err())); - } - - skip_whitespace(loc, ctx); - loc.advance(); // ] - loc.advance(); // ] - - return ok(std::make_pair(std::move(keys_res.unwrap().first), std::move(reg))); -} - -// called after reading [table.keys] and comments around it. -// Since table may already contain a subtable ([x.y.z] can be defined before [x]), -// the table that is being parsed is passed as an argument. -template -result -parse_table(location& loc, context& ctx, basic_value& table) -{ - assert(table.is_table()); - - const auto num_errors = ctx.errors().size(); - const auto& spec = ctx.toml_spec(); - - // clear indent info - table.as_table_fmt().indent_type = indent_char::none; - - bool newline_found = true; - while( ! loc.eof()) - { - const auto start = loc; - - auto sp = skip_multiline_spacer(loc, ctx, newline_found); - - // if reached to EOF, the table ends here. return. - if(loc.eof()) - { - break; - } - // if next table is comming, return. - if(sequence(syntax::ws(spec), character('[')).scan(loc).is_ok()) - { - loc = start; - break; - } - // otherwise, it should be a key-value pair. - newline_found = newline_found || (sp.has_value() && sp.value().newline_found); - if( ! newline_found) - { - return err(make_error_info("toml::parse_table: " - "newline (LF / CRLF) or EOF is expected", - source_location(region(loc)), "here")); - } - if(sp.has_value() && sp.value().indent_type != indent_char::none) - { - table.as_table_fmt().indent_type = sp.value().indent_type; - table.as_table_fmt().body_indent = sp.value().indent; - } - - newline_found = false; // reset - if(auto kv_res = parse_key_value_pair(loc, ctx)) - { - auto keys = std::move(kv_res.unwrap().first.first); - auto key_reg = std::move(kv_res.unwrap().first.second); - auto val = std::move(kv_res.unwrap().second); - - if(sp.has_value()) - { - for(const auto& com : sp.value().comments) - { - val.comments().push_back(com); - } - } - - if(auto com_res = parse_comment_line(loc, ctx)) - { - if(auto com_opt = com_res.unwrap()) - { - val.comments().push_back(com_opt.value()); - newline_found = true; // comment includes newline at the end - } - } - else - { - ctx.report_error(std::move(com_res.unwrap_err())); - } - - auto ins_res = insert_value(inserting_value_kind::dotted_keys, - std::addressof(table.as_table()), - keys, std::move(key_reg), std::move(val)); - if(ins_res.is_err()) - { - ctx.report_error(std::move(ins_res.unwrap_err())); - } - } - else - { - ctx.report_error(std::move(kv_res.unwrap_err())); - skip_key_value_pair(loc, ctx); - } - } - - if(num_errors < ctx.errors().size()) - { - assert(ctx.has_error()); // already reported - return err(ctx.pop_last_error()); - } - return ok(); -} - -template -result, std::vector> -parse_file(location& loc, context& ctx) -{ - using value_type = basic_value; - using table_type = typename value_type::table_type; - - const auto first = loc; - const auto& spec = ctx.toml_spec(); - - if(loc.eof()) - { - return ok(value_type(table_type(), table_format_info{}, {}, region(loc))); - } - - value_type root(table_type(), table_format_info{}, {}, region(loc)); - root.as_table_fmt().fmt = table_format::multiline; - root.as_table_fmt().indent_type = indent_char::none; - - // parse top comment. - // - // ```toml - // # this is a comment for the top-level table. - // - // key = "the first value" - // ``` - // - // ```toml - // # this is a comment for "the first value". - // key = "the first value" - // ``` - while( ! loc.eof()) - { - if(auto com_res = parse_comment_line(loc, ctx)) - { - if(auto com_opt = com_res.unwrap()) - { - root.comments().push_back(std::move(com_opt.value())); - } - else // no comment found. - { - // if it is not an empty line, clear the root comment. - if( ! sequence(syntax::ws(spec), syntax::newline(spec)).scan(loc).is_ok()) - { - loc = first; - root.comments().clear(); - } - break; - } - } - else - { - ctx.report_error(std::move(com_res.unwrap_err())); - skip_comment_block(loc, ctx); - } - } - - // parse root table - { - const auto res = parse_table(loc, ctx, root); - if(res.is_err()) - { - ctx.report_error(std::move(res.unwrap_err())); - skip_until_next_table(loc, ctx); - } - } - - // parse tables - - while( ! loc.eof()) - { - auto sp = skip_multiline_spacer(loc, ctx, /*newline_found=*/true); - - if(auto key_res = parse_array_table_key(loc, ctx)) - { - auto key = std::move(std::get<0>(key_res.unwrap())); - auto reg = std::move(std::get<1>(key_res.unwrap())); - - std::vector com; - if(sp.has_value()) - { - for(std::size_t i=0; i(table_type()); - auto res = parse_table(loc, ctx, tmp); - if(res.is_err()) - { - ctx.report_error(res.unwrap_err()); - skip_until_next_table(loc, ctx); - } - continue; - } - - auto tab_ptr = inserted.unwrap(); - assert(tab_ptr); - - const auto tab_res = parse_table(loc, ctx, *tab_ptr); - if(tab_res.is_err()) - { - ctx.report_error(tab_res.unwrap_err()); - skip_until_next_table(loc, ctx); - } - - // parse_table first clears `indent_type`. - // to keep header indent info, we must store it later. - if(sp.has_value() && sp.value().indent_type != indent_char::none) - { - tab_ptr->as_table_fmt().indent_type = sp.value().indent_type; - tab_ptr->as_table_fmt().name_indent = sp.value().indent; - } - continue; - } - if(auto key_res = parse_table_key(loc, ctx)) - { - auto key = std::move(std::get<0>(key_res.unwrap())); - auto reg = std::move(std::get<1>(key_res.unwrap())); - - std::vector com; - if(sp.has_value()) - { - for(std::size_t i=0; i(table_type()); - auto res = parse_table(loc, ctx, tmp); - if(res.is_err()) - { - ctx.report_error(res.unwrap_err()); - skip_until_next_table(loc, ctx); - } - continue; - } - - auto tab_ptr = inserted.unwrap(); - assert(tab_ptr); - - const auto tab_res = parse_table(loc, ctx, *tab_ptr); - if(tab_res.is_err()) - { - ctx.report_error(tab_res.unwrap_err()); - skip_until_next_table(loc, ctx); - } - if(sp.has_value() && sp.value().indent_type != indent_char::none) - { - tab_ptr->as_table_fmt().indent_type = sp.value().indent_type; - tab_ptr->as_table_fmt().name_indent = sp.value().indent; - } - continue; - } - - // does not match array_table nor std_table. report an error. - const auto keytop = loc; - const auto maybe_array_of_tables = literal("[[").scan(loc).is_ok(); - loc = keytop; - - if(maybe_array_of_tables) - { - ctx.report_error(make_syntax_error("toml::parse_file: invalid array-table key", - syntax::array_table(spec), loc)); - } - else - { - ctx.report_error(make_syntax_error("toml::parse_file: invalid table key", - syntax::std_table(spec), loc)); - } - skip_until_next_table(loc, ctx); - } - - if( ! ctx.errors().empty()) - { - return err(std::move(ctx.errors())); - } - return ok(std::move(root)); -} - -template -result, std::vector> -parse_impl(std::vector cs, std::string fname, const spec& s) -{ - using value_type = basic_value; - using table_type = typename value_type::table_type; - - // an empty file is a valid toml file. - if(cs.empty()) - { - auto src = std::make_shared>(std::move(cs)); - location loc(std::move(src), std::move(fname)); - return ok(value_type(table_type(), table_format_info{}, std::vector{}, region(loc))); - } - - // to simplify parser, add newline at the end if there is no LF. - // But, if it has raw CR, the file is invalid (in TOML, CR is not a valid - // newline char). if it ends with CR, do not add LF and report it. - if(cs.back() != '\n' && cs.back() != '\r') - { - cs.push_back('\n'); - } - - auto src = std::make_shared>(std::move(cs)); - - location loc(std::move(src), std::move(fname)); - - // skip BOM if found - if(loc.source()->size() >= 3) - { - auto first = loc.get_location(); - - const auto c0 = loc.current(); loc.advance(); - const auto c1 = loc.current(); loc.advance(); - const auto c2 = loc.current(); loc.advance(); - - const auto bom_found = (c0 == 0xEF) && (c1 == 0xBB) && (c2 == 0xBF); - if( ! bom_found) - { - loc.set_location(first); - } - } - - context ctx(s); - - return parse_file(loc, ctx); -} - -} // detail - -// ----------------------------------------------------------------------------- -// parse(byte array) - -template -result, std::vector> -try_parse(std::vector content, std::string filename, - spec s = spec::default_version()) -{ - return detail::parse_impl(std::move(content), std::move(filename), std::move(s)); -} -template -basic_value -parse(std::vector content, std::string filename, - spec s = spec::default_version()) -{ - auto res = try_parse(std::move(content), std::move(filename), std::move(s)); - if(res.is_ok()) - { - return res.unwrap(); - } - else - { - std::string msg; - for(const auto& err : res.unwrap_err()) - { - msg += format_error(err); - } - throw syntax_error(std::move(msg), std::move(res.unwrap_err())); - } -} - -// ----------------------------------------------------------------------------- -// parse(istream) - -template -result, std::vector> -try_parse(std::istream& is, std::string fname = "unknown file", spec s = spec::default_version()) -{ - const auto beg = is.tellg(); - is.seekg(0, std::ios::end); - const auto end = is.tellg(); - const auto fsize = end - beg; - is.seekg(beg); - - // read whole file as a sequence of char - assert(fsize >= 0); - std::vector letters(static_cast(fsize), '\0'); - is.read(reinterpret_cast(letters.data()), static_cast(fsize)); - - return detail::parse_impl(std::move(letters), std::move(fname), std::move(s)); -} - -template -basic_value parse(std::istream& is, std::string fname = "unknown file", spec s = spec::default_version()) -{ - auto res = try_parse(is, std::move(fname), std::move(s)); - if(res.is_ok()) - { - return res.unwrap(); - } - else - { - std::string msg; - for(const auto& err : res.unwrap_err()) - { - msg += format_error(err); - } - throw syntax_error(std::move(msg), std::move(res.unwrap_err())); - } -} - -// ----------------------------------------------------------------------------- -// parse(filename) - -template -result, std::vector> -try_parse(std::string fname, spec s = spec::default_version()) -{ - std::ifstream ifs(fname, std::ios_base::binary); - if(!ifs.good()) - { - std::vector e; - e.push_back(error_info("toml::parse: Error opening file \"" + fname + "\"", {})); - return err(std::move(e)); - } - ifs.exceptions(std::ifstream::failbit | std::ifstream::badbit); - - return try_parse(ifs, std::move(fname), std::move(s)); -} - -template -basic_value parse(std::string fname, spec s = spec::default_version()) -{ - std::ifstream ifs(fname, std::ios_base::binary); - if(!ifs.good()) - { - throw file_io_error("toml::parse: error opening file", fname); - } - ifs.exceptions(std::ifstream::failbit | std::ifstream::badbit); - - return parse(ifs, std::move(fname), std::move(s)); -} - -template -result, std::vector> -try_parse(const char (&fname)[N], spec s = spec::default_version()) -{ - return try_parse(std::string(fname), std::move(s)); -} - -template -basic_value parse(const char (&fname)[N], spec s = spec::default_version()) -{ - return parse(std::string(fname), std::move(s)); -} - -// ---------------------------------------------------------------------------- -// parse_str - -template -result, std::vector> -try_parse_str(std::string content, spec s = spec::default_version(), - cxx::source_location loc = cxx::source_location::current()) -{ - std::istringstream iss(std::move(content)); - std::string name("internal string" + cxx::to_string(loc)); - return try_parse(iss, std::move(name), std::move(s)); -} - -template -basic_value parse_str(std::string content, spec s = spec::default_version(), - cxx::source_location loc = cxx::source_location::current()) -{ - auto res = try_parse_str(std::move(content), std::move(s), std::move(loc)); - if(res.is_ok()) - { - return res.unwrap(); - } - else - { - std::string msg; - for(const auto& err : res.unwrap_err()) - { - msg += format_error(err); - } - throw syntax_error(std::move(msg), std::move(res.unwrap_err())); - } -} - -// ---------------------------------------------------------------------------- -// filesystem - -#if defined(TOML11_HAS_FILESYSTEM) - -template -cxx::enable_if_t::value, - result, std::vector>> -try_parse(const FSPATH& fpath, spec s = spec::default_version()) -{ - std::ifstream ifs(fpath, std::ios_base::binary); - if(!ifs.good()) - { - std::vector e; - e.push_back(error_info("toml::parse: Error opening file \"" + fpath.string() + "\"", {})); - return err(std::move(e)); - } - ifs.exceptions(std::ifstream::failbit | std::ifstream::badbit); - - return try_parse(ifs, fpath.string(), std::move(s)); -} - -template -cxx::enable_if_t::value, - basic_value> -parse(const FSPATH& fpath, spec s = spec::default_version()) -{ - std::ifstream ifs(fpath, std::ios_base::binary); - if(!ifs.good()) - { - throw file_io_error("toml::parse: error opening file", fpath.string()); - } - ifs.exceptions(std::ifstream::failbit | std::ifstream::badbit); - - return parse(ifs, fpath.string(), std::move(s)); -} -#endif - -// ----------------------------------------------------------------------------- -// FILE* - -template -result, std::vector> -try_parse(FILE* fp, std::string filename, spec s = spec::default_version()) -{ - const long beg = std::ftell(fp); - if (beg == -1L) - { - return err(std::vector{error_info( - std::string("Failed to access: \"") + filename + - "\", errno = " + std::to_string(errno), {} - )}); - } - - const int res_seekend = std::fseek(fp, 0, SEEK_END); - if (res_seekend != 0) - { - return err(std::vector{error_info( - std::string("Failed to seek: \"") + filename + - "\", errno = " + std::to_string(errno), {} - )}); - } - - const long end = std::ftell(fp); - if (end == -1L) - { - return err(std::vector{error_info( - std::string("Failed to access: \"") + filename + - "\", errno = " + std::to_string(errno), {} - )}); - } - - const auto fsize = end - beg; - - const auto res_seekbeg = std::fseek(fp, beg, SEEK_SET); - if (res_seekbeg != 0) - { - return err(std::vector{error_info( - std::string("Failed to seek: \"") + filename + - "\", errno = " + std::to_string(errno), {} - )}); - - } - - // read whole file as a sequence of char - assert(fsize >= 0); - std::vector letters(static_cast(fsize)); - const auto actual = std::fread(letters.data(), sizeof(char), static_cast(fsize), fp); - if(actual != static_cast(fsize)) - { - return err(std::vector{error_info( - std::string("File size changed: \"") + filename + - std::string("\" make sure that FILE* is in binary mode " - "to avoid LF <-> CRLF conversion"), {} - )}); - } - - return detail::parse_impl(std::move(letters), std::move(filename), std::move(s)); -} - -template -basic_value -parse(FILE* fp, std::string filename, spec s = spec::default_version()) -{ - const long beg = std::ftell(fp); - if (beg == -1L) - { - throw file_io_error(errno, "Failed to access", filename); - } - - const int res_seekend = std::fseek(fp, 0, SEEK_END); - if (res_seekend != 0) - { - throw file_io_error(errno, "Failed to seek", filename); - } - - const long end = std::ftell(fp); - if (end == -1L) - { - throw file_io_error(errno, "Failed to access", filename); - } - - const auto fsize = end - beg; - - const auto res_seekbeg = std::fseek(fp, beg, SEEK_SET); - if (res_seekbeg != 0) - { - throw file_io_error(errno, "Failed to seek", filename); - } - - // read whole file as a sequence of char - assert(fsize >= 0); - std::vector letters(static_cast(fsize)); - const auto actual = std::fread(letters.data(), sizeof(char), static_cast(fsize), fp); - if(actual != static_cast(fsize)) - { - throw file_io_error(errno, "File size changed; make sure that " - "FILE* is in binary mode to avoid LF <-> CRLF conversion", filename); - } - - auto res = detail::parse_impl(std::move(letters), std::move(filename), std::move(s)); - if(res.is_ok()) - { - return res.unwrap(); - } - else - { - std::string msg; - for(const auto& err : res.unwrap_err()) - { - msg += format_error(err); - } - throw syntax_error(std::move(msg), std::move(res.unwrap_err())); - } -} - -} // namespace toml - -#if defined(TOML11_COMPILE_SOURCES) -namespace toml -{ -struct type_config; -struct ordered_type_config; - -extern template result, std::vector> try_parse(std::vector, std::string, spec); -extern template result, std::vector> try_parse(std::istream&, std::string, spec); -extern template result, std::vector> try_parse(std::string, spec); -extern template result, std::vector> try_parse(FILE*, std::string, spec); -extern template result, std::vector> try_parse_str(std::string, spec, cxx::source_location); - -extern template basic_value parse(std::vector, std::string, spec); -extern template basic_value parse(std::istream&, std::string, spec); -extern template basic_value parse(std::string, spec); -extern template basic_value parse(FILE*, std::string, spec); -extern template basic_value parse_str(std::string, spec, cxx::source_location); - -extern template result, std::vector> try_parse(std::vector, std::string, spec); -extern template result, std::vector> try_parse(std::istream&, std::string, spec); -extern template result, std::vector> try_parse(std::string, spec); -extern template result, std::vector> try_parse(FILE*, std::string, spec); -extern template result, std::vector> try_parse_str(std::string, spec, cxx::source_location); - -extern template basic_value parse(std::vector, std::string, spec); -extern template basic_value parse(std::istream&, std::string, spec); -extern template basic_value parse(std::string, spec); -extern template basic_value parse(FILE*, std::string, spec); -extern template basic_value parse_str(std::string, spec, cxx::source_location); - -#if defined(TOML11_HAS_FILESYSTEM) -extern template cxx::enable_if_t::value, result, std::vector>> try_parse(const std::filesystem::path&, spec); -extern template cxx::enable_if_t::value, result, std::vector>> try_parse(const std::filesystem::path&, spec); -extern template cxx::enable_if_t::value, basic_value > parse (const std::filesystem::path&, spec); -extern template cxx::enable_if_t::value, basic_value > parse (const std::filesystem::path&, spec); -#endif // filesystem - -} // toml -#endif // TOML11_COMPILE_SOURCES - -#endif // TOML11_PARSER_HPP -#ifndef TOML11_LITERAL_HPP -#define TOML11_LITERAL_HPP - -#ifndef TOML11_LITERAL_FWD_HPP -#define TOML11_LITERAL_FWD_HPP - - -namespace toml -{ - -namespace detail -{ -// implementation -::toml::value literal_internal_impl(location loc); -} // detail - -inline namespace literals -{ -inline namespace toml_literals -{ - -::toml::value operator"" _toml(const char* str, std::size_t len); - -#if defined(TOML11_HAS_CHAR8_T) -// value of u8"" literal has been changed from char to char8_t and char8_t is -// NOT compatible to char -::toml::value operator"" _toml(const char8_t* str, std::size_t len); -#endif - -} // toml_literals -} // literals -} // toml -#endif // TOML11_LITERAL_FWD_HPP - -#if ! defined(TOML11_COMPILE_SOURCES) -#ifndef TOML11_LITERAL_IMPL_HPP -#define TOML11_LITERAL_IMPL_HPP - - -namespace toml -{ - -namespace detail -{ -// implementation -TOML11_INLINE ::toml::value literal_internal_impl(location loc) -{ - const auto s = ::toml::spec::default_version(); - context ctx(s); - - const auto front = loc; - - // ------------------------------------------------------------------------ - // check if it is a raw value. - - // skip empty lines and comment lines - auto sp = skip_multiline_spacer(loc, ctx); - if(loc.eof()) - { - ::toml::value val; - if(sp.has_value()) - { - for(std::size_t i=0; i(str), - reinterpret_cast(str + len), - c.begin()); - if( ! c.empty() && c.back()) - { - c.push_back('\n'); // to make it easy to parse comment, we add newline - } - - return literal_internal_impl(::toml::detail::location( - std::make_shared(std::move(c)), - "TOML literal encoded in a C++ code")); -} - -#if defined(__cpp_char8_t) -# if __cpp_char8_t >= 201811L -# define TOML11_HAS_CHAR8_T 1 -# endif -#endif - -#if defined(TOML11_HAS_CHAR8_T) -// value of u8"" literal has been changed from char to char8_t and char8_t is -// NOT compatible to char -TOML11_INLINE ::toml::value -operator"" _toml(const char8_t* str, std::size_t len) -{ - if(len == 0) - { - return ::toml::value{}; - } - - ::toml::detail::location::container_type c(len); - std::copy(reinterpret_cast(str), - reinterpret_cast(str + len), - c.begin()); - if( ! c.empty() && c.back()) - { - c.push_back('\n'); // to make it easy to parse comment, we add newline - } - - return literal_internal_impl(::toml::detail::location( - std::make_shared(std::move(c)), - "TOML literal encoded in a C++ code")); -} -#endif - -} // toml_literals -} // literals -} // toml -#endif // TOML11_LITERAL_IMPL_HPP -#endif - -#endif // TOML11_LITERAL_HPP -#ifndef TOML11_SERIALIZER_HPP -#define TOML11_SERIALIZER_HPP - - -#include -#include -#include - -#include -#include - -namespace toml -{ - -struct serialization_error final : public ::toml::exception -{ - public: - explicit serialization_error(std::string what_arg, source_location loc) - : what_(std::move(what_arg)), loc_(std::move(loc)) - {} - ~serialization_error() noexcept override = default; - - const char* what() const noexcept override {return what_.c_str();} - source_location const& location() const noexcept {return loc_;} - - private: - std::string what_; - source_location loc_; -}; - -namespace detail -{ -template -class serializer -{ - public: - - using value_type = basic_value; - - using key_type = typename value_type::key_type ; - using comment_type = typename value_type::comment_type ; - using boolean_type = typename value_type::boolean_type ; - using integer_type = typename value_type::integer_type ; - using floating_type = typename value_type::floating_type ; - using string_type = typename value_type::string_type ; - using local_time_type = typename value_type::local_time_type ; - using local_date_type = typename value_type::local_date_type ; - using local_datetime_type = typename value_type::local_datetime_type ; - using offset_datetime_type = typename value_type::offset_datetime_type; - using array_type = typename value_type::array_type ; - using table_type = typename value_type::table_type ; - - using char_type = typename string_type::value_type; - - public: - - explicit serializer(const spec& sp) - : spec_(sp), force_inline_(false), current_indent_(0) - {} - - string_type operator()(const std::vector& ks, const value_type& v) - { - for(const auto& k : ks) - { - this->keys_.push_back(k); - } - return (*this)(v); - } - - string_type operator()(const key_type& k, const value_type& v) - { - this->keys_.push_back(k); - return (*this)(v); - } - - string_type operator()(const value_type& v) - { - switch(v.type()) - { - case value_t::boolean : {return (*this)(v.as_boolean (), v.as_boolean_fmt (), v.location());} - case value_t::integer : {return (*this)(v.as_integer (), v.as_integer_fmt (), v.location());} - case value_t::floating : {return (*this)(v.as_floating (), v.as_floating_fmt (), v.location());} - case value_t::string : {return (*this)(v.as_string (), v.as_string_fmt (), v.location());} - case value_t::offset_datetime: {return (*this)(v.as_offset_datetime(), v.as_offset_datetime_fmt(), v.location());} - case value_t::local_datetime : {return (*this)(v.as_local_datetime (), v.as_local_datetime_fmt (), v.location());} - case value_t::local_date : {return (*this)(v.as_local_date (), v.as_local_date_fmt (), v.location());} - case value_t::local_time : {return (*this)(v.as_local_time (), v.as_local_time_fmt (), v.location());} - case value_t::array : - { - return (*this)(v.as_array(), v.as_array_fmt(), v.comments(), v.location()); - } - case value_t::table : - { - string_type retval; - if(this->keys_.empty()) // it might be the root table. emit comments here. - { - retval += format_comments(v.comments(), v.as_table_fmt().indent_type); - } - if( ! retval.empty()) // we have comment. - { - retval += char_type('\n'); - } - - retval += (*this)(v.as_table(), v.as_table_fmt(), v.comments(), v.location()); - return retval; - } - case value_t::empty: - { - if(this->spec_.ext_null_value) - { - return string_conv("null"); - } - break; - } - default: - { - break; - } - } - throw serialization_error(format_error( - "[error] toml::serializer: toml::basic_value " - "does not have any valid type.", v.location(), "here"), v.location()); - } - - private: - - string_type operator()(const boolean_type& b, const boolean_format_info&, const source_location&) // {{{ - { - if(b) - { - return string_conv("true"); - } - else - { - return string_conv("false"); - } - } // }}} - - string_type operator()(const integer_type i, const integer_format_info& fmt, const source_location& loc) // {{{ - { - std::ostringstream oss; - this->set_locale(oss); - - const auto insert_spacer = [&fmt](std::string s) -> std::string { - if(fmt.spacer == 0) {return s;} - - std::string sign; - if( ! s.empty() && (s.at(0) == '+' || s.at(0) == '-')) - { - sign += s.at(0); - s.erase(s.begin()); - } - - std::string spaced; - std::size_t counter = 0; - for(auto iter = s.rbegin(); iter != s.rend(); ++iter) - { - if(counter != 0 && counter % fmt.spacer == 0) - { - spaced += '_'; - } - spaced += *iter; - counter += 1; - } - if(!spaced.empty() && spaced.back() == '_') {spaced.pop_back();} - - s.clear(); - std::copy(spaced.rbegin(), spaced.rend(), std::back_inserter(s)); - return sign + s; - }; - - std::string retval; - if(fmt.fmt == integer_format::dec) - { - oss << std::setw(static_cast(fmt.width)) << std::dec << i; - retval = insert_spacer(oss.str()); - - if(this->spec_.ext_num_suffix && ! fmt.suffix.empty()) - { - retval += '_'; - retval += fmt.suffix; - } - } - else - { - if(i < 0) - { - throw serialization_error(format_error("binary, octal, hexadecimal " - "integer does not allow negative value", loc, "here"), loc); - } - switch(fmt.fmt) - { - case integer_format::hex: - { - oss << std::noshowbase - << std::setw(static_cast(fmt.width)) - << std::setfill('0') - << std::hex; - if(fmt.uppercase) - { - oss << std::uppercase; - } - else - { - oss << std::nouppercase; - } - oss << i; - retval = std::string("0x") + insert_spacer(oss.str()); - break; - } - case integer_format::oct: - { - oss << std::setw(static_cast(fmt.width)) << std::setfill('0') << std::oct << i; - retval = std::string("0o") + insert_spacer(oss.str()); - break; - } - case integer_format::bin: - { - integer_type x{i}; - std::string tmp; - std::size_t bits(0); - while(x != 0) - { - if(fmt.spacer != 0) - { - if(bits != 0 && (bits % fmt.spacer) == 0) {tmp += '_';} - } - if(x % 2 == 1) { tmp += '1'; } else { tmp += '0'; } - x >>= 1; - bits += 1; - } - for(; bits < fmt.width; ++bits) - { - if(fmt.spacer != 0) - { - if(bits != 0 && (bits % fmt.spacer) == 0) {tmp += '_';} - } - tmp += '0'; - } - for(auto iter = tmp.rbegin(); iter != tmp.rend(); ++iter) - { - oss << *iter; - } - retval = std::string("0b") + oss.str(); - break; - } - default: - { - throw serialization_error(format_error( - "none of dec, hex, oct, bin: " + to_string(fmt.fmt), - loc, "here"), loc); - } - } - } - return string_conv(retval); - } // }}} - - string_type operator()(const floating_type f, const floating_format_info& fmt, const source_location&) // {{{ - { - using std::isnan; - using std::isinf; - using std::signbit; - - std::ostringstream oss; - this->set_locale(oss); - - if(isnan(f)) - { - if(signbit(f)) - { - oss << '-'; - } - oss << "nan"; - if(this->spec_.ext_num_suffix && ! fmt.suffix.empty()) - { - oss << '_'; - oss << fmt.suffix; - } - return string_conv(oss.str()); - } - - if(isinf(f)) - { - if(signbit(f)) - { - oss << '-'; - } - oss << "inf"; - if(this->spec_.ext_num_suffix && ! fmt.suffix.empty()) - { - oss << '_'; - oss << fmt.suffix; - } - return string_conv(oss.str()); - } - - switch(fmt.fmt) - { - case floating_format::defaultfloat: - { - if(fmt.prec != 0) - { - oss << std::setprecision(static_cast(fmt.prec)); - } - oss << f; - // since defaultfloat may omit point, we need to add it - std::string s = oss.str(); - if (s.find('.') == std::string::npos && - s.find('e') == std::string::npos && - s.find('E') == std::string::npos ) - { - s += ".0"; - } - if(this->spec_.ext_num_suffix && ! fmt.suffix.empty()) - { - s += '_'; - s += fmt.suffix; - } - return string_conv(s); - } - case floating_format::fixed: - { - if(fmt.prec != 0) - { - oss << std::setprecision(static_cast(fmt.prec)); - } - oss << std::fixed << f; - if(this->spec_.ext_num_suffix && ! fmt.suffix.empty()) - { - oss << '_' << fmt.suffix; - } - return string_conv(oss.str()); - } - case floating_format::scientific: - { - if(fmt.prec != 0) - { - oss << std::setprecision(static_cast(fmt.prec)); - } - oss << std::scientific << f; - if(this->spec_.ext_num_suffix && ! fmt.suffix.empty()) - { - oss << '_' << fmt.suffix; - } - return string_conv(oss.str()); - } - case floating_format::hex: - { - if(this->spec_.ext_hex_float) - { - oss << std::hexfloat << f; - // suffix is only for decimal numbers. - return string_conv(oss.str()); - } - else // no hex allowed. output with max precision. - { - oss << std::setprecision(std::numeric_limits::max_digits10) - << std::scientific << f; - // suffix is only for decimal numbers. - return string_conv(oss.str()); - } - } - default: - { - if(this->spec_.ext_num_suffix && ! fmt.suffix.empty()) - { - oss << '_' << fmt.suffix; - } - return string_conv(oss.str()); - } - } - } // }}} - - string_type operator()(string_type s, const string_format_info& fmt, const source_location& loc) // {{{ - { - string_type retval; - switch(fmt.fmt) - { - case string_format::basic: - { - retval += char_type('"'); - retval += this->escape_basic_string(s); - retval += char_type('"'); - return retval; - } - case string_format::literal: - { - if(std::find(s.begin(), s.end(), char_type('\n')) != s.end()) - { - throw serialization_error(format_error("toml::serializer: " - "(non-multiline) literal string cannot have a newline", - loc, "here"), loc); - } - retval += char_type('\''); - retval += s; - retval += char_type('\''); - return retval; - } - case string_format::multiline_basic: - { - retval += string_conv("\"\"\""); - if(fmt.start_with_newline) - { - retval += char_type('\n'); - } - - retval += this->escape_ml_basic_string(s); - - retval += string_conv("\"\"\""); - return retval; - } - case string_format::multiline_literal: - { - retval += string_conv("'''"); - if(fmt.start_with_newline) - { - retval += char_type('\n'); - } - retval += s; - retval += string_conv("'''"); - return retval; - } - default: - { - throw serialization_error(format_error( - "[error] toml::serializer::operator()(string): " - "invalid string_format value", loc, "here"), loc); - } - } - } // }}} - - string_type operator()(const local_date_type& d, const local_date_format_info&, const source_location&) // {{{ - { - std::ostringstream oss; - oss << d; - return string_conv(oss.str()); - } // }}} - - string_type operator()(const local_time_type& t, const local_time_format_info& fmt, const source_location&) // {{{ - { - return this->format_local_time(t, fmt.has_seconds, fmt.subsecond_precision); - } // }}} - - string_type operator()(const local_datetime_type& dt, const local_datetime_format_info& fmt, const source_location&) // {{{ - { - std::ostringstream oss; - oss << dt.date; - switch(fmt.delimiter) - { - case datetime_delimiter_kind::upper_T: { oss << 'T'; break; } - case datetime_delimiter_kind::lower_t: { oss << 't'; break; } - case datetime_delimiter_kind::space: { oss << ' '; break; } - default: { oss << 'T'; break; } - } - return string_conv(oss.str()) + - this->format_local_time(dt.time, fmt.has_seconds, fmt.subsecond_precision); - } // }}} - - string_type operator()(const offset_datetime_type& odt, const offset_datetime_format_info& fmt, const source_location&) // {{{ - { - std::ostringstream oss; - oss << odt.date; - switch(fmt.delimiter) - { - case datetime_delimiter_kind::upper_T: { oss << 'T'; break; } - case datetime_delimiter_kind::lower_t: { oss << 't'; break; } - case datetime_delimiter_kind::space: { oss << ' '; break; } - default: { oss << 'T'; break; } - } - oss << string_conv(this->format_local_time(odt.time, fmt.has_seconds, fmt.subsecond_precision)); - oss << odt.offset; - return string_conv(oss.str()); - } // }}} - - string_type operator()(const array_type& a, const array_format_info& fmt, const comment_type& com, const source_location& loc) // {{{ - { - array_format f = fmt.fmt; - if(fmt.fmt == array_format::default_format) - { - // [[in.this.form]], you cannot add a comment to the array itself - // (but you can add a comment to each table). - // To keep comments, we need to avoid multiline array-of-tables - // if array itself has a comment. - if( ! this->keys_.empty() && - ! a.empty() && - com.empty() && - std::all_of(a.begin(), a.end(), [](const value_type& e) {return e.is_table();})) - { - f = array_format::array_of_tables; - } - else - { - f = array_format::oneline; - - // check if it becomes long - std::size_t approx_len = 0; - for(const auto& e : a) - { - // have a comment. cannot be inlined - if( ! e.comments().empty()) - { - f = array_format::multiline; - break; - } - // possibly long types ... - if(e.is_array() || e.is_table() || e.is_offset_datetime() || e.is_local_datetime()) - { - f = array_format::multiline; - break; - } - else if(e.is_boolean()) - { - approx_len += (*this)(e.as_boolean(), e.as_boolean_fmt(), e.location()).size(); - } - else if(e.is_integer()) - { - approx_len += (*this)(e.as_integer(), e.as_integer_fmt(), e.location()).size(); - } - else if(e.is_floating()) - { - approx_len += (*this)(e.as_floating(), e.as_floating_fmt(), e.location()).size(); - } - else if(e.is_string()) - { - if(e.as_string_fmt().fmt == string_format::multiline_basic || - e.as_string_fmt().fmt == string_format::multiline_literal) - { - f = array_format::multiline; - break; - } - approx_len += 2 + (*this)(e.as_string(), e.as_string_fmt(), e.location()).size(); - } - else if(e.is_local_date()) - { - approx_len += 10; // 1234-56-78 - } - else if(e.is_local_time()) - { - approx_len += 15; // 12:34:56.789012 - } - - if(approx_len > 60) // key, ` = `, `[...]` < 80 - { - f = array_format::multiline; - break; - } - approx_len += 2; // `, ` - } - } - } - if(this->force_inline_ && f == array_format::array_of_tables) - { - f = array_format::multiline; - } - if(a.empty() && f == array_format::array_of_tables) - { - f = array_format::oneline; - } - - // -------------------------------------------------------------------- - - if(f == array_format::array_of_tables) - { - if(this->keys_.empty()) - { - throw serialization_error("array of table must have its key. " - "use format(key, v)", loc); - } - string_type retval; - for(const auto& e : a) - { - assert(e.is_table()); - - this->current_indent_ += e.as_table_fmt().name_indent; - retval += this->format_comments(e.comments(), e.as_table_fmt().indent_type); - retval += this->format_indent(e.as_table_fmt().indent_type); - this->current_indent_ -= e.as_table_fmt().name_indent; - - retval += string_conv("[["); - retval += this->format_keys(this->keys_).value(); - retval += string_conv("]]\n"); - - retval += this->format_ml_table(e.as_table(), e.as_table_fmt()); - } - return retval; - } - else if(f == array_format::oneline) - { - // ignore comments. we cannot emit comments - string_type retval; - retval += char_type('['); - for(const auto& e : a) - { - this->force_inline_ = true; - retval += (*this)(e); - retval += string_conv(", "); - } - if( ! a.empty()) - { - retval.pop_back(); // ` ` - retval.pop_back(); // `,` - } - retval += char_type(']'); - this->force_inline_ = false; - return retval; - } - else - { - assert(f == array_format::multiline); - - string_type retval; - retval += string_conv("[\n"); - - for(const auto& e : a) - { - this->current_indent_ += fmt.body_indent; - retval += this->format_comments(e.comments(), fmt.indent_type); - retval += this->format_indent(fmt.indent_type); - this->current_indent_ -= fmt.body_indent; - - this->force_inline_ = true; - retval += (*this)(e); - retval += string_conv(",\n"); - } - this->force_inline_ = false; - - this->current_indent_ += fmt.closing_indent; - retval += this->format_indent(fmt.indent_type); - this->current_indent_ -= fmt.closing_indent; - - retval += char_type(']'); - return retval; - } - } // }}} - - string_type operator()(const table_type& t, const table_format_info& fmt, const comment_type& com, const source_location& loc) // {{{ - { - if(this->force_inline_) - { - if(fmt.fmt == table_format::multiline_oneline) - { - return this->format_ml_inline_table(t, fmt); - } - else - { - return this->format_inline_table(t, fmt); - } - } - else - { - if(fmt.fmt == table_format::multiline) - { - string_type retval; - // comment is emitted inside format_ml_table - if(auto k = this->format_keys(this->keys_)) - { - this->current_indent_ += fmt.name_indent; - retval += this->format_comments(com, fmt.indent_type); - retval += this->format_indent(fmt.indent_type); - this->current_indent_ -= fmt.name_indent; - retval += char_type('['); - retval += k.value(); - retval += string_conv("]\n"); - } - // otherwise, its the root. - - retval += this->format_ml_table(t, fmt); - return retval; - } - else if(fmt.fmt == table_format::oneline) - { - return this->format_inline_table(t, fmt); - } - else if(fmt.fmt == table_format::multiline_oneline) - { - return this->format_ml_inline_table(t, fmt); - } - else if(fmt.fmt == table_format::dotted) - { - std::vector keys; - if(this->keys_.empty()) - { - throw serialization_error(format_error("toml::serializer: " - "dotted table must have its key. use format(key, v)", - loc, "here"), loc); - } - keys.push_back(this->keys_.back()); - - const auto retval = this->format_dotted_table(t, fmt, loc, keys); - keys.pop_back(); - return retval; - } - else - { - assert(fmt.fmt == table_format::implicit); - - string_type retval; - for(const auto& kv : t) - { - const auto& k = kv.first; - const auto& v = kv.second; - - if( ! v.is_table() && ! v.is_array_of_tables()) - { - throw serialization_error(format_error("toml::serializer: " - "an implicit table cannot have non-table value.", - v.location(), "here"), v.location()); - } - if(v.is_table()) - { - if(v.as_table_fmt().fmt != table_format::multiline && - v.as_table_fmt().fmt != table_format::implicit) - { - throw serialization_error(format_error("toml::serializer: " - "an implicit table cannot have non-multiline table", - v.location(), "here"), v.location()); - } - } - else - { - assert(v.is_array()); - for(const auto& e : v.as_array()) - { - if(e.as_table_fmt().fmt != table_format::multiline && - v.as_table_fmt().fmt != table_format::implicit) - { - throw serialization_error(format_error("toml::serializer: " - "an implicit table cannot have non-multiline table", - e.location(), "here"), e.location()); - } - } - } - - keys_.push_back(k); - retval += (*this)(v); - keys_.pop_back(); - } - return retval; - } - } - } // }}} - - private: - - string_type escape_basic_string(const string_type& s) const // {{{ - { - string_type retval; - for(const char_type c : s) - { - switch(c) - { - case char_type('\\'): {retval += string_conv("\\\\"); break;} - case char_type('\"'): {retval += string_conv("\\\""); break;} - case char_type('\b'): {retval += string_conv("\\b" ); break;} - case char_type('\t'): {retval += string_conv("\\t" ); break;} - case char_type('\f'): {retval += string_conv("\\f" ); break;} - case char_type('\n'): {retval += string_conv("\\n" ); break;} - case char_type('\r'): {retval += string_conv("\\r" ); break;} - default : - { - if(c == char_type(0x1B) && spec_.v1_1_0_add_escape_sequence_e) - { - retval += string_conv("\\e"); - } - else if((char_type(0x00) <= c && c <= char_type(0x08)) || - (char_type(0x0A) <= c && c <= char_type(0x1F)) || - c == char_type(0x7F)) - { - if(spec_.v1_1_0_add_escape_sequence_x) - { - retval += string_conv("\\x"); - } - else - { - retval += string_conv("\\u00"); - } - const auto c1 = c / 16; - const auto c2 = c % 16; - retval += static_cast('0' + c1); - if(c2 < 10) - { - retval += static_cast('0' + c2); - } - else // 10 <= c2 - { - retval += static_cast('A' + (c2 - 10)); - } - } - else - { - retval += c; - } - } - } - } - return retval; - } // }}} - - string_type escape_ml_basic_string(const string_type& s) // {{{ - { - string_type retval; - for(const char_type c : s) - { - switch(c) - { - case char_type('\\'): {retval += string_conv("\\\\"); break;} - case char_type('\b'): {retval += string_conv("\\b" ); break;} - case char_type('\t'): {retval += string_conv("\\t" ); break;} - case char_type('\f'): {retval += string_conv("\\f" ); break;} - case char_type('\n'): {retval += string_conv("\n" ); break;} - case char_type('\r'): {retval += string_conv("\\r" ); break;} - default : - { - if(c == char_type(0x1B) && spec_.v1_1_0_add_escape_sequence_e) - { - retval += string_conv("\\e"); - } - else if((char_type(0x00) <= c && c <= char_type(0x08)) || - (char_type(0x0A) <= c && c <= char_type(0x1F)) || - c == char_type(0x7F)) - { - if(spec_.v1_1_0_add_escape_sequence_x) - { - retval += string_conv("\\x"); - } - else - { - retval += string_conv("\\u00"); - } - const auto c1 = c / 16; - const auto c2 = c % 16; - retval += static_cast('0' + c1); - if(c2 < 10) - { - retval += static_cast('0' + c2); - } - else // 10 <= c2 - { - retval += static_cast('A' + (c2 - 10)); - } - } - else - { - retval += c; - } - } - } - } - // Only 1 or 2 consecutive `"`s are allowed in multiline basic string. - // 3 consecutive `"`s are considered as a closing delimiter. - // We need to check if there are 3 or more consecutive `"`s and insert - // backslash to break them down into several short `"`s like the `str6` - // in the following example. - // ```toml - // str4 = """Here are two quotation marks: "". Simple enough.""" - // # str5 = """Here are three quotation marks: """.""" # INVALID - // str5 = """Here are three quotation marks: ""\".""" - // str6 = """Here are fifteen quotation marks: ""\"""\"""\"""\"""\".""" - // ``` - auto found_3_quotes = retval.find(string_conv("\"\"\"")); - while(found_3_quotes != string_type::npos) - { - retval.replace(found_3_quotes, 3, string_conv("\"\"\\\"")); - found_3_quotes = retval.find(string_conv("\"\"\"")); - } - return retval; - } // }}} - - string_type format_local_time(const local_time_type& t, const bool has_seconds, const std::size_t subsec_prec) // {{{ - { - std::ostringstream oss; - oss << std::setfill('0') << std::setw(2) << static_cast(t.hour); - oss << ':'; - oss << std::setfill('0') << std::setw(2) << static_cast(t.minute); - if(has_seconds) - { - oss << ':'; - oss << std::setfill('0') << std::setw(2) << static_cast(t.second); - if(subsec_prec != 0) - { - std::ostringstream subsec; - subsec << std::setfill('0') << std::setw(3) << static_cast(t.millisecond); - subsec << std::setfill('0') << std::setw(3) << static_cast(t.microsecond); - subsec << std::setfill('0') << std::setw(3) << static_cast(t.nanosecond); - std::string subsec_str = subsec.str(); - oss << '.' << subsec_str.substr(0, subsec_prec); - } - } - return string_conv(oss.str()); - } // }}} - - string_type format_ml_table(const table_type& t, const table_format_info& fmt) // {{{ - { - const auto format_later = [](const value_type& v) -> bool { - - const bool is_ml_table = v.is_table() && - v.as_table_fmt().fmt != table_format::oneline && - v.as_table_fmt().fmt != table_format::multiline_oneline && - v.as_table_fmt().fmt != table_format::dotted ; - - const bool is_ml_array_table = v.is_array_of_tables() && - v.as_array_fmt().fmt != array_format::oneline && - v.as_array_fmt().fmt != array_format::multiline; - - return is_ml_table || is_ml_array_table; - }; - - string_type retval; - this->current_indent_ += fmt.body_indent; - for(const auto& kv : t) - { - const auto& key = kv.first; - const auto& val = kv.second; - if(format_later(val)) - { - continue; - } - this->keys_.push_back(key); - - retval += format_comments(val.comments(), fmt.indent_type); - retval += format_indent(fmt.indent_type); - if(val.is_table() && val.as_table_fmt().fmt == table_format::dotted) - { - retval += (*this)(val); - } - else - { - retval += format_key(key); - retval += string_conv(" = "); - retval += (*this)(val); - retval += char_type('\n'); - } - this->keys_.pop_back(); - } - this->current_indent_ -= fmt.body_indent; - - if( ! retval.empty()) - { - retval += char_type('\n'); // for readability, add empty line between tables - } - for(const auto& kv : t) - { - if( ! format_later(kv.second)) - { - continue; - } - // must be a [multiline.table] or [[multiline.array.of.tables]]. - // comments will be generated inside it. - this->keys_.push_back(kv.first); - retval += (*this)(kv.second); - this->keys_.pop_back(); - } - return retval; - } // }}} - - string_type format_inline_table(const table_type& t, const table_format_info&) // {{{ - { - // comments are ignored because we cannot write without newline - string_type retval; - retval += char_type('{'); - for(const auto& kv : t) - { - this->force_inline_ = true; - retval += this->format_key(kv.first); - retval += string_conv(" = "); - retval += (*this)(kv.second); - retval += string_conv(", "); - } - if( ! t.empty()) - { - retval.pop_back(); // ' ' - retval.pop_back(); // ',' - } - retval += char_type('}'); - this->force_inline_ = false; - return retval; - } // }}} - - string_type format_ml_inline_table(const table_type& t, const table_format_info& fmt) // {{{ - { - string_type retval; - retval += string_conv("{\n"); - this->current_indent_ += fmt.body_indent; - for(const auto& kv : t) - { - this->force_inline_ = true; - retval += format_comments(kv.second.comments(), fmt.indent_type); - retval += format_indent(fmt.indent_type); - retval += kv.first; - retval += string_conv(" = "); - - this->force_inline_ = true; - retval += (*this)(kv.second); - - retval += string_conv(",\n"); - } - if( ! t.empty()) - { - retval.pop_back(); // '\n' - retval.pop_back(); // ',' - } - this->current_indent_ -= fmt.body_indent; - this->force_inline_ = false; - - this->current_indent_ += fmt.closing_indent; - retval += format_indent(fmt.indent_type); - this->current_indent_ -= fmt.closing_indent; - - retval += char_type('}'); - return retval; - } // }}} - - string_type format_dotted_table(const table_type& t, const table_format_info& fmt, // {{{ - const source_location&, std::vector& keys) - { - // lets say we have: `{"a": {"b": {"c": {"d": "foo", "e": "bar"} } }` - // and `a` and `b` are `dotted`. - // - // - in case if `c` is `oneline`: - // ```toml - // a.b.c = {d = "foo", e = "bar"} - // ``` - // - // - in case if and `c` is `dotted`: - // ```toml - // a.b.c.d = "foo" - // a.b.c.e = "bar" - // ``` - - string_type retval; - - for(const auto& kv : t) - { - const auto& key = kv.first; - const auto& val = kv.second; - - keys.push_back(key); - - // format recursive dotted table? - if (val.is_table() && - val.as_table_fmt().fmt != table_format::oneline && - val.as_table_fmt().fmt != table_format::multiline_oneline) - { - retval += this->format_dotted_table(val.as_table(), val.as_table_fmt(), val.location(), keys); - } - else // non-table or inline tables. format normally - { - retval += format_comments(val.comments(), fmt.indent_type); - retval += format_indent(fmt.indent_type); - retval += format_keys(keys).value(); - retval += string_conv(" = "); - this->force_inline_ = true; // sub-table must be inlined - retval += (*this)(val); - retval += char_type('\n'); - this->force_inline_ = false; - } - keys.pop_back(); - } - return retval; - } // }}} - - string_type format_key(const key_type& key) // {{{ - { - if(key.empty()) - { - return string_conv("\"\""); - } - - // check the key can be a bare (unquoted) key - auto loc = detail::make_temporary_location(string_conv(key)); - auto reg = detail::syntax::unquoted_key(this->spec_).scan(loc); - if(reg.is_ok() && loc.eof()) - { - return key; - } - - //if it includes special characters, then format it in a "quoted" key. - string_type formatted = string_conv("\""); - for(const char_type c : key) - { - switch(c) - { - case char_type('\\'): {formatted += string_conv("\\\\"); break;} - case char_type('\"'): {formatted += string_conv("\\\""); break;} - case char_type('\b'): {formatted += string_conv("\\b" ); break;} - case char_type('\t'): {formatted += string_conv("\\t" ); break;} - case char_type('\f'): {formatted += string_conv("\\f" ); break;} - case char_type('\n'): {formatted += string_conv("\\n" ); break;} - case char_type('\r'): {formatted += string_conv("\\r" ); break;} - default : - { - // ASCII ctrl char - if( (char_type(0x00) <= c && c <= char_type(0x08)) || - (char_type(0x0A) <= c && c <= char_type(0x1F)) || - c == char_type(0x7F)) - { - if(spec_.v1_1_0_add_escape_sequence_x) - { - formatted += string_conv("\\x"); - } - else - { - formatted += string_conv("\\u00"); - } - const auto c1 = c / 16; - const auto c2 = c % 16; - formatted += static_cast('0' + c1); - if(c2 < 10) - { - formatted += static_cast('0' + c2); - } - else // 10 <= c2 - { - formatted += static_cast('A' + (c2 - 10)); - } - } - else - { - formatted += c; - } - break; - } - } - } - formatted += string_conv("\""); - return formatted; - } // }}} - cxx::optional format_keys(const std::vector& keys) // {{{ - { - if(keys.empty()) - { - return cxx::make_nullopt(); - } - - string_type formatted; - for(const auto& ky : keys) - { - formatted += format_key(ky); - formatted += char_type('.'); - } - formatted.pop_back(); // remove the last dot '.' - return formatted; - } // }}} - - string_type format_comments(const discard_comments&, const indent_char) const // {{{ - { - return string_conv(""); - } // }}} - string_type format_comments(const preserve_comments& comments, const indent_char indent_type) const // {{{ - { - string_type retval; - for(const auto& c : comments) - { - if(c.empty()) {continue;} - retval += format_indent(indent_type); - if(c.front() != '#') {retval += char_type('#');} - retval += string_conv(c); - if(c.back() != '\n') {retval += char_type('\n');} - } - return retval; - } // }}} - - string_type format_indent(const indent_char indent_type) const // {{{ - { - const auto indent = static_cast((std::max)(0, this->current_indent_)); - if(indent_type == indent_char::space) - { - return string_conv(make_string(indent, ' ')); - } - else if(indent_type == indent_char::tab) - { - return string_conv(make_string(indent, '\t')); - } - else - { - return string_type{}; - } - } // }}} - - std::locale set_locale(std::ostream& os) const - { - return os.imbue(std::locale::classic()); - } - - private: - - spec spec_; - bool force_inline_; // table inside an array without fmt specification - std::int32_t current_indent_; - std::vector keys_; -}; -} // detail - -template -typename basic_value::string_type -format(const basic_value& v, const spec s = spec::default_version()) -{ - detail::serializer ser(s); - return ser(v); -} -template -typename basic_value::string_type -format(const typename basic_value::key_type& k, - const basic_value& v, - const spec s = spec::default_version()) -{ - detail::serializer ser(s); - return ser(k, v); -} -template -typename basic_value::string_type -format(const std::vector::key_type>& ks, - const basic_value& v, - const spec s = spec::default_version()) -{ - detail::serializer ser(s); - return ser(ks, v); -} - -template -std::ostream& operator<<(std::ostream& os, const basic_value& v) -{ - os << format(v); - return os; -} - -} // toml - -#if defined(TOML11_COMPILE_SOURCES) -namespace toml -{ -struct type_config; -struct ordered_type_config; - -extern template typename basic_value::string_type -format(const basic_value&, const spec); - -extern template typename basic_value::string_type -format(const typename basic_value::key_type& k, - const basic_value& v, const spec); - -extern template typename basic_value::string_type -format(const std::vector::key_type>& ks, - const basic_value& v, const spec s); - -extern template typename basic_value::string_type -format(const basic_value&, const spec); - -extern template typename basic_value::string_type -format(const typename basic_value::key_type& k, - const basic_value& v, const spec); - -extern template typename basic_value::string_type -format(const std::vector::key_type>& ks, - const basic_value& v, const spec s); - -namespace detail -{ -extern template class serializer<::toml::type_config>; -extern template class serializer<::toml::ordered_type_config>; -} // detail -} // toml -#endif // TOML11_COMPILE_SOURCES - - -#endif // TOML11_SERIALIZER_HPP -#ifndef TOML11_TOML_HPP -#define TOML11_TOML_HPP - -// The MIT License (MIT) -// -// Copyright (c) 2017-now Toru Niina -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -// IWYU pragma: begin_exports -// IWYU pragma: end_exports - -#endif// TOML11_TOML_HPP diff --git a/desktop/main.cpp b/desktop/main.cpp deleted file mode 100644 index 601b9a2..0000000 --- a/desktop/main.cpp +++ /dev/null @@ -1,51 +0,0 @@ -#include -#include -#include -#include -#include - -#include "config/config.hpp" -#include "datalake/database.hpp" -#include "datalake/indexer.hpp" - -int main(int argc, char* argv[]) { - QQuickStyle::setStyle(QStringLiteral("org.kde.breeze")); - QGuiApplication app(argc, argv); - app.setApplicationDisplayName("Koto"); - app.setDesktopFileName("com.github.joshstrobl.koto.desktop"); - - QQmlApplicationEngine engine; - - engine.loadFromModule("com.github.joshstrobl.koto", "Main"); - - if (engine.rootObjects().isEmpty()) { return -1; } - - std::thread([]() { - KotoConfig::create(); - KotoDatabase::create(); - - KotoDatabase::instance().connect(); - - // If we needed to bootstrap, index all libraries, otherwise load the database - if (KotoDatabase::instance().requiredBootstrap()) { - indexAllLibraries(); - } else { - KotoDatabase::instance().load(); - std::cout << "===== Summary =====" << std::endl; - for (auto artist : Cartographer::instance().getArtists()) { - std::cout << "Artist: " << artist->getName().toStdString() << std::endl; - for (auto album : artist->getAlbums()) { - std::cout << " Album: " << album->getTitle().toStdString() << std::endl; - for (auto track : album->getTracks()) { std::cout << " Track: " << track->getTitle().toStdString() << std::endl; } - } - } - std::cout << "===== Tracks without albums and/or artists =====" << std::endl; - for (auto track : Cartographer::instance().getTracks()) { - if (track->album_uuid.has_value()) continue; - std::cout << "Track: " << track->getTitle().toStdString() << std::endl; - } - } - }).detach(); - - return app.exec(); -} diff --git a/desktop/qml/HomePage.qml b/desktop/qml/HomePage.qml deleted file mode 100644 index 5810f7e..0000000 --- a/desktop/qml/HomePage.qml +++ /dev/null @@ -1,24 +0,0 @@ -import QtQuick -import QtQuick.Controls as Controls -import QtQuick.Layouts -import org.kde.kirigami as Kirigami -import com.github.joshstrobl.koto - -Kirigami.ScrollablePage { - Component { - id: listDelegate - - Controls.ItemDelegate { - required property string name - - text: name - width: ListView.view.width - } - } - ListView { - Layout.fillHeight: true - Layout.fillWidth: true - delegate: listDelegate - model: Cartographer.artists - } -} diff --git a/desktop/qml/Main.qml b/desktop/qml/Main.qml deleted file mode 100644 index 6f729b3..0000000 --- a/desktop/qml/Main.qml +++ /dev/null @@ -1,20 +0,0 @@ -import org.kde.kirigami as Kirigami - -Kirigami.ApplicationWindow { - id: root - - height: 600 - title: "Koto" - visible: true - width: 1000 - - footer: PlayerBar { - } - globalDrawer: PrimaryNavigation { - windowRef: root - } - - // TODO: Implement an onboarding page - pageStack.initialPage: Root { - } -} diff --git a/desktop/qml/PlayerBar/PlayerBar.qml b/desktop/qml/PlayerBar/PlayerBar.qml deleted file mode 100644 index 5072116..0000000 --- a/desktop/qml/PlayerBar/PlayerBar.qml +++ /dev/null @@ -1,74 +0,0 @@ -import QtQuick.Controls as Controls -import QtQuick.Layouts -import org.kde.kirigami as Kirigami - -ColumnLayout { - id: playerBar - - Layout.fillWidth: true - Layout.leftMargin: Kirigami.Units.largeSpacing - Layout.rightMargin: Kirigami.Units.largeSpacing - spacing: 4 - - RowLayout { - Layout.fillWidth: true - Layout.margins: Kirigami.Units.largeSpacing - - Controls.Slider { - id: seekSlider - - Layout.fillWidth: true - } - } - RowLayout { - Layout.fillWidth: true - Layout.margins: Kirigami.Units.largeSpacing - Layout.maximumWidth: parent.width - 2 * Kirigami.Units.largeSpacing - Layout.minimumWidth: parent.width - 2 * Kirigami.Units.largeSpacing - - RowLayout { - anchors.left: parent.left - - Controls.Button { - flat: true - icon.height: Kirigami.Units.iconSizes.small - icon.name: "media-seek-backward" - } - Controls.Button { - flat: true - icon.height: Kirigami.Units.iconSizes.medium - icon.name: "media-playback-start" - icon.width: Kirigami.Units.iconSizes.medium - } - Controls.Button { - flat: true - icon.height: Kirigami.Units.iconSizes.small - icon.name: "media-seek-forward" - } - } - RowLayout { - anchors.right: parent.right - - Controls.Button { - flat: true - icon.height: Kirigami.Units.iconSizes.small - icon.name: "media-playlist-repeat" - } - Controls.Button { - flat: true - icon.height: Kirigami.Units.iconSizes.small - icon.name: "media-playlist-shuffle" - } - Controls.Button { - flat: true - icon.height: Kirigami.Units.iconSizes.small - icon.name: "playlist-symbolic" - } - Controls.Button { - flat: true - icon.height: Kirigami.Units.iconSizes.small - icon.name: "audio-volume-medium" - } - } - } -} \ No newline at end of file diff --git a/desktop/qml/PrimaryNavigation.qml b/desktop/qml/PrimaryNavigation.qml deleted file mode 100644 index a689981..0000000 --- a/desktop/qml/PrimaryNavigation.qml +++ /dev/null @@ -1,105 +0,0 @@ -import QtQuick -import QtQuick.Controls as Controls -import QtQuick.Layouts -import org.kde.kirigami as Kirigami - -Kirigami.GlobalDrawer { - id: primaryNavigation - - property Kirigami.ApplicationWindow windowRef - - function isMobile(width) { - return width < 800; - } - function onWindowSizeChanged(width) { - const mobile = isMobile(width); - drawerOpen = !mobile; - modal = mobile; - height = mobile ? windowRef.height : windowRef.height - windowRef.footer.height; - } - - collapseButtonVisible: false - drawerOpen: !isMobile() - edge: Qt.LeftEdge - height: parent.height - windowRef.footer.height - modal: false - - actions: [ - Kirigami.Action { - icon.name: "go-home" - text: "Home" - - onTriggered: console.log("Home triggered") - }, - Kirigami.Action { - expandible: true - icon.name: "bookmark" - text: "Audiobooks" - - onTriggered: console.log("Audiobooks triggered") - }, - Kirigami.Action { - expandible: true - icon.name: "emblem-music-symbolic" - text: "Music" - - children: [ - Kirigami.Action { - text: "Local Library" - - onTriggered: console.log("Music Local Library triggered") - }, - Kirigami.Action { - text: "Radio" - - onTriggered: console.log("Music Radio triggered") - } - ] - }, - Kirigami.Action { - expandible: true - icon.name: "application-rss+xml-symbolic" - text: "Podcasts" - - children: [ - Kirigami.Action { - text: "Library" - - onTriggered: console.log("Podcasts Library triggered") - }, - Kirigami.Action { - text: "Find new podcasts" - - onTriggered: console.log("Podcasts Find new podcasts triggered") - } - ] - }, - Kirigami.Action { - expandible: true - icon.name: "music-playlist-symbolic" - text: "Playlists" - - children: [ - Kirigami.Action { - text: "Library" - - onTriggered: console.log("Playlists Library triggered") - } - ] - // TODO: Generate list of playlists - } - ] - header: Kirigami.SearchField { - id: searchEntry - - Layout.topMargin: Kirigami.Units.largeSpacing - placeholderText: qsTr("Search") - } - - Component.onCompleted: { - if (Kirigami.Settings.isMobile) - return; - if (windowRef) - windowRef.onWidthChanged.connect(() => onWindowSizeChanged(windowRef.width)); - } -} diff --git a/desktop/qml/Root.qml b/desktop/qml/Root.qml deleted file mode 100644 index 96d72ea..0000000 --- a/desktop/qml/Root.qml +++ /dev/null @@ -1,24 +0,0 @@ -import QtQuick -import QtQuick.Controls as Controls -import QtQuick.Layouts -import org.kde.kirigami as Kirigami - -Kirigami.Page { - id: rootPage - - ColumnLayout { - id: rootLayout - - anchors.fill: parent - - Controls.StackView { - id: rootStack - - Layout.fillHeight: true - Layout.fillWidth: true - - initialItem: HomePage { - } - } - } -} \ No newline at end of file diff --git a/jsc.cfg b/jsc.cfg new file mode 100644 index 0000000..e2ae868 --- /dev/null +++ b/jsc.cfg @@ -0,0 +1,2625 @@ +# 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 = force # 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 = remove # 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 = force # 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 = force # 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 = force # 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 = false # 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 = force # ignore/add/remove/force + +# Add or remove newline after '(' in a function definition. +nl_func_def_start = force # 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 = force # ignore/add/remove/force + +# Add or remove newline after each ',' in a function definition. +nl_func_def_args = force # ignore/add/remove/force + +# Add or remove newline after each ',' in a function call. +nl_func_call_args = remove # 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 = force # ignore/add/remove/force + +# Add or remove newline before the ')' in a function definition. +nl_func_def_end = force # 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 = true # 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 = true # 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 = 2 # 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 = 1 # 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 = 1 # 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 + +# 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´ + +# 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 new file mode 100644 index 0000000..6a7169c --- /dev/null +++ b/meson.build @@ -0,0 +1,35 @@ +project('koto', 'c', + version: '0.1.0', + meson_version: '>= 0.57.0', + default_options: [ + 'c_std=gnu11', + 'warning_level=2', + 'werror=true', + ], +) + +i18n = import('i18n') +gnome = import('gnome') + +config_h = configuration_data() +config_h.set_quoted('PACKAGE_VERSION', meson.project_version()) +config_h.set_quoted('GETTEXT_PACKAGE', 'koto') +config_h.set_quoted('LOCALEDIR', join_paths(get_option('prefix'), get_option('localedir'))) +configure_file( + output: 'koto-config.h', + configuration: config_h, +) + +c = meson.get_compiler('c') +toml_dep = c.find_library('toml', required: true) + +subdir('theme') +subdir('data') +subdir('src') +subdir('po') + +gnome.post_install( + glib_compile_schemas: true, + gtk_update_icon_cache: false +) +meson.add_install_script('build-aux/meson/postinstall.py') diff --git a/po/LINGUAS b/po/LINGUAS new file mode 100644 index 0000000..e69de29 diff --git a/po/POTFILES b/po/POTFILES new file mode 100644 index 0000000..2839995 --- /dev/null +++ b/po/POTFILES @@ -0,0 +1,7 @@ +data/com.github.joshstrobl.koto.desktop.in +data/com.github.joshstrobl.koto.appdata.xml.in +data/com.github.joshstrobl.koto.gschema.xml +src/koto-window.ui +src/main.c +src/koto-window.c + diff --git a/po/meson.build b/po/meson.build new file mode 100644 index 0000000..fe55b6a --- /dev/null +++ b/po/meson.build @@ -0,0 +1 @@ +i18n.gettext('koto', preset: 'glib') diff --git a/src/components/action-bar.c b/src/components/action-bar.c new file mode 100644 index 0000000..698777c --- /dev/null +++ b/src/components/action-bar.c @@ -0,0 +1,423 @@ +/* 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 "../config/config.h" +#include "../db/cartographer.h" +#include "../indexer/album-playlist-funcs.h" +#include "../pages/music/music-local.h" +#include "../playlist/add-remove-track-popover.h" +#include "../playlist/current.h" +#include "../playback/engine.h" +#include "../koto-utils.h" +#include "../koto-window.h" +#include "action-bar.h" +#include "button.h" + +extern KotoAddRemoveTrackPopover * koto_add_remove_track_popup; +extern KotoCartographer * koto_maps; +extern KotoConfig * config; +extern KotoCurrentPlaylist * current_playlist; +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; + } + + KotoTrack * selected_track = g_list_nth_data(self->current_list, 0); // Get the first item + + if (!KOTO_IS_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; + } + + KotoTrack * track = g_list_nth_data(self->current_list, 0); // Get the first track + + if (!KOTO_IS_TRACK(track)) { // Not a track + goto doclose; + } + + KotoPlaylist * playlist = NULL; + + if (self->relative == KOTO_ACTION_BAR_IS_PLAYLIST_RELATIVE) { // Relative to a playlist + playlist = koto_cartographer_get_playlist_by_uuid(koto_maps, self->current_playlist_uuid); + } else if (self->relative == KOTO_ACTION_BAR_IS_ALBUM_RELATIVE) { // Relative to an Album + KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, self->current_album_uuid); // Get the Album + + if (KOTO_IS_ALBUM(album)) { // Have an Album + playlist = koto_album_get_playlist(album); // Create our playlist dynamically for the Album + } + } + + if (KOTO_IS_PLAYLIST(playlist)) { // Is a playlist + koto_current_playlist_set_playlist(current_playlist, playlist, FALSE, FALSE); // Update our playlist to the one associated with the track we are playing + koto_playlist_set_track_as_current(playlist, koto_track_get_uuid(track)); // Get this track as the current track in the position + } + + gboolean continue_playback = FALSE; + g_object_get(config, "playback-continue-on-playlist", &continue_playback, NULL); + koto_playback_engine_set_track_by_uuid(playback_engine, koto_track_get_uuid(track), continue_playback); // 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 (!koto_utils_string_is_valid(self->current_playlist_uuid)) { // 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 KotoTrack + KotoTrack * track = cur_list->data; + koto_playlist_remove_track_by_uuid(playlist, koto_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 (koto_utils_string_is_valid(self->current_album_uuid)) { // Album UUID currently set + g_free(self->current_album_uuid); + } + + if (koto_utils_string_is_valid(self->current_playlist_uuid)) { // 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 (koto_utils_string_is_valid(self->current_album_uuid)) { // Album UUID currently set + g_free(self->current_album_uuid); + } + + if (koto_utils_string_is_valid(self->current_playlist_uuid)) { // 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/action-bar.h b/src/components/action-bar.h new file mode 100644 index 0000000..d52281b --- /dev/null +++ b/src/components/action-bar.h @@ -0,0 +1,105 @@ +/* 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/album-info.c b/src/components/album-info.c new file mode 100644 index 0000000..94f0eab --- /dev/null +++ b/src/components/album-info.c @@ -0,0 +1,289 @@ +/* album-info.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 "../config/config.h" +#include "../db/cartographer.h" +#include "../indexer/structs.h" +#include "../koto-utils.h" +#include "album-info.h" + +extern KotoCartographer * koto_maps; +extern KotoConfig * config; + +enum { + PROP_0, + PROP_TYPE, + N_PROPERTIES +}; + +static GParamSpec * props[N_PROPERTIES] = { + NULL, +}; + +struct _KotoAlbumInfo { + GtkBox parent_instance; + KotoAlbumInfoType type; + + KotoAlbum * album; + + GtkWidget * name_year_box; + GtkWidget * genres_tags_list; + + GtkWidget * name_label; + GtkWidget * year_badge; + GtkWidget * narrator_label; + + GtkWidget * description_label; +}; + +struct _KotoAlbumInfoClass { + GtkBoxClass parent_class; +}; + +G_DEFINE_TYPE(KotoAlbumInfo, koto_album_info, GTK_TYPE_BOX); + +static void koto_album_info_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +); + +static void koto_album_info_class_init(KotoAlbumInfoClass * c) { + GObjectClass * gobject_class; + gobject_class = G_OBJECT_CLASS(c); + gobject_class->set_property = koto_album_info_set_property; + + props[PROP_TYPE] = g_param_spec_string( + "type", + "Type of AlbumInfo component", + "Type of AlbumInfo component", + "album", + G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE + ); + + g_object_class_install_properties(gobject_class, N_PROPERTIES, props); +} + +static void koto_album_info_init(KotoAlbumInfo * self) { + gtk_widget_add_css_class(GTK_WIDGET(self), "album-info"); + self->type = KOTO_ALBUM_INFO_TYPE_ALBUM; // Default to Album here + + self->name_year_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); // Create out name box for the name and year + gtk_widget_add_css_class(self->name_year_box, "album-title-year-combo"); + self->name_label = gtk_label_new(NULL); // Create our name label + gtk_widget_set_halign(self->name_label, GTK_ALIGN_START); + gtk_widget_set_valign(self->name_label, GTK_ALIGN_START); + gtk_widget_add_css_class(self->name_label, "album-title"); + + self->year_badge = gtk_label_new(NULL); // Create our year badge + gtk_widget_add_css_class(self->year_badge, "album-year"); + gtk_widget_add_css_class(self->year_badge, "label-badge"); + gtk_widget_set_valign(self->year_badge, GTK_ALIGN_CENTER); // Center vertically align the year badge + + self->narrator_label = gtk_label_new(NULL); // Create our narrator label + gtk_widget_add_css_class(self->narrator_label, "album-narrator"); + gtk_widget_set_halign(self->narrator_label, GTK_ALIGN_START); + + self->description_label = gtk_label_new(NULL); // Create our description label + gtk_widget_add_css_class(self->description_label, "album-description"); + gtk_widget_set_halign(self->description_label, GTK_ALIGN_START); + + gtk_box_append(GTK_BOX(self->name_year_box), self->name_label); // Add the name label to the name + year box + gtk_box_append(GTK_BOX(self->name_year_box), self->year_badge); // Add the year badge to the name + year box + + gtk_box_append(GTK_BOX(self), self->name_year_box); + gtk_box_append(GTK_BOX(self), self->narrator_label); + gtk_box_append(GTK_BOX(self), self->description_label); + + g_signal_connect(config, "notify::ui-info-show-description", G_CALLBACK(koto_album_info_apply_configuration_state), self); + g_signal_connect(config, "notify::ui-info-show-genres", G_CALLBACK(koto_album_info_apply_configuration_state), self); + g_signal_connect(config, "notify::ui-info-show-narrator", G_CALLBACK(koto_album_info_apply_configuration_state), self); + g_signal_connect(config, "notify::ui-info-show-year", G_CALLBACK(koto_album_info_apply_configuration_state), self); +} + +static void koto_album_info_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +) { + KotoAlbumInfo * self = KOTO_ALBUM_INFO(obj); + + switch (prop_id) { + case PROP_TYPE: + koto_album_info_set_type(self, g_value_get_string(val)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +void koto_album_info_apply_configuration_state( + KotoConfig * c, + guint prop_id, + KotoAlbumInfo * self +) { + (void) c; + (void) prop_id; + + gboolean show_description = TRUE; + gboolean show_genres = TRUE; + gboolean show_narrator = TRUE; + gboolean show_year = TRUE; + + g_object_get( + config, + "ui-album-info-show-description", + &show_description, + "ui-album-info-show-genres", + &show_genres, + "ui-album-info-show-narrator", + &show_narrator, + "ui-album-info-show-year", + &show_year, + NULL + ); + + if (self->type != KOTO_ALBUM_INFO_TYPE_AUDIOBOOK) { // If the type is NOT an audiobook + show_narrator = FALSE; // Narrator should never be shown. Only really applicable to audiobook + } + + if (self->type == KOTO_ALBUM_INFO_TYPE_PODCAST) { // If the type is podcast + show_year = FALSE; // Year isn't really applicable to podcast, at least not the "global" year + } + + if (koto_utils_string_is_valid(koto_album_get_description(self->album)) && show_description) { // Have description to show in the first place, and should show it + gtk_widget_show(self->description_label); + } else { // Don't have content, just hide it + gtk_widget_hide(self->description_label); + } + + if (!KOTO_IS_ALBUM(self->album)) { // Don't have an album defined + return; + } + + if (GTK_IS_FLOW_BOX(self->genres_tags_list)) { + if ((g_list_length(koto_album_get_genres(self->album)) > 0) && show_genres) { // Have genres to show in the first place, and should show it + gtk_widget_show(self->genres_tags_list); + } else { + gtk_widget_hide(self->genres_tags_list); + } + } + + if (koto_utils_string_is_valid(koto_album_get_narrator(self->album)) && show_narrator) { // Have narrator and should show it + gtk_widget_show(self->narrator_label); + } else { + gtk_widget_hide(self->narrator_label); + } + + if ((koto_album_get_year(self->album) != 0) && show_year) { // Have year and should show it + gtk_widget_show(self->year_badge); + } else { + gtk_widget_hide(self->year_badge); + } +} + +void koto_album_info_set_album_uuid( + KotoAlbumInfo * self, + gchar * album_uuid +) { + if (!KOTO_IS_ALBUM_INFO(self)) { + return; + } + + if (!koto_utils_string_is_valid(album_uuid)) { + return; + } + + KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, album_uuid); // Get the album + + if (!KOTO_IS_ALBUM(album)) { // Is not an album + return; + } + + self->album = album; + + gtk_label_set_text(GTK_LABEL(self->name_label), koto_album_get_name(self->album)); // Set the name label text to the album + gtk_label_set_text(GTK_LABEL(self->year_badge), g_strdup_printf("%li", koto_album_get_year(self->album))); // Set the year label text to any year for the album + + gchar * narrator = koto_album_get_narrator(self->album); + + if (koto_utils_string_is_valid(narrator)) { // Have a narrator + gtk_label_set_text(GTK_LABEL(self->narrator_label), g_strdup_printf("Narrated by %s", koto_album_get_narrator(self->album))); // Set narrated by string + } + + gchar * description = koto_album_get_description(self->album); + + if (koto_utils_string_is_valid(description)) { // Have a description + gtk_label_set_markup(GTK_LABEL(self->description_label), description); // Set the markup so we treat it more like HTML and let pango do its thing + } + + if (GTK_IS_BOX(self->genres_tags_list)) { // Genres Flow + gtk_box_remove(GTK_BOX(self), self->genres_tags_list); // Remove the genres flow from the box + } + + self->genres_tags_list = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); // Create our flow box for the genres + gtk_widget_add_css_class(self->genres_tags_list, "genres-tag-list"); + + GList * album_genres = koto_album_get_genres(self->album); + if (g_list_length(album_genres) != 0) { // Have genres + GList * cur_list; + for (cur_list = album_genres; cur_list != NULL; cur_list = cur_list->next) { // Iterate over each genre + GtkWidget * genre_label = gtk_label_new(koto_utils_string_title(cur_list->data)); + gtk_widget_add_css_class(genre_label, "label-badge"); // Add label badge styling + gtk_box_append(GTK_BOX(self->genres_tags_list), genre_label); // Append to the genre tags list + } + } else { + gtk_widget_hide(self->genres_tags_list); // Hide the genres tag list + } + + gtk_box_insert_child_after(GTK_BOX(self), self->genres_tags_list, self->name_year_box); // Add after the name+year flowbox + + koto_album_info_apply_configuration_state(NULL, 0, self); // Apply our configuration state immediately so we know what to show and hide for this album based on the config +} + +void koto_album_info_set_type( + KotoAlbumInfo * self, + const gchar * type +) { + if (!KOTO_IS_ALBUM_INFO(self)) { + return; + } + + if (g_strcmp0(type, "audiobook") == 0) { // If this is an audiobook + self->type = KOTO_ALBUM_INFO_TYPE_AUDIOBOOK; + } else if (g_strcmp0(type, "podcast") == 0) { // If this is a podcast + self->type = KOTO_ALBUM_INFO_TYPE_PODCAST; + } else { + self->type = KOTO_ALBUM_INFO_TYPE_ALBUM; + } +} + +KotoAlbumInfo * koto_album_info_new(gchar * type) { + return g_object_new( + KOTO_TYPE_ALBUM_INFO, + "orientation", + GTK_ORIENTATION_VERTICAL, + "type", + type, + NULL + ); +} \ No newline at end of file diff --git a/src/components/album-info.h b/src/components/album-info.h new file mode 100644 index 0000000..fe2e86c --- /dev/null +++ b/src/components/album-info.h @@ -0,0 +1,52 @@ +/* album-info.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 "../config/config.h" + +G_BEGIN_DECLS + +typedef enum { + KOTO_ALBUM_INFO_TYPE_ALBUM, + KOTO_ALBUM_INFO_TYPE_AUDIOBOOK, + KOTO_ALBUM_INFO_TYPE_PODCAST +} KotoAlbumInfoType; + +#define KOTO_TYPE_ALBUM_INFO (koto_album_info_get_type()) +G_DECLARE_FINAL_TYPE(KotoAlbumInfo, koto_album_info, KOTO, ALBUM_INFO, GtkBox); +#define KOTO_IS_ALBUM_INFO(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_ALBUM_INFO)) + +void koto_album_info_apply_configuration_state( + KotoConfig * c, + guint prop_id, + KotoAlbumInfo * self +); + +void koto_album_info_set_album_uuid( + KotoAlbumInfo * self, + gchar * album_uuid +); + +void koto_album_info_set_type( + KotoAlbumInfo * self, + const gchar * type +); + +KotoAlbumInfo * koto_album_info_new(gchar * type); \ No newline at end of file diff --git a/src/components/button.c b/src/components/button.c new file mode 100644 index 0000000..779128a --- /dev/null +++ b/src/components/button.c @@ -0,0 +1,818 @@ +/* koto-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 "config/config.h" +#include "button.h" +#include "../koto-window.h" +#include "../koto-utils.h" + +extern KotoWindow * main_window; + +struct _PixbufSize { + guint size; +}; + +typedef struct _PixbufSize PixbufSize; + +static PixbufSize * pixbuf_sizes = NULL; +static guint pixbuf_sizes_allocated = 0; + +static void init_pixbuf_sizes() { + if (pixbuf_sizes == NULL) { + pixbuf_sizes = g_new(PixbufSize, NUM_BUILTIN_SIZES); + + pixbuf_sizes[KOTO_BUTTON_PIXBUF_SIZE_INVALID].size = 0; + pixbuf_sizes[KOTO_BUTTON_PIXBUF_SIZE_TINY].size = 16; + pixbuf_sizes[KOTO_BUTTON_PIXBUF_SIZE_SMALL].size = 24; + pixbuf_sizes[KOTO_BUTTON_PIXBUF_SIZE_NORMAL].size = 32; + pixbuf_sizes[KOTO_BUTTON_PIXBUF_SIZE_LARGE].size = 64; + pixbuf_sizes[KOTO_BUTTON_PIXBUF_SIZE_MASSIVE].size = 96; + pixbuf_sizes[KOTO_BUTTON_PIXBUF_SIZE_GODLIKE].size = 128; + pixbuf_sizes_allocated = NUM_BUILTIN_SIZES; + } +} + +guint koto_get_pixbuf_size(KotoButtonPixbufSize s) { + init_pixbuf_sizes(); + + return pixbuf_sizes[s].size; +} + +enum { + PROP_BTN_0, + PROP_PIX_SIZE, + PROP_TEXT, + PROP_BADGE_TEXT, + PROP_USE_FROM_FILE, + PROP_IMAGE_FILE_PATH, + PROP_ICON_NAME, + PROP_ALT_ICON_NAME, + PROP_RESOURCE_PATH, + N_BTN_PROPERTIES +}; + +static GParamSpec * btn_props[N_BTN_PROPERTIES] = { + NULL, +}; + +struct _KotoButton { + GtkBox parent_instance; + guint pix_size; + + gpointer arbitrary_data; + + GtkWidget * button_pic; + 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 * resource_path; + gchar * text; + + KotoButtonImagePosition image_position; + gboolean use_from_file; + gboolean currently_showing_alt; +}; + +struct _KotoButtonClass { + GtkBoxClass parent_class; +}; + +G_DEFINE_TYPE(KotoButton, koto_button, GTK_TYPE_BOX); + +static void koto_button_constructed(GObject * obj); + +static void koto_button_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +); + +static void koto_button_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +); + +static void koto_button_class_init(KotoButtonClass * c) { + GObjectClass * gobject_class; + + gobject_class = G_OBJECT_CLASS(c); + gobject_class->constructed = koto_button_constructed; + gobject_class->set_property = koto_button_set_property; + gobject_class->get_property = koto_button_get_property; + + btn_props[PROP_PIX_SIZE] = g_param_spec_uint( + "pixbuf-size", + "Pixbuf Size", + "Size of the pixbuf", + koto_get_pixbuf_size(KOTO_BUTTON_PIXBUF_SIZE_TINY), + koto_get_pixbuf_size(KOTO_BUTTON_PIXBUF_SIZE_GODLIKE), + koto_get_pixbuf_size(KOTO_BUTTON_PIXBUF_SIZE_SMALL), + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + btn_props[PROP_TEXT] = g_param_spec_string( + "button-text", + "Button Text", + "Text of Button", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + btn_props[PROP_BADGE_TEXT] = g_param_spec_string( + "badge-text", + "Badge Text", + "Text of Badge", + NULL, + 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", + "Name of Icon", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + btn_props[PROP_ALT_ICON_NAME] = g_param_spec_string( + "alt-icon-name", + "Name of an Alternate Icon", + "Name of an Alternate Icon", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + btn_props[PROP_RESOURCE_PATH] = g_param_spec_string( + "resource-path", + "Resource Path to an Icon", + "Resource Path to an Icon", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + g_object_class_install_properties(gobject_class, N_BTN_PROPERTIES, btn_props); +} + +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 + + self->resource_path = NULL; + + 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 + + GtkEventController * motion = gtk_event_controller_motion_new(); // Create a new motion controller + g_signal_connect(motion, "enter", G_CALLBACK(koto_button_handle_mouse_enter), self); // Handle mouse enter on motion + g_signal_connect(motion, "leave", G_CALLBACK(koto_button_handle_mouse_leave), self); // Handle mouse leave on motion + gtk_widget_add_controller(GTK_WIDGET(self), motion); +} + +static void koto_button_constructed(GObject * obj) { + KotoButton * self = KOTO_BUTTON(obj); + GtkStyleContext * style = gtk_widget_get_style_context(GTK_WIDGET(self)); + + gtk_style_context_add_class(style, "koto-button"); + + G_OBJECT_CLASS(koto_button_parent_class)->constructed(obj); +} + +static void koto_button_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +) { + 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; + case PROP_PIX_SIZE: + g_value_set_uint(val, self->pix_size); + break; + case PROP_TEXT: + g_value_set_string(val, self->text); + break; + case PROP_BADGE_TEXT: + g_value_set_string(val, self->badge_text); + break; + case PROP_ICON_NAME: + g_value_set_string(val, self->icon_name); + break; + case PROP_ALT_ICON_NAME: + g_value_set_string(val, self->alt_icon_name); + break; + case PROP_RESOURCE_PATH: + g_value_set_string(val, self->resource_path); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_button_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +) { + KotoButton * self = KOTO_BUTTON(obj); + + switch (prop_id) { + case PROP_PIX_SIZE: + koto_button_set_pixbuf_size(self, g_value_get_uint(val)); + break; + case PROP_TEXT: + if (koto_utils_string_is_valid(g_value_get_string(val))) { + 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 + koto_button_show_image(self, FALSE); + } + break; + case PROP_ALT_ICON_NAME: + koto_button_set_icon_name(self, g_strdup(g_value_get_string(val)), TRUE); + if (self->currently_showing_alt) { // Currently showing the alt image + koto_button_show_image(self, TRUE); + } + break; + case PROP_RESOURCE_PATH: + koto_button_set_resource_path(self, g_strdup(g_value_get_string(val))); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +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); +} + +gpointer koto_button_get_data(KotoButton * self) { + return self->arbitrary_data; +} + +void koto_button_global_page_nav_callback( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer user_data +) { + (void) gesture; + (void) n_press; + (void) x; + (void) y; + + KotoButton * btn = KOTO_BUTTON(user_data); // Cast our user data as a button + + if (!KOTO_IS_BUTTON(btn)) { // Not a button + return; + } + + gchar * btn_nav_uuid = (gchar*) koto_button_get_data(btn); // Get the data + + if (!koto_utils_string_is_valid(btn_nav_uuid)) { // Not a valid string + return; + } + + koto_window_go_to_page(main_window, btn_nav_uuid); +} + +void koto_button_handle_mouse_enter( + GtkEventControllerMotion * controller, + double x, + double y, + gpointer user_data +) { + (void) controller; + (void) x; + (void) y; + + KotoButton * self = user_data; + + if (!KOTO_IS_BUTTON(self)) { // Not a button + return; + } + + koto_button_set_pseudoactive_styling(self); // Set to be pseudoactive in styling +} + +void koto_button_handle_mouse_leave( + GtkEventControllerMotion * controller, + gpointer user_data +) { + (void) controller; + + KotoButton * self = user_data; + + if (!KOTO_IS_BUTTON(self)) { // Not a button + return; + } + + koto_button_unset_pseudoactive_styling(self); // Unset the pseudoactive styling +} + +void koto_button_hide_image(KotoButton * self) { + if (!KOTO_IS_BUTTON(self)) { // Not a button + return; + } + + if (GTK_IS_WIDGET(self->button_pic)) { // Is a widget + gtk_widget_hide(self->button_pic); + } +} + +void koto_button_set_data( + KotoButton * self, + gpointer data +) { + if (!KOTO_IS_BUTTON(self)) { // Not a button + return; + } + + self->arbitrary_data = data; +} + +void koto_button_set_pseudoactive_styling(KotoButton * self) { + if (!KOTO_IS_BUTTON(self)) { // Not a button + return; + } + + if (gtk_widget_has_css_class(GTK_WIDGET(self), "pseudoactive")) { // Already is pseudoactive + return; + } + + gtk_widget_add_css_class(GTK_WIDGET(self), "pseudoactive"); // Set to pseudoactive +} + +void koto_button_set_badge_text( + KotoButton * self, + gchar * text +) { + if (!KOTO_IS_BUTTON(self)) { // Not a button + return; + } + + if (koto_utils_string_is_valid(self->badge_text)) { // Have existing text + g_free(self->badge_text); + } + + if (!koto_utils_string_is_valid(text)) { // If the text is empty + self->badge_text = NULL; + + if (GTK_IS_LABEL(self->badge_label)) { // If badge label already exists + gtk_widget_hide(self->badge_label); // Hide the label + } + + return; + } + + self->badge_text = g_strdup(text); + + if (GTK_IS_LABEL(self->badge_label)) { // If badge label already exists + gtk_label_set_text(GTK_LABEL(self->badge_label), self->badge_text); + } else { + self->badge_label = gtk_label_new(self->badge_text); // Create our label + gtk_box_append(GTK_BOX(self), self->badge_label); + } + + 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 (!koto_utils_string_is_valid(file_path)) { // file path is invalid + return; + } + + if (g_strcmp0(self->image_file_path, file_path) == 0) { // Request setting as same file + return; + } + + if (koto_utils_string_is_valid(self->image_file_path)) { // image file path is valid + g_free(self->image_file_path); + } + + self->use_from_file = TRUE; + 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 +) { + if (!KOTO_IS_BUTTON(self)) { // Not a button + return; + } + + if (!koto_utils_string_is_valid(icon_name)) { // Not a valid icon + return; + } + + gchar * copied_icon_name = g_strdup(icon_name); + + if (for_alt) { // Is for the alternate icon + if ((self->alt_icon_name != NULL) && strcmp(icon_name, self->alt_icon_name) != 0) { // If the icons are different + g_free(self->alt_icon_name); + } + + self->alt_icon_name = copied_icon_name; + } else { + if ((self->icon_name != NULL) && strcmp(icon_name, self->icon_name) != 0) { + g_free(self->icon_name); + } + + self->icon_name = copied_icon_name; + } + + gboolean hide_image = FALSE; + + if (for_alt && self->currently_showing_alt && ((self->alt_icon_name == NULL) || strcmp(self->alt_icon_name, "") == 0)) { // For alt, alt is currently showing, and no longer have alt + hide_image = TRUE; + } else if (!for_alt && ((self->icon_name == NULL) || (strcmp(self->icon_name, "") == 0))) { // Not for alt, no icon + hide_image = TRUE; + } + + if (GTK_IS_IMAGE(self->button_pic)) { // If we already have a button image + if (hide_image) { // Should hide the image + gtk_widget_hide(self->button_pic); // Hide + } else { // Should be showing the image + gtk_image_set_from_icon_name(GTK_IMAGE(self->button_pic), self->icon_name); // Just update the existing image immediately + } + } + + 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 (!KOTO_IS_BUTTON(self)) { // Not a button + return; + } + + 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 +) { + if (!KOTO_IS_BUTTON(self)) { + return; + } + + if (size == self->pix_size) { + return; + } + + self->pix_size = size; + gtk_widget_set_size_request(GTK_WIDGET(self), self->pix_size, self->pix_size); + + g_object_notify_by_pspec(G_OBJECT(self), btn_props[PROP_PIX_SIZE]); +} + +void koto_button_set_resource_path( + KotoButton * self, + gchar * resource_path +) { + if (!KOTO_IS_BUTTON(self)) { + return; + } + + if (!koto_utils_string_is_valid(resource_path)) { // Not a valid string + return; + } + + if (koto_utils_string_is_valid(self->resource_path)) { // Have a resource path already + g_free(self->resource_path); // Free it + } + + self->resource_path = g_strdup(resource_path); + + if (GTK_IS_IMAGE(self->button_pic)) { // Already have a button image + gtk_image_set_from_resource(GTK_IMAGE(self->button_pic), self->resource_path); + } else { + self->button_pic = gtk_image_new_from_resource(self->resource_path); // Create a new image from the resource + 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_box_prepend(GTK_BOX(self), self->button_pic); // Prepend to the box + } +} + +void koto_button_set_text( + KotoButton * self, + gchar * text +) { + if (!KOTO_IS_BUTTON(self)) { + return; + } + + if (!koto_utils_string_is_valid(text)) { // Invalid text + return; + } + + if (koto_utils_string_is_valid(self->text)) { // Text defined + g_free(self->text); // Free existing text + } + + self->text = g_strdup(text); + + if (GTK_IS_LABEL(self->button_label)) { // If we have a button label + if (koto_utils_string_is_valid(self->text)) { // 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 + gtk_box_remove(GTK_BOX(self), self->button_label); + g_free(self->button_label); + } + } else { // If we do not have a button label + if (koto_utils_string_is_valid(self->text)) { // If we have text + self->button_label = gtk_label_new(self->text); // Create our label + gtk_widget_add_css_class(self->button_label, "button-label"); + gtk_widget_set_hexpand(self->button_label, TRUE); + gtk_label_set_xalign(GTK_LABEL(self->button_label), 0); + + if (GTK_IS_IMAGE(self->button_pic)) { // If we have an image + gtk_box_insert_child_after(GTK_BOX(self), self->button_label, self->button_pic); + } else { + gtk_box_prepend(GTK_BOX(self), GTK_WIDGET(self->button_label)); + } + } + } + + g_object_notify_by_pspec(G_OBJECT(self), btn_props[PROP_TEXT]); +} + +void koto_button_set_text_justify( + KotoButton * self, + GtkJustification j +) { + if (!KOTO_IS_BUTTON(self)) { + return; + } + + if (!GTK_IS_LABEL(self->button_label)) { // If we do not have a button label + return; + } + + if (j == GTK_JUSTIFY_CENTER) { // Center text + gtk_label_set_xalign(GTK_LABEL(self->button_label), 0.5); + } else if (j == GTK_JUSTIFY_RIGHT) { // Right align + gtk_label_set_xalign(GTK_LABEL(self->button_label), 1.0); + } else { + gtk_label_set_xalign(GTK_LABEL(self->button_label), 0); + } + + gtk_label_set_justify(GTK_LABEL(self->button_label), j); +} + +void koto_button_set_text_wrap( + KotoButton * self, + gboolean wrap +) { + if (!KOTO_IS_BUTTON(self)) { + return; + } + + if (!GTK_IS_LABEL(self->button_label)) { // If we do not have a button label + return; + } + + gtk_label_set_single_line_mode(GTK_LABEL(self->button_label), !wrap); + gtk_label_set_wrap(GTK_LABEL(self->button_label), wrap); +} + +void koto_button_show_image( + KotoButton * self, + gboolean use_alt +) { + if (!KOTO_IS_BUTTON(self)) { + return; + } + + if (self->use_from_file) { // Use from a file instead of icon name + if (!koto_utils_string_is_valid(self->image_file_path)) { // 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; + + if (GTK_IS_IMAGE(self->button_pic)) { + gtk_image_set_from_icon_name(GTK_IMAGE(self->button_pic), name); // Just update the existing image + } else { // Not an image + self->button_pic = gtk_image_new_from_icon_name(name); // Get our new image + gtk_box_prepend(GTK_BOX(self), self->button_pic); // Prepend to the box + } + } + + 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"); +} + +void koto_button_unset_pseudoactive_styling(KotoButton * self) { + if (!KOTO_IS_BUTTON(self)) { + return; + } + + if (!gtk_widget_has_css_class(GTK_WIDGET(self), "pseudoactive")) { // Don't have the CSS class + return; + } + + gtk_widget_remove_css_class(GTK_WIDGET(self), "pseudoactive"); // Remove pseudoactive class +} + +KotoButton * koto_button_new_plain(gchar * label) { + return g_object_new( + KOTO_TYPE_BUTTON, + "button-text", + label, + NULL + ); +} + +KotoButton * koto_button_new_with_icon( + gchar * label, + gchar * icon_name, + gchar * alt_icon_name, + KotoButtonPixbufSize size +) { + return g_object_new( + KOTO_TYPE_BUTTON, + "button-text", + label, + "icon-name", + icon_name, + "alt-icon-name", + alt_icon_name, + "pixbuf-size", + koto_get_pixbuf_size(size), + 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 + ); +} + +KotoButton * koto_button_new_with_resource ( + gchar * resource_path, + KotoButtonPixbufSize size +) { + return g_object_new( + KOTO_TYPE_BUTTON, + "resource-path", + resource_path, + "pixbuf-size", + koto_get_pixbuf_size(size), + NULL + ); +} diff --git a/src/components/button.h b/src/components/button.h new file mode 100644 index 0000000..70648bc --- /dev/null +++ b/src/components/button.h @@ -0,0 +1,174 @@ +/* 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. + */ + +#pragma once + +#include +#include +#include + +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, + KOTO_BUTTON_PIXBUF_SIZE_SMALL, + KOTO_BUTTON_PIXBUF_SIZE_NORMAL, + KOTO_BUTTON_PIXBUF_SIZE_LARGE, + KOTO_BUTTON_PIXBUF_SIZE_MASSIVE, + 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()) +G_DECLARE_FINAL_TYPE(KotoButton, koto_button, KOTO, BUTTON, GtkBox) +#define KOTO_IS_BUTTON(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_BUTTON)) + +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_file( + gchar * label, + gchar * file_path, + KotoButtonPixbufSize size +); + +KotoButton * koto_button_new_with_resource( + gchar * resource_path, + KotoButtonPixbufSize size +); + +void koto_button_add_click_handler( + KotoButton * self, + KotoButtonClickType button, + GCallback handler, + gpointer user_data +); + +void koto_button_flip(KotoButton * self); + +gpointer koto_button_get_data(KotoButton * self); + +void koto_button_global_page_nav_callback( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer user_data +); + +void koto_button_handle_mouse_enter( + GtkEventControllerMotion * controller, + double x, + double y, + gpointer user_data +); + +void koto_button_handle_mouse_leave( + GtkEventControllerMotion * controller, + gpointer user_data +); + +void koto_button_hide_image(KotoButton * self); + +void koto_button_set_data( + KotoButton * self, + gpointer data +); + +void koto_button_set_pseudoactive_styling(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_resource_path( + KotoButton * self, + gchar * resource_path +); + +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_set_text_justify( + KotoButton * self, + GtkJustification j +); + +void koto_button_set_text_wrap( + KotoButton * self, + gboolean wrap +); + +void koto_button_show_image( + KotoButton * self, + gboolean use_alt +); + +void koto_button_unflatten(KotoButton * self); + +void koto_button_unset_pseudoactive_styling(KotoButton * self); + +G_END_DECLS diff --git a/src/components/cover-art-button.c b/src/components/cover-art-button.c new file mode 100644 index 0000000..e94c2fd --- /dev/null +++ b/src/components/cover-art-button.c @@ -0,0 +1,269 @@ +/* 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-utils.h" +#include "button.h" +#include "cover-art-button.h" + +struct _KotoCoverArtButton { + GObject parent_instance; + + GtkWidget * art; + GtkWidget * main; + GtkWidget * revealer; + KotoButton * play_button; + + gchar * art_path; + + 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_button = koto_button_new_with_icon("", "media-playback-start-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); + gtk_center_box_set_center_widget(GTK_CENTER_BOX(controls), GTK_WIDGET(self->play_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); + + self->art_path = NULL; +} + +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, (gchar*) 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_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, + gchar * art_path +) { + if (!KOTO_IS_COVER_ART_BUTTON(self)) { + return; + } + + gboolean defined_artwork = koto_utils_string_is_valid(art_path); + + 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 + if (g_strcmp0(self->art_path, art_path) != 0) { + self->art_path = g_strdup(art_path); // Set our art path + gtk_image_set_from_file(GTK_IMAGE(self->art), self->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, + 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/cover-art-button.h b/src/components/cover-art-button.h new file mode 100644 index 0000000..d1f7d44 --- /dev/null +++ b/src/components/cover-art-button.h @@ -0,0 +1,63 @@ +/* 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 "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, + 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, + 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/components/track-item.c b/src/components/track-item.c new file mode 100644 index 0000000..368e79f --- /dev/null +++ b/src/components/track-item.c @@ -0,0 +1,158 @@ +/* track-item.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 "indexer/structs.h" +#include "playlist/add-remove-track-popover.h" +#include "button.h" +#include "track-item.h" + +extern KotoAddRemoveTrackPopover * koto_add_remove_track_popup; + +struct _KotoTrackItem { + GtkBox parent_instance; + KotoTrack * track; + + GtkWidget * track_label; +}; + +struct _KotoTrackItemClass { + GtkBoxClass parent_class; +}; + +enum { + PROP_0, + PROP_TRACK, + N_PROPERTIES +}; + +static GParamSpec * props[N_PROPERTIES] = { + NULL, +}; + +G_DEFINE_TYPE(KotoTrackItem, koto_track_item, GTK_TYPE_BOX); + +static void koto_track_item_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +); + +static void koto_track_item_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +); + +static void koto_track_item_class_init(KotoTrackItemClass * c) { + GObjectClass * gobject_class; + + gobject_class = G_OBJECT_CLASS(c); + gobject_class->set_property = koto_track_item_set_property; + gobject_class->get_property = koto_track_item_get_property; + + props[PROP_TRACK] = g_param_spec_object( + "track", + "Track", + "Track", + KOTO_TYPE_TRACK, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + g_object_class_install_properties(gobject_class, N_PROPERTIES, props); +} + +static void koto_track_item_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +) { + KotoTrackItem * self = KOTO_TRACK_ITEM(obj); + + switch (prop_id) { + case PROP_TRACK: + g_value_set_object(val, self->track); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_track_item_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +) { + KotoTrackItem * self = KOTO_TRACK_ITEM(obj); + + switch (prop_id) { + case PROP_TRACK: + koto_track_item_set_track(self, (KotoTrack*) g_value_get_object(val)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +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); + + 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); +} + +KotoTrack * koto_track_item_get_track(KotoTrackItem * self) { + if (!KOTO_IS_TRACK_ITEM(self)) { + return NULL; + } + + return self->track; +} + +void koto_track_item_set_track( + KotoTrackItem * self, + KotoTrack * track +) { + if (track == NULL) { // Not a track + return; + } + + self->track = track; + gchar * track_name; + + g_object_get(self->track, "parsed-name", &track_name, NULL); + gtk_label_set_text(GTK_LABEL(self->track_label), track_name); // Update the text +} + +KotoTrackItem * koto_track_item_new(KotoTrack * track) { + return g_object_new( + KOTO_TYPE_TRACK_ITEM, + "track", + track, + NULL + ); +} diff --git a/src/components/track-item.h b/src/components/track-item.h new file mode 100644 index 0000000..762cf44 --- /dev/null +++ b/src/components/track-item.h @@ -0,0 +1,46 @@ +/* track-item.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 + +#define KOTO_TYPE_TRACK_ITEM (koto_track_item_get_type()) + +G_DECLARE_FINAL_TYPE(KotoTrackItem, koto_track_item, KOTO, TRACK_ITEM, GtkBox) + +KotoTrackItem* koto_track_item_new(KotoTrack * track); +void koto_track_item_handle_add_to_playlist_button_click( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer user_data +); + +KotoTrack * koto_track_item_get_track(KotoTrackItem * self); + +void koto_track_item_set_track( + KotoTrackItem * self, + KotoTrack * track +); + +G_END_DECLS diff --git a/src/components/track-table.c b/src/components/track-table.c new file mode 100644 index 0000000..0907c08 --- /dev/null +++ b/src/components/track-table.c @@ -0,0 +1,540 @@ +/* track-table.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/current.h" +#include "../playlist/playlist.h" +#include "../koto-utils.h" +#include "../koto-window.h" +#include "action-bar.h" +#include "button.h" +#include "track-table.h" + +extern KotoActionBar * action_bar; +extern KotoCartographer * koto_maps; +extern KotoCurrentPlaylist * current_playlist; +extern KotoPlaybackEngine * playback_engine; +extern KotoWindow * main_window; + +struct _KotoTrackTable { + GObject parent_instance; + gchar * uuid; + KotoPlaylist * playlist; + + GtkWidget * main; + + GtkListItemFactory * item_factory; + GListModel * model; + GtkSelectionModel * selection_model; + + GtkWidget * track_list_content; + GtkWidget * track_list_header; + GtkWidget * track_list_view; + + KotoButton * track_album_button; + KotoButton * track_artist_button; + KotoButton * track_num_button; + KotoButton * track_title_button; + + GtkSizeGroup * track_pos_size_group; + GtkSizeGroup * track_name_size_group; + GtkSizeGroup * track_album_size_group; + GtkSizeGroup * track_artist_size_group; +}; + +struct _KotoTrackTableClass { + GObjectClass parent_class; +}; + +G_DEFINE_TYPE(KotoTrackTable, koto_track_table, G_TYPE_OBJECT); + +static void koto_track_table_class_init(KotoTrackTableClass * c) { + (void) c; +} + +static void koto_track_table_init(KotoTrackTable * self) { + self->main = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + + 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->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_track_table_setup_track_item), self); + g_signal_connect(self->item_factory, "bind", G_CALLBACK(koto_track_table_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_track_table_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->main), self->track_list_content); + + g_signal_connect(action_bar, "closed", G_CALLBACK(koto_track_table_handle_action_bar_closed), self); // Handle closed action bar +} + +void koto_track_table_bind_track_item( + GtkListItemFactory * factory, + GtkListItem * item, + KotoTrackTable * self +) { + (void) factory; + + GtkWidget * box = gtk_list_item_get_child(item); + GtkWidget * track_position_label = gtk_widget_get_first_child(box); + 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); + + KotoTrack * track = gtk_list_item_get_item(item); // Get the track UUID from our model + + if (!KOTO_IS_TRACK(track)) { + return; + } + + gchar * track_name = koto_track_get_name(track); + gchar * album_uuid = NULL; + gchar * artist_uuid = NULL; + + g_object_get( + track, + "album-uuid", + &album_uuid, + "artist-uuid", + &artist_uuid, + NULL + ); + + guint track_position = koto_playlist_get_position_of_track(self->playlist, track) + 1; + + 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 + + if (koto_utils_string_is_valid(album_uuid)) { // Is associated with an album + KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, album_uuid); + + if (KOTO_IS_ALBUM(album)) { + gtk_label_set_label(GTK_LABEL(track_album_label), koto_album_get_name(album)); // Get the name of the album and set it to the label + } + } + + if (koto_utils_string_is_valid(artist_uuid)) { // Is associated with an artist + KotoArtist * artist = koto_cartographer_get_artist_by_uuid(koto_maps, artist_uuid); + + if (KOTO_IS_ARTIST(artist)) { + gtk_label_set_label(GTK_LABEL(track_artist_label), koto_artist_get_name(artist)); // Get the name of the artist and set it to the label + } + } + + GList * data = NULL; + data = g_list_append(data, self); // Reference self first + data = g_list_append(data, koto_track_get_uuid(track)); // Next reference the track UUID string + + g_object_set_data(G_OBJECT(box), "data", data); // Bind our list data +} + +void koto_track_table_create_tracks_header(KotoTrackTable * 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_track_table_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_track_table_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_track_table_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_track_table_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_track_table_get_main(KotoTrackTable * self) { + return self->main; +} + +void koto_track_table_handle_action_bar_closed ( + KotoActionBar * bar, + gpointer data +) { + (void) bar; + KotoTrackTable * self = data; + + if (!KOTO_IS_TRACK_TABLE(self)) { // Self is not a track table + return; + } + + 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_track_table_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; + KotoTrackTable * 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_track_table_set_playlist_model(self, KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_ALBUM); +} + +void koto_track_table_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; + KotoTrackTable * 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_track_table_set_playlist_model(self, KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_ARTIST); +} + +void koto_track_table_item_handle_clicked( + GtkGesture * gesture, + int n_press, + double x, + double y, + gpointer user_data +) { + (void) gesture; + (void) x; + (void) y; + + if (n_press != 2) { // Not a double click or tap + return; + } + + GObject * track_item_as_object = G_OBJECT(user_data); + + if (!G_IS_OBJECT(track_item_as_object)) { // Not a GObject + return; + } + + GList * data = g_object_get_data(track_item_as_object, "data"); + + KotoTrackTable * self = g_list_nth_data(data, 0); + gchar * track_uuid = g_list_nth_data(data, 1); + + if (!koto_utils_string_is_valid(track_uuid)) { // Not a valid string + return; + } + + gtk_selection_model_unselect_all(self->selection_model); + gtk_widget_grab_focus(GTK_WIDGET(main_window)); // Focus on the window + koto_action_bar_toggle_reveal(action_bar, FALSE); + koto_action_bar_close(action_bar); // Close the action bar + koto_current_playlist_set_playlist(current_playlist, self->playlist, FALSE, FALSE); // Set the current playlist to the artist's playlist but do not play immediately + koto_playlist_set_track_as_current(self->playlist, track_uuid); // Set this track as the current one for the playlist + koto_playback_engine_set_track_by_uuid(playback_engine, track_uuid, FALSE); // Tell our playback engine to start playing at this track +} + +void koto_track_table_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; + KotoTrackTable * 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_track_table_set_playlist_model(self, KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_TRACK_NAME); +} + +void koto_track_table_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; + KotoTrackTable * self = user_data; + + KotoPreferredPlaylistSortType current_model = koto_playlist_get_current_model(self->playlist); + + if (current_model == KOTO_PREFERRED_PLAYLIST_SORT_TYPE_DEFAULT) { // Set to newest currently + koto_track_table_set_playlist_model(self, KOTO_PREFERRED_PLAYLIST_SORT_TYPE_OLDEST_FIRST); // Sort reversed (oldest) + koto_button_show_image(self->track_num_button, TRUE); // Use inverted value (pan-up-symbolic) + } else { + koto_track_table_set_playlist_model(self, KOTO_PREFERRED_PLAYLIST_SORT_TYPE_DEFAULT); // Sort newest + koto_button_show_image(self->track_num_button, FALSE); // Use pan down default + } +} + +void koto_track_table_handle_tracks_selected( + GtkSelectionModel * model, + guint position, + guint n_items, + gpointer user_data +) { + (void) position; + KotoTrackTable * self = user_data; + + if (!KOTO_IS_TRACK_TABLE(self)) { // Not a track table + return; + } + + 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 + KotoTrack * selected_track = g_list_model_get_item(self->model, GPOINTER_TO_UINT(cur_pos_list->data)); // Get the KotoTrack 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, koto_playlist_get_uuid(self->playlist), selected_tracks); // Set the tracks for the playlist selection + koto_action_bar_toggle_reveal(action_bar, TRUE); // Show the items +} + +void koto_track_table_set_model( + KotoTrackTable * self, + KotoPreferredPlaylistSortType model +) { + if (!KOTO_IS_TRACK_TABLE(self)) { + return; + } + + 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_PLAYLIST_SORT_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_PLAYLIST_SORT_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_PLAYLIST_SORT_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_track_table_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_track_table_set_playlist_model ( + KotoTrackTable * self, + KotoPreferredPlaylistSortType model +) { + if (!KOTO_IS_TRACK_TABLE(self)) { // Not a track table + return; + } + + if (!KOTO_IS_PLAYLIST(self->playlist)) { // Don't have a playlist yet + return; + } + + 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_PLAYLIST_SORT_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_PLAYLIST_SORT_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_PLAYLIST_SORT_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_track_table_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 + + KotoPreferredPlaylistSortType current_model = koto_playlist_get_current_model(self->playlist); // Get the current model + + if (current_model == KOTO_PREFERRED_PLAYLIST_SORT_TYPE_OLDEST_FIRST) { + koto_button_show_image(self->track_num_button, TRUE); // Immediately use pan-up-symbolic + } +} + +void koto_track_table_set_playlist( + KotoTrackTable * self, + KotoPlaylist * playlist +) { + if (!KOTO_IS_TRACK_TABLE(self)) { + return; + } + + if (!KOTO_IS_PLAYLIST(playlist)) { // Not a playlist + return; + } + + self->playlist = playlist; + koto_track_table_set_playlist_model(self, KOTO_PREFERRED_PLAYLIST_SORT_TYPE_DEFAULT); // TODO: Enable this to be changed +} + +void koto_track_table_setup_track_item( + GtkListItemFactory * factory, + GtkListItem * item, + gpointer user_data +) { + (void) factory; + KotoTrackTable * self = user_data; + + if (!KOTO_IS_TRACK_TABLE(self)) { + return; + } + + 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); + + GtkGesture * double_click_gesture = gtk_gesture_click_new(); // Create our new GtkGesture for double-click handling + gtk_widget_add_controller(item_content, GTK_EVENT_CONTROLLER(double_click_gesture)); // Have our item handle double clicking + g_signal_connect(double_click_gesture, "released", G_CALLBACK(koto_track_table_item_handle_clicked), item_content); +} + +KotoTrackTable * koto_track_table_new() { + return g_object_new( + KOTO_TYPE_TRACK_TABLE, + NULL + ); +} \ No newline at end of file diff --git a/src/components/track-table.h b/src/components/track-table.h new file mode 100644 index 0000000..613cbf3 --- /dev/null +++ b/src/components/track-table.h @@ -0,0 +1,122 @@ +/* track-table.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 "../playlist/playlist.h" +#include "action-bar.h" + +G_BEGIN_DECLS + +#define KOTO_TYPE_TRACK_TABLE koto_track_table_get_type() +#define KOTO_TRACK_TABLE(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), KOTO_TYPE_TRACK_TABLE, KotoTrackTable)) +typedef struct _KotoTrackTable KotoTrackTable; +typedef struct _KotoTrackTableClass KotoTrackTableClass; + +GLIB_AVAILABLE_IN_ALL +GType koto_track_type_get_type(void) G_GNUC_CONST; + +#define KOTO_IS_TRACK_TABLE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_TRACK_TABLE)) + +void koto_track_table_bind_track_item( + GtkListItemFactory * factory, + GtkListItem * item, + KotoTrackTable * self +); + +void koto_track_table_create_tracks_header(KotoTrackTable * self); + +GtkWidget * koto_track_table_get_main(KotoTrackTable * self); + +void koto_track_table_handle_action_bar_closed( + KotoActionBar * bar, + gpointer data +); + +void koto_track_table_handle_track_album_clicked( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer user_data +); + +void koto_track_table_handle_track_artist_clicked( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer user_data +); + +void koto_track_table_handle_track_name_clicked( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer user_data +); + +void koto_track_table_handle_track_name_clicked( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer user_data +); + +void koto_track_table_handle_track_num_clicked( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer user_data +); + +void koto_track_table_handle_tracks_selected( + GtkSelectionModel * model, + guint position, + guint n_items, + gpointer user_data +); + +void koto_track_table_set_model( + KotoTrackTable * self, + KotoPreferredPlaylistSortType model +); + +void koto_track_table_set_playlist_model( + KotoTrackTable * self, + KotoPreferredPlaylistSortType model +); + +void koto_track_table_set_playlist( + KotoTrackTable * self, + KotoPlaylist * playlist +); + +void koto_track_table_setup_track_item( + GtkListItemFactory * factory, + GtkListItem * item, + gpointer user_data +); + +KotoTrackTable * koto_track_table_new(); + +G_END_DECLS \ No newline at end of file diff --git a/src/config/config.c b/src/config/config.c new file mode 100644 index 0000000..e31b4d2 --- /dev/null +++ b/src/config/config.c @@ -0,0 +1,768 @@ +/* config.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 + +#include "../db/cartographer.h" +#include "../playback/engine.h" +#include "../koto-paths.h" +#include "../koto-utils.h" + +#include "config.h" + +extern int errno; +extern const gchar * koto_config_template; +extern KotoCartographer * koto_maps; +extern KotoPlaybackEngine * playback_engine; + +enum { + PROP_0, + PROP_PLAYBACK_CONTINUE_ON_PLAYLIST, + PROP_PLAYBACK_LAST_USED_VOLUME, + PROP_PLAYBACK_MAINTAIN_SHUFFLE, + PROP_PLAYBACK_JUMP_BACKWARDS_INCREMENT, + PROP_PLAYBACK_JUMP_FORWARDS_INCREMENT, + PROP_PREFERRED_ALBUM_SORT_TYPE, + PROP_UI_ALBUM_INFO_SHOW_DESCRIPTION, + PROP_UI_ALBUM_INFO_SHOW_GENRES, + PROP_UI_ALBUM_INFO_SHOW_NARRATOR, + PROP_UI_ALBUM_INFO_SHOW_YEAR, + PROP_UI_THEME_DESIRED, + PROP_UI_THEME_OVERRIDE, + N_PROPS, +}; + +static GParamSpec * config_props[N_PROPS] = { + 0 +}; + +struct _KotoConfig { + GObject parent_instance; + toml_table_t * toml_ref; + + GFile * config_file; + GFileMonitor * config_file_monitor; + + gchar * path; + gboolean finalized; + + /* Library Attributes */ + + // These are useful for when we need to determine separately if we need to index initial builtin folders that did not exist previously (during load) + gboolean has_type_audiobook; + gboolean has_type_music; + gboolean has_type_podcast; + + /* Playback Settings */ + + gboolean playback_continue_on_playlist; + gdouble playback_last_used_volume; + gboolean playback_maintain_shuffle; + guint playback_jump_backwards_increment; + guint playback_jump_forwards_increment; + + /* Misc Prefs */ + + KotoPreferredAlbumSortType preferred_album_sort_type; + + /* UI Settings */ + + gboolean ui_album_info_show_description; + gboolean ui_album_info_show_genres; + gboolean ui_album_info_show_narrator; + gboolean ui_album_info_show_year; + + gchar * ui_theme_desired; + gboolean ui_theme_override; +}; + +struct _KotoConfigClass { + GObjectClass parent_class; +}; + +G_DEFINE_TYPE(KotoConfig, koto_config, G_TYPE_OBJECT); + +KotoConfig * config; + +static void koto_config_constructed(GObject * obj); + +static void koto_config_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +); + +static void koto_config_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +); + +static void koto_config_class_init(KotoConfigClass * c) { + GObjectClass * gobject_class; + gobject_class = G_OBJECT_CLASS(c); + gobject_class->constructed = koto_config_constructed; + gobject_class->get_property = koto_config_get_property; + gobject_class->set_property = koto_config_set_property; + + config_props[PROP_PLAYBACK_CONTINUE_ON_PLAYLIST] = g_param_spec_boolean( + "playback-continue-on-playlist", + "Continue Playback of Playlist", + "Continue playback of a Playlist after playing a specific track in the playlist", + FALSE, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + config_props[PROP_PLAYBACK_LAST_USED_VOLUME] = g_param_spec_double( + "playback-last-used-volume", + "Last Used Volume", + "Last Used Volume", + 0, + 1, + 0.5, // 50% + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + config_props[PROP_PLAYBACK_MAINTAIN_SHUFFLE] = g_param_spec_boolean( + "playback-maintain-shuffle", + "Maintain Shuffle on Playlist Change", + "Maintain shuffle setting when changing playlists", + TRUE, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + config_props[PROP_PLAYBACK_JUMP_BACKWARDS_INCREMENT] = g_param_spec_uint( + "playback-jump-backwards-increment", + "Jump Backwards Increment", + "Jump Backwards Increment", + 5, // 5s + 90, // 1min30s + 10, // 10s + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + config_props[PROP_PLAYBACK_JUMP_FORWARDS_INCREMENT] = g_param_spec_uint( + "playback-jump-forwards-increment", + "Jump Forwards Increment", + "Jump Forwards Increment", + 5, // 5s + 90, // 1min30s + 30, // 30s + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + config_props[PROP_PREFERRED_ALBUM_SORT_TYPE] = g_param_spec_string( + "artist-preferred-album-sort-type", + "Preferred album sort type (chronological or alphabetical-only)", + "Preferred album sort type (chronological or alphabetical-only)", + "default", + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + config_props[PROP_UI_ALBUM_INFO_SHOW_DESCRIPTION] = g_param_spec_boolean( + "ui-album-info-show-description", + "Show Description in Album Info", + "Show Description in Album Info", + TRUE, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + config_props[PROP_UI_ALBUM_INFO_SHOW_GENRES] = g_param_spec_boolean( + "ui-album-info-show-genres", + "Show Genres in Album Info", + "Show Genres in Album Info", + TRUE, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + config_props[PROP_UI_ALBUM_INFO_SHOW_NARRATOR] = g_param_spec_boolean( + "ui-album-info-show-narrator", + "Show Narrator in Album Info", + "Show Narrator in Album Info", + TRUE, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + config_props[PROP_UI_ALBUM_INFO_SHOW_YEAR] = g_param_spec_boolean( + "ui-album-info-show-year", + "Show Year in Album Info", + "Show Year in Album Info", + TRUE, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + config_props[PROP_UI_THEME_DESIRED] = g_param_spec_string( + "ui-theme-desired", + "Desired Theme", + "Desired Theme", + "dark", // Like my soul + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + config_props[PROP_UI_THEME_OVERRIDE] = g_param_spec_boolean( + "ui-theme-override", + "Override built-in theming", + "Override built-in theming", + FALSE, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + g_object_class_install_properties(gobject_class, N_PROPS, config_props); +} + +static void koto_config_init(KotoConfig * self) { + self->finalized = FALSE; + self->has_type_audiobook = FALSE; + self->has_type_music = FALSE; + self->has_type_podcast = FALSE; +} + +static void koto_config_constructed(GObject * obj) { + KotoConfig * self = KOTO_CONFIG(obj); + self->finalized = TRUE; +} + +static void koto_config_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +) { + KotoConfig * self = KOTO_CONFIG(obj); + + switch (prop_id) { + case PROP_PLAYBACK_CONTINUE_ON_PLAYLIST: + g_value_set_boolean(val, self->playback_continue_on_playlist); + break; + case PROP_PLAYBACK_LAST_USED_VOLUME: + g_value_set_double(val, self->playback_last_used_volume); + break; + case PROP_PLAYBACK_MAINTAIN_SHUFFLE: + g_value_set_boolean(val, self->playback_maintain_shuffle); + break; + case PROP_PLAYBACK_JUMP_BACKWARDS_INCREMENT: + g_value_set_uint(val, self->playback_jump_backwards_increment); + break; + case PROP_PLAYBACK_JUMP_FORWARDS_INCREMENT: + g_value_set_uint(val, self->playback_jump_forwards_increment); + break; + case PROP_UI_ALBUM_INFO_SHOW_DESCRIPTION: + g_value_set_boolean(val, self->ui_album_info_show_description); + break; + case PROP_UI_ALBUM_INFO_SHOW_GENRES: + g_value_set_boolean(val, self->ui_album_info_show_genres); + break; + case PROP_UI_ALBUM_INFO_SHOW_NARRATOR: + g_value_set_boolean(val, self->ui_album_info_show_narrator); + break; + case PROP_UI_ALBUM_INFO_SHOW_YEAR: + g_value_set_boolean(val, self->ui_album_info_show_year); + break; + case PROP_UI_THEME_DESIRED: + g_value_set_string(val, g_strdup(self->ui_theme_desired)); + break; + case PROP_UI_THEME_OVERRIDE: + g_value_set_boolean(val, self->ui_theme_override); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_config_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +) { + KotoConfig * self = KOTO_CONFIG(obj); + + switch (prop_id) { + case PROP_PLAYBACK_CONTINUE_ON_PLAYLIST: + self->playback_continue_on_playlist = g_value_get_boolean(val); + break; + case PROP_PLAYBACK_LAST_USED_VOLUME: + self->playback_last_used_volume = g_value_get_double(val); + break; + case PROP_PLAYBACK_MAINTAIN_SHUFFLE: + self->playback_maintain_shuffle = g_value_get_boolean(val); + break; + case PROP_PLAYBACK_JUMP_BACKWARDS_INCREMENT: + self->playback_jump_backwards_increment = g_value_get_uint(val); + break; + case PROP_PLAYBACK_JUMP_FORWARDS_INCREMENT: + self->playback_jump_forwards_increment = g_value_get_uint(val); + break; + case PROP_PREFERRED_ALBUM_SORT_TYPE: + self->preferred_album_sort_type = g_strcmp0(g_value_get_string(val), "alphabetical") ? KOTO_PREFERRED_ALBUM_ALWAYS_ALPHABETICAL : KOTO_PREFERRED_ALBUM_SORT_TYPE_DEFAULT; + break; + case PROP_UI_ALBUM_INFO_SHOW_DESCRIPTION: + self->ui_album_info_show_description = g_value_get_boolean(val); + break; + case PROP_UI_ALBUM_INFO_SHOW_GENRES: + self->ui_album_info_show_genres = g_value_get_boolean(val); + break; + case PROP_UI_ALBUM_INFO_SHOW_NARRATOR: + self->ui_album_info_show_narrator = g_value_get_boolean(val); + break; + case PROP_UI_ALBUM_INFO_SHOW_YEAR: + self->ui_album_info_show_year = g_value_get_boolean(val); + break; + case PROP_UI_THEME_DESIRED: + self->ui_theme_desired = g_strdup(g_value_get_string(val)); + break; + case PROP_UI_THEME_OVERRIDE: + self->ui_theme_override = g_value_get_boolean(val); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } + + if (self->finalized) { // Loaded the config + g_object_notify_by_pspec(obj, config_props[prop_id]); // Notify that a change happened + } +} + +toml_table_t * koto_config_get_library( + KotoConfig * self, + gchar * library_uuid +) { + toml_array_t * libraries = toml_array_in(self->toml_ref, "library"); // Get the array of tables + for (int i = 0; i < toml_array_nelem(libraries); i++) { // For each library + toml_table_t * library = toml_table_at(libraries, i); // Get this library + toml_datum_t uuid_datum = toml_string_in(library, "uuid"); // Get the datum for the uuid + + gchar * lib_uuid = (uuid_datum.ok) ? (gchar*) uuid_datum.u.s : NULL; + + if (koto_utils_string_is_valid(lib_uuid) && (g_strcmp0(library_uuid, lib_uuid) == 0)) { // Is a valid string and the libraries match + return library; + } + } + + return NULL; +} + +/** + * Load our TOML file from the specified path into our KotoConfig + **/ +void koto_config_load( + KotoConfig * self, + gchar * path +) { + if (!koto_utils_string_is_valid(path)) { // Path is not valid + return; + } + + self->path = g_strdup(path); + + self->config_file = g_file_new_for_path(path); + gboolean config_file_exists = g_file_query_exists(self->config_file, NULL); + + if (!config_file_exists) { // File does not exist + GError * create_err; + GFileOutputStream * stream = g_file_create( + self->config_file, + G_FILE_CREATE_PRIVATE, + NULL, + &create_err + ); + + if (create_err != NULL) { + if (create_err->code != G_IO_ERROR_EXISTS) { // Not an error indicating the file already exists + g_warning("Failed to create or open file: %s", create_err->message); + return; + } + } + + g_object_unref(stream); + } + + GError * file_info_query_err; + + GFileInfo * file_info = g_file_query_info( + // Get the size of our TOML file + self->config_file, + G_FILE_ATTRIBUTE_STANDARD_SIZE, + G_FILE_QUERY_INFO_NONE, + NULL, + &file_info_query_err + ); + + if (file_info != NULL) { // Got info + goffset size = g_file_info_get_size(file_info); // Get the size from info + g_object_unref(file_info); // Unref immediately + + if (size == 0) { // If we don't have any file contents (new file), skip parsing + goto monitor; + } + } else { // Failed to get the info + g_warning("Failed to get size info of %s: %s", self->path, file_info_query_err->message); + } + + FILE * file; + file = fopen(self->path, "r"); // Open the file as read only + + if (file == NULL) { // Failed to get the file + /** Handle error checking here*/ + return; + } + + char errbuf[200]; + toml_table_t * conf = toml_parse_file(file, errbuf, sizeof(errbuf)); + fclose(file); // Close the file + + if (!conf) { + g_error("Failed to read our config file. %s", errbuf); + return; + } + + self->toml_ref = conf; + + /** Supplemental Libraries (Excludes Built-in) */ + + toml_array_t * libraries = toml_array_in(conf, "library"); // Get all of our libraries + if (libraries) { // If we have libraries already + for (int i = 0; i < toml_array_nelem(libraries); i++) { // Iterate over each library + toml_table_t * lib = toml_table_at(libraries, i); // Get the library datum + KotoLibrary * koto_lib = koto_library_new_from_toml_table(lib); // Get the library based on the TOML data for this specific type + + if (!KOTO_IS_LIBRARY(koto_lib)) { // Something wrong with it, not a library + continue; + } + + koto_cartographer_add_library(koto_maps, koto_lib); // Add library to Cartographer + KotoLibraryType lib_type = koto_library_get_lib_type(koto_lib); // Get the type + + if (lib_type == KOTO_LIBRARY_TYPE_AUDIOBOOK) { // Is an audiobook lib + self->has_type_audiobook = TRUE; + } else if (lib_type == KOTO_LIBRARY_TYPE_MUSIC) { // Is a music lib + self->has_type_music = TRUE; + } else if (lib_type == KOTO_LIBRARY_TYPE_PODCAST) { // Is a podcast lib + self->has_type_podcast = TRUE; + } + } + } + + /** Playback Section */ + + toml_table_t * playback_section = toml_table_in(conf, "playback"); + + if (playback_section) { // Have playback section + toml_datum_t continue_on_playlist = toml_bool_in(playback_section, "continue-on-playlist"); + toml_datum_t jump_backwards_increment = toml_int_in(playback_section, "jump-backwards-increment"); + toml_datum_t jump_forwards_increment = toml_int_in(playback_section, "jump-forwards-increment"); + toml_datum_t last_used_volume = toml_double_in(playback_section, "last-used-volume"); + toml_datum_t maintain_shuffle = toml_bool_in(playback_section, "maintain-shuffle"); + + if (continue_on_playlist.ok && (self->playback_continue_on_playlist != continue_on_playlist.u.b)) { // If we have a continue-on-playlist set and they are different + g_object_set(self, "playback-continue-on-playlist", continue_on_playlist.u.b, NULL); + } + + if (jump_backwards_increment.ok && (self->playback_jump_backwards_increment != jump_backwards_increment.u.i)) { // If we have a jump-backwards-increment set and it is different + g_object_set(self, "playback-jump-backwards-increment", (guint) jump_backwards_increment.u.i, NULL); + } + + if (jump_forwards_increment.ok && (self->playback_jump_forwards_increment != jump_forwards_increment.u.i)) { // If we have a jump-backwards-increment set and it is different + g_object_set(self, "playback-jump-forwards-increment", (guint) jump_forwards_increment.u.i, NULL); + } + + if (last_used_volume.ok && (self->playback_last_used_volume != last_used_volume.u.d)) { // If we have last-used-volume set and they are different + g_object_set(self, "playback-last-used-volume", last_used_volume.u.d, NULL); + } + + if (maintain_shuffle.ok && (self->playback_maintain_shuffle != maintain_shuffle.u.b)) { // If we have a "maintain shuffle set" and they are different + g_object_set(self, "playback-maintain-shuffle", maintain_shuffle.u.b, NULL); + } + } + + /* UI Section */ + + toml_table_t * ui_section = toml_table_in(conf, "ui"); + + if (ui_section) { // Have UI section + toml_datum_t album_info_show_description = toml_bool_in(ui_section, "album-info-show-description"); + + if (album_info_show_description.ok && (album_info_show_description.u.b != self->ui_album_info_show_description)) { // Changed if we are disabling description + g_object_set(self, "ui-album-info-show-description", album_info_show_description.u.b, NULL); + } + + toml_datum_t album_info_show_genres = toml_bool_in(ui_section, "album-info-show-genres"); + + if (album_info_show_genres.ok && (album_info_show_genres.u.b != self->ui_album_info_show_genres)) { // Changed if we are disabling description + g_object_set(self, "ui-album-info-show-genres", album_info_show_genres.u.b, NULL); + } + + toml_datum_t album_info_show_narrator = toml_bool_in(ui_section, "album-info-show-narrator"); + + if (album_info_show_narrator.ok && (album_info_show_narrator.u.b != self->ui_album_info_show_narrator)) { // Changed if we are disabling description + g_object_set(self, "ui-album-info-show-narrator", album_info_show_description.u.b, NULL); + } + + toml_datum_t album_info_show_year = toml_bool_in(ui_section, "album-info-show-year"); + + if (album_info_show_year.ok && (album_info_show_year.u.b != self->ui_album_info_show_year)) { // Changed if we are disabling description + g_object_set(self, "ui-album-info-show-year", album_info_show_year.u.b, NULL); + } + + toml_datum_t name = toml_string_in(ui_section, "theme-desired"); + + if (name.ok && (g_strcmp0(name.u.s, self->ui_theme_desired) != 0)) { // Have a name specified and they are different + g_object_set(self, "ui-theme-desired", g_strdup(name.u.s), NULL); + free(name.u.s); + } + + toml_datum_t override_app = toml_bool_in(ui_section, "theme-override"); + + if (override_app.ok && (override_app.u.b != self->ui_theme_override)) { // Changed if we are overriding theme + g_object_set(self, "ui-theme-override", override_app.u.b, NULL); + } + } + +monitor: + if (self->config_file_monitor != NULL) { // If we already have a file monitor for the file + return; + } + + self->config_file_monitor = g_file_monitor_file( + self->config_file, + G_FILE_MONITOR_NONE, + NULL, + NULL + ); + + g_signal_connect(self->config_file_monitor, "changed", G_CALLBACK(koto_config_monitor_handle_changed), self); // Monitor changes to our config file + + if (!config_file_exists) { // File did not originally exist + koto_config_save(self); // Save immediately + } +} + +void koto_config_load_libs(KotoConfig * self) { + gchar * home_dir = g_strdup(g_get_home_dir()); // Get the home directory + + if (!self->has_type_audiobook) { // If we do not have a KotoLibrary for Audiobooks + gchar * audiobooks_path = g_build_path(G_DIR_SEPARATOR_S, home_dir, "Audiobooks", NULL); + koto_utils_mkdir(audiobooks_path); // Create the directory just in case + KotoLibrary * lib = koto_library_new(KOTO_LIBRARY_TYPE_AUDIOBOOK, NULL, audiobooks_path); // Audiobooks relative to home directory + + if (KOTO_IS_LIBRARY(lib)) { // Created built-in audiobooks lib successfully + koto_cartographer_add_library(koto_maps, lib); + koto_config_save(config); + koto_library_index(lib); // Index this library + } + + g_free(audiobooks_path); + } + + if (!self->has_type_music) { // If we do not have a KotoLibrary for Music + KotoLibrary * lib = koto_library_new(KOTO_LIBRARY_TYPE_MUSIC, NULL, g_get_user_special_dir(G_USER_DIRECTORY_MUSIC)); // Create a library using the user's MUSIC directory defined + + if (KOTO_IS_LIBRARY(lib)) { // Created built-in music lib successfully + koto_cartographer_add_library(koto_maps, lib); + koto_config_save(config); + koto_library_index(lib); // Index this library + } + } + + if (!self->has_type_podcast) { // If we do not have a KotoLibrary for Podcasts + gchar * podcasts_path = g_build_path(G_DIR_SEPARATOR_S, home_dir, "Podcasts", NULL); + koto_utils_mkdir(podcasts_path); // Create the directory just in case + KotoLibrary * lib = koto_library_new(KOTO_LIBRARY_TYPE_PODCAST, NULL, podcasts_path); // Podcasts relative to home dir + + if (KOTO_IS_LIBRARY(lib)) { // Created built-in podcasts lib successfully + koto_cartographer_add_library(koto_maps, lib); + koto_config_save(config); + koto_library_index(lib); // Index this library + } + + g_free(podcasts_path); + } + + g_free(home_dir); + g_thread_exit(0); +} + +void koto_config_monitor_handle_changed( + GFileMonitor * monitor, + GFile * file, + GFile * other_file, + GFileMonitorEvent ev, + gpointer user_data +) { + (void) monitor; + (void) file; + (void) other_file; + KotoConfig * config = user_data; + + if ( + (ev == G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED) || // Attributes changed + (ev == G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT) // Changes done + ) { + koto_config_refresh(config); // Refresh the config + } +} + +KotoPreferredAlbumSortType koto_config_get_preferred_album_sort_type(KotoConfig * self) { + return self->preferred_album_sort_type; +} + +/** + * Refresh will handle any FS notify change on our Koto config file and call load + **/ +void koto_config_refresh(KotoConfig * self) { + koto_config_load(self, self->path); +} + +/** + * Save will write our config back out + **/ +void koto_config_save(KotoConfig * self) { + GStrvBuilder * root_builder = g_strv_builder_new(); // Create a new strv builder + + /* Iterate over our libraries */ + + GList * libs = koto_cartographer_get_libraries(koto_maps); // Get our libraries + GList * current_libs; + + for (current_libs = libs; current_libs != NULL; current_libs = current_libs->next) { // Iterate over our libraries + KotoLibrary * lib = current_libs->data; + gchar * lib_config = koto_library_to_config_string(lib); // Get the config string + g_strv_builder_add(root_builder, lib_config); // Add the config to our string builder + g_free(lib_config); + } + + GParamSpec ** props_list = g_object_class_list_properties(G_OBJECT_GET_CLASS(self), NULL); // Get the propreties associated with our settings + + GHashTable * sections_to_prop_keys = g_hash_table_new(g_str_hash, g_str_equal); // Create our section to hold our various sections based on props + + /* Section Hashes*/ + + gchar * playback_hash = g_strdup("playback"); + gchar * ui_hash = g_strdup("ui"); + + gdouble current_playback_volume = 1.0; + + if (KOTO_IS_PLAYBACK_ENGINE(playback_engine)) { // Have a playback engine (useful since it may not be initialized before the config performs saving on first application load) + current_playback_volume = koto_playback_engine_get_volume(playback_engine); // Get the last used volume in the playback engine + } + + self->playback_last_used_volume = current_playback_volume; // Update our value so we have it during save + + int i; + for (i = 0; i < N_PROPS; i++) { // For each property + GParamSpec * spec = props_list[i]; // Get the prop + + if (!G_IS_PARAM_SPEC(spec)) { // Not a spec + continue; // Skip + } + + const gchar * prop_name = g_param_spec_get_name(spec); + + gpointer respective_prop = NULL; + + if (g_str_has_prefix(prop_name, "playback")) { // Is playback + respective_prop = playback_hash; + } else if (g_str_has_prefix(prop_name, "ui")) { // Is UI + respective_prop = ui_hash; + } + + if (respective_prop == NULL) { // No property + continue; + } + + GList * keys; + + if (g_hash_table_contains(sections_to_prop_keys, respective_prop)) { // Already has list + keys = g_hash_table_lookup(sections_to_prop_keys, respective_prop); // Get the list + } else { // Don't have list + keys = NULL; + } + + keys = g_list_append(keys, g_strdup(prop_name)); // Add the name in full + g_hash_table_insert(sections_to_prop_keys, respective_prop, keys); // Replace list (or add it) + } + + GHashTableIter iter; + gpointer section_name, section_props; + + g_hash_table_iter_init(&iter, sections_to_prop_keys); + + while (g_hash_table_iter_next(&iter, §ion_name, §ion_props)) { + GStrvBuilder * section_builder = g_strv_builder_new(); // Make our string builder + g_strv_builder_add(section_builder, g_strdup_printf("[%s]", (gchar*) section_name)); // Add section as [section] + + GList * current_section_keyname; + for (current_section_keyname = section_props; current_section_keyname != NULL; current_section_keyname = current_section_keyname->next) { // Iterate over property names + GValue prop_val_raw = G_VALUE_INIT; // Initialize our GValue + g_object_get_property(G_OBJECT(self), current_section_keyname->data, &prop_val_raw); + gchar * prop_val = g_strdup_value_contents(&prop_val_raw); + + if ((g_strcmp0(prop_val, "TRUE") == 0) || (g_strcmp0(prop_val, "FALSE") == 0)) { // TRUE or FALSE from a boolean type + prop_val = g_utf8_strdown(prop_val, -1); // Change it to be lowercased + } + + gchar * key_name = g_strdup(current_section_keyname->data); + gchar * key_name_replaced = koto_utils_string_replace_all(key_name, g_strdup_printf("%s-", (gchar*) section_name), ""); // Remove SECTIONNAME- + + const gchar * line = g_strdup_printf("\t%s = %s", key_name_replaced, prop_val); + + g_strv_builder_add(section_builder, line); // Add the line + g_free(key_name_replaced); + g_free(key_name); + } + + GStrv lines = g_strv_builder_end(section_builder); // Get all the lines as a GStrv which is a gchar ** + gchar * content = g_strjoinv("\n", lines); // Separate all lines with newline + g_strfreev(lines); // Free our lines + + g_strv_builder_add(root_builder, content); // Add section content to root builder + g_strv_builder_unref(section_builder); // Unref our builder + } + + g_hash_table_unref(sections_to_prop_keys); // Free our hash table + + GStrv lines = g_strv_builder_end(root_builder); // Get all the lines as a GStrv which is a gchar ** + gchar * content = g_strjoinv("\n", lines); // Separate all lines with newline + g_strfreev(lines); // Free our lines + + g_strv_builder_unref(root_builder); // Unref our root builder + + ulong file_content_length = g_utf8_strlen(content, -1); + + g_file_replace_contents( + self->config_file, + content, + file_content_length, + NULL, + FALSE, + G_FILE_CREATE_PRIVATE, + NULL, + NULL, + NULL + ); +} + +KotoConfig * koto_config_new() { + return g_object_new(KOTO_TYPE_CONFIG, NULL); +} \ No newline at end of file diff --git a/src/config/config.h b/src/config/config.h new file mode 100644 index 0000000..5aa5f02 --- /dev/null +++ b/src/config/config.h @@ -0,0 +1,55 @@ +/* config.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/misc-types.h" + +G_BEGIN_DECLS + +/** + * Type Definition + **/ + +#define KOTO_TYPE_CONFIG (koto_config_get_type()) + +G_DECLARE_FINAL_TYPE(KotoConfig, koto_config, KOTO, CONFIG, GObject) + +KotoConfig* koto_config_new(); +void koto_config_load( + KotoConfig * self, + gchar * path +); + +void koto_config_load_libs(KotoConfig * self); + +void koto_config_monitor_handle_changed( + GFileMonitor * monitor, + GFile * file, + GFile * other_file, + GFileMonitorEvent ev, + gpointer user_data +); + +KotoPreferredAlbumSortType koto_config_get_preferred_album_sort_type(KotoConfig * self); + +void koto_config_refresh(KotoConfig * self); + +void koto_config_save(KotoConfig * self); + +G_END_DECLS \ No newline at end of file diff --git a/src/db/cartographer.c b/src/db/cartographer.c new file mode 100644 index 0000000..829a5c6 --- /dev/null +++ b/src/db/cartographer.c @@ -0,0 +1,910 @@ +/* cartographer.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 "../koto-utils.h" +#include "cartographer.h" + +enum { + SIGNAL_ALBUM_ADDED, + SIGNAL_ALBUM_REMOVED, + SIGNAL_ARTIST_ADDED, + SIGNAL_ARTIST_REMOVED, + SIGNAL_LIBRARY_ADDED, + SIGNAL_LIBRARY_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; + + GHashTable * albums; + GHashTable * artists; + GHashTable * artists_name_to_uuid; + GHashTable * libraries; + GHashTable * playlists; + GHashTable * tracks; + GHashTable * tracks_by_uniqueish_key; +}; + +struct _KotoCartographerClass { + GObjectClass parent_class; + + void (* album_added) ( + KotoCartographer * cartographer, + KotoAlbum * album + ); + void (* album_removed) ( + KotoCartographer * cartographer, + KotoAlbum * album + ); + void (* artist_added) ( + KotoCartographer * cartographer, + KotoArtist * artist + ); + void (* artist_removed) ( + KotoCartographer * cartographer, + KotoArtist * artist + ); + void (* library_added) ( + KotoCartographer * cartographer, + KotoLibrary * library + ); + void (* library_removed) ( + KotoCartographer * cartographer, + KotoLibrary * library + ); + void (* playlist_added) ( + KotoCartographer * cartographer, + KotoPlaylist * playlist + ); + void (* playlist_removed) ( + KotoCartographer * cartographer, + KotoPlaylist * playlist + ); + void (* track_added) ( + KotoCartographer * cartographer, + KotoTrack * track + ); + void (* track_removed) ( + KotoCartographer * cartographer, + KotoTrack * track + ); +}; + +G_DEFINE_TYPE(KotoCartographer, koto_cartographer, G_TYPE_OBJECT); + +KotoCartographer * koto_maps = NULL; + +static void koto_cartographer_class_init(KotoCartographerClass * 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_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_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, + 2, + G_TYPE_CHAR, + G_TYPE_CHAR + ); + + cartographer_signals[SIGNAL_LIBRARY_ADDED] = g_signal_new( + "library-added", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoCartographerClass, library_added), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + KOTO_TYPE_LIBRARY + ); + + cartographer_signals[SIGNAL_LIBRARY_ADDED] = g_signal_new( + "library-removed", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoCartographerClass, library_removed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + KOTO_TYPE_LIBRARY + ); + + 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_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) { + self->albums = g_hash_table_new(g_str_hash, g_str_equal); + self->artists = g_hash_table_new(g_str_hash, g_str_equal); + self->artists_name_to_uuid = g_hash_table_new(g_str_hash, g_str_equal); + self->libraries = g_hash_table_new(g_str_hash, g_str_equal); + self->playlists = g_hash_table_new(g_str_hash, g_str_equal); + self->tracks = g_hash_table_new(g_str_hash, g_str_equal); + self->tracks_by_uniqueish_key = g_hash_table_new(g_str_hash, g_str_equal); +} + +void koto_cartographer_add_album( + KotoCartographer * self, + KotoAlbum * album +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return; + } + + if (!KOTO_IS_ALBUM(album)) { + return; + } + + gchar * album_uuid = koto_album_get_uuid(album); // Get the album UUID + + if (!koto_utils_string_is_valid(album_uuid) || 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, + KotoArtist * artist +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return; + } + + if (!KOTO_IS_ARTIST(artist)) { // Not an artist + return; + } + + gchar * artist_uuid = koto_artist_get_uuid(artist); + + if (!koto_utils_string_is_valid(artist_uuid) || koto_cartographer_has_artist_by_uuid(self, artist_uuid)) { // Have the artist or invalid UUID + return; + } + + g_hash_table_replace(self->artists_name_to_uuid, koto_artist_get_name(artist), artist_uuid); // Add the UUID as a value with the key being the name of the artist + g_hash_table_replace(self->artists, artist_uuid, artist); + + g_signal_emit( + self, + cartographer_signals[SIGNAL_ARTIST_ADDED], + 0, + artist + ); +} + +void koto_cartographer_add_library( + KotoCartographer * self, + KotoLibrary * library +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return; + } + + if (!KOTO_IS_LIBRARY(library)) { // Not a library + return; + } + + gchar * library_uuid = koto_library_get_uuid(library); + + if (!koto_utils_string_is_valid(library_uuid) || koto_cartographer_has_library_by_uuid(self, library_uuid)) { // Have the library or invalid UUID + return; + } + + g_hash_table_replace(self->libraries, library_uuid, library); // Add the library + g_signal_emit( + // Emit our library added signal + self, + cartographer_signals[SIGNAL_LIBRARY_ADDED], + 0, + library + ); +} + +void koto_cartographer_add_playlist( + KotoCartographer * self, + KotoPlaylist * playlist +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return; + } + + if (!KOTO_IS_PLAYLIST(playlist)) { // Not a playlist + return; + } + + gchar * playlist_uuid = koto_playlist_get_uuid(playlist); + + if (!koto_playlist_get_uuid(playlist) || 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 +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return; + } + + g_signal_emit( + self, + cartographer_signals[SIGNAL_PLAYLIST_ADDED], + 0, + playlist + ); +} + +void koto_cartographer_add_track( + KotoCartographer * self, + KotoTrack * track +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return; + } + + if (!KOTO_IS_TRACK(track)) { // Not a track + return; + } + + gchar * track_uuid = koto_track_get_uuid(track); + + if (!koto_utils_string_is_valid(track_uuid) || 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 + ); +} + +KotoAlbum * koto_cartographer_get_album_by_uuid( + KotoCartographer * self, + gchar * album_uuid +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return NULL; + } + + return g_hash_table_lookup(self->albums, album_uuid); +} + +GHashTable * koto_cartographer_get_artists(KotoCartographer * self) { + return self->artists; +} + +KotoArtist * koto_cartographer_get_artist_by_name( + KotoCartographer * self, + gchar * artist_name +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return NULL; + } + + if (!koto_utils_string_is_valid(artist_name)) { // Not a valid name + return NULL; + } + + return koto_cartographer_get_artist_by_uuid(self, g_hash_table_lookup(self->artists_name_to_uuid, artist_name)); +} + +KotoArtist * koto_cartographer_get_artist_by_uuid( + KotoCartographer * self, + gchar * artist_uuid +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return NULL; + } + + if (!koto_utils_string_is_valid(artist_uuid)) { + return NULL; + } + + return g_hash_table_lookup(self->artists, artist_uuid); +} + +KotoLibrary * koto_cartographer_get_library_by_uuid( + KotoCartographer * self, + gchar * library_uuid +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return NULL; + } + + if (!koto_utils_string_is_valid(library_uuid)) { // Not a valid string + return NULL; + } + + return g_hash_table_lookup(self->libraries, library_uuid); +} + +KotoLibrary * koto_cartographer_get_library_containing_path( + KotoCartographer * self, + gchar * relative_path +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return NULL; + } + + if (!koto_utils_string_is_valid(relative_path)) { // Not a valid string + return NULL; + } + + GList * libs = koto_cartographer_get_libraries(self); // Get all the libraries, sorted based on priority + GList * current; + + for (current = libs; current != NULL; current = current->next) { // For each library + KotoLibrary * lib = (KotoLibrary*) current->data; + gchar * lib_path = koto_library_get_path(lib); // Get the path for the library + + GFile * track_file = g_file_new_build_filename(lib_path, relative_path, NULL); // Build a path from storage to file + + if (g_file_query_exists(track_file, NULL)) { // If this library contains this file + g_object_unref(track_file); + return lib; + } + + g_object_unref(track_file); + } + + return NULL; +} + +GList * koto_cartographer_get_libraries_for_storage_uuid( + KotoCartographer * self, + gchar * storage_uuid +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return NULL; + } + + GList * libraries = NULL; // Initialize our list + + if (!koto_utils_string_is_valid(storage_uuid)) { // Not a valid storage UUID string + return libraries; + } + + // TODO: Implement koto_cartographer_get_libraries_for_storage_uuid + return libraries; +} + +GList * koto_cartographer_get_libraries(KotoCartographer * self) { + GList * libraries = g_hash_table_get_values(self->libraries); + // TODO: Implement priority mechanism + return libraries; +} + +GHashTable * koto_cartographer_get_playlists(KotoCartographer * self) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return NULL; + } + + return self->playlists; +} + +KotoPlaylist * koto_cartographer_get_playlist_by_uuid( + KotoCartographer * self, + gchar * playlist_uuid +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return NULL; + } + + return g_hash_table_lookup(self->playlists, playlist_uuid); +} + +KotoTrack * koto_cartographer_get_track_by_uuid( + KotoCartographer * self, + gchar * track_uuid +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return NULL; + } + + if (!koto_utils_string_is_valid(track_uuid)) { + return NULL; + } + + return g_hash_table_lookup(self->tracks, track_uuid); +} + +KotoTrack * koto_cartographer_get_track_by_uniqueish_key( + KotoCartographer * self, + gchar * key +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return NULL; + } + + if (!koto_utils_string_is_valid(key)) { + return NULL; + } + + return g_hash_table_lookup(self->tracks_by_uniqueish_key, key); +} + +gboolean koto_cartographer_has_album( + KotoCartographer * self, + KotoAlbum * album +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return FALSE; + } + + if (!KOTO_IS_ALBUM(album)) { + return FALSE; + } + + gchar * album_uuid = NULL; + + g_object_get(album, "uuid", &album_uuid, NULL); + return koto_cartographer_has_album_by_uuid(self, album_uuid); +} + +gboolean koto_cartographer_has_album_by_uuid( + KotoCartographer * self, + gchar * album_uuid +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return FALSE; + } + + if (!koto_utils_string_is_valid(album_uuid)) { // Not a valid UUID + return FALSE; + } + + return g_hash_table_contains(self->albums, album_uuid); +} + +gboolean koto_cartographer_has_artist( + KotoCartographer * self, + KotoArtist * artist +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return FALSE; + } + + if (!KOTO_IS_ARTIST(artist)) { + return FALSE; + } + + return koto_cartographer_has_artist_by_uuid(self, koto_artist_get_uuid(artist)); +} + +gboolean koto_cartographer_has_artist_by_uuid( + KotoCartographer * self, + gchar * artist_uuid +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return FALSE; + } + + if (!koto_utils_string_is_valid(artist_uuid)) { // Not a valid UUID + return FALSE; + } + + return g_hash_table_contains(self->artists, artist_uuid); +} + +gboolean koto_cartographer_has_library( + KotoCartographer * self, + KotoLibrary * library +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return FALSE; + } + + if (!KOTO_IS_LIBRARY(library)) { + return FALSE; + } + + // TODO: return koto_cartographer_has_library_by_uuid(self, koto_library_get_uuid(library)) -- Need to implement get uuid func + return FALSE; +} + +gboolean koto_cartographer_has_library_by_uuid( + KotoCartographer * self, + gchar * library_uuid +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return FALSE; + } + + if (!koto_utils_string_is_valid(library_uuid)) { // Not a valid UUID + return FALSE; + } + + return g_hash_table_contains(self->libraries, library_uuid); +} + +gboolean koto_cartographer_has_playlist( + KotoCartographer * self, + KotoPlaylist * playlist +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return FALSE; + } + + if (!KOTO_IS_PLAYLIST(playlist)) { // Not a playlist + return FALSE; + } + + return koto_cartographer_has_playlist_by_uuid(self, koto_playlist_get_uuid(playlist)); +} + +gboolean koto_cartographer_has_playlist_by_uuid( + KotoCartographer * self, + gchar * playlist_uuid +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return FALSE; + } + + if (!koto_utils_string_is_valid(playlist_uuid)) { // Not a valid UUID + return FALSE; + } + + return g_hash_table_contains(self->playlists, playlist_uuid); +} + +gboolean koto_cartographer_has_track( + KotoCartographer * self, + KotoTrack * track +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return FALSE; + } + + if (!KOTO_IS_TRACK(track)) { // Not a track + return FALSE; + } + + return koto_cartographer_has_album_by_uuid(self, koto_track_get_uuid(track)); +} + +gboolean koto_cartographer_has_track_by_uuid( + KotoCartographer * self, + gchar * track_uuid +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return FALSE; + } + + if (!koto_utils_string_is_valid(track_uuid)) { // Not a valid UUID + return FALSE; + } + + return g_hash_table_contains(self->tracks, track_uuid); +} + +void koto_cartographer_remove_album( + KotoCartographer * self, + KotoAlbum * album +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return; + } + + if (!KOTO_IS_ALBUM(album)) { // Not an album + return; + } + + koto_cartographer_remove_album_by_uuid(self, koto_album_get_uuid(album)); +} + +void koto_cartographer_remove_album_by_uuid( + KotoCartographer * self, + gchar * album_uuid +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return; + } + + if (!koto_utils_string_is_valid(album_uuid)) { + return; + } + + if (!g_hash_table_contains(self->albums, album_uuid)) { // Album does not exist in albums + return; + } + + g_hash_table_remove(self->albums, album_uuid); + + g_signal_emit( + self, + cartographer_signals[SIGNAL_ALBUM_REMOVED], + 0, + album_uuid + ); +} + +void koto_cartographer_remove_artist( + KotoCartographer * self, + KotoArtist * artist +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return; + } + + if (!KOTO_IS_ARTIST(artist)) { // Not an artist + return; + } + + gchar * artist_uuid = koto_artist_get_uuid(artist); + gchar * artist_name = koto_artist_get_name(artist); + + g_hash_table_remove(self->artists_name_to_uuid, artist_name); // Add the UUID as a value with the key being the name of the artist + g_hash_table_remove(self->artists, artist_uuid); + + g_signal_emit( + self, + cartographer_signals[SIGNAL_ARTIST_REMOVED], + 0, + artist_uuid, + artist_name + ); +} + +void koto_cartographer_remove_artist_by_uuid( + KotoCartographer * self, + gchar * artist_uuid +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return; + } + + if (!koto_utils_string_is_valid(artist_uuid)) { // Artist UUID not valid + return; + } + + if (!g_hash_table_contains(self->artists, artist_uuid)) { // Not in hash table + return; + } + + KotoArtist * artist = koto_cartographer_get_artist_by_uuid(self, artist_uuid); + + if (!KOTO_IS_ARTIST(artist)) { + return; + } + + gchar * artist_name = koto_artist_get_name(artist); + + g_hash_table_remove(self->artists_name_to_uuid, artist_name); // Add the UUID as a value with the key being the name of the artist + g_hash_table_remove(self->artists, artist_uuid); + + g_signal_emit( + self, + cartographer_signals[SIGNAL_ARTIST_REMOVED], + 0, + artist_uuid, + artist_name + ); +} + +void koto_cartographer_remove_playlist( + KotoCartographer * self, + KotoPlaylist * playlist +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return; + } + + if (!KOTO_IS_PLAYLIST(playlist)) { + return; + } + koto_cartographer_remove_playlist_by_uuid(self, koto_playlist_get_uuid(playlist)); +} + +void koto_cartographer_remove_playlist_by_uuid( + KotoCartographer * self, + gchar * playlist_uuid +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return; + } + + if (!koto_utils_string_is_valid(playlist_uuid)) { // Not a valid playlist UUID string + return; + } + + if (!g_hash_table_contains(self->playlists, playlist_uuid)) { // Not in hash table + return; + } + + g_hash_table_remove(self->playlists, playlist_uuid); + + g_signal_emit( + self, + cartographer_signals[SIGNAL_PLAYLIST_REMOVED], + 0, + playlist_uuid + ); +} + +void koto_cartographer_remove_track( + KotoCartographer * self, + KotoTrack * track +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return; + } + + if (!KOTO_IS_TRACK(track)) { // Not a track + return; + } + + koto_cartographer_remove_track_by_uuid(self, koto_track_get_uuid(track)); +} + +void koto_cartographer_remove_track_by_uuid( + KotoCartographer * self, + gchar * track_uuid +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return; + } + + if (!koto_utils_string_is_valid(track_uuid)) { + return; + } + + if (!g_hash_table_contains(self->tracks, track_uuid)) { // Not in hash table + return; + } + + g_hash_table_remove(self->tracks, track_uuid); + + g_signal_emit( + self, + cartographer_signals[SIGNAL_TRACK_REMOVED], + 0, + track_uuid + ); +} + +KotoCartographer * koto_cartographer_new() { + return g_object_new(KOTO_TYPE_CARTOGRAPHER, NULL); +} diff --git a/src/db/cartographer.h b/src/db/cartographer.h new file mode 100644 index 0000000..8b752bc --- /dev/null +++ b/src/db/cartographer.h @@ -0,0 +1,221 @@ +/* cartographer.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 "../indexer/structs.h" +#include "../playlist/playlist.h" + +G_BEGIN_DECLS + +/** + * Type Definition + **/ + +#define KOTO_TYPE_CARTOGRAPHER koto_cartographer_get_type() + +typedef struct _KotoCartographer KotoCartographer; +typedef struct _KotoCartographerClass KotoCartographerClass; + +GLIB_AVAILABLE_IN_ALL +GType koto_cartographer_get_type(void) G_GNUC_CONST; + +#define KOTO_IS_CARTOGRAPHER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_CARTOGRAPHER)) + +/** + * Cartographer Functions + **/ + +KotoCartographer * koto_cartographer_new(); + +void koto_cartographer_add_album( + KotoCartographer * self, + KotoAlbum * album +); + +void koto_cartographer_add_artist( + KotoCartographer * self, + KotoArtist * artist +); + +void koto_cartographer_add_library( + KotoCartographer * self, + KotoLibrary * library +); + +void koto_cartographer_add_playlist( + KotoCartographer * self, + KotoPlaylist * playlist +); + +void koto_cartographer_add_track( + KotoCartographer * self, + KotoTrack * track +); + +void koto_cartographer_emit_playlist_added( + KotoPlaylist * playlist, + KotoCartographer * self +); + +KotoAlbum * koto_cartographer_get_album_by_uuid( + KotoCartographer * self, + gchar * album_uuid +); + +GHashTable * koto_cartographer_get_artists(KotoCartographer * self); + +KotoArtist * koto_cartographer_get_artist_by_name( + KotoCartographer * self, + gchar * artist_name +); + +KotoArtist * koto_cartographer_get_artist_by_uuid( + KotoCartographer * self, + gchar * artist_uuid +); + +KotoLibrary * koto_cartographer_get_library_by_uuid( + KotoCartographer * self, + gchar * library_uuid +); + +KotoLibrary * koto_cartographer_get_library_containing_path( + KotoCartographer * self, + gchar * path +); + +GList * koto_cartographer_get_libraries(KotoCartographer * self); + +GList * koto_cartographer_get_libraries_for_storage_uuid( + KotoCartographer * self, + gchar * storage_uuid +); + +KotoPlaylist * koto_cartographer_get_playlist_by_uuid( + KotoCartographer * self, + gchar * playlist_uuid +); + +GHashTable * koto_cartographer_get_playlists(KotoCartographer * self); + +KotoTrack * koto_cartographer_get_track_by_uuid( + KotoCartographer * self, + gchar * track_uuid +); + +KotoTrack * koto_cartographer_get_track_by_uniqueish_key( + KotoCartographer * self, + gchar * key +); + +gboolean koto_cartographer_has_album( + KotoCartographer * self, + KotoAlbum * album +); + +gboolean koto_cartographer_has_album_by_uuid( + KotoCartographer * self, + gchar * album_uuid +); + +gboolean koto_cartographer_has_artist( + KotoCartographer * self, + KotoArtist * artist +); + +gboolean koto_cartographer_has_artist_by_uuid( + KotoCartographer * self, + gchar * artist_uuid +); + +gboolean koto_cartographer_has_library( + KotoCartographer * self, + KotoLibrary * library +); + +gboolean koto_cartographer_has_library_by_uuid( + KotoCartographer * self, + gchar * library_uuid +); + +gboolean koto_cartographer_has_playlist( + KotoCartographer * self, + KotoPlaylist * playlist +); + +gboolean koto_cartographer_has_playlist_by_uuid( + KotoCartographer * self, + gchar * playlist_uuid +); + +gboolean koto_cartographer_has_track( + KotoCartographer * self, + KotoTrack * track +); + +gboolean koto_cartographer_has_track_by_uuid( + KotoCartographer * self, + gchar * track_uuid +); + +void koto_cartographer_remove_album( + KotoCartographer * self, + KotoAlbum * album +); + +void koto_cartographer_remove_album_by_uuid( + KotoCartographer * self, + gchar * album_uuid +); + +void koto_cartographer_remove_artist( + KotoCartographer * self, + KotoArtist * artist +); + +void koto_cartographer_remove_artist_by_uuid( + KotoCartographer * self, + gchar * artist_uuid +); + +void koto_cartographer_remove_library( + KotoCartographer * self, + KotoLibrary * library +); + +void koto_cartographer_remove_playlist( + KotoCartographer * self, + KotoPlaylist * playlist +); + +void koto_cartographer_remove_playlist_by_uuid( + KotoCartographer * self, + gchar * playlist_uuid +); + +void koto_cartographer_remove_track( + KotoCartographer * self, + KotoTrack * track +); + +void koto_cartographer_remove_track_by_uuid( + KotoCartographer * self, + gchar * track_uuid +); + +G_END_DECLS diff --git a/src/db/db.c b/src/db/db.c new file mode 100644 index 0000000..045d067 --- /dev/null +++ b/src/db/db.c @@ -0,0 +1,110 @@ +/* db.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 +#include +#include "db.h" +#include "../koto-paths.h" + +extern gchar * koto_path_to_db; + +int KOTO_DB_SUCCESS = 0; +int KOTO_DB_NEW = 1; +int KOTO_DB_FAIL = 2; + +sqlite3 * koto_db = NULL; +gchar * db_filepath = NULL; +gboolean created_new_db = FALSE; + +void close_db() { + sqlite3_close(koto_db); +} + +int create_db_tables() { + gchar * tables_creation_queries = "CREATE TABLE IF NOT EXISTS artists(id string UNIQUE PRIMARY KEY, name string, art_path string);" + "CREATE TABLE IF NOT EXISTS albums(id string UNIQUE PRIMARY KEY, artist_id string, name string, description string, narrator string, art_path string, genres string, year int, FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE);" + "CREATE TABLE IF NOT EXISTS tracks(id string UNIQUE PRIMARY KEY, artist_id string, album_id string, name string, disc int, position int, duration int, genres string, FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE);" + "CREATE TABLE IF NOT EXISTS libraries_albums(id string, album_id string, path string, PRIMARY KEY (id, album_id) FOREIGN KEY(album_id) REFERENCES albums(id) ON DELETE CASCADE);" + "CREATE TABLE IF NOT EXISTS libraries_artists(id string, artist_id string, path string, PRIMARY KEY(id, artist_id) FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE);" + "CREATE TABLE IF NOT EXISTS libraries_tracks(id string, track_id string, path string, PRIMARY KEY(id, track_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, album_id string, track_id string, playback_position_of_track int);" + "CREATE TABLE IF NOT EXISTS playlist_tracks(position INTEGER PRIMARY KEY AUTOINCREMENT, playlist_id string, track_id string, FOREIGN KEY(playlist_id) REFERENCES playlist_meta(id), FOREIGN KEY(track_id) REFERENCES tracks(id) ON DELETE CASCADE);"; + + return (new_transaction(tables_creation_queries, "Failed to create required tables", TRUE) == SQLITE_OK) ? KOTO_DB_SUCCESS : KOTO_DB_FAIL; +} + +int enable_foreign_keys() { + gchar * commit_op = g_strdup("PRAGMA foreign_keys = ON;"); + const gchar * transaction_err_msg = "Failed to enable foreign key support. Ensure your sqlite3 is compiled with neither SQLITE_OMIT_FOREIGN_KEY or SQLITE_OMIT_TRIGGER defined"; + + return (new_transaction(commit_op, transaction_err_msg, FALSE) == SQLITE_OK) ? KOTO_DB_SUCCESS : KOTO_DB_FAIL; +} + +int have_existing_db() { + struct stat db_stat; + int success = stat(koto_path_to_db, &db_stat); + return ((success == 0) && S_ISREG(db_stat.st_mode)) ? 0 : 1; +} + +int new_transaction( + gchar * operation, + const gchar * transaction_err_msg, + gboolean fatal +) { + gchar * commit_op_errmsg = NULL; + int rc = sqlite3_exec(koto_db, operation, 0, 0, &commit_op_errmsg); + + if (rc != SQLITE_OK) { + (fatal) ? g_critical("%s: %s", transaction_err_msg, commit_op_errmsg) : g_warning("%s: %s", transaction_err_msg, commit_op_errmsg); + } + + if (commit_op_errmsg == NULL) { + g_free(commit_op_errmsg); + } + + return rc; +} + +int open_db() { + int ret = KOTO_DB_SUCCESS; // Default to last return being SUCCESS + + if (have_existing_db() == 1) { // If we do not have an existing DB + ret = KOTO_DB_NEW; + } + + if (sqlite3_open(koto_path_to_db, &koto_db) != KOTO_DB_SUCCESS) { // If we failed to open the database file + g_critical("Failed to open or create database: %s", sqlite3_errmsg(koto_db)); + return KOTO_DB_FAIL; + } + + if (enable_foreign_keys() != KOTO_DB_SUCCESS) { // If we failed to enable foreign keys + return KOTO_DB_FAIL; + } + + if (create_db_tables() != KOTO_DB_SUCCESS) { // Failed to create our database tables + return KOTO_DB_FAIL; + } + + if (ret == KOTO_DB_NEW) { + created_new_db = TRUE; + } + + return ret; +} diff --git a/src/db/db.h b/src/db/db.h new file mode 100644 index 0000000..9ff7b74 --- /dev/null +++ b/src/db/db.h @@ -0,0 +1,41 @@ +/* db.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 + +extern int KOTO_DB_SUCCESS; +extern int KOTO_DB_NEW; +extern int KOTO_DB_FAIL; + +void close_db(); + +int create_db_tables(); + +gchar * get_db_path(); + +int enable_foreign_keys(); + +int have_existing_db(); + +int new_transaction( + gchar * operation, + const gchar * transaction_err_msg, + gboolean fatal +); + +int open_db(); diff --git a/src/db/loaders.c b/src/db/loaders.c new file mode 100644 index 0000000..ebbee80 --- /dev/null +++ b/src/db/loaders.c @@ -0,0 +1,392 @@ +/* loaders.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 "cartographer.h" +#include "db.h" +#include "loaders.h" +#include "../indexer/album-playlist-funcs.h" +#include "../indexer/structs.h" +#include "../koto-utils.h" + +extern KotoCartographer * koto_maps; +extern sqlite3 * koto_db; + +int process_artists( + 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 * artist_uuid = g_strdup(koto_utils_string_unquote(fields[0])); // First column is UUID + gchar * artist_name = g_strdup(koto_utils_string_unquote(fields[1])); // Second column is artist name + + KotoArtist * artist = koto_artist_new_with_uuid(artist_uuid); // Create our artist with the UUID + + g_object_set( + artist, + "name", + artist_name, // Set name + NULL); + + int artist_paths = sqlite3_exec(koto_db, g_strdup_printf("SELECT * FROM libraries_artists WHERE artist_id=\"%s\"", artist_uuid), process_artist_paths, artist, NULL); // Process all the paths for this given artist + + if (artist_paths != SQLITE_OK) { // Failed to get our artists_paths + g_critical("Failed to read our paths for this artist: %s", sqlite3_errmsg(koto_db)); + return 1; + } + + koto_cartographer_add_artist(koto_maps, artist); // Add the artist to our global cartographer + + int albums_rc = sqlite3_exec(koto_db, g_strdup_printf("SELECT * FROM albums WHERE artist_id=\"%s\"", artist_uuid), process_albums, artist, NULL); // Process our albums + + if (albums_rc != SQLITE_OK) { // Failed to get our albums + g_critical("Failed to read our albums: %s", sqlite3_errmsg(koto_db)); + return 1; + } + + koto_artist_set_as_finalized(artist); // Indicate it is finalized + + int tracks_rc = sqlite3_exec(koto_db, g_strdup_printf("SELECT * FROM tracks WHERE artist_id=\"%s\" AND album_id=''", artist_uuid), process_tracks, NULL, NULL); // Load all tracks for an artist that are NOT in an album (e.g. artists without albums) + + if (tracks_rc != SQLITE_OK) { // Failed to get our tracks + g_critical("Failed to read our tracks: %s", sqlite3_errmsg(koto_db)); + return 1; + } + + g_free(artist_uuid); + g_free(artist_name); + + return 0; +} + +int process_artist_paths( + void * data, + int num_columns, + char ** fields, + char ** column_names +) { + (void) num_columns; + (void) column_names; // Don't need these + + KotoArtist * artist = (KotoArtist*) data; + + gchar * library_uuid = g_strdup(koto_utils_string_unquote(fields[0])); + gchar * relative_path = g_strdup(koto_utils_string_unquote(fields[2])); + + KotoLibrary * lib = koto_cartographer_get_library_by_uuid(koto_maps, library_uuid); // Get the library for this artist + + if (!KOTO_IS_LIBRARY(lib)) { // Failed to get the library for this UUID + return 0; + } + + koto_artist_set_path(artist, lib, relative_path, FALSE); // Add the relative path from the db for this artist and lib to the Artist, do not commit + + return 0; +} + +int process_albums( + void * data, + int num_columns, + char ** fields, + char ** column_names +) { + (void) num_columns; + (void) column_names; // Don't need these + + KotoArtist * artist = (KotoArtist*) data; + + gchar * album_uuid = g_strdup(koto_utils_string_unquote(fields[0])); + gchar * artist_uuid = g_strdup(koto_utils_string_unquote(fields[1])); + gchar * album_name = g_strdup(koto_utils_string_unquote(fields[2])); + gchar * album_description = (fields[3] != NULL) ? g_strdup(koto_utils_string_unquote(fields[3])) : NULL; + gchar * album_narrator = (fields[4] != NULL) ? g_strdup(koto_utils_string_unquote(fields[4])) : NULL; + gchar * album_art = (fields[5] != NULL) ? g_strdup(koto_utils_string_unquote(fields[5])) : NULL; + gchar * album_genres = (fields[6] != NULL) ? g_strdup(koto_utils_string_unquote(fields[6])) : NULL; + guint64 * album_year = (guint64*) g_ascii_strtoull(fields[7], NULL, 10); + + KotoAlbum * album = koto_album_new_with_uuid(artist, album_uuid); // Create our album + + g_object_set( + album, + "name", + album_name, // Set name + "description", + album_description, + "narrator", + album_narrator, + "art-path", + album_art, // Set art path if any + "preparsed-genres", + album_genres, + "year", + album_year, + NULL + ); + + koto_cartographer_add_album(koto_maps, album); // Add the album to our global cartographer + koto_artist_add_album(artist, album); // Add the album + + int tracks_rc = sqlite3_exec(koto_db, g_strdup_printf("SELECT * FROM tracks WHERE album_id=\"%s\"", album_uuid), process_tracks, NULL, NULL); // Process all the tracks for this specific album + + if (tracks_rc != SQLITE_OK) { // Failed to get our tracks + g_critical("Failed to read our tracks: %s", sqlite3_errmsg(koto_db)); + return 1; + } + + koto_album_mark_as_finalized(album); // Mark the album as finalized now that all tracks have been loaded, allowing our internal album playlist to re-sort itself + + g_free(album_uuid); + g_free(artist_uuid); + g_free(album_name); + g_free(album_genres); + + if (album_art != NULL) { + g_free(album_art); + } + + 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_string_unquote(fields[0])); // First column is UUID + gchar * playlist_name = g_strdup(koto_utils_string_unquote(fields[1])); // Second column is playlist name + gchar * playlist_art_path = g_strdup(koto_utils_string_unquote(fields[2])); // Third column is any art path + guint64 playlist_preferred_sort = g_ascii_strtoull(koto_utils_string_unquote(fields[3]), NULL, 10); // Fourth column is preferred model which is an int + gchar * playlist_album_id = g_strdup(koto_utils_string_unquote(fields[4])); // Fifth column is any album ID + gchar * playlist_current_track_id = g_strdup(koto_utils_string_unquote(fields[5])); // Sixth column is any track ID + guint64 playlist_track_current_playback_pos = g_ascii_strtoull(koto_utils_string_unquote(fields[6]), NULL, 10); // Seventh column is any playback position for the track + + KotoPreferredPlaylistSortType sort_type = (KotoPreferredPlaylistSortType) playlist_preferred_sort; + + KotoPlaylist * playlist = NULL; + + gboolean for_album = FALSE; + + if (koto_utils_string_is_valid(playlist_album_id)) { // Album UUID is set + KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, playlist_album_id); // Get the album based on the album ID set for the playlist + + if (KOTO_IS_ALBUM(album)) { // Is an album + playlist = koto_album_get_playlist(album); // Get the playlist + for_album = TRUE; + } + } else { + playlist = koto_playlist_new_with_uuid(playlist_uuid); // Create a playlist using the existing UUID + koto_playlist_apply_model(playlist, sort_type); // Set the model based on what is stored + koto_cartographer_add_playlist(koto_maps, playlist); // Add to cartographer + } + + if (!KOTO_IS_PLAYLIST(playlist)) { // Not a playlist yet, in this scenario it is likely the album no longer exists + goto free; + } + + g_object_set( + playlist, + "name", + playlist_name, + "art-path", + playlist_art_path, + "ephemeral", + for_album, + NULL + ); + + if (!for_album) { // Isn't for an album + 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)); + goto free; + } + } + + if (koto_utils_string_is_valid(playlist_current_track_id)) { // If we have a track UUID (probably) + KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, playlist_current_track_id); // Get the track UUID + + if (KOTO_IS_TRACK(track)) { // If this is a track + koto_track_set_playback_position(track, playlist_track_current_playback_pos); // Set the playback position of the track + koto_playlist_set_track_as_current(playlist, playlist_current_track_id); // Ensure we have this track set as the current one in the playlist + } + } + + koto_playlist_mark_as_finalized(playlist); // Mark as finalized since loading should be complete + +free: + 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_string_unquote(fields[1])); + gchar * track_uuid = g_strdup(koto_utils_string_unquote(fields[2])); + + KotoPlaylist * playlist = koto_cartographer_get_playlist_by_uuid(koto_maps, playlist_uuid); // Get the playlist + KotoTrack * 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, FALSE, 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) data; + (void) num_columns; + (void) column_names; // Don't need these + + gchar * track_uuid = g_strdup(koto_utils_string_unquote(fields[0])); + + KotoTrack * existing_track = koto_cartographer_get_track_by_uuid(koto_maps, track_uuid); + + if (KOTO_IS_TRACK(existing_track)) { // Already have track + g_free(track_uuid); + return 0; + } + + gchar * artist_uuid = g_strdup(koto_utils_string_unquote(fields[1])); + gchar * album_uuid = g_strdup(koto_utils_string_unquote(fields[2])); + gchar * name = g_strdup(koto_utils_string_unquote(fields[3])); + guint * disc_num = (guint*) g_ascii_strtoull(fields[4], NULL, 10); + guint64 * position = (guint64*) g_ascii_strtoull(fields[5], NULL, 10); + guint64 * duration = (guint64*) g_ascii_strtoull(fields[6], NULL, 10); + gchar * genres = g_strdup(koto_utils_string_unquote(fields[7])); + + KotoTrack * track = koto_track_new_with_uuid(track_uuid); // Create our file + + g_object_set( + track, + "artist-uuid", + artist_uuid, + "album-uuid", + album_uuid, + "parsed-name", + name, + "cd", + disc_num, + "position", + position, + "duration", + duration, + "preparsed-genres", + genres, + NULL + ); + + g_free(name); + + int track_paths = sqlite3_exec(koto_db, g_strdup_printf("SELECT id, path FROM libraries_tracks WHERE track_id=\"%s\"", track_uuid), process_track_paths, track, NULL); // Process all pathes associated with the track + + if (track_paths != SQLITE_OK) { // Failed to read the paths + g_warning("Failed to read paths associated with track %s: %s", track_uuid, sqlite3_errmsg(koto_db)); + g_free(track_uuid); + g_free(artist_uuid); + g_free(album_uuid); + return 1; + } + + koto_cartographer_add_track(koto_maps, track); // Add the track to cartographer if necessary + + KotoArtist * artist = koto_cartographer_get_artist_by_uuid(koto_maps, artist_uuid); // Get the artist + koto_artist_add_track(artist, track); // Add the track for the artist + + if (koto_utils_string_is_valid(album_uuid)) { // If we have an album UUID + KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, album_uuid); // Attempt to get album + + if (KOTO_IS_ALBUM(album)) { // This is an album + koto_album_add_track(album, track); // Add the track + } + } + + g_free(track_uuid); + g_free(artist_uuid); + g_free(album_uuid); + + return 0; +} + +int process_track_paths( + void * data, + int num_columns, + char ** fields, + char ** column_names +) { + KotoTrack * track = (KotoTrack*) data; + (void) num_columns; + (void) column_names; // Don't need these + + KotoLibrary * library = koto_cartographer_get_library_by_uuid(koto_maps, koto_utils_string_unquote(fields[0])); + + if (!KOTO_IS_LIBRARY(library)) { // Not a library + return 1; + } + + koto_track_set_path(track, library, koto_utils_string_unquote(fields[1])); + return 0; +} + + +void read_from_db() { + int artists_rc = sqlite3_exec(koto_db, "SELECT * FROM artists", process_artists, NULL, NULL); // Process our artists + + if (artists_rc != SQLITE_OK) { // Failed to get our artists + g_critical("Failed to read our artists: %s", sqlite3_errmsg(koto_db)); + return; + } + + int playlist_rc = sqlite3_exec(koto_db, "SELECT * FROM playlist_meta", process_playlists, NULL, 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; + } +} \ No newline at end of file diff --git a/src/db/loaders.h b/src/db/loaders.h new file mode 100644 index 0000000..07c90d7 --- /dev/null +++ b/src/db/loaders.h @@ -0,0 +1,67 @@ +/* loaders.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. + */ + +int process_artists( + void * data, + int num_columns, + char ** fields, + char ** column_names +); + +int process_artist_paths( + 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 +); + +int process_track_paths( + void * data, + int num_columns, + char ** fields, + char ** column_names +); + +void read_from_db(); \ No newline at end of file diff --git a/src/indexer/album-playlist-funcs.h b/src/indexer/album-playlist-funcs.h new file mode 100644 index 0000000..3a795cc --- /dev/null +++ b/src/indexer/album-playlist-funcs.h @@ -0,0 +1,25 @@ +/* album-playlist-funcs.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 "../playlist/playlist.h" +#include "structs.h" + +G_BEGIN_DECLS + +KotoPlaylist * koto_album_get_playlist(KotoAlbum * self); + +G_END_DECLS \ No newline at end of file diff --git a/src/indexer/album.c b/src/indexer/album.c new file mode 100644 index 0000000..0011601 --- /dev/null +++ b/src/indexer/album.c @@ -0,0 +1,851 @@ +/* album.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 +#include "../db/cartographer.h" +#include "../db/db.h" +#include "../playlist/current.h" +#include "../playlist/playlist.h" +#include "../koto-utils.h" +#include "album-playlist-funcs.h" +#include "track-helpers.h" + +extern KotoCartographer * koto_maps; +extern KotoCurrentPlaylist * current_playlist; +extern magic_t magic_cookie; +extern sqlite3 * koto_db; + +enum { + PROP_0, + PROP_UUID, + PROP_DO_INITIAL_INDEX, + PROP_NAME, + PROP_ART_PATH, + PROP_ARTIST_UUID, + PROP_ALBUM_PREPARED_GENRES, + PROP_DESCRIPTION, + PROP_NARRATOR, + PROP_YEAR, + N_PROPERTIES +}; + +static GParamSpec * props[N_PROPERTIES] = { + NULL, +}; + +enum { + SIGNAL_TRACK_ADDED, + SIGNAL_TRACK_REMOVED, + N_SIGNALS +}; + +static guint album_signals[N_SIGNALS] = { + 0 +}; + +struct _KotoAlbum { + GObject parent_instance; + gchar * uuid; + + gchar * name; + guint64 year; + gchar * description; + gchar * narrator; + gchar * art_path; + gchar * artist_uuid; + + GList * genres; + + KotoPlaylist * playlist; + GHashTable * paths; + + gboolean has_album_art; + gboolean do_initial_index; + gboolean finalized; +}; + +struct _KotoAlbumClass { + GObjectClass parent_class; + + void (* track_added) ( + KotoAlbum * album, + KotoTrack * track + ); + void (* track_removed) ( + KotoAlbum * album, + KotoTrack * track + ); +}; + +G_DEFINE_TYPE(KotoAlbum, koto_album, G_TYPE_OBJECT); + +static void koto_album_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +); + +static void koto_album_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +); + +static void koto_album_class_init(KotoAlbumClass * c) { + GObjectClass * gobject_class; + + gobject_class = G_OBJECT_CLASS(c); + gobject_class->set_property = koto_album_set_property; + gobject_class->get_property = koto_album_get_property; + + props[PROP_UUID] = g_param_spec_string( + "uuid", + "UUID to Album in database", + "UUID to Album in database", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_DO_INITIAL_INDEX] = g_param_spec_boolean( + "do-initial-index", + "Do an initial indexing operating instead of pulling from the database", + "Do an initial indexing operating instead of pulling from the database", + FALSE, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_NAME] = g_param_spec_string( + "name", + "Name", + "Name of Album", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_ART_PATH] = g_param_spec_string( + "art-path", + "Path to Artwork", + "Path to Artwork", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_ARTIST_UUID] = g_param_spec_string( + "artist-uuid", + "UUID of Artist associated with Album", + "UUID of Artist associated with Album", + NULL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_ALBUM_PREPARED_GENRES] = g_param_spec_string( + "preparsed-genres", + "Preparsed Genres", + "Preparsed Genres", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE + ); + + props[PROP_DESCRIPTION] = g_param_spec_string( + "description", + "Description of Album, typically for an audiobook or podcast", + "Description of Album, typically for an audiobook or podcast", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE + ); + + props[PROP_NARRATOR] = g_param_spec_string( + "narrator", + "Narrator of audiobook", + "Narrator of audiobook", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE + ); + + props[PROP_YEAR] = g_param_spec_uint64( + "year", + "Year", + "Year of release", + 0, + G_MAXSHORT, + 0, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE + ); + + g_object_class_install_properties(gobject_class, N_PROPERTIES, props); + + album_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(KotoAlbumClass, track_added), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + KOTO_TYPE_TRACK + ); + + album_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(KotoAlbumClass, track_removed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + KOTO_TYPE_TRACK + ); +} + +static void koto_album_init(KotoAlbum * self) { + self->description = NULL; + self->genres = NULL; + self->has_album_art = FALSE; + self->narrator = NULL; + self->paths = g_hash_table_new(g_str_hash, g_str_equal); + + self->playlist = koto_playlist_new_with_uuid(NULL); // Create a playlist, with UUID set to NULL temporarily (until we know the album UUID) + koto_playlist_apply_model(self->playlist, KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_TRACK_POS); // Sort by track position + koto_playlist_set_album_uuid(self->playlist, self->uuid); // Set the playlist UUID to be the same as the album + g_object_set(self->playlist, "ephemeral", TRUE, NULL); // Set as ephemeral / temporary + koto_cartographer_add_playlist(koto_maps, self->playlist); // Add to cartographer + + self->year = 0; +} + +void koto_album_add_track( + KotoAlbum * self, + KotoTrack * track +) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return; + } + + if (!KOTO_IS_TRACK(track)) { // Not a track + return; + } + + if (koto_playlist_get_position_of_track(self->playlist, track) != -1) { // Have added it already + return; + } + + koto_cartographer_add_track(koto_maps, track); // Add the track to cartographer if necessary + + GList * track_genres = koto_track_get_genres(track); // Get the genres for the track + GList * current_genre_list; + + gchar * existing_genres_as_string = koto_utils_join_string_list(self->genres, ";"); + + for (current_genre_list = track_genres; current_genre_list != NULL; current_genre_list = current_genre_list->next) { // Iterate over each item in the track genres + gchar * track_genre = current_genre_list->data; // Get this genre + + if (g_strcmp0(track_genre, "") == 0) { + continue; + } + + if (koto_utils_string_contains_substring(existing_genres_as_string, track_genre)) { // Genres list contains this genre + continue; + } + + self->genres = g_list_append(self->genres, g_strdup(track_genre)); // Duplicate the genre and add it to our list + } + + if (self->year == 0) { // Don't have a year set yet + guint64 track_year = koto_track_get_year(track); // Get the year from this track + + if (track_year > 0) { // Have a probably valid year set + self->year = track_year; // Set our track + } + } + + if (!koto_utils_string_is_valid(self->narrator)) { // No narrator set yet + gchar * track_narrator = koto_track_get_narrator(track); // Get the narrator for the track + + if (koto_utils_string_is_valid(track_narrator)) { // If this track has a narrator + self->narrator = g_strdup(track_narrator); + g_free(track_narrator); + } + } + + koto_playlist_add_track(self->playlist, track, FALSE, FALSE); // Add the track to our internal playlist + + g_signal_emit( + self, + album_signals[SIGNAL_TRACK_ADDED], + 0, + track + ); +} + +void koto_album_commit(KotoAlbum * self) { + if (self->art_path == NULL) { // If art_path isn't defined when committing + koto_album_set_album_art(self, ""); // Set to an empty string + } + + gchar * genres_string = koto_utils_join_string_list(self->genres, ";"); + + gchar * commit_op = g_strdup_printf( + "INSERT INTO albums(id, artist_id, name, description, narrator, art_path, genres, year)" + "VALUES('%s', '%s', quote(\"%s\"), quote(\"%s\"), quote(\"%s\"), quote(\"%s\"), '%s', %ld)" + "ON CONFLICT(id) DO UPDATE SET artist_id=excluded.artist_id, name=excluded.name, description=excluded.description, narrator=excluded.narrator, art_path=excluded.art_path, genres=excluded.genres, year=excluded.year;", + self->uuid, + self->artist_uuid, + koto_utils_string_get_valid(self->name), + koto_utils_string_get_valid(self->description), + koto_utils_string_get_valid(self->narrator), + koto_utils_string_get_valid(self->art_path), + koto_utils_string_get_valid(genres_string), + self->year + ); + + new_transaction(commit_op, "Failed to write our album to the database", FALSE); + + g_free(genres_string); + + GHashTableIter paths_iter; + g_hash_table_iter_init(&paths_iter, self->paths); // Create an iterator for our paths + gpointer lib_uuid_ptr, album_rel_path_ptr; + while (g_hash_table_iter_next(&paths_iter, &lib_uuid_ptr, &album_rel_path_ptr)) { + gchar * lib_uuid = lib_uuid_ptr; + gchar * album_rel_path = album_rel_path_ptr; + + gchar * commit_op = g_strdup_printf( + "INSERT INTO libraries_albums(id, album_id, path)" + "VALUES ('%s', '%s', quote(\"%s\"))" + "ON CONFLICT(id, album_id) DO UPDATE SET path=excluded.path;", + lib_uuid, + self->uuid, + album_rel_path + ); + + new_transaction(commit_op, "Failed to add this path for the album", FALSE); + } +} + +void koto_album_find_album_art(KotoAlbum * self) { + if (self->has_album_art) { // If we already have album art + return; + } + + gchar * optimal_album_path = koto_album_get_path(self); + DIR * dir = opendir(optimal_album_path); // Attempt to open our directory + + if (dir == NULL) { + return; + } + + struct dirent * entry; + + while ((entry = readdir(dir))) { + if (entry->d_type != DT_REG) { // Not a regular file + continue; // SKIP + } + + if (g_str_has_prefix(entry->d_name, ".")) { // Reference to parent dir, self, or a hidden item + continue; // Skip + } + + gchar * full_path = g_strdup_printf("%s%s%s", optimal_album_path, G_DIR_SEPARATOR_S, entry->d_name); + + const char * mime_type = magic_file(magic_cookie, full_path); + + if ( + (mime_type == NULL) || // Failed to get the mimetype + ((mime_type != NULL) && !g_str_has_prefix(mime_type, "image/")) // Got the mimetype but it is not an image + ) { + g_free(full_path); + continue; // Skip + } + + gchar * album_art_no_ext = g_strdup(koto_utils_get_filename_without_extension(entry->d_name)); // Get the name of the file without the extension + + gchar * lower_art = g_strdup(g_utf8_strdown(album_art_no_ext, -1)); // Lowercase + g_free(album_art_no_ext); + + gboolean should_set = (g_strrstr(lower_art, "Small") == NULL) && (g_strrstr(lower_art, "back") == NULL); // Not back or small + + g_free(lower_art); + + if (should_set) { + koto_album_set_album_art(self, full_path); + g_free(full_path); + break; + } + + g_free(full_path); + } + + closedir(dir); +} + +static void koto_album_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +) { + KotoAlbum * self = KOTO_ALBUM(obj); + + switch (prop_id) { + case PROP_UUID: + g_value_set_string(val, self->uuid); + break; + case PROP_DO_INITIAL_INDEX: + g_value_set_boolean(val, self->do_initial_index); + break; + case PROP_NAME: + g_value_set_string(val, self->name); + break; + case PROP_ART_PATH: + g_value_set_string(val, koto_album_get_art(self)); + break; + case PROP_ARTIST_UUID: + g_value_set_string(val, self->artist_uuid); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_album_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +) { + KotoAlbum * self = KOTO_ALBUM(obj); + + switch (prop_id) { + case PROP_UUID: + koto_album_set_uuid(self, g_value_get_string(val)); + break; + case PROP_DO_INITIAL_INDEX: + self->do_initial_index = g_value_get_boolean(val); + break; + case PROP_NAME: // Name of album + koto_album_set_album_name(self, g_value_get_string(val)); + break; + case PROP_ART_PATH: // Path to art + koto_album_set_album_art(self, g_value_get_string(val)); + break; + case PROP_ARTIST_UUID: + koto_album_set_artist_uuid(self, g_value_get_string(val)); + break; + case PROP_ALBUM_PREPARED_GENRES: + koto_album_set_preparsed_genres(self, g_strdup(g_value_get_string(val))); + break; + case PROP_DESCRIPTION: + koto_album_set_description(self, g_value_get_string(val)); + break; + case PROP_NARRATOR: + koto_album_set_narrator(self, g_value_get_string(val)); + break; + case PROP_YEAR: + koto_album_set_year(self, g_value_get_uint64(val)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +gchar * koto_album_get_art(KotoAlbum * self) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return g_strdup(""); + } + + return g_strdup((self->has_album_art && koto_utils_string_is_valid(self->art_path)) ? self->art_path : ""); +} + +gchar * koto_album_get_artist_uuid(KotoAlbum * self) { + return KOTO_IS_ALBUM(self) ? self->artist_uuid : NULL; +} + +gchar * koto_album_get_description(KotoAlbum * self) { + return KOTO_IS_ALBUM(self) ? self->description : NULL; +} + +GList * koto_album_get_genres(KotoAlbum * self) { + return KOTO_IS_ALBUM(self) ? self->genres : NULL; +} + +KotoLibraryType koto_album_get_lib_type(KotoAlbum * self) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return KOTO_LIBRARY_TYPE_UNKNOWN; + } + + return koto_artist_get_lib_type(koto_cartographer_get_artist_by_uuid(koto_maps, self->artist_uuid)); // Get the lib type for the artist. If artist isn't valid, we just return UNKNOWN +} + +gchar * koto_album_get_name(KotoAlbum * self) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return NULL; + } + + if (!koto_utils_string_is_valid(self->name)) { // Not set + return NULL; + } + + return self->name; // Return name of the album +} + +gchar * koto_album_get_narrator(KotoAlbum * self) { + return KOTO_IS_ALBUM(self) ? self->narrator : NULL; +} + +gchar * koto_album_get_path(KotoAlbum * self) { + if (!KOTO_IS_ALBUM(self) || (KOTO_IS_ALBUM(self) && (g_list_length(g_hash_table_get_keys(self->paths)) == 0))) { // If this is not an album or is but we have no paths associated with it + return NULL; + } + + GList * libs = koto_cartographer_get_libraries(koto_maps); // Get all of our libraries + GList * cur_lib_list; + + for (cur_lib_list = libs; cur_lib_list != NULL; cur_lib_list = libs->next) { // Iterate over our libraries + KotoLibrary * cur_library = libs->data; // Get this as a KotoLibrary + gchar * library_relative_path = g_hash_table_lookup(self->paths, koto_library_get_uuid(cur_library)); // Get any relative path in our paths based on the current UUID + + if (!koto_utils_string_is_valid(library_relative_path)) { // Not a valid path + continue; + } + + return g_strdup(g_build_path(G_DIR_SEPARATOR_S, koto_library_get_path(cur_library), library_relative_path, NULL)); // Build our full library path using library's path and our file relative path + } + + return NULL; +} + +KotoPlaylist * koto_album_get_playlist(KotoAlbum * self) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return NULL; + } + + if (!KOTO_IS_PLAYLIST(self->playlist)) { // Not a playlist + return NULL; + } + + return self->playlist; +} + +GListStore * koto_album_get_store(KotoAlbum * self) { + KotoPlaylist * playlist = koto_album_get_playlist(self); + + if (!KOTO_IS_PLAYLIST(playlist)) { // Not a playlist + return NULL; + } + + return koto_playlist_get_store(playlist); // Return the store from the playlist +} + +gchar * koto_album_get_uuid(KotoAlbum * self) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return NULL; + } + + return self->uuid; // Return the UUID +} + +guint64 koto_album_get_year(KotoAlbum * self) { + return KOTO_IS_ALBUM(self) ? self->year : 0; +} + +void koto_album_mark_as_finalized(KotoAlbum * self) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return; + } + + if (self->finalized) { // Already finalized + return; + } + + self->finalized = TRUE; + koto_playlist_mark_as_finalized(self->playlist); + //koto_playlist_apply_model(self->playlist, self->model); // Resort our playlist +} + +void koto_album_remove_track( + KotoAlbum * self, + KotoTrack * track +) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return; + } + + if (!KOTO_IS_TRACK(track)) { // Not a track + return; + } + + koto_playlist_remove_track_by_uuid( + self->playlist, + koto_track_get_uuid(track) + ); + + g_signal_emit( + self, + album_signals[SIGNAL_TRACK_REMOVED], + 0, + track + ); +} + +void koto_album_set_album_name( + KotoAlbum * self, + const gchar * album_name +) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return; + } + + if (album_name == NULL) { // Not valid album name + return; + } + + if (self->name != NULL) { + g_free(self->name); + } + + self->name = g_strdup(album_name); + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_NAME]); +} + +void koto_album_set_artist_uuid( + KotoAlbum * self, + const gchar * artist_uuid +) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return; + } + + if (artist_uuid == NULL) { + return; + } + + if (self->artist_uuid != NULL) { + g_free(self->artist_uuid); + } + + self->artist_uuid = g_strdup(artist_uuid); + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_ARTIST_UUID]); +} + +void koto_album_set_album_art( + KotoAlbum * self, + const gchar * album_art +) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return; + } + + if (album_art == NULL) { // Not valid album art + return; + } + + if (self->art_path != NULL) { + g_free(self->art_path); + } + + self->art_path = g_strdup(album_art); + + self->has_album_art = TRUE; + + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_ART_PATH]); +} + +void koto_album_set_as_current_playlist(KotoAlbum * self) { + if (!KOTO_IS_ALBUM(self)) { + return; + } + + if (!KOTO_IS_CURRENT_PLAYLIST(current_playlist)) { + return; + } + + if (!KOTO_IS_PLAYLIST(self->playlist)) { // Don't have a playlist for the album for some reason + return; + } + + koto_current_playlist_set_playlist(current_playlist, self->playlist, TRUE, FALSE); // Set our new current playlist and start playing immediately +} + +void koto_album_set_description( + KotoAlbum * self, + const gchar * description +) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return; + } + + if (!koto_utils_string_is_valid(description)) { + return; + } + + self->description = g_strdup(description); + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_DESCRIPTION]); +} + +void koto_album_set_narrator( + KotoAlbum * self, + const gchar * narrator +) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return; + } + + if (!koto_utils_string_is_valid(narrator)) { + return; + } + + self->narrator = g_strdup(narrator); + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_NARRATOR]); +} + +void koto_album_set_path( + KotoAlbum * self, + KotoLibrary * lib, + const gchar * fixed_path +) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return; + } + + gchar * path = g_strdup(fixed_path); // Duplicate our fixed_path + gchar * relative_path = koto_library_get_relative_path_to_file(lib, path); // Get the relative path to the file for the given library + + gchar * library_uuid = koto_library_get_uuid(lib); // Get the library for this path + g_hash_table_replace(self->paths, library_uuid, relative_path); // Replace any existing value or add this one + + koto_album_set_album_name(self, g_path_get_basename(relative_path)); // Update our album name based on the base name + + if (!self->do_initial_index) { // Not doing our initial index + return; + } + + koto_album_find_album_art(self); // Update our path for the album art + self->do_initial_index = FALSE; +} + +void koto_album_set_preparsed_genres( + KotoAlbum * self, + gchar * genrelist +) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return; + } + + if (!koto_utils_string_is_valid(genrelist)) { // If it is an empty string + return; + } + + GList * preparsed_genres_list = koto_utils_string_to_string_list(genrelist, ";"); + + if (g_list_length(preparsed_genres_list) == 0) { // No genres + g_list_free(preparsed_genres_list); + return; + } + + // TODO: Do a pass on in first memory optimization phase to ensure string elements are freed. + g_list_free_full(self->genres, NULL); // Free the existing genres list + self->genres = preparsed_genres_list; +}; + +void koto_album_set_uuid( + KotoAlbum * self, + const gchar * uuid +) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return; + } + + if (!koto_utils_string_is_valid(uuid)) { + return; + } + + self->uuid = g_strdup(uuid); + g_object_set( + self->playlist, + "album-uuid", + self->uuid, + "uuid", + self->uuid, // Ensure the playlist has the same UUID as the album + NULL + ); + + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_UUID]); +} + +void koto_album_set_year( + KotoAlbum * self, + guint64 year +) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return; + } + + if (year <= 0) { + return; + } + + self->year = year; + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_YEAR]); +} + +KotoAlbum * koto_album_new(gchar * artist_uuid) { + if (!koto_utils_string_is_valid(artist_uuid)) { // Invalid artist UUID provided + return NULL; + } + + KotoAlbum * album = g_object_new( + KOTO_TYPE_ALBUM, + "artist-uuid", + artist_uuid, + "uuid", + g_strdup(g_uuid_string_random()), + "do-initial-index", + TRUE, + NULL + ); + + return album; +} + +KotoAlbum * koto_album_new_with_uuid( + KotoArtist * artist, + const gchar * uuid +) { + gchar * artist_uuid = koto_artist_get_uuid(artist); + + return g_object_new( + KOTO_TYPE_ALBUM, + "artist-uuid", + artist_uuid, + "uuid", + g_strdup(uuid), + "do-initial-index", + FALSE, + NULL + ); +} diff --git a/src/indexer/artist-playlist-funcs.h b/src/indexer/artist-playlist-funcs.h new file mode 100644 index 0000000..2c9677f --- /dev/null +++ b/src/indexer/artist-playlist-funcs.h @@ -0,0 +1,25 @@ +/* artist-playlist-funcs.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 "../playlist/playlist.h" +#include "structs.h" + +G_BEGIN_DECLS + +KotoPlaylist * koto_artist_get_playlist(KotoArtist * self); + +G_END_DECLS \ No newline at end of file diff --git a/src/indexer/artist.c b/src/indexer/artist.c new file mode 100644 index 0000000..0bf30e4 --- /dev/null +++ b/src/indexer/artist.c @@ -0,0 +1,606 @@ +/* artist.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 "../config/config.h" +#include "../db/db.h" +#include "../db/cartographer.h" +#include "../playlist/playlist.h" +#include "../koto-utils.h" +#include "artist-playlist-funcs.h" +#include "structs.h" +#include "track-helpers.h" + +extern KotoCartographer * koto_maps; +extern KotoConfig * config; + +enum { + PROP_0, + PROP_UUID, + PROP_ARTIST_NAME, + N_PROPERTIES +}; + +static GParamSpec * props[N_PROPERTIES] = { + NULL, +}; + +enum { + SIGNAL_ALBUM_ADDED, + SIGNAL_ALBUM_REMOVED, + SIGNAL_HAS_NO_ALBUMS, + SIGNAL_TRACK_ADDED, + SIGNAL_TRACK_REMOVED, + N_SIGNALS +}; + +static guint artist_signals[N_SIGNALS] = { + 0 +}; + +struct _KotoArtist { + GObject parent_instance; + gchar * uuid; + + KotoPlaylist * content_playlist; + + gboolean finalized; + gboolean has_artist_art; + gchar * artist_name; + GList * tracks; + GHashTable * paths; + KotoLibraryType type; + + GQueue * albums; + GListStore * albums_store; +}; + +struct _KotoArtistClass { + GObjectClass parent_class; + + void (* album_added) ( + KotoArtist * artist, + KotoAlbum * album + ); + void (* album_removed) ( + KotoArtist * artist, + KotoAlbum * album + ); + void (* has_no_albums) (KotoArtist * artist); + void (* track_added) ( + KotoArtist * artist, + KotoTrack * track + ); + void (* track_removed) ( + KotoArtist * artist, + KotoTrack * track + ); +}; + +G_DEFINE_TYPE(KotoArtist, koto_artist, G_TYPE_OBJECT); + +static void koto_artist_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +); + +static void koto_artist_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +); + +static void koto_artist_class_init(KotoArtistClass * c) { + GObjectClass * gobject_class; + + gobject_class = G_OBJECT_CLASS(c); + gobject_class->set_property = koto_artist_set_property; + gobject_class->get_property = koto_artist_get_property; + + artist_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(KotoArtistClass, album_added), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + KOTO_TYPE_ALBUM + ); + + artist_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(KotoArtistClass, album_removed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + KOTO_TYPE_ALBUM + ); + + artist_signals[SIGNAL_HAS_NO_ALBUMS] = g_signal_new( + "has-no-albums", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoArtistClass, has_no_albums), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0 + ); + + artist_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(KotoArtistClass, track_added), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + KOTO_TYPE_TRACK + ); + + artist_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(KotoArtistClass, track_removed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + KOTO_TYPE_TRACK + ); + + props[PROP_UUID] = g_param_spec_string( + "uuid", + "UUID to Artist in database", + "UUID to Artist in database", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_ARTIST_NAME] = g_param_spec_string( + "name", + "Name", + "Name of Artist", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + g_object_class_install_properties(gobject_class, N_PROPERTIES, props); +} + +static void koto_artist_init(KotoArtist * self) { + self->albums = g_queue_new(); // Create a new GQueue + self->albums_store = g_list_store_new(KOTO_TYPE_ALBUM); // Create our GListStore of type KotoAlbum + + self->content_playlist = koto_playlist_new(); // Create our playlist + g_object_set( + self->content_playlist, + "ephemeral", // Indicate that it is temporary + TRUE, + NULL + ); + + self->finalized = FALSE; // Indicate we not finalized + self->has_artist_art = FALSE; + self->paths = g_hash_table_new(g_str_hash, g_str_equal); + self->tracks = NULL; + self->type = KOTO_LIBRARY_TYPE_UNKNOWN; +} + +void koto_artist_commit(KotoArtist * self) { + if ((self->uuid == NULL) || strcmp(self->uuid, "")) { // UUID not set + self->uuid = g_strdup(g_uuid_string_random()); + } + + // TODO: Support multiple types instead of just local music artist + gchar * commit_op = g_strdup_printf( + "INSERT INTO artists(id , name, art_path)" + "VALUES ('%s', quote(\"%s\"), NULL)" + "ON CONFLICT(id) DO UPDATE SET name=excluded.name, art_path=excluded.art_path;", + self->uuid, + koto_utils_string_get_valid(self->artist_name) + ); + + new_transaction(commit_op, "Failed to write our artist to the database", FALSE); + + GHashTableIter paths_iter; + g_hash_table_iter_init(&paths_iter, self->paths); // Create an iterator for our paths + gpointer lib_uuid_ptr, artist_rel_path_ptr; + while (g_hash_table_iter_next(&paths_iter, &lib_uuid_ptr, &artist_rel_path_ptr)) { + gchar * lib_uuid = lib_uuid_ptr; + gchar * artist_rel_path = artist_rel_path_ptr; + + gchar * commit_op = g_strdup_printf( + "INSERT INTO libraries_artists(id, artist_id, path)" + "VALUES ('%s', '%s', quote(\"%s\"))" + "ON CONFLICT(id, artist_id) DO UPDATE SET path=excluded.path;", + lib_uuid, + self->uuid, + artist_rel_path + ); + + new_transaction(commit_op, "Failed to add this path for the artist", FALSE); + } +} + +static void koto_artist_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +) { + KotoArtist * self = KOTO_ARTIST(obj); + + switch (prop_id) { + case PROP_UUID: + g_value_set_string(val, self->uuid); + break; + case PROP_ARTIST_NAME: + g_value_set_string(val, self->artist_name); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_artist_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +) { + KotoArtist * self = KOTO_ARTIST(obj); + + switch (prop_id) { + case PROP_UUID: + self->uuid = g_strdup(g_value_get_string(val)); + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_UUID]); + break; + case PROP_ARTIST_NAME: + koto_artist_set_artist_name(self, (gchar*) g_value_get_string(val)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +void koto_artist_add_album( + KotoArtist * self, + KotoAlbum * album +) { + if (!KOTO_IS_ARTIST(self)) { // Not an artist + return; + } + + if (!KOTO_IS_ALBUM(album)) { // Album provided is not an album + return; + } + + GList * found_albums = g_queue_find(self->albums, album); // Try finding the album + + if (found_albums != NULL) { // Already has been added + g_list_free(found_albums); + return; + } + + g_list_free(found_albums); + + g_queue_push_tail(self->albums, album); // Add the album to end of albums GQueue + g_list_store_append(self->albums_store, album); // Add the album to th estore as well + + if (self->finalized) { // Is already finalized + koto_artist_apply_model(self, koto_config_get_preferred_album_sort_type(config)); // Apply our model to sort our albums gqueue and store + } + + g_signal_emit( + self, + artist_signals[SIGNAL_ALBUM_ADDED], + 0, + album + ); +} + +void koto_artist_add_track( + KotoArtist * self, + KotoTrack * track +) { + if (!KOTO_IS_ARTIST(self)) { // Not an artist + return; + } + + if (!KOTO_IS_TRACK(track)) { // Not a track + return; + } + + gchar * track_uuid = koto_track_get_uuid(track); + + if (g_list_index(self->tracks, track_uuid) != -1) { // If we have already added the track + return; + } + + koto_cartographer_add_track(koto_maps, track); // Add the track to cartographer if necessary + self->tracks = g_list_insert_sorted_with_data(self->tracks, track_uuid, koto_track_helpers_sort_tracks_by_uuid, NULL); + + koto_playlist_add_track(self->content_playlist, track, FALSE, FALSE); // Add this new track for the artist to its playlist + + g_signal_emit( + self, + artist_signals[SIGNAL_TRACK_ADDED], + 0, + track + ); +} + +void koto_artist_apply_model( + KotoArtist * self, + KotoPreferredAlbumSortType model +) { + if (!KOTO_IS_ARTIST(self)) { + return; + } + + g_queue_sort(self->albums, koto_artist_model_sort_albums, &model); + g_list_store_sort(self->albums_store, koto_artist_model_sort_albums, &model); +} + +GQueue * koto_artist_get_albums(KotoArtist * self) { + if (!KOTO_IS_ARTIST(self)) { // Not an artist + return NULL; + } + + return self->albums; +} + +GListStore * koto_artist_get_albums_store(KotoArtist * self) { + if (!KOTO_IS_ARTIST(self)) { // Not an artist + return NULL; + } + + return self->albums_store; +} + +KotoAlbum * koto_artist_get_album_by_name( + KotoArtist * self, + gchar * album_name +) { + if (!KOTO_IS_ARTIST(self)) { // Not an artist + return NULL; + } + + KotoAlbum * album = NULL; + + GList * cur_list_iter; + for (cur_list_iter = self->albums->head; cur_list_iter != NULL; cur_list_iter = cur_list_iter->next) { // Iterate through our albums by their UUIDs + KotoAlbum * this_album = cur_list_iter->data; + + if (!KOTO_IS_ALBUM(this_album)) { // Not an album + continue; + } + + if (g_strcmp0(koto_album_get_name(this_album), album_name) == 0) { // These album names match + album = this_album; + break; + } + } + + return album; +} + +gchar * koto_artist_get_name(KotoArtist * self) { + if (!KOTO_IS_ARTIST(self)) { // Not an artist + return g_strdup(""); + } + + return g_strdup(koto_utils_string_is_valid(self->artist_name) ? self->artist_name : ""); // Return artist name if set +} + +KotoPlaylist * koto_artist_get_playlist(KotoArtist * self) { + if (!KOTO_IS_ARTIST(self)) { + return NULL; + } + + return self->content_playlist; +} + +GList * koto_artist_get_tracks(KotoArtist * self) { + return KOTO_IS_ARTIST(self) ? self->tracks : NULL; +} + +KotoLibraryType koto_artist_get_lib_type(KotoArtist * self) { + return KOTO_IS_ARTIST(self) ? self->type : KOTO_LIBRARY_TYPE_UNKNOWN; +} + +gchar * koto_artist_get_uuid(KotoArtist * self) { + return KOTO_IS_ARTIST(self) ? self->uuid : NULL; +} + +void koto_artist_remove_album( + KotoArtist * self, + KotoAlbum * album +) { + if (!KOTO_IS_ARTIST(self)) { // Not an artist + return; + } + + if (!KOTO_ALBUM(album)) { // No album defined + return; + } + + g_queue_remove(self->albums, album); // Remove the album + + guint position = 0; + if (g_list_store_find(self->albums_store, album, &position)) { // Found the album in the list store + g_list_store_remove(self->albums_store, position); // Remove from the list store + } + + g_signal_emit( + self, + artist_signals[SIGNAL_ALBUM_REMOVED], + 0, + album + ); +} + +void koto_artist_remove_track( + KotoArtist * self, + KotoTrack * track +) { + if (!KOTO_IS_ARTIST(self)) { // Not an artist + return; + } + + if (!KOTO_IS_TRACK(track)) { // Not a track + return; + } + + gchar * track_uuid = koto_track_get_uuid(track); + self->tracks = g_list_remove(self->tracks, koto_track_get_uuid(track)); + + koto_playlist_remove_track_by_uuid(self->content_playlist, track_uuid); // Remove the track from our playlist + + g_signal_emit( + self, + artist_signals[SIGNAL_TRACK_ADDED], + 0, + track + ); +} + +void koto_artist_set_artist_name( + KotoArtist * self, + gchar * artist_name +) { + if (!KOTO_IS_ARTIST(self)) { // Not an artist + return; + } + + if (!koto_utils_string_is_valid(artist_name)) { // No artist name + return; + } + + if (koto_utils_string_is_valid(self->artist_name)) { // Has artist name + g_free(self->artist_name); + } + + self->artist_name = g_strdup(artist_name); + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_ARTIST_NAME]); +} + +void koto_artist_set_as_finalized(KotoArtist * self) { + if (!KOTO_IS_ARTIST(self)) { // Not an artist + return; + } + + self->finalized = TRUE; + + if (g_queue_get_length(self->albums) == 0) { // Have no albums + g_signal_emit_by_name(self, "has-no-albums"); + } +} + +void koto_artist_set_path( + KotoArtist * self, + KotoLibrary * lib, + const gchar * fixed_path, + gboolean should_commit +) { + if (!KOTO_IS_ARTIST(self)) { // Not an artist + return; + } + + gchar * path = g_strdup(fixed_path); // Duplicate our fixed_path + gchar * relative_path = koto_library_get_relative_path_to_file(lib, path); // Get the relative path to the file for the given library + + gchar * library_uuid = koto_library_get_uuid(lib); // Get the library for this path + g_hash_table_replace(self->paths, library_uuid, relative_path); // Replace any existing value or add this one + + if (should_commit) { // Should commit to the DB + koto_artist_commit(self); // Save the artist + } + + self->type = koto_library_get_lib_type(lib); // Define our artist type as the type from the library +} + +gint koto_artist_model_sort_albums( + gconstpointer first_item, + gconstpointer second_item, + gpointer user_data +) { + KotoPreferredAlbumSortType * preferred_model = (KotoPreferredAlbumSortType*) user_data; + + KotoAlbum * first_album = KOTO_ALBUM(first_item); + KotoAlbum * second_album = KOTO_ALBUM(second_item); + + if (KOTO_IS_ALBUM(first_album) && !KOTO_IS_ALBUM(second_album)) { // First album is valid, second isn't + return -1; + } else if (!KOTO_IS_ALBUM(first_album) && KOTO_IS_ALBUM(second_album)) { // Second album is valid, first isn't + return 1; + } + + if (preferred_model == KOTO_PREFERRED_ALBUM_SORT_TYPE_DEFAULT) { // Sort chronological before alphabetical + guint64 fa_year = koto_album_get_year(first_album); + guint64 sa_year = koto_album_get_year(second_album); + + if (fa_year > sa_year) { // First album is newer than second + return -1; + } else if (fa_year < sa_year) { // Second album is newer than first + return 1; + } + } + + return g_utf8_collate(koto_album_get_name(first_album), koto_album_get_name(second_album)); +} + +KotoArtist * koto_artist_new(gchar * artist_name) { + KotoArtist * artist = g_object_new( + KOTO_TYPE_ARTIST, + "uuid", + g_uuid_string_random(), + "name", + artist_name, + NULL + ); + + return artist; +} + +KotoArtist * koto_artist_new_with_uuid(const gchar * uuid) { + return g_object_new( + KOTO_TYPE_ARTIST, + "uuid", + g_strdup(uuid), + NULL + ); +} diff --git a/src/indexer/file-indexer.c b/src/indexer/file-indexer.c new file mode 100644 index 0000000..9bd9e34 --- /dev/null +++ b/src/indexer/file-indexer.c @@ -0,0 +1,239 @@ +/* file-indexer.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 +#include "../db/cartographer.h" +#include "../koto-utils.h" +#include "structs.h" +#include "track-helpers.h" + +extern KotoCartographer * koto_maps; +extern magic_t magic_cookie; + +void index_folder( + KotoLibrary * self, + gchar * path, + guint depth +) { + depth++; + + DIR * dir = opendir(path); // Attempt to open our directory + + if (dir == NULL) { + return; + } + + struct dirent * entry; + + while ((entry = readdir(dir))) { + if (g_str_has_prefix(entry->d_name, ".")) { // A reference to parent dir, self, or a hidden item + continue; + } + + gchar * full_path = g_strdup_printf("%s%s%s", path, G_DIR_SEPARATOR_S, entry->d_name); + + if (entry->d_type == DT_DIR) { // Directory + if (depth == 1) { // If we are following (ARTIST,AUTHOR,PODCAST)/ALBUM then this would be artist + KotoArtist * artist = koto_artist_new(entry->d_name); // Attempt to get the artist + + if (KOTO_IS_ARTIST(artist)) { + koto_artist_set_path(artist, self, full_path, TRUE); // Add the path for this library on this Artist and commit immediately + koto_cartographer_add_artist(koto_maps, artist); // Add the artist to cartographer + index_folder(self, full_path, depth); // Index this directory + koto_artist_set_as_finalized(artist); // Indicate it is finalized + } + } else if (depth == 2) { // If we are following FOLDER/ARTIST/ALBUM then this would be album + gchar * artist_name = g_path_get_basename(path); // Get the last entry from our path which is probably the artist + KotoArtist * artist = koto_cartographer_get_artist_by_name(koto_maps, artist_name); + + if (!KOTO_IS_ARTIST(artist)) { // Not an artist + continue; + } + + gchar * artist_uuid = koto_artist_get_uuid(artist); // Get the artist's UUID + + KotoAlbum * album = koto_album_new(artist_uuid); + + koto_album_set_path(album, self, full_path); + + koto_cartographer_add_album(koto_maps, album); // Add our album to the cartographer + koto_artist_add_album(artist, album); // Add the album + + index_folder(self, full_path, depth); // Index inside the album + koto_album_commit(album); // Save to database immediately + g_free(artist_name); + } else if (depth == 3) { // Possibly CD within album + gchar ** split = g_strsplit(full_path, G_DIR_SEPARATOR_S, -1); + guint split_len = g_strv_length(split); + + if (split_len < 4) { + g_strfreev(split); + continue; + } + + gchar * album_name = g_strdup(split[split_len - 2]); + gchar * artist_name = g_strdup(split[split_len - 3]); + g_strfreev(split); + + if (!koto_utils_string_is_valid(album_name)) { + g_free(album_name); + continue; + } + + if (!koto_utils_string_is_valid(artist_name)) { + g_free(album_name); + g_free(artist_name); + continue; + } + + KotoArtist * artist = koto_cartographer_get_artist_by_name(koto_maps, artist_name); + g_free(artist_name); + + if (!KOTO_IS_ARTIST(artist)) { + continue; + } + + KotoAlbum * album = koto_artist_get_album_by_name(artist, album_name); // Get the album + g_free(album_name); + + if (!KOTO_IS_ALBUM(album)) { + continue; + } + + index_folder(self, full_path, depth); // Index inside the album + } + } else if ((entry->d_type == DT_REG)) { // Is a file in artist folder or lower in FS hierarchy + index_file(self, full_path); // Index this audio file or weird ogg thing + } + + g_free(full_path); + } + + closedir(dir); // Close the directory +} + +void index_file( + KotoLibrary * lib, + const gchar * path +) { + const char * mime_type = magic_file(magic_cookie, path); + + if (mime_type == NULL) { // Failed to get the mimetype + return; + } + + if (!g_str_has_prefix(mime_type, "audio/") && !g_str_has_prefix(mime_type, "video/ogg")) { // Is not an audio file or ogg + return; + } + + gboolean for_audiobook = (koto_library_get_lib_type(lib) == KOTO_LIBRARY_TYPE_AUDIOBOOK); + + gchar * relative_path_to_file = koto_library_get_relative_path_to_file(lib, g_strdup(path)); // Strip out library path so we have a relative path to the file + gchar * file_basename = g_path_get_basename(relative_path_to_file); + + gchar ** split_on_relative_slashes = g_strsplit(relative_path_to_file, G_DIR_SEPARATOR_S, -1); // Split based on separator (e.g. / ) + guint slash_sep_count = g_strv_length(split_on_relative_slashes); + + gchar * artist_author_podcast_name = g_strdup(split_on_relative_slashes[0]); // No matter what, artist should be first + gchar * album_or_audiobook_name = NULL; + guint cd = (guint) 0; + + if (slash_sep_count >= 3) { // If this it is at least "artist" + "album" + "file" (or with CD) + album_or_audiobook_name = g_strdup(split_on_relative_slashes[1]); // Duplicate the second item as the album or audiobook name + } + + // #region CD parsing logic + + if ((slash_sep_count == 4)) { // If is at least "artist" + "album" + "cd" + "file" + gchar * cd_str = g_strdup(g_strstrip(koto_utils_string_replace_all(g_utf8_strdown(split_on_relative_slashes[2], -1), "cd", ""))); // Replace a lowercased version of our CD ("cd") and trim any whitespace + + cd = (guint) g_ascii_strtoull(cd_str, NULL, 10); // Attempt to convert} + } + + if (for_audiobook && (cd == 0)) { // No CD yet and is for an audiobook + cd = koto_track_helpers_get_cd_based_on_file_name(file_basename); // Base on file name + } else { + cd = 1; + } + + // #endregion + + gchar * file_name = NULL; + + if (for_audiobook) { // If this is for an audiobook library + guint64 pos = koto_track_helpers_get_position_based_on_file_name(file_basename); + + if (cd != 1) { // On a non "Part 1" audiobook + file_name = g_strdup_printf("%s - Part %u - %lu", album_or_audiobook_name, cd, pos); + } else { // Part 1 / CD 1 + file_name = g_strdup_printf("%s - %lu", album_or_audiobook_name, pos); + } + } + + if (!koto_utils_string_is_valid(file_name)) { // No valid file name yet + file_name = koto_track_helpers_get_name_for_file(path, artist_author_podcast_name); // Get the name of the file + } + + g_strfreev(split_on_relative_slashes); + g_free(file_basename); + + gchar * sorta_uniqueish_key = NULL; + + if (koto_utils_string_is_valid(album_or_audiobook_name)) { // Have audiobook or album name + sorta_uniqueish_key = g_strdup_printf("%s-%s-%s", artist_author_podcast_name, album_or_audiobook_name, file_name); + } else { // No audiobook or album name + sorta_uniqueish_key = g_strdup_printf("%s-%s", artist_author_podcast_name, file_name); + } + + KotoTrack * track = koto_cartographer_get_track_by_uniqueish_key(koto_maps, sorta_uniqueish_key); // Attempt to get any existing KotoTrack + + if (KOTO_IS_TRACK(track)) { // Got a track already + koto_track_set_path(track, lib, relative_path_to_file); // Add this path, which will determine the associated library within that function + } else { // Don't already have a track for this file + KotoArtist * artist = koto_cartographer_get_artist_by_name(koto_maps, artist_author_podcast_name); // Get the possible artist + + if (!KOTO_IS_ARTIST(artist)) { // Have an artist for this already + return; + } + + KotoAlbum * album = NULL; + + if (koto_utils_string_is_valid(album_or_audiobook_name)) { // Have an album or audiobook name + KotoAlbum * possible_album = koto_artist_get_album_by_name(artist, album_or_audiobook_name); + album = KOTO_IS_ALBUM(possible_album) ? possible_album : NULL; + } + + gchar * album_uuid = KOTO_IS_ALBUM(album) ? koto_album_get_uuid(album) : NULL; + + track = koto_track_new(koto_artist_get_uuid(artist), album_uuid, file_name, cd); + koto_track_set_path(track, lib, relative_path_to_file); // Immediately add the path to this file, for this Library + koto_artist_add_track(artist, track); // Add the track to the artist in the event this is a podcast (no album) or the track is directly in the artist directory + + if (KOTO_IS_ALBUM(album)) { // Have an album + koto_album_add_track(album, track); // Add this track since we haven't yet + } + + koto_cartographer_add_track(koto_maps, track); // Add to our cartographer tracks hashtable + } + + if (KOTO_IS_TRACK(track)) { // Is a track + koto_track_commit(track); // Save the track immediately + } +} \ No newline at end of file diff --git a/src/indexer/library.c b/src/indexer/library.c new file mode 100644 index 0000000..1365e8d --- /dev/null +++ b/src/indexer/library.c @@ -0,0 +1,511 @@ +/* library.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 "../config/config.h" +#include "../koto-utils.h" +#include "structs.h" + +extern KotoConfig * config; +extern GVolumeMonitor * volume_monitor; + +enum { + PROP_0, + PROP_TYPE, + PROP_UUID, + PROP_STORAGE_UUID, + PROP_CONSTRUCTION_PATH, + PROP_NAME, + N_PROPERTIES +}; + +static GParamSpec * props[N_PROPERTIES] = { + NULL, +}; + +enum { + SIGNAL_NOW_AVAILABLE, + SIGNAL_NOW_UNAVAILABLE, + N_SIGNALS +}; + +static guint library_signals[N_SIGNALS] = { + 0 +}; + +struct _KotoLibrary { + GObject parent_instance; + gchar * uuid; + + KotoLibraryType type; + gchar * directory; + gchar * storage_uuid; + + GMount * mount; + gulong mount_unmounted_handler; + gchar * mount_path; + + gboolean should_index; + + gchar * path; + gchar * relative_path; + gchar * name; +}; + +struct _KotoLibraryClass { + GObjectClass parent_class; + + void (* now_available) (KotoLibrary * library); + + void (* now_unavailable) (KotoLibrary * library); +}; + +G_DEFINE_TYPE(KotoLibrary, koto_library, G_TYPE_OBJECT); + +static void koto_library_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +); + +static void koto_library_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +); + +static void koto_library_class_init(KotoLibraryClass * c) { + GObjectClass * gobject_class; + + gobject_class = G_OBJECT_CLASS(c); + gobject_class->set_property = koto_library_set_property; + gobject_class->get_property = koto_library_get_property; + + library_signals[SIGNAL_NOW_AVAILABLE] = g_signal_new( + "now-available", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoLibraryClass, now_available), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0 + ); + + library_signals[SIGNAL_NOW_UNAVAILABLE] = g_signal_new( + "now-unavailable", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoLibraryClass, now_unavailable), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0 + ); + + props[PROP_UUID] = g_param_spec_string( + "uuid", + "UUID of Library", + "UUID of Library", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_TYPE] = g_param_spec_string( + "type", + "Type of Library", + "Type of Library", + koto_library_type_to_string(KOTO_LIBRARY_TYPE_MUSIC), + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_STORAGE_UUID] = g_param_spec_string( + "storage-uuid", + "Storage UUID to associated Mount of Library", + "Storage UUID to associated Mount of Library", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_CONSTRUCTION_PATH] = g_param_spec_string( + "construction-path", + "Construction Path", + "Path to this library during construction", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE + ); + + props[PROP_NAME] = g_param_spec_string( + "name", + "Name of the Library", + "Name of the Library", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE + ); + + g_object_class_install_properties(gobject_class, N_PROPERTIES, props); +} + +static void koto_library_init(KotoLibrary * self) { + (void) self; +} + +static void koto_library_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +) { + KotoLibrary * self = KOTO_LIBRARY(obj); + + switch (prop_id) { + case PROP_NAME: + g_value_set_string(val, g_strdup(self->name)); + break; + case PROP_UUID: + g_value_set_string(val, self->uuid); + break; + case PROP_STORAGE_UUID: + g_value_set_string(val, self->storage_uuid); + break; + case PROP_TYPE: + g_value_set_string(val, koto_library_type_to_string(self->type)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_library_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +) { + KotoLibrary * self = KOTO_LIBRARY(obj); + + switch (prop_id) { + case PROP_UUID: + self->uuid = g_strdup(g_value_get_string(val)); + break; + case PROP_TYPE: + self->type = koto_library_type_from_string(g_strdup(g_value_get_string(val))); + break; + case PROP_STORAGE_UUID: + koto_library_set_storage_uuid(self, g_strdup(g_value_get_string(val))); + break; + case PROP_CONSTRUCTION_PATH: + koto_library_set_path(self, g_strdup(g_value_get_string(val))); + break; + case PROP_NAME: + koto_library_set_name(self, g_strdup(g_value_get_string(val))); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +gchar * koto_library_get_path(KotoLibrary * self) { + if (!KOTO_IS_LIBRARY(self)) { + return NULL; + } + + if (G_IS_MOUNT(self->mount)) { + + } + + return self->path; +} + +gchar * koto_library_get_relative_path_to_file( + KotoLibrary * self, + gchar * full_path +) { + if (!KOTO_IS_LIBRARY(self)) { + return NULL; + } + + gchar * appended_slash_to_library_path = g_str_has_suffix(self->path, G_DIR_SEPARATOR_S) ? g_strdup(self->path) : g_strdup_printf("%s%s", g_strdup(self->path), G_DIR_SEPARATOR_S); + gchar * cleaned_path = koto_utils_string_replace_all(full_path, appended_slash_to_library_path, ""); // Replace the full path + + g_free(appended_slash_to_library_path); + return cleaned_path; +} + +gchar * koto_library_get_storage_uuid(KotoLibrary * self) { + if (!KOTO_IS_LIBRARY(self)) { + return NULL; + } + + return self->storage_uuid; +} + +KotoLibraryType koto_library_get_lib_type(KotoLibrary * self) { + if (!KOTO_IS_LIBRARY(self)) { + return KOTO_LIBRARY_TYPE_UNKNOWN; + } + + return self->type; +} + +gchar * koto_library_get_uuid(KotoLibrary * self) { + if (!KOTO_IS_LIBRARY(self)) { + return NULL; + } + + return self->uuid; +} + +void koto_library_index(KotoLibrary * self) { + if (!KOTO_IS_LIBRARY(self) || !self->should_index) { // Not a library or should not index + return; + } + + index_folder(self, self->path, 0); // Start index operation at the top +} + +gboolean koto_library_is_available(KotoLibrary * self) { + if (!KOTO_IS_LIBRARY(self)) { + return FALSE; + } + + return FALSE; +} + +void koto_library_set_name( + KotoLibrary * self, + gchar * library_name +) { + if (!KOTO_IS_LIBRARY(self)) { + return; + } + + if (!koto_utils_string_is_valid(library_name)) { // Not a string + return; + } + + if (koto_utils_string_is_valid(self->name)) { // Name already set + g_free(self->name); // Free the existing value + } + + self->name = g_strdup(library_name); + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_NAME]); +} + +void koto_library_set_path( + KotoLibrary * self, + gchar * path +) { + if (!KOTO_IS_LIBRARY(self)) { + return; + } + + if (!koto_utils_string_is_valid(path)) { // Not a valid string + return; + } + + if (koto_utils_string_is_valid(self->path)) { + g_free(self->path); + } + + self->relative_path = g_path_is_absolute(path) ? koto_utils_string_replace_all(path, self->mount_path, "") : path; // Ensure path is relative to our mount, even if the mount is really our own system partition + self->path = g_build_path(G_DIR_SEPARATOR_S, self->mount_path, self->relative_path, NULL); // Ensure our path is to whatever the current path of the mount + relative path is +} + +void koto_library_set_storage_uuid( + KotoLibrary * self, + gchar * storage_uuid +) { + if (!KOTO_IS_LIBRARY(self)) { + return; + } + + if (G_IS_MOUNT(self->mount)) { // Already have a mount + g_signal_handler_disconnect(self->mount, self->mount_unmounted_handler); // Stop listening to the unmounted signal for this existing mount + g_object_unref(self->mount); // Dereference the mount + g_free(self->mount_path); + } + + if (!koto_utils_string_is_valid(storage_uuid)) { // Not a valid string, which actually is allowed for built-ins + self->mount = NULL; + self->mount_path = g_strdup_printf("%s%s", g_get_home_dir(), G_DIR_SEPARATOR_S); // Set mount path to user's home directory + self->storage_uuid = NULL; + return; + } + + GMount * mount = g_volume_monitor_get_mount_for_uuid(volume_monitor, storage_uuid); // Attempt to get the mount by this UUID + + if (!G_IS_MOUNT(mount)) { + g_warning("Failed to get mount for UUID: %s", storage_uuid); + self->mount = NULL; + return; + } + + if (g_mount_is_shadowed(mount)) { // Is shadowed and should not use + g_warning("This mount is considered \"shadowed\" and will not be used."); + return; + } + + GFile * mount_file = g_mount_get_default_location(mount); // Get the file for the entry location of the mount + + self->mount = mount; + self->mount_path = g_strdup(g_file_get_path(mount_file)); // Set the mount path to the path defined for the Mount File + self->storage_uuid = g_strdup(storage_uuid); +} + +gchar * koto_library_to_config_string(KotoLibrary * self) { + GStrvBuilder * lib_builder = g_strv_builder_new(); // Create new strv builder + g_strv_builder_add(lib_builder, g_strdup("[[library]]")); // Add our library array header + + g_strv_builder_add(lib_builder, g_strdup_printf("\tdirectory=\"%s\"", self->relative_path)); // Add the directory + + if (koto_utils_string_is_valid(self->name)) { // Have a library name + g_strv_builder_add(lib_builder, g_strdup_printf("\tname=\"%s\"", self->name)); // Add the name + } + + if (koto_utils_string_is_valid(self->storage_uuid)) { // Have a storage UUID (not applicable to built-ins) + g_strv_builder_add(lib_builder, g_strdup_printf("\tstorage_uuid=\"%s\"", self->storage_uuid)); // Add the storage UUID + } + + g_strv_builder_add(lib_builder, g_strdup_printf("\ttype=\"%s\"", koto_library_type_to_string(self->type))); // Add the type + g_strv_builder_add(lib_builder, g_strdup_printf("\tuuid=\"%s\"", self->uuid)); + + GStrv lines = g_strv_builder_end(lib_builder); // Get all the lines as a GStrv which is a gchar ** + gchar * content = g_strjoinv("\n", lines); // Separate all lines with newline + g_strfreev(lines); // Free our lines + + g_strv_builder_unref(lib_builder); // Unref our builder + return g_strdup(content); +} + +KotoLibrary * koto_library_new( + KotoLibraryType type, + const gchar * storage_uuid, + const gchar * path +) { + KotoLibrary * lib = g_object_new( + KOTO_TYPE_LIBRARY, + "type", + koto_library_type_to_string(type), + "uuid", + g_uuid_string_random(), // Create a new Library with a new UUID + "storage-uuid", + storage_uuid, + "construction-path", + path, + NULL + ); + + lib->should_index = TRUE; + return lib; +} + +KotoLibrary * koto_library_new_from_toml_table(toml_table_t * lib_datum) { + toml_datum_t uuid_datum = toml_string_in(lib_datum, "uuid"); // Get the library UUID + + if (!uuid_datum.ok) { // No UUID defined + g_warning("No UUID set for this library. Ignoring"); + return NULL; + } + + gchar * uuid = g_strdup(uuid_datum.u.s); // Duplicate our UUID + + toml_datum_t type_datum = toml_string_in(lib_datum, "type"); + + if (!type_datum.ok) { // No type defined + g_warning("Unknown type for library with UUID of %s", uuid); + return NULL; + } + + gchar * lib_type_as_str = g_strdup(type_datum.u.s); + KotoLibraryType lib_type = koto_library_type_from_string(lib_type_as_str); // Get the library's type + + if (lib_type == KOTO_LIBRARY_TYPE_UNKNOWN) { // Known type + return NULL; + } + + toml_datum_t dir_datum = toml_string_in(lib_datum, "directory"); + + if (!dir_datum.ok) { + g_critical("Failed to get directory path for library with UUID of %s", uuid); + return NULL; + } + + gchar * path = g_strdup(dir_datum.u.s); // Duplicate the path string + + toml_datum_t storage_uuid_datum = toml_string_in(lib_datum, "storage_uuid"); // Get the datum for the storage UUID + gchar * storage_uuid = g_strdup((storage_uuid_datum.ok) ? storage_uuid_datum.u.s : ""); + + toml_datum_t name_datum = toml_string_in(lib_datum, "name"); // Get the datum for the name + gchar * name = g_strdup((name_datum.ok) ? name_datum.u.s : ""); + + KotoLibrary * lib = g_object_new( + KOTO_TYPE_LIBRARY, + "type", + lib_type_as_str, + "uuid", + uuid, + "storage-uuid", + storage_uuid, + "construction-path", + path, + "name", + name, + NULL + ); + + lib->should_index = FALSE; + return lib; +} + +KotoLibraryType koto_library_type_from_string(gchar * t) { + if ( + (g_strcmp0(t, "audiobooks") == 0) || + (g_strcmp0(t, "audiobook") == 0) + ) { + return KOTO_LIBRARY_TYPE_AUDIOBOOK; + } else if (g_strcmp0(t, "music") == 0) { + return KOTO_LIBRARY_TYPE_MUSIC; + } else if ( + (g_strcmp0(t, "podcasts") == 0) || + (g_strcmp0(t, "podcast") == 0) + ) { + return KOTO_LIBRARY_TYPE_PODCAST; + } + + + g_warning("Invalid type provided for koto_library_type_from_string: %s", t); + return KOTO_LIBRARY_TYPE_UNKNOWN; +} + +gchar * koto_library_type_to_string(KotoLibraryType t) { + switch (t) { + case KOTO_LIBRARY_TYPE_AUDIOBOOK: + return g_strdup("audiobook"); + case KOTO_LIBRARY_TYPE_MUSIC: + return g_strdup("music"); + case KOTO_LIBRARY_TYPE_PODCAST: + return g_strdup("podcast"); + default: + return g_strdup("UNKNOWN"); + } +} \ No newline at end of file diff --git a/src/indexer/misc-types.h b/src/indexer/misc-types.h new file mode 100644 index 0000000..e3963e6 --- /dev/null +++ b/src/indexer/misc-types.h @@ -0,0 +1,22 @@ +/* misc-types.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 +typedef enum { + KOTO_PREFERRED_ALBUM_SORT_TYPE_DEFAULT, // Chronological is considered default + KOTO_PREFERRED_ALBUM_ALWAYS_ALPHABETICAL, // Prefer sorting alphabetically +} KotoPreferredAlbumSortType; \ No newline at end of file diff --git a/src/indexer/structs.h b/src/indexer/structs.h new file mode 100644 index 0000000..786e4c7 --- /dev/null +++ b/src/indexer/structs.h @@ -0,0 +1,425 @@ +/* structs.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 +#include +#include "misc-types.h" + +typedef enum { + KOTO_LIBRARY_TYPE_AUDIOBOOK = 1, + KOTO_LIBRARY_TYPE_MUSIC = 2, + KOTO_LIBRARY_TYPE_PODCAST = 3, + KOTO_LIBRARY_TYPE_UNKNOWN = 4 +} KotoLibraryType; + +G_BEGIN_DECLS + +/** + * Type Definition + **/ + +#define KOTO_TYPE_LIBRARY koto_library_get_type() +#define KOTO_LIBRARY(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), KOTO_TYPE_LIBRARY, KotoLibrary)) +typedef struct _KotoLibrary KotoLibrary; +typedef struct _KotoLibraryClass KotoLibraryClass; + +GLIB_AVAILABLE_IN_ALL +GType koto_library_get_type(void) G_GNUC_CONST; + +#define KOTO_IS_LIBRARY(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_LIBRARY)) + +#define KOTO_TYPE_ARTIST koto_artist_get_type() +#define KOTO_ARTIST(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), KOTO_TYPE_ARTIST, KotoArtist)) +typedef struct _KotoArtist KotoArtist; +typedef struct _KotoArtistClass KotoArtistClass; + +GLIB_AVAILABLE_IN_ALL +GType koto_artist_get_type(void) G_GNUC_CONST; + +#define KOTO_IS_ARTIST(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_ARTIST)) + +#define KOTO_TYPE_ALBUM koto_album_get_type() +#define KOTO_ALBUM(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), KOTO_TYPE_ALBUM, KotoAlbum)) +typedef struct _KotoAlbum KotoAlbum; +typedef struct _KotoAlbumClass KotoAlbumClass; + +GLIB_AVAILABLE_IN_ALL +GType koto_album_get_type(void) G_GNUC_CONST; + +#define KOTO_IS_ALBUM(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_ALBUM)) + +#define KOTO_TYPE_TRACK koto_track_get_type() +G_DECLARE_FINAL_TYPE(KotoTrack, koto_track, KOTO, TRACK, GObject); +#define KOTO_IS_TRACK(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_TRACK)) + +/** + * Library Functions + **/ + +KotoLibrary * koto_library_new( + KotoLibraryType type, + const gchar * storage_uuid, + const gchar * path +); + +KotoLibrary * koto_library_new_from_toml_table(toml_table_t * lib_datum); + +gchar * koto_library_get_path(KotoLibrary * self); + +gchar * koto_library_get_relative_path_to_file( + KotoLibrary * self, + gchar * full_path +); + +gchar * koto_library_get_storage_uuid(KotoLibrary * self); + +KotoLibraryType koto_library_get_lib_type(KotoLibrary * self); + +gchar * koto_library_get_uuid(KotoLibrary * self); + +void koto_library_index(KotoLibrary * self); + +gboolean koto_library_is_available(KotoLibrary * self); + +gchar * koto_library_get_storage_uuid(KotoLibrary * self); + +void koto_library_set_name( + KotoLibrary * self, + gchar * library_name +); + +void koto_library_set_path( + KotoLibrary * self, + gchar * path +); + +void koto_library_set_storage_uuid( + KotoLibrary * self, + gchar * uuid +); + +gchar * koto_library_to_config_string(KotoLibrary * self); + +KotoLibraryType koto_library_type_from_string(gchar * t); + +gchar * koto_library_type_to_string(KotoLibraryType t); + +void index_folder( + KotoLibrary * self, + gchar * path, + guint depth +); + +void index_file( + KotoLibrary * lib, + const gchar * path +); + +/** + * Artist Functions + **/ + +KotoArtist * koto_artist_new(gchar * artist_name); + +KotoArtist * koto_artist_new_with_uuid(const gchar * uuid); + +void koto_artist_add_album( + KotoArtist * self, + KotoAlbum * album +); + +void koto_artist_add_track( + KotoArtist * self, + KotoTrack * track +); + +void koto_artist_apply_model( + KotoArtist * self, + KotoPreferredAlbumSortType model +); + +void koto_artist_commit(KotoArtist * self); + +GQueue * koto_artist_get_albums(KotoArtist * self); + +GListStore * koto_artist_get_albums_store(KotoArtist * self); + +KotoAlbum * koto_artist_get_album_by_name( + KotoArtist * self, + gchar * album_name +); + +gchar * koto_artist_get_name(KotoArtist * self); + +gchar * koto_artist_get_path(KotoArtist * self); + +GList * koto_artist_get_tracks(KotoArtist * self); + +gchar * koto_artist_get_uuid(KotoArtist * self); + +KotoLibraryType koto_artist_get_lib_type(KotoArtist * self); + +gint koto_artist_model_sort_albums( + gconstpointer first_item, + gconstpointer second_item, + gpointer user_data +); + +void koto_artist_remove_album( + KotoArtist * self, + KotoAlbum * album +); + +void koto_artist_remove_track( + KotoArtist * self, + KotoTrack * track +); + +void koto_artist_set_artist_name( + KotoArtist * self, + gchar * artist_name +); + +void koto_artist_set_as_finalized(KotoArtist * self); + +void koto_artist_set_path( + KotoArtist * self, + KotoLibrary * lib, + const gchar * fixed_path, + gboolean should_commit +); + +/** + * Album Functions + **/ + +KotoAlbum * koto_album_new(gchar * artist_uuid); + +KotoAlbum * koto_album_new_with_uuid( + KotoArtist * artist, + const gchar * uuid +); + +void koto_album_add_track( + KotoAlbum * self, + KotoTrack * track +); + +void koto_album_commit(KotoAlbum * self); + +void koto_album_find_album_art(KotoAlbum * self); + +gchar * koto_album_get_art(KotoAlbum * self); + +gchar * koto_album_get_artist_uuid(KotoAlbum * self); + +gchar * koto_album_get_description(KotoAlbum * self); + +GList * koto_album_get_genres(KotoAlbum * self); + +KotoLibraryType koto_album_get_lib_type(KotoAlbum * self); + +gchar * koto_album_get_name(KotoAlbum * self); + +gchar * koto_album_get_narrator(KotoAlbum * self); + +gchar * koto_album_get_path(KotoAlbum * self); + +GListStore * koto_album_get_store(KotoAlbum * self); + +gchar * koto_album_get_uuid(KotoAlbum * self); + +guint64 koto_album_get_year(KotoAlbum * self); + +void koto_album_mark_as_finalized(KotoAlbum * self); + +void koto_album_remove_track( + KotoAlbum * self, + KotoTrack * track +); + +void koto_album_set_album_art( + KotoAlbum * self, + const gchar * album_art +); + +void koto_album_set_album_name( + KotoAlbum * self, + const gchar * album_name +); + +void koto_album_set_artist_uuid( + KotoAlbum * self, + const gchar * artist_uuid +); + +void koto_album_set_description( + KotoAlbum * self, + const char * description +); + +void koto_album_set_as_current_playlist(KotoAlbum * self); + +void koto_album_set_narrator( + KotoAlbum * self, + const char * narrator +); + +void koto_album_set_path( + KotoAlbum * self, + KotoLibrary * lib, + const gchar * fixed_path +); + +void koto_album_set_preparsed_genres( + KotoAlbum * self, + gchar * genrelist +); + +void koto_album_set_uuid( + KotoAlbum * self, + const gchar * uuid +); + +void koto_album_set_year( + KotoAlbum * self, + guint64 year +); + +/** + * File / Track Functions + **/ + +KotoTrack * koto_track_new( + const gchar * artist_uuid, + const gchar * album_uuid, + const gchar * parsed_name, + guint cd +); + +KotoTrack * koto_track_new_with_uuid(const gchar * uuid); + +void koto_track_commit(KotoTrack * self); + +gchar * koto_track_get_description(KotoTrack * self); + +guint koto_track_get_disc_number(KotoTrack * self); + +guint64 koto_track_get_duration(KotoTrack * self); + +GList * koto_track_get_genres(KotoTrack * self); + +GVariant * koto_track_get_metadata_vardict(KotoTrack * self); + +gchar * koto_track_get_path(KotoTrack * self); + +gchar * koto_track_get_name(KotoTrack * self); + +gchar * koto_track_get_narrator(KotoTrack * self); + +guint64 koto_track_get_playback_position(KotoTrack * self); + +guint64 koto_track_get_position(KotoTrack * self); + +gchar * koto_track_get_uniqueish_key(KotoTrack * self); + +gchar * koto_track_get_uuid(KotoTrack * self); + +guint64 koto_track_get_year(KotoTrack * self); + +void koto_track_remove_from_playlist( + KotoTrack * self, + gchar * playlist_uuid +); + +void koto_track_set_album_uuid( + KotoTrack * self, + const gchar * album_uuid +); + +void koto_track_save_to_playlist( + KotoTrack * self, + gchar * playlist_uuid +); + +void koto_track_set_cd( + KotoTrack * self, + guint cd +); + +void koto_track_set_description( + KotoTrack * self, + const gchar * description +); + +void koto_track_set_duration( + KotoTrack * self, + guint64 duration +); + +void koto_track_set_file_name( + KotoTrack * self, + gchar * new_file_name +); + +void koto_track_set_genres( + KotoTrack * self, + char * genrelist +); + +void koto_track_set_narrator( + KotoTrack * self, + const gchar * narrator +); + +void koto_track_set_parsed_name( + KotoTrack * self, + gchar * new_parsed_name +); + +void koto_track_set_path( + KotoTrack * self, + KotoLibrary * lib, + gchar * fixed_path +); + +void koto_track_set_playback_position( + KotoTrack * self, + guint64 position +); + +void koto_track_set_position( + KotoTrack * self, + guint64 pos +); + +void koto_track_set_preparsed_genres( + KotoTrack * self, + gchar * genrelist +); + +void koto_track_set_year( + KotoTrack * self, + guint64 year +); + +void koto_track_update_metadata(KotoTrack * self); + +G_END_DECLS diff --git a/src/indexer/track-helpers.c b/src/indexer/track-helpers.c new file mode 100644 index 0000000..076212e --- /dev/null +++ b/src/indexer/track-helpers.c @@ -0,0 +1,243 @@ +/* track-helpers.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/track-item.h" +#include "../db/cartographer.h" +#include "../koto-utils.h" +#include "structs.h" + +extern KotoCartographer * koto_maps; + +GHashTable * genre_replacements; + +void koto_track_helpers_init() { + genre_replacements = g_hash_table_new(g_str_hash, g_str_equal); + + gchar * corrected_genre_hiphop = g_strdup("hip-hop"); + gchar * correct_genre_indie = g_strdup("indie"); + gchar * correct_genre_scifi = g_strdup("sci-fi"); + + g_hash_table_insert(genre_replacements, g_strdup("alternative/indie"), correct_genre_indie); // Change Alternative/Indie (lowercased) to indie + g_hash_table_insert(genre_replacements, g_strdup("rap-&-hip-hop"), corrected_genre_hiphop); // Change rap-&-hip-hop to just hip-hop + g_hash_table_insert(genre_replacements, g_strdup("science-fiction"), correct_genre_scifi); // Change science-fiction to sci-fi +} + +gchar * koto_track_helpers_get_corrected_genre(gchar * original_genre) { + gchar * lookedup_genre = g_hash_table_lookup(genre_replacements, original_genre); // Look up the genre + return koto_utils_string_is_valid(lookedup_genre) ? lookedup_genre : original_genre; +} + +guint64 koto_track_helpers_get_cd_based_on_file_name(const gchar * file_name) { + gchar ** part_split = g_strsplit(file_name, "Part", -1); + gchar * part_str = NULL; + + for (guint i = 0; i < g_strv_length(part_split); i++) { // Iterate on the parts + gchar * stripped_part = g_strdup(g_strstrip(part_split[i])); // Trim the whitespace around this part + gchar ** split = g_regex_split_simple("^([\\d]+)", stripped_part, G_REGEX_JAVASCRIPT_COMPAT, 0); + + g_free(stripped_part); // Free the stripped part + + if (g_strv_length(split) > 1) { // Has positional info at the beginning of the string + part_str = g_strdup(split[1]); + g_strfreev(split); + break; + } else { + g_strfreev(split); + } + } + + guint64 cd = 0; + + if (koto_utils_string_is_valid(part_str)) { // Have a valid string for the part + cd = g_ascii_strtoull(part_str, NULL, 10); + g_free(part_str); + } + + if (cd == 0) { + cd = 1; // Should be first CD, not 0 + } + + return cd; +} + +gchar * koto_track_helpers_get_name_for_file( + const gchar * path, + gchar * optional_artist_name +) { + gchar * file_name = NULL; + TagLib_File * t_file = taglib_file_new(path); // Get a taglib file for this file + + if ((t_file != NULL) && taglib_file_is_valid(t_file)) { // If we got the taglib file and it is valid + TagLib_Tag * tag = taglib_file_tag(t_file); // Get our tag + file_name = g_strdup(taglib_tag_title(tag)); // Get the tag title and duplicate it + } + + taglib_tag_free_strings(); // Free strings + taglib_file_free(t_file); // Free the file + + if (koto_utils_string_is_valid(file_name)) { // File name not set yet + return file_name; + } + + gchar * name_without_hyphen_surround = koto_utils_string_replace_all(koto_utils_get_filename_without_extension((gchar*) path), " - ", ""); // Remove - surrounded by spaces + + gchar * name_without_hyphen = koto_utils_string_replace_all(name_without_hyphen_surround, "-", ""); // Remove just - + g_free(name_without_hyphen_surround); + + file_name = koto_utils_string_replace_all(name_without_hyphen, "_", " "); // Replace underscore with whitespace + + if (koto_utils_string_is_valid(optional_artist_name)) { // Was provided an optional artist name + gchar * replaced_artist = koto_utils_string_replace_all(file_name, optional_artist_name, ""); // Remove the artist + g_free(file_name); + file_name = g_strdup(replaced_artist); + g_free(replaced_artist); + } + + gchar ** split = g_regex_split_simple("^([\\d]+)", file_name, G_REGEX_JAVASCRIPT_COMPAT, 0); // Split based on any possible position + if (g_strv_length(split) > 1) { // Has positional info at the beginning of the file name + g_free(file_name); // Free the prior name + file_name = g_strdup(split[2]); // Set to our second item which is the rest of the song name without the prefixed numbers + } + + g_strfreev(split); + return g_strdup(g_strstrip(file_name)); +} + +guint64 koto_track_helpers_get_position_based_on_file_name(const gchar * file_name) { + GRegex * num_pat = g_regex_new("^([\\d]+)", G_REGEX_JAVASCRIPT_COMPAT, 0, NULL); + gchar ** split = g_regex_split(num_pat, file_name, 0); + + if (g_strv_length(split) > 1) { // Has positional info at the beginning of the file + gchar * num = g_strdup(split[1]); + g_strfreev(split); + + if ((strcmp(num, "0") != 0) && (strcmp(num, "00") != 0)) { // Is not zero + guint64 potential_pos = g_ascii_strtoull(num, NULL, 10); // Attempt to convert + + if (potential_pos != 0) { // Got a legitimate position + g_free(num_pat); // Free our regex + return potential_pos; // Return this position + } + } + } + + gchar * fn_no_ext = koto_utils_get_filename_without_extension((gchar*) file_name); // Get the filename without the extension + split = g_strsplit(fn_no_ext, ".", -1); // Split every time we see . + + guint len_of_extension_split = g_strv_length(split); + + gchar * fn_last_split_on_period = g_strdup(split[len_of_extension_split - 1]); + g_free(fn_no_ext); + + g_strfreev(split); // Free our split + + gchar ** whitespace_split = g_strsplit(fn_last_split_on_period, " ", -1); // Split on whitespace + g_free(fn_last_split_on_period); + + gchar * last_item = koto_utils_string_replace_all(whitespace_split[g_strv_length(split) - 1], "#", ""); // Get last item, removing any # from it + g_strfreev(whitespace_split); + + gchar ** hyphen_split = g_strsplit(last_item, "-", -1); // Split on hyphen + g_free(last_item); + + gchar * position_str = NULL; + for (guint i = 0; i < g_strv_length(hyphen_split); i++) { // Iterate over each item + gchar * pos_str = hyphen_split[i]; + + if (!g_regex_match(num_pat, pos_str, 0, NULL)) { // Is not a number + continue; + } + + position_str = g_strdup(pos_str); + break; + } + + g_strfreev(hyphen_split); + + guint64 position = 0; + + if (position_str != NULL) { // If we have a string defined + if (g_regex_match(num_pat, position_str, 0, NULL)) { // Matches being a number + position = g_ascii_strtoull(position_str, NULL, 10); // Attempt to convert + } + + g_free(position_str); + } + + g_free(num_pat); + return position; +} + +gint koto_track_helpers_sort_tracks( + gconstpointer track1, + gconstpointer track2, + gpointer user_data +) { + (void) user_data; + KotoTrack * track1_real = (KotoTrack*) track1; + KotoTrack * track2_real = (KotoTrack*) track2; + + if (!KOTO_IS_TRACK(track1_real) && !KOTO_IS_TRACK(track2_real)) { // Neither tracks actually exist + return 0; + } else if (KOTO_IS_TRACK(track1_real) && !KOTO_IS_TRACK(track2_real)) { // Only track2 does not exist + return -1; + } else if (!KOTO_IS_TRACK(track1_real) && KOTO_IS_TRACK(track2_real)) { // Only track1 does not exist + return 1; + } + + guint track1_disc = koto_track_get_disc_number(track1_real); + guint track2_disc = koto_track_get_disc_number(track2_real); + + if (track1_disc < track2_disc) { // Track 2 is in a later CD / Disc + return -1; + } else if (track1_disc > track2_disc) { // Track1 is later + return 1; + } + + guint track1_pos = koto_track_get_position(track1_real); + guint track2_pos = koto_track_get_position(track2_real); + + if (track1_pos == track2_pos) { // Identical positions (like reported as 0) + return g_utf8_collate(koto_track_get_name(track1_real), koto_track_get_name(track2_real)); + } else if (track1_pos < track2_pos) { + return -1; + } else { + return 1; + } +} + +gint koto_track_helpers_sort_track_items( + gconstpointer track1_item, + gconstpointer track2_item, + gpointer user_data +) { + KotoTrack * track1 = koto_track_item_get_track((KotoTrackItem*) track1_item); + KotoTrack * track2 = koto_track_item_get_track((KotoTrackItem*) track2_item); + return koto_track_helpers_sort_tracks(track1, track2, user_data); +} + +gint koto_track_helpers_sort_tracks_by_uuid( + gconstpointer track1_uuid, + gconstpointer track2_uuid, + gpointer user_data +) { + KotoTrack * track1 = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) track1_uuid); + KotoTrack * track2 = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) track2_uuid); + return koto_track_helpers_sort_tracks(track1, track2, user_data); +} \ No newline at end of file diff --git a/src/indexer/track-helpers.h b/src/indexer/track-helpers.h new file mode 100644 index 0000000..e918766 --- /dev/null +++ b/src/indexer/track-helpers.h @@ -0,0 +1,49 @@ +/* track-helpers.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 + +void koto_track_helpers_init(); + +guint64 koto_track_helpers_get_cd_based_on_file_name(const gchar * file_name); + +gchar * koto_track_helpers_get_corrected_genre(gchar * original_genre); + +gchar * koto_track_helpers_get_name_for_file( + const gchar * path, + gchar * optional_artist_name +); + +guint64 koto_track_helpers_get_position_based_on_file_name(const gchar * file_name); + +gint koto_track_helpers_sort_track_items( + gconstpointer track1_item, + gconstpointer track2_item, + gpointer user_data +); + +gint koto_track_helpers_sort_tracks( + gconstpointer track1, + gconstpointer track2, + gpointer user_data +); + +gint koto_track_helpers_sort_tracks_by_uuid( + gconstpointer track1_uuid, + gconstpointer track2_uuid, + gpointer user_data +); \ No newline at end of file diff --git a/src/indexer/track.c b/src/indexer/track.c new file mode 100644 index 0000000..f5eb42b --- /dev/null +++ b/src/indexer/track.c @@ -0,0 +1,879 @@ +/* track.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/db.h" +#include "../db/cartographer.h" +#include "structs.h" +#include "track-helpers.h" +#include "koto-utils.h" + +extern KotoCartographer * koto_maps; +extern sqlite3 * koto_db; + +struct _KotoTrack { + GObject parent_instance; + gchar * artist_uuid; + gchar * album_uuid; + gchar * uuid; + + GHashTable * paths; + + gchar * parsed_name; + guint cd; + guint64 position; + guint64 duration; + gchar * description; + gchar * narrator; + guint64 playback_position; + guint64 year; + + GList * genres; + + gboolean do_initial_index; +}; + +G_DEFINE_TYPE(KotoTrack, koto_track, G_TYPE_OBJECT); + +enum { + PROP_0, + PROP_ARTIST_UUID, + PROP_ALBUM_UUID, + PROP_UUID, + PROP_DO_INITIAL_INDEX, + PROP_PARSED_NAME, + PROP_CD, + PROP_POSITION, + PROP_DURATION, + PROP_DESCRIPTION, + PROP_NARRATOR, + PROP_YEAR, + PROP_PLAYBACK_POSITION, + PROP_PREPARSED_GENRES, + N_PROPERTIES +}; + +static GParamSpec * props[N_PROPERTIES] = { + NULL +}; + +static void koto_track_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +); + +static void koto_track_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +); + +static void koto_track_class_init(KotoTrackClass * c) { + GObjectClass * gobject_class; + + gobject_class = G_OBJECT_CLASS(c); + gobject_class->set_property = koto_track_set_property; + gobject_class->get_property = koto_track_get_property; + + props[PROP_ARTIST_UUID] = g_param_spec_string( + "artist-uuid", + "UUID to Artist associated with the File", + "UUID to Artist associated with the File", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_ALBUM_UUID] = g_param_spec_string( + "album-uuid", + "UUID to Album associated with the File", + "UUID to Album associated with the File", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_UUID] = g_param_spec_string( + "uuid", + "UUID to File in database", + "UUID to File in database", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_DO_INITIAL_INDEX] = g_param_spec_boolean( + "do-initial-index", + "Do an initial indexing operating instead of pulling from the database", + "Do an initial indexing operating instead of pulling from the database", + FALSE, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_PARSED_NAME] = g_param_spec_string( + "parsed-name", + "Parsed Name of File", + "Parsed Name of File", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_CD] = g_param_spec_uint( + "cd", + "CD the Track belongs to", + "CD the Track belongs to", + 0, + G_MAXUINT16, + 1, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_POSITION] = g_param_spec_uint64( + "position", + "Position in Audiobook, Album, etc.", + "Position in Audiobook, Album, etc.", + 0, + G_MAXUINT64, + 0, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_DURATION] = g_param_spec_uint64( + "duration", + "Duration of Track", + "Duration of Track", + 0, + G_MAXUINT64, + 0, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_DESCRIPTION] = g_param_spec_string( + "description", + "Description of Track, typically show notes for a podcast episode", + "Description of Track, typically show notes for a podcast episode", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_NARRATOR] = g_param_spec_string( + "narrator", + "Narrator, typically of an Audiobook", + "Narrator, typically of an Audiobook", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_YEAR] = g_param_spec_uint64( + "year", + "Year", + "Year", + 0, + G_MAXUINT64, + 0, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_PLAYBACK_POSITION] = g_param_spec_uint64( + "playback-position", + "Current playback position", + "Current playback position", + 0, + G_MAXUINT64, + 0, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_PREPARSED_GENRES] = g_param_spec_string( + "preparsed-genres", + "Preparsed Genres", + "Preparsed Genres", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE + ); + + g_object_class_install_properties(gobject_class, N_PROPERTIES, props); +} + +static void koto_track_init(KotoTrack * self) { + self->description = NULL; // Initialize our description + self->duration = 0; // Initialize our duration + self->genres = NULL; // Initialize our genres list + self->narrator = NULL, // Initialize our narrator + self->paths = g_hash_table_new(g_str_hash, g_str_equal); // Create our hash table of paths + self->position = 0; // Initialize our duration + self->year = 0; // Initialize our year +} + +static void koto_track_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +) { + KotoTrack * self = KOTO_TRACK(obj); + + switch (prop_id) { + case PROP_ARTIST_UUID: + g_value_set_string(val, self->artist_uuid); + break; + case PROP_ALBUM_UUID: + g_value_set_string(val, self->album_uuid); + break; + case PROP_UUID: + g_value_set_string(val, self->uuid); + break; + case PROP_PARSED_NAME: + g_value_set_string(val, self->parsed_name); + break; + case PROP_CD: + g_value_set_uint(val, self->cd); + break; + case PROP_DESCRIPTION: + g_value_set_string(val, self->description); + break; + case PROP_NARRATOR: + g_value_set_string(val, self->narrator); + break; + case PROP_YEAR: + g_value_set_uint64(val, self->year); + break; + case PROP_POSITION: + g_value_set_uint64(val, self->position); + break; + case PROP_PLAYBACK_POSITION: + g_value_set_uint64(val, koto_track_get_playback_position(self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_track_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +) { + KotoTrack * self = KOTO_TRACK(obj); + + switch (prop_id) { + case PROP_ARTIST_UUID: + self->artist_uuid = g_strdup(g_value_get_string(val)); + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_ARTIST_UUID]); + break; + case PROP_ALBUM_UUID: + koto_track_set_album_uuid(self, g_value_get_string(val)); + break; + case PROP_UUID: + self->uuid = g_strdup(g_value_get_string(val)); + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_UUID]); + break; + case PROP_DO_INITIAL_INDEX: + self->do_initial_index = g_value_get_boolean(val); + break; + case PROP_PARSED_NAME: + koto_track_set_parsed_name(self, g_strdup(g_value_get_string(val))); + break; + case PROP_CD: + koto_track_set_cd(self, g_value_get_uint(val)); + break; + case PROP_POSITION: + koto_track_set_position(self, g_value_get_uint64(val)); + break; + case PROP_PLAYBACK_POSITION: + koto_track_set_playback_position(self, g_value_get_uint64(val)); + break; + case PROP_DURATION: + koto_track_set_duration(self, g_value_get_uint64(val)); + break; + case PROP_DESCRIPTION: + koto_track_set_description(self, g_value_get_string(val)); + break; + case PROP_NARRATOR: + koto_track_set_narrator(self, g_strdup(g_value_get_string(val))); + break; + case PROP_YEAR: + koto_track_set_year(self, g_value_get_uint64(val)); + break; + case PROP_PREPARSED_GENRES: + koto_track_set_preparsed_genres(self, g_strdup(g_value_get_string(val))); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +void koto_track_commit(KotoTrack * self) { + if (!KOTO_IS_TRACK(self)) { + return; + } + + if (!koto_utils_string_is_valid(self->artist_uuid)) { // No valid required artist UUID + return; + } + + gchar * commit_msg = "INSERT INTO tracks(id, artist_id, album_id, name, disc, position, duration, genres)" \ + "VALUES('%s', '%s', '%s', quote(\"%s\"), %d, %d, %d, '%s')" \ + "ON CONFLICT(id) DO UPDATE SET album_id=excluded.album_id, artist_id=excluded.artist_id, name=excluded.name, disc=excluded.disc, position=excluded.position, duration=excluded.duration, genres=excluded.genres;"; + + // Combine our list items into a semi-colon separated string + gchar * genres = koto_utils_join_string_list(self->genres, ";"); // Join our GList of strings into a single + + gchar * commit_op = g_strdup_printf( + commit_msg, + self->uuid, + self->artist_uuid, + koto_utils_string_get_valid(self->album_uuid), + g_strescape(self->parsed_name, NULL), + (int) self->cd, + (int) self->position, + (int) self->duration, + genres + ); + + if (new_transaction(commit_op, "Failed to write our file to the database", FALSE) != SQLITE_OK) { + return; + } + + g_free(genres); // Free the genres string + + GHashTableIter paths_iter; + g_hash_table_iter_init(&paths_iter, self->paths); // Create an iterator for our paths + gpointer lib_uuid_ptr, track_rel_path_ptr; + while (g_hash_table_iter_next(&paths_iter, &lib_uuid_ptr, &track_rel_path_ptr)) { + gchar * lib_uuid = lib_uuid_ptr; + gchar * track_rel_path = track_rel_path_ptr; + + gchar * commit_op = g_strdup_printf( + "INSERT INTO libraries_tracks(id, track_id, path)" + "VALUES ('%s', '%s', quote(\"%s\"))" + "ON CONFLICT(id, track_id) DO UPDATE SET path=excluded.path;", + lib_uuid, + self->uuid, + track_rel_path + ); + + new_transaction(commit_op, "Failed to add this path for the track", FALSE); + } +} + +gchar * koto_track_get_description(KotoTrack * self) { + return KOTO_IS_TRACK(self) ? g_strdup(self->description) : NULL; +} + +guint koto_track_get_disc_number(KotoTrack * self) { + return KOTO_IS_TRACK(self) ? self->cd : 1; +} + +guint64 koto_track_get_duration(KotoTrack * self) { + return KOTO_IS_TRACK(self) ? self->duration : 0; +} + +GList * koto_track_get_genres(KotoTrack * self) { + return KOTO_IS_TRACK(self) ? self->genres : NULL; +} + +GVariant * koto_track_get_metadata_vardict(KotoTrack * self) { + if (!KOTO_IS_TRACK(self)) { + return NULL; + } + + GVariantBuilder * builder = g_variant_builder_new(G_VARIANT_TYPE_VARDICT); + KotoArtist * artist = koto_cartographer_get_artist_by_uuid(koto_maps, self->artist_uuid); + gchar * artist_name = koto_artist_get_name(artist); + + if (koto_utils_string_is_valid(self->album_uuid)) { // Have an album associated + KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, self->album_uuid); + + if (KOTO_IS_ALBUM(album)) { + gchar * album_art_path = koto_album_get_art(album); + gchar * album_name = koto_album_get_name(album); + + if (koto_utils_string_is_valid(album_art_path)) { // Valid album art path + album_art_path = g_strconcat("file://", album_art_path, NULL); // Prepend with file:// + g_variant_builder_add(builder, "{sv}", "mpris:artUrl", g_variant_new_string(album_art_path)); + } + + g_variant_builder_add(builder, "{sv}", "xesam:album", g_variant_new_string(album_name)); + } + } else { + } // TODO: Implement artist artwork fetching here + + g_variant_builder_add(builder, "{sv}", "mpris:trackid", g_variant_new_string(self->uuid)); + + if (koto_utils_string_is_valid(artist_name)) { // Valid artist name + GVariant * artist_name_variant; + GVariantBuilder * artist_list_builder = g_variant_builder_new(G_VARIANT_TYPE("as")); + g_variant_builder_add(artist_list_builder, "s", artist_name); + artist_name_variant = g_variant_new("as", artist_list_builder); + g_variant_builder_unref(artist_list_builder); + + g_variant_builder_add(builder, "{sv}", "xesam:artist", artist_name_variant); + g_variant_builder_add(builder, "{sv}", "playbackengine:artist", g_variant_new_string(artist_name)); // Add a sort of "meta" string val for our playback engine so we don't need to mess about with the array + } + + g_variant_builder_add(builder, "{sv}", "xesam:discNumber", g_variant_new_uint64(self->cd)); + g_variant_builder_add(builder, "{sv}", "xesam:title", g_variant_new_string(self->parsed_name)); + g_variant_builder_add(builder, "{sv}", "xesam:url", g_variant_new_string(koto_track_get_path(self))); + g_variant_builder_add(builder, "{sv}", "xesam:trackNumber", g_variant_new_uint64(self->position)); + + GVariant * metadata_ret = g_variant_builder_end(builder); + + return metadata_ret; +} + +gchar * koto_track_get_name(KotoTrack * self) { + return KOTO_IS_TRACK(self) ? g_strdup(self->parsed_name) : NULL; +} + +gchar * koto_track_get_narrator(KotoTrack * self) { + return KOTO_IS_TRACK(self) ? g_strdup(self->narrator) : NULL; +} + +gchar * koto_track_get_path(KotoTrack * self) { + if (!KOTO_IS_TRACK(self) || (KOTO_IS_TRACK(self) && (g_list_length(g_hash_table_get_keys(self->paths)) == 0))) { // If this is not a track or is but we have no paths associated with it + return NULL; + } + + GHashTableIter iter; + + g_hash_table_iter_init(&iter, self->paths); // Create an iterator for our paths + gpointer uuidptr; + gpointer relpathptr; + + gchar * path = NULL; + + while (g_hash_table_iter_next(&iter, &uuidptr, &relpathptr)) { // Iterate over all the paths for this file + KotoLibrary * library = koto_cartographer_get_library_by_uuid(koto_maps, (gchar*) uuidptr); + + if (KOTO_IS_LIBRARY(library)) { + path = g_strdup(g_build_path(G_DIR_SEPARATOR_S, koto_library_get_path(library), koto_library_get_relative_path_to_file(library, (gchar*) relpathptr), NULL)); // Build our full library path using library's path and our file relative path + break; + } + } + + return path; +} + +guint64 koto_track_get_playback_position(KotoTrack * self) { + return KOTO_IS_TRACK(self) ? self->playback_position : 0; +} + +guint64 koto_track_get_position(KotoTrack * self) { + return KOTO_IS_TRACK(self) ? self->position : 0; +} + +gchar * koto_track_get_uniqueish_key(KotoTrack * self) { + KotoArtist * artist = koto_cartographer_get_artist_by_uuid(koto_maps, self->artist_uuid); // Get the artist associated with this track + + if (!KOTO_IS_ARTIST(artist)) { // Don't have an artist + return g_strdup(self->parsed_name); // Just return the name of the file, which is very likely not unique + } + + gchar * artist_name = koto_artist_get_name(artist); // Get the artist name + + if (koto_utils_string_is_valid(self->album_uuid)) { // If we have an album associated with this track (not necessarily guaranteed) + KotoAlbum * possible_album = koto_cartographer_get_album_by_uuid(koto_maps, self->album_uuid); + + if (KOTO_IS_ALBUM(possible_album)) { // Album exists + gchar * album_name = koto_album_get_name(possible_album); // Get the name of the album + + if (koto_utils_string_is_valid(album_name)) { + return g_strdup_printf("%s-%s-%s", artist_name, album_name, self->parsed_name); // Create a key of (ARTIST/WRITER)-(ALBUM/AUDIOBOOK)-(CHAPTER/TRACK) + } + } + } + + return g_strdup_printf("%s-%s", artist_name, self->parsed_name); // Create a key of just (ARTIST/WRITER)-(CHAPTER/TRACK) +} + +gchar * koto_track_get_uuid(KotoTrack * self) { + if (!KOTO_IS_TRACK(self)) { + return NULL; + } + + return self->uuid; // Do not return a duplicate since otherwise comparison refs fail due to pointer positions being different +} + +guint64 koto_track_get_year(KotoTrack * self) { + if (!KOTO_IS_TRACK(self)) { + return 0; + } + + return self->year; +} + +void koto_track_remove_from_playlist( + KotoTrack * self, + gchar * playlist_uuid +) { + if (!KOTO_IS_TRACK(self)) { + return; + } + + gchar * commit_op = g_strdup_printf( + "DELETE FROM playlist_tracks WHERE track_id='%s' AND playlist_id='%s'", + self->uuid, + playlist_uuid + ); + + new_transaction(commit_op, "Failed to remove track from playlist", FALSE); +} + +void koto_track_set_album_uuid( + KotoTrack * self, + const gchar * album_uuid +) { + if (!KOTO_IS_TRACK(self)) { + return; + } + + if (album_uuid == NULL) { + return; + } + + gchar * uuid = g_strdup(album_uuid); + + if (!koto_utils_string_is_valid(uuid)) { // If this is not a valid string + return; + } + + self->album_uuid = uuid; + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_ALBUM_UUID]); +} + +void koto_track_save_to_playlist( + KotoTrack * self, + gchar * playlist_uuid +) { + if (!KOTO_IS_TRACK(self)) { + return; + } + + gchar * commit_op = g_strdup_printf( + "INSERT INTO playlist_tracks(playlist_id, track_id)" + "VALUES('%s', '%s')", + playlist_uuid, + self->uuid + ); + + new_transaction(commit_op, "Failed to save track to playlist", FALSE); +} + +void koto_track_set_cd( + KotoTrack * self, + guint cd +) { + if (!KOTO_IS_TRACK(self)) { + return; + } + + if (cd == 0) { // No change really + return; + } + + self->cd = cd; + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_CD]); +} + +void koto_track_set_description( + KotoTrack * self, + const gchar * description +) { + if (!KOTO_IS_TRACK(self)) { + return; + } + + if (!koto_utils_string_is_valid(description)) { + return; + } + + if (g_strcmp0(self->description, description) == 0) { // Same description + return; + } + + if (koto_utils_string_is_valid(self->description)) { + g_free(self->description); // Free the existing narrator + } + + self->description = g_strdup(description); // Duplicate our description + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_DESCRIPTION]); +} + +void koto_track_set_duration( + KotoTrack * self, + guint64 duration +) { + if (!KOTO_IS_TRACK(self)) { + return; + } + + if (duration == 0) { + return; + } + + self->duration = duration; + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_DURATION]); +} + +void koto_track_set_genres( + KotoTrack * self, + char * genrelist +) { + if (!KOTO_IS_TRACK(self)) { + return; + } + + if (!koto_utils_string_is_valid((gchar*) genrelist)) { // If it is an empty string + return; + } + + gchar ** genres = g_strsplit(genrelist, ";", -1); // Split on semicolons for each genre, e.g. Electronic;Rock + guint len = g_strv_length(genres); + + if (len == 0) { // No genres + g_strfreev(genres); // Free the list + return; + } + + for (guint i = 0; i < len; i++) { // Iterate over each item + gchar * genre = genres[i]; // Get the genre + gchar * lowercased_genre = g_utf8_strdown(g_strstrip(genre), -1); // Lowercase the genre + + gchar * lowercased_hyphenated_genre = koto_utils_string_replace_all(lowercased_genre, " ", "-"); + g_free(lowercased_genre); // Free the lowercase genre string since we no longer need it + + gchar * corrected_genre = koto_track_helpers_get_corrected_genre(lowercased_hyphenated_genre); // Get any corrected genre + + if (g_list_index(self->genres, corrected_genre) == -1) { // Don't have this genre added + self->genres = g_list_append(self->genres, g_strdup(corrected_genre)); // Add the genre to our list + } + + g_free(lowercased_hyphenated_genre); // Free our remaining string + } + + g_strfreev(genres); // Free the list of genres locally +} + +void koto_track_set_narrator( + KotoTrack * self, + const gchar * narrator +) { + if (!KOTO_IS_TRACK(self)) { + return; + } + + if (!koto_utils_string_is_valid(narrator)) { + return; + } + + if (g_strcmp0(self->narrator, narrator) == 0) { // Same narrator + return; + } + + if (koto_utils_string_is_valid(self->narrator)) { + g_free(self->narrator); // Free the existing narrator + } + + self->narrator = g_strdup(narrator); // Duplicate our narrator + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_NARRATOR]); +} + +void koto_track_set_parsed_name( + KotoTrack * self, + gchar * new_parsed_name +) { + if (!KOTO_IS_TRACK(self)) { + return; + } + + if (!koto_utils_string_is_valid(new_parsed_name)) { + return; + } + + gboolean have_existing_name = koto_utils_string_is_valid(self->parsed_name); + + if (have_existing_name && (strcmp(self->parsed_name, new_parsed_name) == 0)) { // Have existing name that matches one provided + return; // Don't do anything + } + + if (have_existing_name) { + g_free(self->parsed_name); + } + + self->parsed_name = g_strdup(new_parsed_name); + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_PARSED_NAME]); +} + +void koto_track_set_path( + KotoTrack * self, + KotoLibrary * lib, + gchar * fixed_path +) { + if (!KOTO_IS_TRACK(self)) { + return; + } + + if (!koto_utils_string_is_valid(fixed_path)) { // Not a valid path + return; + } + + gchar * path = g_strdup(fixed_path); // Duplicate our fixed_path + gchar * relative_path = koto_library_get_relative_path_to_file(lib, path); // Get the relative path to the file for the given library + + gchar * library_uuid = koto_library_get_uuid(lib); // Get the library for this path + g_hash_table_replace(self->paths, library_uuid, relative_path); // Replace any existing value or add this one + + if (self->do_initial_index) { + koto_track_update_metadata(self); // Attempt to get ID3 info + } +} + +void koto_track_set_playback_position( + KotoTrack * self, + guint64 position +) { + if (!KOTO_IS_TRACK(self)) { // Not a track + return; + } + + self->playback_position = position; +} + +void koto_track_set_position( + KotoTrack * self, + guint64 pos +) { + if (!KOTO_IS_TRACK(self)) { // Not a track + return; + } + + if (pos == 0) { // No position change really + return; + } + + self->position = pos; + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_POSITION]); +} + +void koto_track_set_year( + KotoTrack * self, + guint64 year +) { + if (!KOTO_IS_TRACK(self)) { + return; + } + + self->year = year; + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_YEAR]); +} + +void koto_track_update_metadata(KotoTrack * self) { + if (!KOTO_IS_TRACK(self)) { // Not a track + return; + } + + gchar * optimal_track_path = koto_track_get_path(self); // Check all the libraries associated with this track, based on priority, return a built path using lib path + relative file path + + if (!koto_utils_string_is_valid(optimal_track_path)) { // Not a valid string + return; + } + + TagLib_File * t_file = taglib_file_new(optimal_track_path); // Get a taglib file for this file + + if ((t_file != NULL) && taglib_file_is_valid(t_file)) { // If we got the taglib file and it is valid + TagLib_Tag * tag = taglib_file_tag(t_file); // Get our tag + koto_track_set_genres(self, taglib_tag_genre(tag)); // Set our genres to any genres listed for the track + koto_track_set_position(self, (uint) taglib_tag_track(tag)); // Get the track, convert to uint and cast as a pointer + koto_track_set_year(self, (guint64) taglib_tag_year(tag)); // Get the track year and convert it to guint64 + const TagLib_AudioProperties * tag_props = taglib_file_audioproperties(t_file); // Get the audio properties of the file + koto_track_set_duration(self, taglib_audioproperties_length(tag_props)); // Get the length of the track and set it as our duration + } + + if (self->position == 0) { // Failed to get tag info or got the tag info but position is zero + guint64 position = koto_track_helpers_get_position_based_on_file_name(g_path_get_basename(optimal_track_path)); // Get the likely position + koto_track_set_position(self, position); // Set our position + } + + taglib_tag_free_strings(); // Free strings + taglib_file_free(t_file); // Free the file + g_free(optimal_track_path); +} + +void koto_track_set_preparsed_genres( + KotoTrack * self, + gchar * genrelist +) { + if (!KOTO_IS_TRACK(self)) { + return; + } + + if (!koto_utils_string_is_valid(genrelist)) { // If it is an empty string + return; + } + + GList * preparsed_genres_list = koto_utils_string_to_string_list(genrelist, ";"); + + if (g_list_length(preparsed_genres_list) == 0) { // No genres + g_list_free(preparsed_genres_list); + return; + } + + // TODO: Do a pass on in first memory optimization phase to ensure string elements are freed. + g_list_free_full(self->genres, NULL); // Free the existing genres list + self->genres = preparsed_genres_list; +} + +KotoTrack * koto_track_new( + const gchar * artist_uuid, + const gchar * album_uuid, + const gchar * parsed_name, + guint cd +) { + KotoTrack * track = g_object_new( + KOTO_TYPE_TRACK, + "artist-uuid", + artist_uuid, + "album-uuid", + album_uuid, + "do-initial-index", + TRUE, + "uuid", + g_uuid_string_random(), + "cd", + cd, + "parsed-name", + parsed_name, + NULL + ); + + return track; +} + +KotoTrack * koto_track_new_with_uuid(const gchar * uuid) { + return g_object_new( + KOTO_TYPE_TRACK, + "uuid", + g_strdup(uuid), + NULL + ); +} diff --git a/src/koto-dialog-container.c b/src/koto-dialog-container.c new file mode 100644 index 0000000..fb7c2ac --- /dev/null +++ b/src/koto-dialog-container.c @@ -0,0 +1,111 @@ +/* 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 "components/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/koto-dialog-container.h b/src/koto-dialog-container.h new file mode 100644 index 0000000..886be2c --- /dev/null +++ b/src/koto-dialog-container.h @@ -0,0 +1,59 @@ +/* koto-dialog-container.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_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)) + +/** + * Functions + **/ + +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 new file mode 100644 index 0000000..f95af87 --- /dev/null +++ b/src/koto-expander.c @@ -0,0 +1,323 @@ +/* koto-expander.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/button.h" +#include "config/config.h" +#include "koto-expander.h" +#include "koto-utils.h" + +enum { + PROP_EXP_0, + PROP_HEADER_ICON_NAME, + PROP_HEADER_LABEL, + PROP_HEADER_SECONDARY_BUTTON, + PROP_CONTENT, + N_EXP_PROPERTIES +}; + +static GParamSpec * expander_props[N_EXP_PROPERTIES] = { + NULL, +}; + +struct _KotoExpander { + GtkBox parent_instance; + gboolean constructed; + GtkWidget * header; + KotoButton * header_button; + + gchar * icon_name; + gchar * label; + + KotoButton * header_secondary_button; + KotoButton * header_expand_button; + + GtkWidget * revealer; + GtkWidget * content; +}; + +struct _KotoExpanderClass { + GtkBoxClass parent_class; +}; + +G_DEFINE_TYPE(KotoExpander, koto_expander, GTK_TYPE_BOX); + +static void koto_expander_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +); + +static void koto_expander_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +); + +static void koto_expander_class_init(KotoExpanderClass * c) { + GObjectClass * gobject_class = G_OBJECT_CLASS(c); + + gobject_class->set_property = koto_expander_set_property; + gobject_class->get_property = koto_expander_get_property; + + expander_props[PROP_HEADER_ICON_NAME] = g_param_spec_string( + "icon-name", + "Icon Name", + "Name of the icon to use in the Expander", + "emblem-favorite-symbolic", + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + expander_props[PROP_HEADER_LABEL] = g_param_spec_string( + "label", + "Label", + "Label for the Expander", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + expander_props[PROP_HEADER_SECONDARY_BUTTON] = g_param_spec_object( + "secondary-button", + "Secondary Button", + "Secondary Button to be placed next to Expander button", + KOTO_TYPE_BUTTON, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + expander_props[PROP_CONTENT] = g_param_spec_object( + "content", + "Content", + "Content inside the Expander", + GTK_TYPE_WIDGET, + G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + g_object_class_install_properties(gobject_class, N_EXP_PROPERTIES, expander_props); +} + +static void koto_expander_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +) { + KotoExpander * self = KOTO_EXPANDER(obj); + + switch (prop_id) { + case PROP_HEADER_ICON_NAME: + g_value_set_string(val, self->icon_name); + break; + case PROP_HEADER_LABEL: + g_value_set_string(val, self->label); + break; + case PROP_HEADER_SECONDARY_BUTTON: + g_value_set_object(val, (GObject*) self->header_secondary_button); + break; + case PROP_CONTENT: + g_value_set_object(val, (GObject*) self->content); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_expander_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +) { + KotoExpander * self = KOTO_EXPANDER(obj); + + switch (prop_id) { + case PROP_HEADER_ICON_NAME: + koto_expander_set_icon_name(self, g_strdup(g_value_get_string(val))); + break; + case PROP_HEADER_LABEL: + g_return_if_fail(GTK_IS_WIDGET(self->header_button)); + koto_button_set_text(self->header_button, g_strdup(g_value_get_string(val))); + break; + case PROP_HEADER_SECONDARY_BUTTON: + koto_expander_set_secondary_button(self, (KotoButton*) g_value_get_object(val)); + break; + case PROP_CONTENT: + koto_expander_set_content(self, (GtkWidget*) g_value_get_object(val)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_expander_init(KotoExpander * self) { + GtkStyleContext * style = gtk_widget_get_style_context(GTK_WIDGET(self)); + + gtk_style_context_add_class(style, "expander"); + gtk_widget_set_hexpand((GTK_WIDGET(self)), TRUE); + + self->header = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); + + GtkStyleContext * header_style = gtk_widget_get_style_context(self->header); + + gtk_style_context_add_class(header_style, "expander-header"); + + self->revealer = gtk_revealer_new(); + gtk_revealer_set_reveal_child(GTK_REVEALER(self->revealer), TRUE); // Set to be revealed by default + gtk_revealer_set_transition_type(GTK_REVEALER(self->revealer), GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN); + + KotoButton * new_button = koto_button_new_with_icon(NULL, "emblem-favorite-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_SMALL); + + if (KOTO_IS_BUTTON(new_button)) { // Created our widget successfully + self->header_button = new_button; + gtk_widget_set_hexpand(GTK_WIDGET(self->header_button), TRUE); + gtk_box_prepend(GTK_BOX(self->header), GTK_WIDGET(self->header_button)); + } + + self->header_expand_button = koto_button_new_with_icon("", "pan-down-symbolic", "pan-up-symbolic", KOTO_BUTTON_PIXBUF_SIZE_SMALL); + gtk_box_append(GTK_BOX(self->header), GTK_WIDGET(self->header_expand_button)); + + gtk_box_prepend(GTK_BOX(self), self->header); + gtk_box_append(GTK_BOX(self), self->revealer); + + self->constructed = TRUE; + + koto_button_add_click_handler(self->header_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_expander_toggle_content), self); + koto_button_add_click_handler(self->header_expand_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_expander_toggle_content), self); +} + +// koto_expander_set_icon_name will set the icon for our inner KotoButton for this Expander +void koto_expander_set_icon_name( + KotoExpander * self, + gchar * icon +) { + if (!KOTO_IS_EXPANDER(self)) { // Not a KotoExpander + return; + } + + if (!koto_utils_string_is_valid(icon)) { // Not a valid string + return; + } + + if (!KOTO_IS_BUTTON(self->header_button)) { // Don't have a header button for whatever reason + return; + } + + koto_button_set_icon_name(self->header_button, icon, FALSE); +} + +void koto_expander_set_secondary_button( + KotoExpander * self, + KotoButton * new_button +) { + if (!self->constructed) { + return; + } + + if (!GTK_IS_WIDGET(new_button)) { + return; + } + + if (GTK_IS_WIDGET(self->header_secondary_button)) { // Already have a button + gtk_box_remove(GTK_BOX(self->header), GTK_WIDGET(self->header_secondary_button)); + } + + self->header_secondary_button = new_button; + gtk_box_append(GTK_BOX(self->header), GTK_WIDGET(self->header_secondary_button)); + + g_object_notify_by_pspec(G_OBJECT(self), expander_props[PROP_HEADER_SECONDARY_BUTTON]); +} + +void koto_expander_set_content( + KotoExpander * self, + GtkWidget * new_content +) { + if (!self->constructed) { + return; + } + + if (GTK_IS_WIDGET(self->content)) { // Already have content + gtk_revealer_set_child(GTK_REVEALER(self->revealer), NULL); + g_object_unref(self->content); + } + + self->content = new_content; + gtk_revealer_set_child(GTK_REVEALER(self->revealer), self->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; + + koto_button_flip(KOTO_BUTTON(self->header_expand_button)); + GtkRevealer * rev = GTK_REVEALER(self->revealer); + + gtk_revealer_set_reveal_child(rev, !gtk_revealer_get_reveal_child(rev)); // Invert our values +} + +KotoExpander * koto_expander_new( + gchar * primary_icon_name, + gchar * primary_label_text +) { + return g_object_new( + KOTO_TYPE_EXPANDER, + "orientation", + GTK_ORIENTATION_VERTICAL, + "icon-name", + primary_icon_name, + "label", + primary_label_text, + NULL + ); +} + +KotoExpander * koto_expander_new_with_button( + gchar * primary_icon_name, + gchar * primary_label_text, + KotoButton * secondary_button +) { + return g_object_new( + KOTO_TYPE_EXPANDER, + "orientation", + GTK_ORIENTATION_VERTICAL, + "icon-name", + primary_icon_name, + "label", + primary_label_text, + "secondary-button", + secondary_button, + NULL + ); +} diff --git a/src/koto-expander.h b/src/koto-expander.h new file mode 100644 index 0000000..558ebcf --- /dev/null +++ b/src/koto-expander.h @@ -0,0 +1,66 @@ +/* koto-expander.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 "components/button.h" + +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, + gchar * icon +); + +void koto_expander_set_label( + KotoExpander * self, + const gchar * label +); + +void koto_expander_set_secondary_button( + KotoExpander * self, + KotoButton * new_button +); + +void koto_expander_set_content( + KotoExpander * self, + GtkWidget * new_content +); + +void koto_expander_toggle_content( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer data +); + +G_END_DECLS diff --git a/src/koto-headerbar.ui b/src/koto-headerbar.ui new file mode 100644 index 0000000..6bad467 --- /dev/null +++ b/src/koto-headerbar.ui @@ -0,0 +1,40 @@ +?xml version="1.0" encoding="UTF-8"?> + + + + + False + audio-headphones + 3 + + + diff --git a/src/koto-nav.c b/src/koto-nav.c new file mode 100644 index 0000000..7c67785 --- /dev/null +++ b/src/koto-nav.c @@ -0,0 +1,318 @@ +/* koto-nav.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 "components/button.h" +#include "config/config.h" +#include "db/cartographer.h" +#include "indexer/structs.h" +#include "playlist/playlist.h" +#include "koto-expander.h" +#include "koto-nav.h" +#include "koto-utils.h" +#include "koto-window.h" + +extern KotoCartographer * koto_maps; +extern KotoWindow * main_window; + +struct _KotoNav { + GObject parent_instance; + GtkWidget * win; + GtkWidget * content; + + KotoButton * home_button; + KotoExpander * audiobook_expander; + KotoExpander * music_expander; + KotoExpander * podcast_expander; + KotoExpander * playlists_expander; + + // Audiobooks + + KotoButton * audiobooks_local; + KotoButton * audiobooks_audible; + KotoButton * audiobooks_librivox; + + // Music + + KotoButton * music_local; + KotoButton * music_radio; + + // Playlists + + GHashTable * playlist_buttons; + + // Podcasts + + KotoButton * podcasts_local; + KotoButton * podcasts_discover; +}; + +struct _KotoNavClass { + GObjectClass parent_class; +}; + +G_DEFINE_TYPE(KotoNav, koto_nav, G_TYPE_OBJECT); + +static void koto_nav_class_init(KotoNavClass * c) { + (void) 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); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(self->win), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_min_content_width(GTK_SCROLLED_WINDOW(self->win), 300); + gtk_scrolled_window_set_max_content_width(GTK_SCROLLED_WINDOW(self->win), 300); + gtk_scrolled_window_set_propagate_natural_height(GTK_SCROLLED_WINDOW(self->win), TRUE); + + self->content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); + + gtk_widget_add_css_class(self->win, "primary-nav"); + gtk_widget_set_vexpand(self->win, TRUE); + + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(self->win), self->content); + + KotoButton * h_button = koto_button_new_with_icon("Home", "user-home-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_SMALL); + + if (h_button != NULL) { + self->home_button = h_button; + gtk_box_append(GTK_BOX(self->content), GTK_WIDGET(self->home_button)); + } + + koto_nav_create_audiobooks_section(self); + koto_nav_create_music_section(self); + koto_nav_create_podcasts_section(self); + koto_nav_create_playlist_section(self); +} + +void koto_nav_create_audiobooks_section(KotoNav * self) { + KotoExpander * a_expander = koto_expander_new("ephy-bookmarks-symbolic", "Audiobooks"); + + self->audiobook_expander = a_expander; + gtk_box_append(GTK_BOX(self->content), GTK_WIDGET(self->audiobook_expander)); + + GtkWidget * new_content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + + koto_expander_set_content(a_expander, new_content); + + self->audiobooks_local = koto_button_new_plain("Library"); + koto_button_set_data(self->audiobooks_local, &"audiobooks.library"); + + self->audiobooks_audible = koto_button_new_plain("Audible"); + self->audiobooks_librivox = koto_button_new_plain("LibriVox"); + + gtk_box_append(GTK_BOX(new_content), GTK_WIDGET(self->audiobooks_local)); + gtk_box_append(GTK_BOX(new_content), GTK_WIDGET(self->audiobooks_audible)); + gtk_box_append(GTK_BOX(new_content), GTK_WIDGET(self->audiobooks_librivox)); + + koto_button_add_click_handler(self->audiobooks_local, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_button_global_page_nav_callback), self->audiobooks_local); + +} + +void koto_nav_create_music_section(KotoNav * self) { + KotoExpander * m_expander = koto_expander_new("emblem-music-symbolic", "Music"); + + self->music_expander = m_expander; + gtk_box_append(GTK_BOX(self->content), GTK_WIDGET(self->music_expander)); + + GtkWidget * new_content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + + self->music_local = koto_button_new_plain("Library"); + koto_button_set_data(self->music_local, &"music.library"); + + self->music_radio = koto_button_new_plain("Radio"); + + gtk_box_append(GTK_BOX(new_content), GTK_WIDGET(self->music_local)); + 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_button_global_page_nav_callback), self->music_local); +} + +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) { + KotoExpander * p_expander = koto_expander_new("microphone-sensitivity-high-symbolic", "Podcasts"); + + self->podcast_expander = p_expander; + gtk_box_append(GTK_BOX(self->content), GTK_WIDGET(self->podcast_expander)); + + GtkWidget * new_content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + + self->podcasts_local = koto_button_new_plain("Library"); + self->podcasts_discover = koto_button_new_plain("Find New Podcasts"); + + gtk_box_append(GTK_BOX(new_content), GTK_WIDGET(self->podcasts_local)); + gtk_box_append(GTK_BOX(new_content), GTK_WIDGET(self->podcasts_discover)); + + koto_expander_set_content(p_expander, new_content); +} + +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; + koto_window_show_dialog(main_window, "create-modify-playlist"); +} + +void koto_nav_handle_playlist_added( + KotoCartographer * carto, + KotoPlaylist * playlist, + gpointer user_data +) { + (void) carto; + if (!KOTO_IS_PLAYLIST(playlist)) { + return; + } + + if (koto_playlist_get_is_hidden(playlist)) { // Should be hidden from nav + return; + } + + KotoNav * self = user_data; + + if (!KOTO_IS_NAV(self)) { + return; + } + + 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 (koto_utils_string_is_valid(playlist_art_path)) { // 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)) { // Failed to create the playlist button + return; + } + + koto_button_set_data(playlist_button, playlist_uuid); // Set our data to the playlist UUID string + 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_button_global_page_nav_callback), playlist_button); + koto_window_handle_playlist_added(koto_maps, playlist, main_window); // TODO: MOVE THIS + g_signal_connect(playlist, "modified", G_CALLBACK(koto_nav_handle_playlist_modified), self); + } +} + +void koto_nav_handle_playlist_modified( + KotoPlaylist * playlist, + gpointer user_data +) { + if (!KOTO_IS_PLAYLIST(playlist)) { + return; + } + + KotoNav * self = user_data; + + if (!KOTO_IS_NAV(self)) { + return; + } + + gchar * playlist_uuid = koto_playlist_get_uuid(playlist); // Get the UUID for a playlist + + KotoButton * playlist_button = g_hash_table_lookup(self->playlist_buttons, playlist_uuid); + + if (!KOTO_IS_BUTTON(playlist_button)) { + return; + } + + gchar * artwork = koto_playlist_get_artwork(playlist); // Get the artwork + + if (koto_utils_string_is_valid(artwork)) { // Have valid artwork + koto_button_set_file_path(playlist_button, artwork); // Update the artwork path + } + + gchar * name = koto_playlist_get_name(playlist); // Get the name + + if (koto_utils_string_is_valid(name)) { // Have valid name + koto_button_set_text(playlist_button, name); // Update the button text + } +} + +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) { + return self->win; +} + +KotoNav * koto_nav_new(void) { + return g_object_new(KOTO_TYPE_NAV, NULL); +} diff --git a/src/koto-nav.h b/src/koto-nav.h new file mode 100644 index 0000000..6583eb4 --- /dev/null +++ b/src/koto-nav.h @@ -0,0 +1,65 @@ +/* koto-nav.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. + */ + +#pragma once +#include +#include "db/cartographer.h" +#include "indexer/structs.h" + +G_BEGIN_DECLS + +#define KOTO_TYPE_NAV (koto_nav_get_type()) + +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_modified( + KotoPlaylist * playlist, + gpointer user_data +); + +void koto_nav_handle_playlist_removed( + KotoCartographer * carto, + gchar * playlist_uuid, + gpointer user_data +); + +GtkWidget * koto_nav_get_nav(KotoNav * self); + +G_END_DECLS diff --git a/src/koto-paths.c b/src/koto-paths.c new file mode 100644 index 0000000..07eed76 --- /dev/null +++ b/src/koto-paths.c @@ -0,0 +1,46 @@ +/* koto-paths.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 +#include "koto-paths.h" +#include "koto-utils.h" + +gchar * koto_rev_dns; +gchar * koto_path_cache; +gchar * koto_path_config; + +gchar * koto_path_to_conf; +gchar * koto_path_to_db; + +void koto_paths_setup() { + koto_rev_dns = "com.github.joshstrobl.koto"; + gchar * user_cache_dir = g_strdup(g_get_user_data_dir()); + gchar * user_config_dir = g_strdup(g_get_user_config_dir()); + + koto_path_cache = g_build_path(G_DIR_SEPARATOR_S, user_cache_dir, koto_rev_dns, NULL); + koto_path_config = g_build_path(G_DIR_SEPARATOR_S, user_config_dir, koto_rev_dns, NULL); + koto_path_to_conf = g_build_filename(koto_path_config, "config.toml", NULL); + koto_path_to_db = g_build_filename( koto_path_cache, "db", NULL); + + koto_utils_mkdir(user_cache_dir); + koto_utils_mkdir(user_config_dir); + koto_utils_mkdir(koto_path_cache); + koto_utils_mkdir(koto_path_config); +} \ No newline at end of file diff --git a/src/koto-paths.h b/src/koto-paths.h new file mode 100644 index 0000000..a791c88 --- /dev/null +++ b/src/koto-paths.h @@ -0,0 +1,20 @@ +/* koto-paths.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 + +void koto_paths_setup(); \ No newline at end of file diff --git a/src/koto-playerbar.c b/src/koto-playerbar.c new file mode 100644 index 0000000..be1d5ce --- /dev/null +++ b/src/koto-playerbar.c @@ -0,0 +1,809 @@ +/* koto-playerbar.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/button.h" +#include "config/config.h" +#include "db/cartographer.h" +#include "playlist/add-remove-track-popover.h" +#include "playlist/current.h" +#include "playlist/playlist.h" +#include "playback/engine.h" +#include "config/config.h" +#include "koto-playerbar.h" +#include "koto-utils.h" + +extern KotoAddRemoveTrackPopover * koto_add_remove_track_popup; +extern KotoCartographer * koto_maps; +extern KotoConfig * config; +extern KotoCurrentPlaylist * current_playlist; +extern KotoPlaybackEngine * playback_engine; + +struct _KotoPlayerBar { + GObject parent_instance; + GtkWidget * main; + GtkWidget * controls; + + /* Sections */ + GtkWidget * playback_section; + GtkWidget * primary_controls_section; + GtkWidget * secondary_controls_section; + + /* Primary Buttons */ + KotoButton * back_button; + KotoButton * play_pause_button; + KotoButton * forward_button; + KotoButton * repeat_button; + KotoButton * shuffle_button; + KotoButton * playlist_button; + KotoButton * eq_button; + GtkWidget * volume_button; + + GtkWidget * progress_bar; + + /* Selected Playback Section */ + + GtkWidget * playback_details_section; + GtkWidget * artwork; + GtkWidget * playback_title; + GtkWidget * playback_album; + GtkWidget * playback_artist; + + /* Advanced Controls (Jump Back / Forwards, Speed) */ + GtkWidget * advanced_controls_section; + KotoButton * backwards_jump_button; + KotoButton * forwards_jump_button; + GtkWidget * playback_speed_input; + + /* Misc Widgets */ + GtkWidget * playback_position_label; + + gint64 last_recorded_duration; + + gboolean is_progressbar_seeking; +}; + +struct _KotoPlayerBarClass { + GObjectClass parent_class; +}; + +G_DEFINE_TYPE(KotoPlayerBar, koto_playerbar, G_TYPE_OBJECT); + +static void koto_playerbar_constructed(GObject * obj); + +static void koto_playerbar_class_init(KotoPlayerBarClass * c) { + GObjectClass * gobject_class; + + gobject_class = G_OBJECT_CLASS(c); + + gobject_class->constructed = koto_playerbar_constructed; +} + +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); + gtk_range_set_round_digits(GTK_RANGE(self->progress_bar), 1); + + GtkGesture * press_controller = gtk_gesture_click_new(); // Create a new GtkGestureLongPress + + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(press_controller), 1); // Set to left click + + 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); + + gtk_widget_add_controller(GTK_WIDGET(self->progress_bar), GTK_EVENT_CONTROLLER(press_controller)); + + g_signal_connect(GTK_RANGE(self->progress_bar), "value-changed", G_CALLBACK(koto_playerbar_handle_progressbar_value_changed), self); + + self->controls = gtk_center_box_new(); + gtk_center_box_set_baseline_position(GTK_CENTER_BOX(self->controls), GTK_BASELINE_POSITION_CENTER); + + 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); + 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)); + gtk_center_box_set_end_widget(GTK_CENTER_BOX(self->controls), GTK_WIDGET(self->secondary_controls_section)); + + koto_playerbar_create_playback_details(self); + koto_playerbar_create_primary_controls(self); + koto_playerbar_create_secondary_controls(self); + + gtk_box_prepend(GTK_BOX(self->main), self->progress_bar); + gtk_box_append(GTK_BOX(self->main), self->controls); + + gtk_widget_set_hexpand(GTK_WIDGET(self->main), TRUE); + + // Set up our volume and other playback state bits from our config + + koto_playerbar_apply_configuration_state(config, 0, self); + + // Set up the bindings + + g_signal_connect(config, "notify::playback-last-used-volume", G_CALLBACK(koto_playerbar_apply_configuration_state), self); // Bind onto the playback last used volume config option + g_signal_connect(playback_engine, "is-playing", G_CALLBACK(koto_playerbar_handle_is_playing), self); + g_signal_connect(playback_engine, "is-paused", G_CALLBACK(koto_playerbar_handle_is_paused), self); + g_signal_connect(playback_engine, "tick-duration", G_CALLBACK(koto_playerbar_handle_tick_duration), self); + g_signal_connect(playback_engine, "tick-track", G_CALLBACK(koto_playerbar_handle_tick_track), self); + g_signal_connect(playback_engine, "track-changed", G_CALLBACK(koto_playerbar_update_track_info), self); + g_signal_connect(playback_engine, "track-repeat-changed", G_CALLBACK(koto_playerbar_handle_track_repeat), self); + g_signal_connect(playback_engine, "track-shuffle-changed", G_CALLBACK(koto_playerbar_handle_track_shuffle), self); +} + +static void koto_playerbar_init(KotoPlayerBar * self) { + self->last_recorded_duration = 0; + self->is_progressbar_seeking = FALSE; +} + +KotoPlayerBar * koto_playerbar_new(void) { + return g_object_new(KOTO_TYPE_PLAYERBAR, NULL); +} + +void koto_playerbar_apply_configuration_state( + KotoConfig * config, + guint prop_id, + KotoPlayerBar * self +) { + (void) prop_id; + gdouble config_last_used_volume = 0; + g_object_get( + config, + "playback-last-used-volume", + &config_last_used_volume, + NULL + ); + + gtk_scale_button_set_value(GTK_SCALE_BUTTON(self->volume_button), config_last_used_volume * 100); +} + +void koto_playerbar_create_playback_details(KotoPlayerBar * self) { + self->playback_details_section = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + + 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->main)); + GtkIconPaintable * audio_paintable = gtk_icon_theme_lookup_icon(default_icon_theme, "audio-x-generic-symbolic", NULL, 96, scale_factor, GTK_TEXT_DIR_NONE, GTK_ICON_LOOKUP_PRELOAD); + + if (GTK_IS_ICON_PAINTABLE(audio_paintable)) { + if (GTK_IS_IMAGE(self->artwork)) { + gtk_image_set_from_paintable(GTK_IMAGE(self->artwork), GDK_PAINTABLE(audio_paintable)); + } else { // Not an image + self->artwork = gtk_image_new_from_paintable(GDK_PAINTABLE(audio_paintable)); + gtk_widget_add_css_class(self->artwork, "circular"); + gtk_widget_set_size_request(self->artwork, 96, 96); + gtk_box_append(GTK_BOX(self->playback_section), self->artwork); + } + } + } + + self->playback_title = gtk_label_new("Title"); + gtk_label_set_xalign(GTK_LABEL(self->playback_title), 0); + + self->playback_album = gtk_label_new("Album"); + gtk_label_set_xalign(GTK_LABEL(self->playback_album), 0); + + self->playback_artist = gtk_label_new("Artist"); + gtk_label_set_xalign(GTK_LABEL(self->playback_artist), 0); + + gtk_box_append(GTK_BOX(self->playback_details_section), GTK_WIDGET(self->playback_title)); + gtk_box_append(GTK_BOX(self->playback_details_section), GTK_WIDGET(self->playback_album)); + gtk_box_append(GTK_BOX(self->playback_details_section), GTK_WIDGET(self->playback_artist)); + + gtk_box_append(GTK_BOX(self->playback_section), GTK_WIDGET(self->playback_details_section)); +} + +void koto_playerbar_create_primary_controls(KotoPlayerBar * self) { + self->back_button = koto_button_new_with_icon("", "media-skip-backward-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); + self->play_pause_button = koto_button_new_with_icon("", "media-playback-start-symbolic", "media-playback-pause-symbolic", KOTO_BUTTON_PIXBUF_SIZE_LARGE); // TODO: Have this take in a state and switch to a different icon if necessary + self->forward_button = koto_button_new_with_icon("", "media-skip-forward-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); + + if (KOTO_IS_BUTTON(self->back_button)) { + gtk_box_append(GTK_BOX(self->primary_controls_section), GTK_WIDGET(self->back_button)); + koto_button_add_click_handler(self->back_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_go_backwards), self); + } + + if (KOTO_IS_BUTTON(self->play_pause_button)) { + gtk_box_append(GTK_BOX(self->primary_controls_section), GTK_WIDGET(self->play_pause_button)); + koto_button_add_click_handler(self->play_pause_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_toggle_play_pause), self); + } + + if (KOTO_IS_BUTTON(self->forward_button)) { + gtk_box_append(GTK_BOX(self->primary_controls_section), GTK_WIDGET(self->forward_button)); + koto_button_add_click_handler(self->forward_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_go_forwards), self); + } + + self->advanced_controls_section = gtk_center_box_new(); + gtk_widget_add_css_class(self->advanced_controls_section, "playerbar-advanced-controls"); + + self->backwards_jump_button = koto_button_new_with_resource("/com/github/joshstrobl/koto/multimedia-backwards-jump", KOTO_BUTTON_PIXBUF_SIZE_NORMAL); + koto_button_add_click_handler(self->backwards_jump_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_jump_backwards), self); + + self->forwards_jump_button = koto_button_new_with_resource("/com/github/joshstrobl/koto/multimedia-forwards-jump", KOTO_BUTTON_PIXBUF_SIZE_NORMAL); + koto_button_add_click_handler(self->forwards_jump_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_jump_forwards), self); + + self->playback_speed_input = gtk_entry_new_with_buffer(gtk_entry_buffer_new("1.0", -1)); // Create an input that defaults to 1.0 + gtk_entry_set_max_length(GTK_ENTRY(self->playback_speed_input), 4); // Max of 4 characters (e.g. "0.25" but allow for "2" or "2.0") + gtk_editable_set_max_width_chars(GTK_EDITABLE(self->playback_speed_input), 4); // Set to be as wide as 4 characters + gtk_widget_set_valign(self->playback_speed_input, GTK_ALIGN_CENTER); // Only align center, don't vexpand + + gtk_center_box_set_start_widget(GTK_CENTER_BOX(self->advanced_controls_section), GTK_WIDGET(self->backwards_jump_button)); + gtk_center_box_set_center_widget(GTK_CENTER_BOX(self->advanced_controls_section), GTK_WIDGET(self->playback_speed_input)); + gtk_center_box_set_end_widget(GTK_CENTER_BOX(self->advanced_controls_section), GTK_WIDGET(self->forwards_jump_button)); + + gtk_box_append(GTK_BOX(self->primary_controls_section), self->advanced_controls_section); + + gtk_entry_set_input_hints( + GTK_ENTRY(self->playback_speed_input), + GTK_INPUT_HINT_NONE | GTK_INPUT_HINT_NO_SPELLCHECK | GTK_INPUT_HINT_NO_EMOJI // No fanciness please + ); + + gtk_entry_set_input_purpose( + GTK_ENTRY(self->playback_speed_input), + GTK_INPUT_PURPOSE_NUMBER // Numbers allow . so use that + ); + + gtk_entry_set_placeholder_text(GTK_ENTRY(self->playback_speed_input), "1.0"); + + g_signal_connect(self->playback_speed_input, "activate", G_CALLBACK(koto_playerbar_handle_speed_input_activate), self); // Handle our activate (enter key on entry) +} + +void koto_playerbar_create_secondary_controls(KotoPlayerBar * self) { + self->playback_position_label = gtk_label_new(NULL); // Create our label to communicate position and duration of a track + + self->repeat_button = koto_button_new_with_icon("", "media-playlist-repeat-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); + self->shuffle_button = koto_button_new_with_icon("", "media-playlist-shuffle-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); + self->playlist_button = koto_button_new_with_icon("", "playlist-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); + self->eq_button = koto_button_new_with_icon("", "multimedia-equalizer-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); + + self->volume_button = gtk_volume_button_new(); // Have this take in a state and switch to a different icon if necessary + + if (GTK_IS_LABEL(self->playback_position_label)) { + gtk_box_append(GTK_BOX(self->secondary_controls_section), self->playback_position_label); + } + + if (KOTO_IS_BUTTON(self->repeat_button)) { + gtk_box_append(GTK_BOX(self->secondary_controls_section), GTK_WIDGET(self->repeat_button)); + koto_button_add_click_handler(self->repeat_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_toggle_track_repeat), self); + } + + if (KOTO_IS_BUTTON(self->shuffle_button)) { + gtk_box_append(GTK_BOX(self->secondary_controls_section), GTK_WIDGET(self->shuffle_button)); + koto_button_add_click_handler(self->shuffle_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_toggle_playlist_shuffle), self); + } + + if (KOTO_IS_BUTTON(self->playlist_button)) { + gtk_box_append(GTK_BOX(self->secondary_controls_section), GTK_WIDGET(self->playlist_button)); + koto_button_add_click_handler(self->playlist_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_handle_playlist_button_clicked), self); + } + + if (KOTO_IS_BUTTON(self->eq_button)) { + gtk_box_append(GTK_BOX(self->secondary_controls_section), GTK_WIDGET(self->eq_button)); + } + + if (GTK_IS_VOLUME_BUTTON(self->volume_button)) { + GtkAdjustment * granular_volume_change = gtk_adjustment_new(50.0, 0, 100.0, 1.0, 1.0, 1.0); + g_object_set(self->volume_button, "use-symbolic", TRUE, NULL); + gtk_scale_button_set_adjustment(GTK_SCALE_BUTTON(self->volume_button), granular_volume_change); // Set our adjustment + gtk_box_append(GTK_BOX(self->secondary_controls_section), self->volume_button); + + g_signal_connect(GTK_SCALE_BUTTON(self->volume_button), "value-changed", G_CALLBACK(koto_playerbar_handle_volume_button_change), self); + } +} + +void koto_playerbar_go_backwards( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer data +) { + (void) gesture; + (void) n_press; + (void) x; + (void) y; + (void) data; + + koto_playback_engine_backwards(playback_engine); +} + +void koto_playerbar_go_forwards( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer data +) { + (void) gesture; + (void) n_press; + (void) x; + (void) y; + (void) data; + + koto_playback_engine_forwards(playback_engine); +} + +void koto_playerbar_handle_speed_input_activate( + GtkEntry * playback_speed_input, + gpointer user_data +) { + KotoPlayerBar * self = user_data; + + if (!KOTO_IS_PLAYERBAR(self)) { + return; + } + + GtkEntryBuffer * entry_buffer = gtk_entry_get_buffer(playback_speed_input); // Get the entry buffer + const gchar * text = gtk_entry_buffer_get_text(entry_buffer); // Get the text for the buffer + + gdouble rate = g_strtod(text, NULL); // Attempt to convert + gdouble fixed_rate = koto_playback_engine_get_sane_playback_rate(rate); + + gchar * conv = g_strdup_printf("%f", fixed_rate); + gtk_entry_buffer_set_text(entry_buffer, g_utf8_substring(conv, 0, 4), -1); // Set our entry value to the converted string of the new rate, or if it is something like 2 then it sets it to 2.0 + g_free(conv); + + koto_playback_engine_set_playback_rate(playback_engine, fixed_rate); +} + +void koto_playerbar_handle_is_playing( + KotoPlaybackEngine * engine, + gpointer user_data +) { + if (!KOTO_IS_PLAYBACK_ENGINE(engine)) { + return; + } + + KotoPlayerBar * self = user_data; + + if (!KOTO_IS_PLAYERBAR(self)) { + return; + } + + koto_button_show_image(self->play_pause_button, TRUE); // Set to TRUE to show pause as the next action +} + +void koto_playerbar_handle_is_paused( + KotoPlaybackEngine * engine, + gpointer user_data +) { + if (!KOTO_IS_PLAYBACK_ENGINE(engine)) { + return; + } + + KotoPlayerBar * self = user_data; + + if (!KOTO_IS_PLAYERBAR(self)) { + return; + } + + koto_button_show_image(self->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 * self = data; + + if (!KOTO_IS_PLAYERBAR(self)) { + return; + } + + self->is_progressbar_seeking = TRUE; +} + +void koto_playerbar_handle_progressbar_gesture_end( + GtkGesture * gesture, + GdkEventSequence * seq, + gpointer data +) { + (void) gesture; + (void) seq; + KotoPlayerBar * self = data; + + if (!KOTO_IS_PLAYERBAR(self)) { + return; + } + self->is_progressbar_seeking = FALSE; +} + +void koto_playerbar_handle_progressbar_pressed( + 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)) { + return; + } + + self->is_progressbar_seeking = TRUE; +} + +void koto_playerbar_handle_progressbar_value_changed( + GtkRange * progress_bar, + gpointer data +) { + KotoPlayerBar * self = data; + + if (!KOTO_IS_PLAYERBAR(self)) { + return; + } + + if (!self->is_progressbar_seeking) { + return; + } + + int desired_position = (int) gtk_range_get_value(progress_bar); + + koto_playback_engine_set_position(playback_engine, desired_position); // Update our position +} + +void koto_playerbar_handle_tick_duration( + KotoPlaybackEngine * engine, + gpointer user_data +) { + if (!KOTO_IS_PLAYBACK_ENGINE(engine)) { + return; + } + + KotoPlayerBar * self = user_data; + + if (!KOTO_IS_PLAYERBAR(self)) { + return; + } + + koto_playerbar_set_progressbar_duration(self, koto_playback_engine_get_duration(engine)); +} + +void koto_playerbar_handle_tick_track( + KotoPlaybackEngine * engine, + gpointer user_data +) { + if (!KOTO_IS_PLAYBACK_ENGINE(engine)) { + return; + } + + KotoPlayerBar * self = user_data; + + if (!KOTO_IS_PLAYERBAR(self)) { + return; + } + + if (self->is_progressbar_seeking) { // Currently seeking + return; + } + + KotoTrack * current_track = koto_playback_engine_get_current_track(engine); + + gtk_label_set_text( + GTK_LABEL(self->playback_position_label), + g_strdup_printf( + "%s / %s", + koto_utils_seconds_to_time_format(koto_track_get_playback_position(current_track)), + koto_utils_seconds_to_time_format(koto_track_get_duration(current_track)) + )); + + koto_playerbar_set_progressbar_value(self, koto_playback_engine_get_progress(engine)); +} + +void koto_playerbar_handle_track_repeat( + KotoPlaybackEngine * engine, + gpointer user_data +) { + if (!KOTO_IS_PLAYBACK_ENGINE(engine)) { + return; + } + + KotoPlayerBar * self = user_data; + + if (!KOTO_IS_PLAYERBAR(self)) { + return; + } + + if (koto_playback_engine_get_track_repeat(engine)) { // Is repeating + gtk_widget_add_css_class(GTK_WIDGET(self->repeat_button), "active"); // Add active CSS class + } else { // Is not repeating + gtk_widget_remove_css_class(GTK_WIDGET(self->repeat_button), "active"); // Remove active CSS class + } +} + +void koto_playerbar_handle_track_shuffle( + KotoPlaybackEngine * engine, + gpointer user_data +) { + if (!KOTO_IS_PLAYBACK_ENGINE(engine)) { + return; + } + + KotoPlayerBar * self = user_data; + + if (!KOTO_IS_PLAYERBAR(self)) { + return; + } + + if (koto_playback_engine_get_track_shuffle(engine)) { // Is repeating + gtk_widget_add_css_class(GTK_WIDGET(self->shuffle_button), "active"); // Add active CSS class + } else { // Is not repeating + gtk_widget_remove_css_class(GTK_WIDGET(self->shuffle_button), "active"); // Remove active CSS class + } +} + +void koto_playerbar_handle_volume_button_change( + GtkScaleButton * button, + double value, + gpointer user_data +) { + (void) button; + (void) user_data; + koto_playback_engine_set_volume(playback_engine, (double) value / 100); +} + +void koto_playerbar_jump_backwards( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer data +) { + (void) gesture; + (void) n_press; + (void) x; + (void) y; + (void) data; + + koto_playback_engine_jump_backwards(playback_engine); +} + +void koto_playerbar_jump_forwards( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer data +) { + (void) gesture; + (void) n_press; + (void) x; + (void) y; + (void) data; + + koto_playback_engine_jump_forwards(playback_engine); +} + +void koto_playerbar_reset_progressbar(KotoPlayerBar * self) { + if (!KOTO_IS_PLAYERBAR(self)) { + return; + } + + if (!GTK_IS_RANGE(self->progress_bar)) { + return; + } + + gtk_range_set_range(GTK_RANGE(self->progress_bar), 0, 0); // Reset range + gtk_range_set_value(GTK_RANGE(self->progress_bar), 0); // Set value to 0 +} + +void koto_playerbar_set_progressbar_duration( + KotoPlayerBar * self, + gint64 duration +) { + if (!KOTO_IS_PLAYERBAR(self)) { + return; + } + + if (!GTK_IS_RANGE(self->progress_bar)) { + return; + } + + if (duration <= 0) { + return; + } + + if (duration != self->last_recorded_duration) { // Duration is different than what we recorded + self->last_recorded_duration = duration; + gtk_range_set_range(GTK_RANGE(self->progress_bar), 0, self->last_recorded_duration); + } +} + +void koto_playerbar_set_progressbar_value( + KotoPlayerBar * self, + double progress +) { + if (!KOTO_IS_PLAYERBAR(self)) { + return; + } + + if (!GTK_IS_RANGE(self->progress_bar)) { + return; + } + + gtk_range_set_value(GTK_RANGE(self->progress_bar), progress); +} + +void koto_playerbar_toggle_play_pause( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer data +) { + (void) gesture; + (void) n_press; + (void) x; + (void) y; + (void) data; + + koto_playback_engine_toggle(playback_engine); +} + +void koto_playerbar_toggle_playlist_shuffle( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer data +) { + (void) gesture; + (void) n_press; + (void) x; + (void) y; + (void) data; + + koto_playback_engine_toggle_track_shuffle(playback_engine); // Call our playback engine's toggle track shuffle function +} + +void koto_playerbar_toggle_track_repeat( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer data +) { + (void) gesture; + (void) n_press; + (void) x; + (void) y; + (void) data; + koto_playback_engine_toggle_track_repeat(playback_engine); // Call our playback engine's toggle track repeat function +} + +void koto_playerbar_update_track_info( + KotoPlaybackEngine * engine, + gpointer user_data +) { + if (!KOTO_IS_PLAYBACK_ENGINE(engine)) { + return; + } + + KotoPlayerBar * self = user_data; + + if (!KOTO_IS_PLAYERBAR(self)) { + return; + } + + KotoTrack * current_track = koto_playback_engine_get_current_track(playback_engine); // Get the current track from the playback engine + + if (!KOTO_IS_TRACK(current_track)) { + return; + } + + gchar * track_name = NULL; + gchar * artist_uuid = NULL; + gchar * album_uuid = NULL; + + g_object_get(current_track, "parsed-name", &track_name, "artist-uuid", &artist_uuid, "album-uuid", &album_uuid, NULL); + + KotoArtist * artist = koto_cartographer_get_artist_by_uuid(koto_maps, artist_uuid); + + g_free(artist_uuid); + + if (koto_utils_string_is_valid(track_name)) { // Have a track name + gtk_label_set_text(GTK_LABEL(self->playback_title), track_name); // Set the label + } + + if (KOTO_IS_ARTIST(artist)) { + gchar * artist_name = NULL; + g_object_get(artist, "name", &artist_name, NULL); + + if ((artist_name != NULL) && (strcmp(artist_name, "") != 0)) { // Have an artist name + gtk_label_set_text(GTK_LABEL(self->playback_artist), artist_name); + gtk_widget_show(self->playback_artist); + } else { // Don't have an artist name somehow + gtk_widget_hide(self->playback_artist); + } + } + + gchar * art_path = NULL; + + gboolean set_album_label = FALSE; + if (koto_utils_string_is_valid(album_uuid)) { // Have a valid album UUID + KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, album_uuid); + g_free(album_uuid); + + if (KOTO_IS_ALBUM(album)) { + gchar * album_name = NULL; + g_object_get(album, "name", &album_name, "art-path", &art_path, NULL); // Get album name and art path + + if ((album_name != NULL) && (strcmp(album_name, "") != 0)) { // Have an album name + gtk_label_set_text(GTK_LABEL(self->playback_album), album_name); + set_album_label = TRUE; + gtk_widget_show(self->playback_album); + } + } + } + + (set_album_label) ? gtk_widget_show(self->playback_album) : gtk_widget_hide(self->playback_album); + + if ((art_path != NULL) && g_path_is_absolute(art_path)) { // Have an album artist path + gtk_image_set_from_file(GTK_IMAGE(self->artwork), art_path); // Update the art + } else { + gtk_image_set_from_icon_name(GTK_IMAGE(self->artwork), "audio-x-generic-symbolic"); // Use generic instead + } +} + +GtkWidget * koto_playerbar_get_main(KotoPlayerBar * self) { + return self->main; +} diff --git a/src/koto-playerbar.h b/src/koto-playerbar.h new file mode 100644 index 0000000..8abaeb7 --- /dev/null +++ b/src/koto-playerbar.h @@ -0,0 +1,196 @@ +/* koto-playerbar.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 "playback/engine.h" + +G_BEGIN_DECLS + +#define KOTO_TYPE_PLAYERBAR (koto_playerbar_get_type()) +G_DECLARE_FINAL_TYPE(KotoPlayerBar, koto_playerbar, KOTO, PLAYERBAR, GObject) +#define KOTO_IS_PLAYERBAR(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_PLAYERBAR)) + +KotoPlayerBar * koto_playerbar_new(void); +GtkWidget * koto_playerbar_get_main(KotoPlayerBar * self); + +void koto_playerbar_apply_configuration_state( + KotoConfig * config, + guint prop_id, + KotoPlayerBar * self +); + +void koto_playerbar_create_playback_details(KotoPlayerBar * self); + +void koto_playerbar_create_primary_controls(KotoPlayerBar * self); + +void koto_playerbar_create_secondary_controls(KotoPlayerBar * self); + +void koto_playerbar_go_backwards( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer data +); + +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 +); + +void koto_playerbar_handle_progressbar_pressed( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer data +); + +void koto_playerbar_handle_progressbar_value_changed( + GtkRange * progress_bar, + gpointer data +); + +void koto_playerbar_handle_speed_input_activate( + GtkEntry * playback_speed_input, + gpointer user_data +); + +void koto_playerbar_handle_tick_duration( + KotoPlaybackEngine * engine, + gpointer user_data +); + +void koto_playerbar_handle_tick_track( + KotoPlaybackEngine * engine, + gpointer user_data +); + +void koto_playerbar_handle_track_repeat( + KotoPlaybackEngine * engine, + gpointer user_data +); + +void koto_playerbar_handle_track_shuffle( + KotoPlaybackEngine * engine, + gpointer user_data +); + +void koto_playerbar_handle_volume_button_change( + GtkScaleButton * button, + double value, + gpointer user_data +); + +void koto_playerbar_jump_backwards( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer data +); + +void koto_playerbar_jump_forwards( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer data +); + +void koto_playerbar_reset_progressbar(KotoPlayerBar * self); + +void koto_playerbar_set_progressbar_duration( + KotoPlayerBar * self, + gint64 duration +); + +void koto_playerbar_set_progressbar_value( + KotoPlayerBar * self, + 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 +); + +void koto_playerbar_update_track_info( + KotoPlaybackEngine * engine, + gpointer user_data +); + +G_END_DECLS diff --git a/src/koto-utils.c b/src/koto-utils.c new file mode 100644 index 0000000..3a536f5 --- /dev/null +++ b/src/koto-utils.c @@ -0,0 +1,254 @@ +/* koto-utils.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 +#include +#include "koto-utils.h" + +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; + + if ((filepath != NULL) && (strcmp(filepath, "") != 0)) { // If we have a filepath + if (g_file_test(filepath, G_FILE_TEST_EXISTS)) { // File exists + image = gtk_image_new_from_file(filepath); // Load from the filepath + } + } + + if (!GTK_IS_IMAGE(image)) { // If we failed to get the image or never passed a valid filepath to begin with + image = gtk_image_new_from_icon_name(fallback_icon); // Set to the fallback icon + } + + gtk_image_set_icon_size(GTK_IMAGE(image), GTK_ICON_SIZE_INHERIT); + gtk_image_set_pixel_size(GTK_IMAGE(image), width); + gtk_widget_set_size_request(image, width, height); + + return image; +} + +gchar * koto_utils_gboolean_to_string(gboolean b) { + return g_strdup(b ? "true" : "false"); +} + +gchar * koto_utils_get_filename_without_extension(gchar * filename) { + gchar * trimmed_file_name = g_strdup(g_path_get_basename(filename)); // Ensure the filename provided is the base name of any possible path and duplicate it + gchar ** split = g_strsplit(trimmed_file_name, ".", -1); // Split every time we see . + + g_free(trimmed_file_name); + guint len_of_extension_split = g_strv_length(split); + + if (len_of_extension_split == 2) { // Only have two elements + trimmed_file_name = g_strdup(split[0]); // Get the first element + } else { + gchar * new_parsed_name = ""; + for (guint i = 0; i < len_of_extension_split - 1; i++) { // Iterate over everything except the last item + if (g_strcmp0(new_parsed_name, "") == 0) { // Currently empty + new_parsed_name = g_strdup(split[i]); // Just duplicate this string + } else { + gchar * tmp_copy = g_strdup(new_parsed_name); + g_free(new_parsed_name); // Free the old + new_parsed_name = g_strjoin(".", tmp_copy, split[i], NULL); // Join the two strings with a . again and duplicate it, setting it to our new_parsed_name + g_free(tmp_copy); // Free our temporary copy + } + } + + trimmed_file_name = g_strdup(new_parsed_name); + g_free(new_parsed_name); + } + + gchar * stripped_file_name = g_strstrip(g_strdup(trimmed_file_name)); // Strip leading and trailing whitespace + + g_free(trimmed_file_name); + g_strfreev(split); + return stripped_file_name; +} + +gchar * koto_utils_join_string_list ( + GList * list, + gchar * sep +) { + gchar * liststring = NULL; + GList * cur_list; + for (cur_list = list; cur_list != NULL; cur_list = cur_list->next) { // For each item in the list + gchar * current_item = cur_list->data; + if (!koto_utils_string_is_valid(current_item)) { // Not a valid string + continue; + } + + gchar * item_plus_sep = g_strdup_printf("%s%s", current_item, sep); + + if (koto_utils_string_is_valid(liststring)) { // Is a valid string + gchar * new_string = g_strconcat(liststring, item_plus_sep, NULL); + g_free(liststring); + liststring = new_string; + } else { // Don't have any content yet + liststring = item_plus_sep; + } + } + + return (liststring == NULL) ? g_strdup("") : liststring; +} +void koto_utils_mkdir(gchar * path) { + mkdir(path, 0755); + chown(path, getuid(), getgid()); +} + +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_seconds_to_time_format(guint64 seconds) { + GDateTime * date = g_date_time_new_from_unix_utc(seconds); // Add our seconds after UTC + gchar * date_string = g_date_time_format(date, (g_date_time_get_hour(date) != 0) ? "%H:%M:%S" : "%M:%S"); + g_date_time_unref(date); + return date_string; +} + +gboolean koto_utils_string_contains_substring( + gchar * s, + gchar * sub +) { + gchar ** separated_string = g_strsplit(s, sub, -1); // Split on our substring + gboolean contains = (g_strv_length(separated_string) > 1); + g_strfreev(separated_string); + return contains; +} + +gchar * koto_utils_string_get_valid(gchar * str) { + return koto_utils_string_is_valid(str) ? str : g_strdup(""); // Return string if a string, otherwise return an empty string +} + +gboolean koto_utils_string_is_valid(const gchar * str) { + return ((str != NULL) && (g_strcmp0(str, "") != 0)); +} + +gchar * koto_utils_string_replace_all( + gchar * str, + gchar * find, + gchar * repl +) { + if (!koto_utils_string_is_valid(str)) { // Not a valid string + return g_strdup(""); + } + + gchar ** split = g_strsplit(str, find, -1); // Split on find + + guint split_len = g_strv_length(split); + + if (split_len == 1) { // Only one item + g_strfreev(split); + return g_strdup(str); // Just set to the string we were provided + } + + return g_strdup(g_strjoinv(repl, split)); +} + +GList * koto_utils_string_to_string_list( + gchar * s, + gchar * sep +) { + GList * list = NULL; + if (!koto_utils_string_is_valid(s)) { // Provided string is not valid + return list; + } + + gchar ** separated_strings = g_strsplit(s, sep, -1); // Split on separator for the string + + for (guint i = 0; i < g_strv_length(separated_strings); i++) { // Iterate over each item + gchar * item = separated_strings[i]; + if (g_strcmp0(item, "") != 0) { // Not an empty string + list = g_list_append(list, g_strdup(item)); + } + } + + g_strfreev(separated_strings); // Free our strings + return list; +} + +gchar * koto_utils_string_title(const gchar * s) { + if (!koto_utils_string_is_valid(s)) { // Not a valid string + return NULL; + } + + glong len = g_utf8_strlen(s, -1); + + if (len == 0) { // Empty string + return g_strdup(s); // Just duplicate itself + } else if (len == 1) { // One char + return g_utf8_strup(s, -1); // Uppercase all relevant cases + } else { + gchar * first_char = g_utf8_substring(s, 0, 1); + gchar * rest_of_string = g_utf8_substring(s, 1, len); // Rest of string + gchar * titled_string = g_strdup_printf("%s%s", g_utf8_strup(first_char, -1), rest_of_string); + + g_free(first_char); + g_free(rest_of_string); + return titled_string; + } +} + +gchar * koto_utils_string_unquote(gchar * s) { + gchar * new_s = NULL; + + if (!koto_utils_string_is_valid(s)) { // Not a valid string + new_s = g_strdup(""); + return new_s; + } + + if (g_str_has_prefix(s, "'") && g_str_has_suffix(s, "'")) { // Begins and ends with ' + new_s = g_utf8_substring(s, 1, g_utf8_strlen(s, -1) - 1); // Start at 1 and end at n-1 + } else { + new_s = g_strdup(s); + } + + gchar ** split_on_double_single = g_strsplit(new_s, "''", -1); // Split on instances of '' + + new_s = g_strjoinv("'", split_on_double_single); // Rejoin as ' + g_strfreev(split_on_double_single); // Free our array + + return new_s; +} diff --git a/src/koto-utils.h b/src/koto-utils.h new file mode 100644 index 0000000..131fe36 --- /dev/null +++ b/src/koto-utils.h @@ -0,0 +1,75 @@ +/* koto-utils.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 + +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_gboolean_to_string(gboolean b); + +gchar * koto_utils_get_filename_without_extension(gchar * filename); + +gchar * koto_utils_join_string_list( + GList * list, + gchar * sep +); + +void koto_utils_mkdir(gchar * path); + +void koto_utils_push_queue_element_to_store( + gpointer data, + gpointer user_data +); + +gchar * koto_utils_seconds_to_time_format(guint64 seconds); + +gboolean koto_utils_string_contains_substring( + gchar * s, + gchar * sub +); + +gchar * koto_utils_string_get_valid(gchar * str); + +gboolean koto_utils_string_is_valid(const gchar * str); + +gchar * koto_utils_string_replace_all( + gchar * str, + gchar * find, + gchar * repl +); + +GList * koto_utils_string_to_string_list( + gchar * s, + gchar * sep +); + +gchar * koto_utils_string_title(const gchar * s); + +gchar * koto_utils_string_unquote(gchar * s); + +G_END_DECLS diff --git a/src/koto-window.c b/src/koto-window.c new file mode 100644 index 0000000..dcf014d --- /dev/null +++ b/src/koto-window.c @@ -0,0 +1,332 @@ +/* koto-window.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 "components/action-bar.h" +#include "db/cartographer.h" +#include "indexer/structs.h" +#include "pages/audiobooks/library.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/create-modify-dialog.h" +#include "config/config.h" +#include "koto-dialog-container.h" +#include "koto-nav.h" +#include "koto-playerbar.h" +#include "koto-paths.h" +#include "koto-window.h" + +extern KotoActionBar * action_bar; +extern KotoAddRemoveTrackPopover * koto_add_remove_track_popup; +extern KotoAudiobooksLibraryPage * audiobooks_library_page; +extern KotoCartographer * koto_maps; +extern KotoCreateModifyPlaylistDialog * playlist_create_modify_dialog; +extern KotoConfig * config; +extern KotoPageMusicLocal * music_local_page; + +extern gchar * koto_rev_dns; + +struct _KotoWindow { + GtkApplicationWindow parent_instance; + KotoLibrary * library; + + KotoDialogContainer * dialogs; + + GtkCssProvider * provider; + + GtkWidget * overlay; + GtkWidget * header_bar; + GtkWidget * menu_button; + GtkWidget * search_entry; + + GtkWidget * primary_layout; + GtkWidget * content_layout; + + KotoNav * nav; + GtkWidget * pages; + KotoPlayerBar * player_bar; +}; + +G_DEFINE_TYPE(KotoWindow, koto_window, GTK_TYPE_APPLICATION_WINDOW) + +static void koto_window_class_init (KotoWindowClass * klass) { + (void) klass; +} + +static void koto_window_init (KotoWindow * self) { + koto_window_manage_style(config, 0, self); // Immediately apply the theme + g_signal_connect(config, "notify::ui-theme-desired", G_CALLBACK(koto_window_manage_style), self); // Handle changes to desired theme + g_signal_connect(config, "notify::ui-theme-override", G_CALLBACK(koto_window_manage_style), self); // Handle changes to theme overriding + + create_new_headerbar(self); // Create our headerbar + + self->overlay = gtk_overlay_new(); // Create our overlay + 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"); + gtk_widget_set_hexpand(self->content_layout, TRUE); + gtk_widget_set_vexpand(self->content_layout, TRUE); + + self->nav = koto_nav_new(); + + if (self->nav != NULL) { + gtk_box_prepend(GTK_BOX(self->content_layout), koto_nav_get_nav(self->nav)); + } + + self->pages = gtk_stack_new(); // New stack to hold our pages + + if (GTK_IS_STACK(self->pages)) { // Created our stack successfully + gtk_stack_set_transition_type(GTK_STACK(self->pages), GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT); + gtk_box_append(GTK_BOX(self->content_layout), self->pages); + gtk_widget_set_hexpand(self->pages, TRUE); + gtk_widget_set_vexpand(self->pages, TRUE); + } + + 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 (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); + } + + gtk_window_set_child(GTK_WINDOW(self), self->overlay); +#ifdef GDK_WINDOWING_X11 + set_optimal_default_window_size(self); +#endif + gtk_window_set_title(GTK_WINDOW(self), "Koto"); + gtk_window_set_icon_name(GTK_WINDOW(self), "audio-headphones"); + gtk_window_set_startup_id(GTK_WINDOW(self), koto_rev_dns); + + gtk_widget_queue_draw(self->content_layout); + + audiobooks_library_page = koto_audiobooks_library_page_new(); // Create our audiobooks library page + music_local_page = koto_page_music_local_new(); + + // TODO: Remove and do some fancy state loading + koto_window_add_page(self, "audiobooks.library", GTK_WIDGET(koto_audiobooks_library_page_get_main(audiobooks_library_page))); + koto_window_add_page(self, "music.library", GTK_WIDGET(music_local_page)); + koto_window_go_to_page(self, "audiobooks.library"); + gtk_widget_show(self->pages); // Do not remove this. Will cause sporadic hiding of the local page content otherwise. +} + +void koto_window_add_page( + KotoWindow * self, + gchar * page_name, + GtkWidget * page +) { + if (!KOTO_IS_WINDOW(self)) { + return; + } + + if (!GTK_IS_STACK(self->pages)) { + return; + } + + if (!GTK_IS_WIDGET(page)) { + return; + } + + gtk_stack_add_named(GTK_STACK(self->pages), page, page_name); +} + +void koto_window_manage_style( + KotoConfig * c, + guint prop_id, + KotoWindow * self +) { + (void) prop_id; + + if (!KOTO_IS_WINDOW(self)) { // Not a Koto Window + return; + } + + gchar * desired_theme = NULL; + gboolean overriding_theme = FALSE; + g_object_get( + c, + "ui-theme-desired", + &desired_theme, + "ui-theme-override", + &overriding_theme, + NULL + ); + + if (!koto_utils_string_is_valid(desired_theme)) { // Theme not valid + desired_theme = "dark"; + } + + if (!GTK_IS_CSS_PROVIDER(self->provider)) { // Don't have a CSS provider yet + self->provider = gtk_css_provider_new(); + } + + gtk_css_provider_load_from_resource(self->provider, g_strdup_printf("/com/github/joshstrobl/koto/koto-builtin-%s.css", desired_theme)); + + if (!overriding_theme) { // If we are not overriding the theme + gtk_style_context_add_provider_for_display(gdk_display_get_default(), GTK_STYLE_PROVIDER(self->provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + GList * themes = NULL; + themes = g_list_append(themes, "dark"); + themes = g_list_append(themes, "gruvbox"); + themes = g_list_append(themes, "light"); + + GList * themes_head; + + for (themes_head = themes; themes_head != NULL; themes_head = themes_head->next) { // For each theme + gchar * theme_class_name = g_strdup_printf("koto-theme-%s", (gchar*) themes->data); // Get the theme + + if (g_strcmp0((gchar*) themes->data, desired_theme) == 0) { // If we are using this theme + gtk_widget_add_css_class(GTK_WIDGET(self), theme_class_name); // Add the CSS class + } else { + gtk_widget_remove_css_class(GTK_WIDGET(self), theme_class_name); // Remove the CSS class + } + + g_free(theme_class_name); // Free the CSS class + } + + g_list_free(themes_head); + g_list_free(themes); + } else { // Overriding the built-in theme + gtk_style_context_remove_provider_for_display(gdk_display_get_default(), GTK_STYLE_PROVIDER(self->provider)); // Remove the provider + } +} + +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); + + if (GTK_IS_WIDGET(page)) { + 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"); + g_return_if_fail(GTK_IS_HEADER_BAR(self->header_bar)); + + self->menu_button = gtk_button_new_from_icon_name("audio-headphones"); + + self->search_entry = gtk_search_entry_new(); + gtk_widget_add_css_class(self->menu_button, "flat"); + gtk_widget_set_can_focus(self->search_entry, TRUE); + gtk_widget_set_size_request(self->search_entry, 400, -1); // Have 400px width + g_object_set(self->search_entry, "placeholder-text", "Search...", NULL); + + gtk_header_bar_pack_start(GTK_HEADER_BAR(self->header_bar), self->menu_button); + gtk_header_bar_set_show_title_buttons(GTK_HEADER_BAR(self->header_bar), TRUE); + gtk_header_bar_set_title_widget(GTK_HEADER_BAR(self->header_bar), self->search_entry); + + gtk_window_set_titlebar(GTK_WINDOW(self), self->header_bar); +} + +void set_optimal_default_window_size(KotoWindow * self) { + GdkDisplay * default_display = gdk_display_get_default(); + + if (!GDK_IS_X11_DISPLAY(default_display)) { // Not an X11 display + return; + } + + GdkMonitor * default_monitor = gdk_x11_display_get_primary_monitor(GDK_X11_DISPLAY(default_display)); // Get primary monitor for the X11 + + if (!GDK_IS_X11_MONITOR(default_monitor)) { // Not an X11 Monitor + return; + } + + GdkRectangle workarea = { + 0 + }; + + gdk_monitor_get_geometry(default_monitor, &workarea); + + if (workarea.width <= 1280) { // Honestly how do you even get anything done? + gtk_widget_set_size_request(GTK_WIDGET(self), 1200, 675); + } else if ((workarea.width > 1280) && (workarea.width <= 1600)) { // Plebian monitor resolution + gtk_widget_set_size_request(GTK_WIDGET(self), 1300, 709); + } else if ((workarea.width > 1600) && (workarea.width <= 1920)) { // Something slightly normal + gtk_widget_set_size_request(GTK_WIDGET(self), 1600, 900); + } else if ((workarea.width > 1920) && (workarea.width <= 2560)) { // Well aren't you hot stuff? + gtk_widget_set_size_request(GTK_WIDGET(self), 1920, 1080); + } else { // Now you're just flexing + gtk_widget_set_size_request(GTK_WIDGET(self), 2560, 1440); + } +} diff --git a/src/koto-window.h b/src/koto-window.h new file mode 100644 index 0000000..30896a2 --- /dev/null +++ b/src/koto-window.h @@ -0,0 +1,74 @@ +/* koto-window.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 "config/config.h" +#include "db/cartographer.h" +#include "playlist/playlist.h" + +G_BEGIN_DECLS + +#define KOTO_TYPE_WINDOW (koto_window_get_type()) + +G_DECLARE_FINAL_TYPE(KotoWindow, koto_window, KOTO, WINDOW, GtkApplicationWindow) + +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_manage_style( + KotoConfig * config, + guint prop_id, + KotoWindow * self +); + +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); + +G_END_DECLS diff --git a/src/koto.gresource.xml b/src/koto.gresource.xml new file mode 100644 index 0000000..daa9ffe --- /dev/null +++ b/src/koto.gresource.xml @@ -0,0 +1,15 @@ + + + + ../data/genres/business-and-personal-finance.png + ../data/genres/foreign-languages.png + ../data/vectors/multimedia-backwards-jump.svg + ../data/vectors/multimedia-forwards-jump.svg + ../data/genres/mystery-and-thriller.png + ../data/genres/sci-fi.png + ../data/genres/travel.png + ../theme/koto-builtin-dark.css + ../theme/koto-builtin-gruvbox.css + ../theme/koto-builtin-light.css + + diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..01e9637 --- /dev/null +++ b/src/main.c @@ -0,0 +1,131 @@ +/* main.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 +#include "config/config.h" +#include "db/cartographer.h" +#include "db/db.h" +#include "db/loaders.h" +#include "indexer/track-helpers.h" +#include "playback/engine.h" +#include "playback/media-keys.h" +#include "playback/mimes.h" +#include "playback/mpris.h" +#include "paths.h" +#include "playlist/current.h" + +#include "config/config.h" +#include "koto-paths.h" +#include "koto-window.h" + +extern KotoConfig * config; + +extern guint mpris_bus_id; +extern GDBusNodeInfo * introspection_data; + +extern KotoPlaybackEngine * playback_engine; +extern KotoCartographer * koto_maps; +extern KotoCurrentPlaylist * current_playlist; +extern sqlite3 * koto_db; + +extern GHashTable * supported_mimes_hash; +extern GList * supported_mimes; + +extern gchar * koto_path_to_conf; +extern gchar * koto_rev_dns; + +extern gboolean created_new_db; + +GVolumeMonitor * volume_monitor = NULL; +GtkApplication * app = NULL; +GtkWindow * main_window; +magic_t magic_cookie; + +static void on_activate (GtkApplication * app) { + g_assert(GTK_IS_APPLICATION(app)); + + main_window = gtk_application_get_active_window(app); + if (main_window == NULL) { + main_window = g_object_new(KOTO_TYPE_WINDOW, "application", app, "default-width", 1200, "default-height", 675, NULL); + setup_mpris_interfaces(); // Set up our MPRIS interfaces + setup_mediakeys_interface(); // Set up our media key support + + if (!created_new_db) { + read_from_db(); // Read the database, allowing us to propagate the UI with various data such as artists and playlist navigation elements + } + } + + gtk_window_present(main_window); +} + +static void on_shutdown(GtkApplication * app) { + (void) app; + koto_current_playlist_save_playlist_state(current_playlist); // Save the current playlist state if necessary before closure + koto_config_save(config); // Save our config + close_db(); // Close the database + g_bus_unown_name(mpris_bus_id); + g_dbus_node_info_unref(introspection_data); +} + +int main ( + int argc, + char * argv[] +) { + int ret; + + gtk_init(); + gst_init(&argc, &argv); + + koto_track_helpers_init(); // Init our track helpers (primarily our genre replacement hashtable) + koto_paths_setup(); // Set up our required paths + + supported_mimes_hash = g_hash_table_new(g_str_hash, g_str_equal); + supported_mimes = NULL; // Ensure our mimes GList is initialized + koto_playback_engine_get_supported_mimetypes(supported_mimes); + + koto_maps = koto_cartographer_new(); // Create our new cartographer and their collection of maps + + volume_monitor = g_volume_monitor_get(); // Get a VolumeMonitor + + config = koto_config_new(); // Set our config + koto_config_load(config, koto_path_to_conf); + playback_engine = koto_playback_engine_new(); // Initialize the engine now that the config is available, since it listens on various config signals + + taglib_id3v2_set_default_text_encoding(TagLib_ID3v2_UTF8); // Ensure our id3v2 text encoding is UTF-8 + magic_cookie = magic_open(MAGIC_MIME); + + if (magic_cookie == NULL) { // Failed to open + g_critical("Failed to allocate a cookie pointer from libmagic."); + } + + if (magic_load(magic_cookie, NULL) != 0) { // Failed to load data + magic_close(magic_cookie); + g_critical("Failed to load the system magic database."); + } + + open_db(); // Open our database + g_thread_new("indexing-any-necessary-libs", (void*) koto_config_load_libs, config); // Load our libraries, now that our database is set up. Note that read_from_db is called in koto-window.c + + app = gtk_application_new(koto_rev_dns, G_APPLICATION_FLAGS_NONE); + g_signal_connect(app, "activate", G_CALLBACK(on_activate), NULL); + g_signal_connect(app, "shutdown", G_CALLBACK(on_shutdown), NULL); + ret = g_application_run(G_APPLICATION(app), argc, argv); + + return ret; +} diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..01ac94b --- /dev/null +++ b/src/meson.build @@ -0,0 +1,75 @@ +add_global_arguments([ + '-I' + meson.current_build_dir(), + '-Db_sanitize=address', + '-Dwerror=true', +], language: 'c') + +koto_sources = [ + 'components/album-info.c', + 'components/action-bar.c', + 'components/button.c', + 'components/cover-art-button.c', + 'components/track-item.c', + 'components/track-table.c', + 'config/config.c', + 'db/cartographer.c', + 'db/db.c', + 'db/loaders.c', + 'indexer/album.c', + 'indexer/artist.c', + 'indexer/file-indexer.c', + 'indexer/library.c', + 'indexer/track-helpers.c', + 'indexer/track.c', + 'pages/audiobooks/audiobook-view.c', + 'pages/audiobooks/genres-banner.c', + 'pages/audiobooks/genre-button.c', + 'pages/audiobooks/library.c', + 'pages/audiobooks/writer-page.c', + 'pages/music/album-view.c', + '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/add-remove-track-popover.c', + 'playlist/create-modify-dialog.c', + 'playlist/current.c', + 'playlist/playlist.c', + 'main.c', + 'koto-dialog-container.c', + 'koto-expander.c', + 'koto-nav.c', + 'koto-playerbar.c', + 'koto-paths.c', + 'koto-utils.c', + 'koto-window.c', +] + +koto_deps = [ + dependency('glib-2.0', version: '>= 2.66'), + dependency('gio-2.0', version: '>= 2.66'), + dependency('gstreamer-1.0', version: '>= 1.18'), + dependency('gstreamer-player-1.0', version: '>= 1.18'), + dependency('gtk4', version: '>= 4.0'), + dependency('libmagic', version: '>=5.39'), + dependency('sqlite3', version: '>=3.34'), + dependency('taglib_c', version: '>=1.11'), + toml_dep, +] + +gnome = import('gnome') + +koto_sources += gnome.compile_resources('koto-resources', + 'koto.gresource.xml', + dependencies: themes, + c_name: 'koto', +) + +executable('com.github.joshstrobl.koto', koto_sources, + dependencies: koto_deps, + install: true, +) diff --git a/src/pages/audiobooks/audiobook-view.c b/src/pages/audiobooks/audiobook-view.c new file mode 100644 index 0000000..be2b1bc --- /dev/null +++ b/src/pages/audiobooks/audiobook-view.c @@ -0,0 +1,255 @@ +/* audiobook-view.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/album-info.h" +#include "../../components/button.h" +#include "../../components/track-item.h" +#include "../../db/cartographer.h" +#include "../../indexer/album-playlist-funcs.h" +#include "../../indexer/structs.h" +#include "../../playlist/current.h" +#include "../../koto-utils.h" +#include "../../koto-window.h" +#include "audiobook-view.h" + +extern KotoCartographer * koto_maps; +extern KotoCurrentPlaylist * current_playlist; +extern KotoWindow * main_window; + +struct _KotoAudiobookView { + GtkBox parent_instance; + + KotoAlbum * album; // Album associated with this view + + GtkWidget * side_info; // Our side info (artwork, playback button, position) + GtkWidget * info_contents; // Our info and contents vertical box + + KotoAlbumInfo * album_info; // Our "Album" (Audiobook) info + + GtkWidget * audiobook_art; // Our GtkImage for the audiobook art + GtkWidget * playback_button; // Our GtkButton for playback + GtkWidget * chapter_info; // Our GtkLabel of the position, effectively + GtkWidget * playback_position; // Our GtkLabel for the track playback position + + GtkWidget * tracks_list; // GtkListBox of tracks +}; + +struct _KotoAudiobookViewClass { + GtkBoxClass parent_class; +}; + +G_DEFINE_TYPE(KotoAudiobookView, koto_audiobook_view, GTK_TYPE_BOX); + +static void koto_audiobook_view_class_init(KotoAudiobookViewClass * c) { + (void) c; +} + +static void koto_audiobook_view_init(KotoAudiobookView * self) { + gtk_widget_add_css_class(GTK_WIDGET(self), "audiobook-view"); + + self->side_info = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_add_css_class(self->side_info, "side-info"); + gtk_widget_set_size_request(self->side_info, 220, -1); // Match audiobook art size + + self->info_contents = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + self->audiobook_art = gtk_image_new_from_icon_name("audio-x-generic-symbolic"); // Create an image with a symbolic icon + gtk_widget_set_size_request(self->audiobook_art, 220, 220); // Set to 220 like our cover art button + + self->playback_button = gtk_button_new_with_label("Play"); // Default to our button saying Play instead of continue + gtk_widget_add_css_class(self->playback_button, "suggested-action"); + g_signal_connect(self->playback_button, "clicked", G_CALLBACK(koto_audiobook_view_handle_play_clicked), self); + + self->chapter_info = gtk_label_new(NULL); + self->playback_position = gtk_label_new(NULL); + + gtk_widget_set_halign(self->chapter_info, GTK_ALIGN_START); + gtk_widget_set_halign(self->playback_position, GTK_ALIGN_START); + + gtk_box_append(GTK_BOX(self->side_info), self->audiobook_art); // Add our audiobook art to the side info + gtk_box_append(GTK_BOX(self->side_info), GTK_WIDGET(self->playback_button)); // Add our playback button to the side info + gtk_box_append(GTK_BOX(self->side_info), self->chapter_info); + gtk_box_append(GTK_BOX(self->side_info), self->playback_position); + + self->album_info = koto_album_info_new("audiobook"); // Create an "audiobook" type KotoAlbumInfo + gtk_box_append(GTK_BOX(self->info_contents), GTK_WIDGET(self->album_info)); // Add the album info the info contents + + GtkWidget * chapters_label = gtk_label_new("Chapters"); + gtk_widget_add_css_class(chapters_label, "chapters-label"); + gtk_widget_set_halign(chapters_label, GTK_ALIGN_START); // Align to the start + + gtk_box_append(GTK_BOX(self->info_contents), chapters_label); + + self->tracks_list = gtk_list_box_new(); // Create our list of our tracks + gtk_list_box_set_activate_on_single_click(GTK_LIST_BOX(self->tracks_list), FALSE); + gtk_list_box_set_selection_mode(GTK_LIST_BOX(self->tracks_list), GTK_SELECTION_MULTIPLE); + gtk_widget_add_css_class(self->tracks_list, "track-list"); + gtk_box_append(GTK_BOX(self->info_contents), self->tracks_list); // Add our listbox to the info contents + + gtk_box_append(GTK_BOX(self), self->side_info); // Add our side info to the box + gtk_box_append(GTK_BOX(self), self->info_contents); // Add our info contents to the box +} + +GtkWidget * koto_audiobook_view_create_track_item( + gpointer item, + gpointer user_data +) { + (void) user_data; + + if (!KOTO_IS_TRACK(item)) { // Not actually a track + return NULL; + } + + KotoTrack * track = KOTO_TRACK(item); // Cast our item as a track + KotoTrackItem * track_item = koto_track_item_new(track); // Create our track item + + if (!KOTO_IS_TRACK_ITEM(track_item)) { // Not a track item + return NULL; + } + + return GTK_WIDGET(track_item); // Cast as a widget and return the track item +} + +void koto_audiobook_view_handle_play_clicked( + GtkButton * button, + gpointer user_data +) { + (void) button; + KotoAudiobookView * self = user_data; + + if (!KOTO_IS_AUDIOBOOK_VIEW(self)) { + return; + } + + if (!KOTO_IS_ALBUM(self->album)) { // Don't have an album associated with this view + return; + } + + koto_playlist_commit(koto_album_get_playlist(self->album)); // Ensure we commit the current playlist to the database. This is needed to ensure we are able to save the playlist state going forward + koto_current_playlist_set_playlist(current_playlist, koto_album_get_playlist(self->album), TRUE, TRUE); // Set this playlist to be played immediately, play current track +} + +void koto_audiobook_view_handle_playlist_updated( + KotoPlaylist * playlist, + gpointer user_data +) { + (void) playlist; + + KotoAudiobookView * self = user_data; + + if (!KOTO_IS_AUDIOBOOK_VIEW(self)) { + return; + } + + koto_audiobook_view_update_side_info(self); // Update the side info based on the playlist modification +} + +void koto_audiobook_view_set_album( + KotoAudiobookView * self, + KotoAlbum * album +) { + if (!KOTO_IS_AUDIOBOOK_VIEW(self)) { + return; + } + + if (!KOTO_IS_ALBUM(album)) { // Not an album + return; + } + + self->album = album; + + gchar * album_art_path = koto_album_get_art(album); // Get any artwork + + if (koto_utils_string_is_valid(album_art_path)) { // Have album art + gtk_image_set_from_file(GTK_IMAGE(self->audiobook_art), album_art_path); // Set our album art + } + + koto_album_info_set_album_uuid(self->album_info, koto_album_get_uuid(album)); // Apply our album info + + gtk_list_box_bind_model( + // Apply our binding for the GtkListBox + GTK_LIST_BOX(self->tracks_list), + G_LIST_MODEL(koto_album_get_store(album)), + koto_audiobook_view_create_track_item, + NULL, + koto_audiobook_view_destroy_associated_user_data + ); + + + KotoPlaylist * album_playlist = koto_album_get_playlist(self->album); // Get the album playlist + if (!KOTO_IS_PLAYLIST(album_playlist)) { // Not a playlist + return; + } + + g_signal_connect(album_playlist, "modified", G_CALLBACK(koto_audiobook_view_handle_playlist_updated), self); // Handle modifications of a playlist + g_signal_connect(album_playlist, "track-load-finalized", G_CALLBACK(koto_audiobook_view_handle_playlist_updated), self); // Handle when a playlist is finalized + koto_audiobook_view_update_side_info(self); // Update our side info +} + +void koto_audiobook_view_update_side_info(KotoAudiobookView * self) { + if (!KOTO_IS_AUDIOBOOK_VIEW(self)) { + return; + } + + if (!KOTO_IS_ALBUM(self->album)) { // Not an album + return; + } + + KotoPlaylist * album_playlist = koto_album_get_playlist(self->album); // Get the album playlist + if (!KOTO_IS_PLAYLIST(album_playlist)) { // Not a playlist + return; + } + + gint playlist_position = koto_playlist_get_current_position(album_playlist); // Get the current position in the playlist + gint playlist_length = koto_playlist_get_length(album_playlist); // Get the length of the playlist + + KotoTrack * current_track = koto_playlist_get_current_track(album_playlist); // Get the track for the playlist + + if (!KOTO_IS_TRACK(current_track)) { // Not a track + return; + } + + guint playback_position = koto_track_get_playback_position(current_track); + guint duration = koto_track_get_duration(current_track); + gboolean continuing = (playlist_position >= 1) || ((playlist_position <= 0) && (playback_position != 0)); + + gtk_button_set_label(GTK_BUTTON(self->playback_button), continuing ? "Resume" : "Play"); + + if (continuing) { // Have been playing track + gtk_label_set_text(GTK_LABEL(self->chapter_info), g_strdup_printf("Track %i of %i", (playlist_position <= 0) ? 1 : (playlist_position + 1), playlist_length)); + gtk_label_set_text(GTK_LABEL(self->playback_position), g_strdup_printf("%s / %s", koto_utils_seconds_to_time_format(playback_position), koto_utils_seconds_to_time_format(duration))); + gtk_widget_show(self->chapter_info); + gtk_widget_show(self->playback_position); + } else { + gtk_widget_hide(self->chapter_info); // Hide by default + gtk_widget_hide(self->playback_position); // Hide by default + } +} + +void koto_audiobook_view_destroy_associated_user_data(gpointer user_data) { + (void) user_data; +} + +KotoAudiobookView * koto_audiobook_view_new() { + return g_object_new( + KOTO_TYPE_AUDIOBOOK_VIEW, + "orientation", + GTK_ORIENTATION_HORIZONTAL, + NULL + ); +} \ No newline at end of file diff --git a/src/pages/audiobooks/audiobook-view.h b/src/pages/audiobooks/audiobook-view.h new file mode 100644 index 0000000..fe5cee4 --- /dev/null +++ b/src/pages/audiobooks/audiobook-view.h @@ -0,0 +1,56 @@ +/* audiobook-view.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 + +#define KOTO_TYPE_AUDIOBOOK_VIEW (koto_audiobook_view_get_type()) +G_DECLARE_FINAL_TYPE(KotoAudiobookView, koto_audiobook_view, KOTO, AUDIOBOOK_VIEW, GtkBox); +#define KOTO_IS_AUDIOBOOK_VIEW(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_AUDIOBOOK_VIEW)) + +void koto_audiobook_view_set_album( + KotoAudiobookView * self, + KotoAlbum * album +); + +GtkWidget * koto_audiobook_view_create_track_item( + gpointer item, + gpointer user_data +); + +void koto_audiobook_view_handle_play_clicked( + GtkButton * button, + gpointer user_data +); + +void koto_audiobook_view_handle_playlist_updated( + KotoPlaylist * playlist, + gpointer user_data +); + +void koto_audiobook_view_update_side_info(KotoAudiobookView * self); + +void koto_audiobook_view_destroy_associated_user_data(gpointer user_data); + +KotoAudiobookView * koto_audiobook_view_new(); + +G_END_DECLS \ No newline at end of file diff --git a/src/pages/audiobooks/genre-button.c b/src/pages/audiobooks/genre-button.c new file mode 100644 index 0000000..a706a5e --- /dev/null +++ b/src/pages/audiobooks/genre-button.c @@ -0,0 +1,216 @@ +/* genres-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 "../../components/button.h" +#include "../../koto-utils.h" +#include "genre-button.h" + +enum { + PROP_0, + PROP_GENRE_ID, + PROP_GENRE_NAME, + N_PROPS +}; + +static GParamSpec * genre_button_props[N_PROPS] = { + NULL, +}; + +struct _KotoAudiobooksGenreButton { + GtkBox parent_instance; + + gchar * genre_id; + gchar * genre_name; + + GtkWidget * bg_image; + KotoButton * button; +}; + +struct _KotoAudiobooksGenreButtonClass { + GtkBoxClass parent_class; +}; + +G_DEFINE_TYPE(KotoAudiobooksGenreButton, koto_audiobooks_genre_button, GTK_TYPE_BOX); + +static void koto_audiobooks_genre_button_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +); + +static void koto_audiobooks_genre_button_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +); + +static void koto_audiobooks_genre_button_class_init(KotoAudiobooksGenreButtonClass * c) { + GObjectClass * gobject_class; + + gobject_class = G_OBJECT_CLASS(c); + gobject_class->set_property = koto_audiobooks_genre_button_set_property; + gobject_class->get_property = koto_audiobooks_genre_button_get_property; + + genre_button_props[PROP_GENRE_ID] = g_param_spec_string( + "genre-id", + "Genre ID", + "Genre ID", + NULL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + genre_button_props[PROP_GENRE_NAME] = g_param_spec_string( + "genre-name", + "Genre Name", + "Genre Name", + NULL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + g_object_class_install_properties(gobject_class, N_PROPS, genre_button_props); +} + +static void koto_audiobooks_genre_button_init(KotoAudiobooksGenreButton * self) { + gtk_widget_add_css_class(GTK_WIDGET(self), "audiobook-genre-button"); + gtk_widget_set_hexpand(GTK_WIDGET(self), FALSE); + gtk_widget_set_vexpand(GTK_WIDGET(self), FALSE); + + gtk_widget_set_size_request(GTK_WIDGET(self), 260, 120); + GtkWidget * overlay = gtk_overlay_new(); // Create our overlay + self->bg_image = gtk_picture_new(); // Create our new image + + self->button = koto_button_new_plain(NULL); // Create a new button + koto_button_set_text_wrap(self->button, TRUE); // Enable text wrapping for long genre lines + gtk_widget_set_valign(GTK_WIDGET(self->button), GTK_ALIGN_END); // Align towards bottom of button + + gtk_widget_set_hexpand(overlay, TRUE); + + gtk_overlay_set_child(GTK_OVERLAY(overlay), self->bg_image); + gtk_overlay_add_overlay(GTK_OVERLAY(overlay), GTK_WIDGET(self->button)); + + gtk_box_append(GTK_BOX(self), overlay); // Add the overlay to self +} + +static void koto_audiobooks_genre_button_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +) { + KotoAudiobooksGenreButton * self = KOTO_AUDIOBOOKS_GENRE_BUTTON(obj); + + switch (prop_id) { + case PROP_GENRE_ID: + g_value_set_string(val, self->genre_id); + break; + case PROP_GENRE_NAME: + g_value_set_string(val, self->genre_name); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_audiobooks_genre_button_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +) { + KotoAudiobooksGenreButton * self = KOTO_AUDIOBOOKS_GENRE_BUTTON(obj); + + switch (prop_id) { + case PROP_GENRE_ID: + koto_audiobooks_genre_button_set_id(self, g_strdup(g_value_get_string(val))); + break; + case PROP_GENRE_NAME: + koto_audiobooks_genre_button_set_name(self, g_strdup(g_value_get_string(val))); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +KotoButton * koto_audiobooks_genre_button_get_button(KotoAudiobooksGenreButton * self) { + return KOTO_IS_AUDIOBOOKS_GENRE_BUTTON(self) ? self->button : NULL; +} + +gchar * koto_audiobooks_genre_button_get_id(KotoAudiobooksGenreButton * self) { + return KOTO_IS_AUDIOBOOKS_GENRE_BUTTON(self) ? self->genre_id : NULL; +} + +gchar * koto_audiobooks_genre_button_get_name(KotoAudiobooksGenreButton * self) { + return KOTO_IS_AUDIOBOOKS_GENRE_BUTTON(self) ? self->genre_name : NULL; +} + +void koto_audiobooks_genre_button_set_id( + KotoAudiobooksGenreButton * self, + gchar * genre_id +) { + if (!KOTO_IS_AUDIOBOOKS_GENRE_BUTTON(self)) { + return; + } + + if (!koto_utils_string_is_valid(genre_id)) { + return; + } + + self->genre_id = genre_id; + + gtk_picture_set_resource(GTK_PICTURE(self->bg_image), g_strdup_printf("/com/github/joshstrobl/koto/%s.png", genre_id)); + gtk_widget_set_size_request(self->bg_image, 260, 120); +} + +void koto_audiobooks_genre_button_set_name( + KotoAudiobooksGenreButton * self, + gchar * genre_name +) { + if (!KOTO_IS_AUDIOBOOKS_GENRE_BUTTON(self)) { + return; + } + + if (!koto_utils_string_is_valid(genre_name)) { + return; + } + + if (koto_utils_string_is_valid(self->genre_name)) { // Already have a genre name + g_free(self->genre_name); + } + + self->genre_name = genre_name; + koto_button_set_text(self->button, genre_name); +} + +KotoAudiobooksGenreButton * koto_audiobooks_genre_button_new( + gchar * genre_id, + gchar * genre_name +) { + return g_object_new( + KOTO_TYPE_AUDIOBOOKS_GENRE_BUTTON, + "genre-id", + genre_id, + "genre-name", + genre_name, + NULL + ); +} \ No newline at end of file diff --git a/src/pages/audiobooks/genre-button.h b/src/pages/audiobooks/genre-button.h new file mode 100644 index 0000000..dc98a4b --- /dev/null +++ b/src/pages/audiobooks/genre-button.h @@ -0,0 +1,49 @@ +/* genres-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 "../../components/button.h" + +G_BEGIN_DECLS + +#define KOTO_TYPE_AUDIOBOOKS_GENRE_BUTTON koto_audiobooks_genre_button_get_type() +G_DECLARE_FINAL_TYPE(KotoAudiobooksGenreButton, koto_audiobooks_genre_button, KOTO, AUDIOBOOKS_GENRE_BUTTON, GtkBox) +#define KOTO_IS_AUDIOBOOKS_GENRE_BUTTON(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_AUDIOBOOKS_GENRE_BUTTON)) + +KotoButton * koto_audiobooks_genre_button_get_button(KotoAudiobooksGenreButton * self); + +gchar * koto_audiobooks_genre_button_get_id(KotoAudiobooksGenreButton * self); + +gchar * koto_audiobooks_genre_button_get_name(KotoAudiobooksGenreButton * self); + +void koto_audiobooks_genre_button_set_id( + KotoAudiobooksGenreButton * self, + gchar * genre_id +); + +void koto_audiobooks_genre_button_set_name( + KotoAudiobooksGenreButton * self, + gchar * genre_name +); + +KotoAudiobooksGenreButton * koto_audiobooks_genre_button_new( + gchar * genre_id, + gchar * genre_name +); + +G_END_DECLS \ No newline at end of file diff --git a/src/pages/audiobooks/genres-banner.c b/src/pages/audiobooks/genres-banner.c new file mode 100644 index 0000000..72d6374 --- /dev/null +++ b/src/pages/audiobooks/genres-banner.c @@ -0,0 +1,153 @@ +/* genres-banner.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/button.h" +#include "../../koto-utils.h" +#include "genre-button.h" +#include "genres-banner.h" + +enum { + SIGNAL_GENRE_CLICKED, + N_SIGNALS +}; + +static guint banner_signals[N_SIGNALS] = { + 0 +}; + +struct _KotoAudiobooksGenresBanner { + GObject parent_instance; + + GtkWidget * main; // Our main content + + GtkWidget * banner_revealer; // Our GtkRevealer for the strip + GtkWidget * banner_viewport; // Our banner viewport + GtkWidget * banner_content; // Our banner content as a horizontal box + + GtkWidget * strip_revealer; // Our GtkRevealer for the strip + GtkWidget * strip_viewport; // Our strip viewport + GtkWidget * strip_content; // Our strip content + + GHashTable * genre_ids_to_names; // Our genre "ids" to the name + GHashTable * genre_ids_to_ptrs; // Our HashTable of genre IDs to our GPtrArray with pointers to banner and strip buttons +}; + +struct _KotoAudiobooksGenresBannerClass { + GObjectClass parent_instance; + void (* genre_clicked) (gchar * genre); +}; + +G_DEFINE_TYPE(KotoAudiobooksGenresBanner, koto_audiobooks_genres_banner, G_TYPE_OBJECT); + +static void koto_audiobooks_genres_banner_class_init(KotoAudiobooksGenresBannerClass * c) { + GObjectClass * gobject_class = G_OBJECT_CLASS(c); + + banner_signals[SIGNAL_GENRE_CLICKED] = g_signal_new( + "genre-clicked", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoAudiobooksGenresBannerClass, genre_clicked), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_CHAR + ); +} + +static void koto_audiobooks_genres_banner_init(KotoAudiobooksGenresBanner * self) { + self->genre_ids_to_ptrs = g_hash_table_new(g_str_hash, g_str_equal); + self->genre_ids_to_names = g_hash_table_new(g_str_hash, g_str_equal); + g_hash_table_insert(self->genre_ids_to_names, g_strdup("hip-hop"), g_strdup("Hip-hop")); + g_hash_table_insert(self->genre_ids_to_names, g_strdup("indie"), g_strdup("Indie")); + g_hash_table_insert(self->genre_ids_to_names, g_strdup("sci-fi"), g_strdup("Science Fiction")); + + self->main = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); // Create the main box as a vertical box + gtk_widget_add_css_class(self->main, "genres-banner"); + gtk_widget_set_halign(self->main, GTK_ALIGN_START); + gtk_widget_set_vexpand(self->main, FALSE); + + self->banner_revealer = gtk_revealer_new(); // Create our revealer for the banner + self->strip_revealer = gtk_revealer_new(); // Create a revealer for the strip + + gtk_revealer_set_transition_type(GTK_REVEALER(self->strip_revealer), GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN); + gtk_revealer_set_transition_type(GTK_REVEALER(self->banner_revealer), GTK_REVEALER_TRANSITION_TYPE_SLIDE_UP); + + self->banner_viewport = gtk_viewport_new(NULL, NULL); // Create our viewport for enabling the banner content to be scrollable + self->strip_viewport = gtk_viewport_new(NULL, NULL); // Create our viewport for enabling the strip content to be scrollable + + self->banner_content = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); // Create our horizontal box for the content + gtk_widget_add_css_class(self->banner_content, "large-banner"); + + self->strip_content = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); // Create the horizontal box for the strip content + gtk_widget_add_css_class(self->strip_content, "strip"); + + gtk_revealer_set_child(GTK_REVEALER(self->banner_revealer), self->banner_viewport); // Set the viewport to be the content which gets revealed + gtk_revealer_set_child(GTK_REVEALER(self->strip_revealer), self->strip_viewport); // Set the viewport to be the cont ent which gets revealed + gtk_revealer_set_reveal_child(GTK_REVEALER(self->banner_revealer), TRUE); // Show the banner by default + + gtk_viewport_set_child(GTK_VIEWPORT(self->banner_viewport), self->banner_content); // Set the banner content for this viewport + gtk_viewport_set_child(GTK_VIEWPORT(self->strip_viewport), self->strip_content); // Set the strip content for this viewport + + gtk_box_append(GTK_BOX(self->main), self->strip_revealer); + gtk_box_append(GTK_BOX(self->main), self->banner_revealer); +} + +void koto_audiobooks_genres_banner_add_genre( + KotoAudiobooksGenresBanner * self, + gchar * genre_id +) { + if (!KOTO_IS_AUDIOBOOKS_GENRES_BANNER(self)) { // Not a banner + return; + } + + if (!koto_utils_string_is_valid(genre_id)) { // Not a valid genre ID + return; + } + + if (g_hash_table_contains(self->genre_ids_to_ptrs, genre_id)) { // Already have added this + return; + } + + gchar * name = g_hash_table_lookup(self->genre_ids_to_names, genre_id); // Get the named equivelant for this ID + + if (name == NULL) { // Didn't find a name equivelant + name = g_strdup(genre_id); // Just duplicate the genre ID for now + } + + KotoButton * genre_strip_button = koto_button_new_plain(name); // Create a new button with the name + gtk_box_append(GTK_BOX(self->strip_content), GTK_WIDGET(genre_strip_button)); // Add our KotoButton to the strip content + KotoAudiobooksGenreButton * genre_banner_button = koto_audiobooks_genre_button_new(genre_id, name); // Create our big button + gtk_box_append(GTK_BOX(self->banner_content), GTK_WIDGET(genre_banner_button)); + + GPtrArray * buttons = g_ptr_array_new(); + g_ptr_array_add(buttons, (gpointer) genre_strip_button); // Add our KotoButton as a gpointer to our GPtrArray as the first item + g_ptr_array_add(buttons, (gpointer) genre_banner_button); // Add our KotoAudiobooksGenresButton as a gpointer to our GPtrArray as the second item + + g_hash_table_replace(self->genre_ids_to_ptrs, genre_id, buttons); // Add our GPtrArray to our genre_ids_to_ptrs +} + +GtkWidget * koto_audiobooks_genres_banner_get_main(KotoAudiobooksGenresBanner * self) { + return KOTO_IS_AUDIOBOOKS_GENRES_BANNER(self) ? self->main : NULL; +} + +KotoAudiobooksGenresBanner * koto_audiobooks_genres_banner_new() { + return g_object_new(KOTO_TYPE_AUDIOBOOKS_GENRE_BANNER, NULL); +} \ No newline at end of file diff --git a/src/pages/audiobooks/genres-banner.h b/src/pages/audiobooks/genres-banner.h new file mode 100644 index 0000000..7a4e1b7 --- /dev/null +++ b/src/pages/audiobooks/genres-banner.h @@ -0,0 +1,44 @@ +/* genres-banner.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 + +#define KOTO_TYPE_AUDIOBOOKS_GENRE_BANNER koto_audiobooks_genres_banner_get_type() +#define KOTO_AUDIOBOOKS_GENRES_BANNER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), KOTO_TYPE_AUDIOBOOKS_GENRE_BANNER, KotoAudiobooksGenresBanner)) +typedef struct _KotoAudiobooksGenresBanner KotoAudiobooksGenresBanner; +typedef struct _KotoAudiobooksGenresBannerClass KotoAudiobooksGenresBannerClass; + +GLIB_AVAILABLE_IN_ALL +GType koto_audiobooks_genres_banner_get_type(void) G_GNUC_CONST; + +#define KOTO_IS_AUDIOBOOKS_GENRES_BANNER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_AUDIOBOOKS_GENRE_BANNER)) + +void koto_audiobooks_genres_banner_add_genre( + KotoAudiobooksGenresBanner * self, + gchar * genre_id +); + +GtkWidget * koto_audiobooks_genres_banner_get_main(KotoAudiobooksGenresBanner * self); + +KotoAudiobooksGenresBanner * koto_audiobooks_genres_banner_new(); + +G_END_DECLS \ No newline at end of file diff --git a/src/pages/audiobooks/library.c b/src/pages/audiobooks/library.c new file mode 100644 index 0000000..b6c9864 --- /dev/null +++ b/src/pages/audiobooks/library.c @@ -0,0 +1,213 @@ +/* library.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/button.h" +#include "../../db/cartographer.h" +#include "../../indexer/structs.h" +#include "../../koto-utils.h" +#include "../../koto-window.h" +#include "genres-banner.h" +#include "library.h" +#include "writer-page.h" + +extern KotoCartographer * koto_maps; +extern KotoWindow * main_window; + +struct _KotoAudiobooksLibraryPage { + GObject parent_instance; + + GtkWidget * main; // Our main content, contains banner and scrolled window + GtkWidget * content_scroll; // Our Scrolled Window + GtkWidget * content; // Content inside scrolled window + + KotoAudiobooksGenresBanner * banner; + + GtkWidget * writers_flow; + GHashTable * writers_to_buttons; // HashTable of UUIDs of "Artists" to their KotoButton + GHashTable * writers_to_pages; // HashTable of UUIDs of "Artists" to their KotoAudiobooksWritersPage +}; + +struct _KotoAudiobooksLibraryPageClass { + GObjectClass parent_instance; +}; + +G_DEFINE_TYPE(KotoAudiobooksLibraryPage, koto_audiobooks_library_page, G_TYPE_OBJECT); + +KotoAudiobooksLibraryPage * audiobooks_library_page; + +static void koto_audiobooks_library_page_init(KotoAudiobooksLibraryPage * self) { + self->main = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_add_css_class(self->main, "audiobook-library"); + self->content_scroll = gtk_scrolled_window_new(); // Create our GtkScrolledWindow + self->content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_vexpand(self->content, TRUE); // Ensure content expands vertically to allow flowboxchild to take up as much space as it requests + gtk_widget_add_css_class(self->content, "content"); + + self->banner = koto_audiobooks_genres_banner_new(); // Create our banner + + self->writers_flow = gtk_flow_box_new(); // Create our flow box + //gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(self->writers_flow), TRUE); + gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(self->writers_flow), 100); // Set to a random amount that is not realistic, however GTK sets a default to 7 which is too small. + gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(self->writers_flow), GTK_SELECTION_NONE); + gtk_widget_add_css_class(self->writers_flow, "writers-button-flow"); + + self->writers_to_buttons = g_hash_table_new(g_str_hash, g_str_equal); + self->writers_to_pages = g_hash_table_new(g_str_hash, g_str_equal); + + g_signal_connect(koto_maps, "album-added", G_CALLBACK(koto_audiobooks_library_page_handle_add_album), self); // Notify when we have a new Album + g_signal_connect(koto_maps, "artist-added", G_CALLBACK(koto_audiobooks_library_page_handle_add_artist), self); // Notify when we have a new Artist + + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(self->content_scroll), self->content); // Add our content to the content scroll + gtk_box_append(GTK_BOX(self->main), koto_audiobooks_genres_banner_get_main(self->banner)); // Add the banner to the content + gtk_box_append(GTK_BOX(self->main), self->content_scroll); // Add our scroll window to the main content + gtk_box_append(GTK_BOX(self->content), self->writers_flow); // Add our flowbox to the content +} + +static void koto_audiobooks_library_page_class_init(KotoAudiobooksLibraryPageClass * c) { + (void) c; +} + +void koto_audiobooks_library_page_create_artist_ux( + KotoAudiobooksLibraryPage * self, + KotoArtist * artist +) { + if (!KOTO_IS_AUDIOBOOKS_LIBRARY_PAGE(self)) { // Not a AudiobooksLibraryPage + return; + } + + if (!KOTO_IS_ARTIST(artist)) { // Not an artist + return; + } + + gchar * artist_uuid = koto_artist_get_uuid(artist); // Get the artist UUID + + if (!g_hash_table_contains(self->writers_to_pages, artist_uuid)) { // Don't have the page + KotoWriterPage * writers_page = koto_writer_page_new(artist); + koto_window_add_page(main_window, artist_uuid, koto_writer_page_get_main(writers_page)); // Add the page to the stack + } + + if (!g_hash_table_contains(self->writers_to_buttons, artist_uuid)) { // Don't have the button for this + KotoButton * artist_button = koto_button_new_plain(koto_artist_get_name(artist)); // Create a KotoButton for this artist + koto_button_set_data(artist_button, artist_uuid); // Set the artist_uuid as the data for the button + + koto_button_set_text_justify(artist_button, GTK_JUSTIFY_CENTER); // Center the text + koto_button_set_text_wrap(artist_button, TRUE); + gtk_widget_add_css_class(GTK_WIDGET(artist_button), "writer-button"); + gtk_widget_set_size_request(GTK_WIDGET(artist_button), 260, 120); + + koto_button_add_click_handler(artist_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_button_global_page_nav_callback), artist_button); + + g_hash_table_replace(self->writers_to_buttons, artist_uuid, artist_button); // Add the button to the Writers to Buttons hashtable + gtk_flow_box_insert(GTK_FLOW_BOX(self->writers_flow), GTK_WIDGET(artist_button), -1); // Append the button to our flowbox + + GtkWidget * button_parent = gtk_widget_get_parent(GTK_WIDGET(artist_button)); // This is the GtkFlowboxChild that the button is in + gtk_widget_set_halign(button_parent, GTK_ALIGN_START); + } +} + +void koto_audiobooks_library_page_handle_add_album( + KotoCartographer * carto, + KotoAlbum * album, + KotoAudiobooksLibraryPage * self +) { + + if (!KOTO_IS_CARTOGRAPHER(carto)) { // Not cartographer + return; + } + + if (!KOTO_IS_ALBUM(album)) { // Not an album + return; + } + + if (!KOTO_IS_AUDIOBOOKS_LIBRARY_PAGE(self)) { // Not a AudiobooksLibraryPage + return; + } + + gchar * artist_uuid = koto_album_get_artist_uuid(album); // Get the Album's artist UUID + KotoArtist * artist = koto_cartographer_get_artist_by_uuid(koto_maps, artist_uuid); + + if (!KOTO_IS_ARTIST(artist)) { // Failed to get artist + return; + } + + if (koto_artist_get_lib_type(artist) != KOTO_LIBRARY_TYPE_AUDIOBOOK) { // Not in an Audiobook library + return; + } + + koto_audiobooks_library_page_add_genres(self, koto_album_get_genres(album)); // Add all the genres necessary for this album +} + +void koto_audiobooks_library_page_handle_add_artist( + KotoCartographer * carto, + KotoArtist * artist, + KotoAudiobooksLibraryPage * self +) { + if (!KOTO_IS_CARTOGRAPHER(carto)) { // Not cartographer + return; + } + + if (!KOTO_IS_ARTIST(artist)) { // Not an artist + return; + } + + if (!KOTO_IS_AUDIOBOOKS_LIBRARY_PAGE(self)) { // Not a AudiobooksLibraryPage + return; + } + + if (koto_artist_get_lib_type(artist) != KOTO_LIBRARY_TYPE_AUDIOBOOK) { // Not in an Audiobook library + return; + } + + koto_audiobooks_library_page_create_artist_ux(self, artist); // Create the UX for the artist if necessary +} + +void koto_audiobooks_library_page_add_genres( + KotoAudiobooksLibraryPage * self, + GList * genres +) { + if (!KOTO_IS_AUDIOBOOKS_LIBRARY_PAGE(self)) { // Not a AudiobooksLibraryPage + return; + } + + if (g_list_length(genres) == 0) { // No contents + return; + } + + GList * cur_list; + for (cur_list = genres; cur_list != NULL; cur_list = cur_list->next) { // Iterate over each genre + gchar * genre = (gchar*) cur_list->data; + if (!koto_utils_string_is_valid(genre)) { + continue; + } + + if (g_strcmp0(genre, "audiobook") == 0) { // Is generic + continue; + } + + koto_audiobooks_genres_banner_add_genre(self->banner, genre); // Add this genre + } +} + +GtkWidget * koto_audiobooks_library_page_get_main(KotoAudiobooksLibraryPage * self) { + return KOTO_IS_AUDIOBOOKS_LIBRARY_PAGE(self) ? self->main : NULL; +} + +KotoAudiobooksLibraryPage * koto_audiobooks_library_page_new() { + return g_object_new(KOTO_TYPE_AUDIOBOOKS_LIBRARY_PAGE, NULL); +} \ No newline at end of file diff --git a/src/pages/audiobooks/library.h b/src/pages/audiobooks/library.h new file mode 100644 index 0000000..88e633f --- /dev/null +++ b/src/pages/audiobooks/library.h @@ -0,0 +1,56 @@ +/* library.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 "../../indexer/structs.h" + +G_BEGIN_DECLS + +#define KOTO_TYPE_AUDIOBOOKS_LIBRARY_PAGE koto_audiobooks_library_page_get_type() +#define KOTO_AUDIOBOOKS_LIBRARY_PAGE(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), KOTO_TYPE_AUDIOBOOKS_LIBRARY_PAGE, KotoAudiobooksLibraryPage)) +typedef struct _KotoAudiobooksLibraryPage KotoAudiobooksLibraryPage; +typedef struct _KotoAudiobooksLibraryPageClass KotoAudiobooksLibraryPageClass; + +GLIB_AVAILABLE_IN_ALL +GType koto_audiobooks_library_page_get_type(void) G_GNUC_CONST; + +#define KOTO_IS_AUDIOBOOKS_LIBRARY_PAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_AUDIOBOOKS_LIBRARY_PAGE)) + +void koto_audiobooks_library_page_add_genres( + KotoAudiobooksLibraryPage * self, + GList * genres +); + +void koto_audiobooks_library_page_handle_add_album( + KotoCartographer * carto, + KotoAlbum * album, + KotoAudiobooksLibraryPage * self +); + +void koto_audiobooks_library_page_handle_add_artist( + KotoCartographer * carto, + KotoArtist * artist, + KotoAudiobooksLibraryPage * self +); + +GtkWidget * koto_audiobooks_library_page_get_main(KotoAudiobooksLibraryPage * self); + +KotoAudiobooksLibraryPage * koto_audiobooks_library_page_new(); \ No newline at end of file diff --git a/src/pages/audiobooks/writer-page.c b/src/pages/audiobooks/writer-page.c new file mode 100644 index 0000000..f0944ce --- /dev/null +++ b/src/pages/audiobooks/writer-page.c @@ -0,0 +1,206 @@ +/* writers-page.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 "../../indexer/structs.h" +#include "../../koto-utils.h" +#include "../../koto-window.h" +#include "audiobook-view.h" +#include "writer-page.h" + +extern KotoCartographer * koto_maps; +extern KotoWindow * main_window; + +enum { + PROP_0, + PROP_ARTIST, + N_PROPERTIES +}; + +static GParamSpec * props[N_PROPERTIES] = { + NULL, +}; + +struct _KotoWriterPage { + GObject parent_instance; + + KotoArtist * artist; + + GtkWidget * main; // Our main content will be a scrolled Scrolled Window + GtkWidget * content; // Content inside scrolled window + + GtkWidget * writers_header; // Header GtkLabel for the writer + GListModel * model; + + GtkWidget * audiobooks_flow; +}; + +struct _KotoWriterPageClass { + GObjectClass parent_class; +}; + +G_DEFINE_TYPE(KotoWriterPage, koto_writer_page, G_TYPE_OBJECT); + +static void koto_writer_page_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +); + +static void koto_writer_page_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +); + +static void koto_writer_page_class_init(KotoWriterPageClass * c) { + GObjectClass * gobject_class; + + gobject_class = G_OBJECT_CLASS(c); + gobject_class->set_property = koto_writer_page_set_property; + gobject_class->get_property = koto_writer_page_get_property; + + props[PROP_ARTIST] = g_param_spec_object( + "artist", + "Artist", + "Artist", + KOTO_TYPE_ARTIST, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + g_object_class_install_properties(gobject_class, N_PROPERTIES, props); +} + +static void koto_writer_page_init(KotoWriterPage * self) { + self->main = gtk_scrolled_window_new(); // Set main to be a scrolled window + self->content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_add_css_class(self->content, "writer-page"); + + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(self->main), self->content); // Set content to be the child of the scrolled window + + self->writers_header = gtk_label_new(NULL); // Create an empty label + gtk_widget_add_css_class(self->writers_header, "writer-header"); + gtk_widget_set_halign(self->writers_header, GTK_ALIGN_START); + + self->audiobooks_flow = gtk_flow_box_new(); // Create our flowbox of the audiobooks views + gtk_widget_add_css_class(self->audiobooks_flow, "audiobooks-flow"); + + gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(self->audiobooks_flow), 2); // Allow 2 to ensure adequate spacing for description + gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(self->audiobooks_flow), GTK_SELECTION_NONE); // Do not allow selection + + gtk_widget_set_hexpand(self->audiobooks_flow, TRUE); // Expand horizontally + gtk_widget_set_vexpand(self->audiobooks_flow, TRUE); // Expand vertically + + gtk_box_append(GTK_BOX(self->content), self->writers_header); + gtk_box_append(GTK_BOX(self->content), self->audiobooks_flow); // Add the audiobooks flow to the content +} + +static void koto_writer_page_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +) { + KotoWriterPage * self = KOTO_WRITER_PAGE(obj); + + switch (prop_id) { + case PROP_ARTIST: + g_value_set_object(val, self->artist); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_writer_page_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +) { + KotoWriterPage * self = KOTO_WRITER_PAGE(obj); + + switch (prop_id) { + case PROP_ARTIST: + koto_writer_page_set_artist(self, (KotoArtist*) g_value_get_object(val)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +GtkWidget * koto_writer_page_create_item( + gpointer item, + gpointer user_data +) { + (void) user_data; + + KotoAlbum * album = KOTO_ALBUM(item); // Cast the model data as an album + + if (!KOTO_IS_ALBUM(album)) { // Fetched item from list is not album + return NULL; + } + + KotoAudiobookView * view = koto_audiobook_view_new(); // Create our KotoAudiobookView + koto_audiobook_view_set_album(view, album); // Set the item for this set up audiobook view + return GTK_WIDGET(view); +} + +GtkWidget * koto_writer_page_get_main(KotoWriterPage * self) { + return self->main; +} + +void koto_writer_page_set_artist( + KotoWriterPage * self, + KotoArtist * artist +) { + if (!KOTO_IS_WRITER_PAGE(self)) { // Not a writers page + return; + } + + if (!KOTO_IS_ARTIST(artist)) { // Not an artist + return; + } + + self->artist = artist; + gtk_label_set_text(GTK_LABEL(self->writers_header), koto_artist_get_name(self->artist)); // Get the label for the writers header + + self->model = G_LIST_MODEL(koto_artist_get_albums_store(artist)); // Get the store and cast it as a list model for our model + + gtk_flow_box_bind_model( + GTK_FLOW_BOX(self->audiobooks_flow), + self->model, + koto_writer_page_create_item, + NULL, + NULL + ); +} + +KotoWriterPage * koto_writer_page_new(KotoArtist * artist) { + return g_object_new( + KOTO_TYPE_WRITER_PAGE, + "artist", + artist, + NULL + ); +} \ No newline at end of file diff --git a/src/pages/audiobooks/writer-page.h b/src/pages/audiobooks/writer-page.h new file mode 100644 index 0000000..2a2a913 --- /dev/null +++ b/src/pages/audiobooks/writer-page.h @@ -0,0 +1,41 @@ +/* writers-page.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 + +#define KOTO_TYPE_WRITER_PAGE (koto_writer_page_get_type()) +G_DECLARE_FINAL_TYPE(KotoWriterPage, koto_writer_page, KOTO, WRITER_PAGE, GObject) + +GtkWidget* koto_writer_page_create_item( + gpointer item, + gpointer user_data +); + +GtkWidget * koto_writer_page_get_main(KotoWriterPage * self); + +void koto_writer_page_set_artist( + KotoWriterPage * self, + KotoArtist * artist +); + +KotoWriterPage * koto_writer_page_new(KotoArtist * artist); + +G_END_DECLS \ No newline at end of file diff --git a/src/pages/music/album-view.c b/src/pages/music/album-view.c new file mode 100644 index 0000000..678f23b --- /dev/null +++ b/src/pages/music/album-view.c @@ -0,0 +1,297 @@ +/* album-view.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/album-info.h" +#include "../../components/button.h" +#include "../../components/cover-art-button.h" +#include "../../db/cartographer.h" +#include "../../indexer/structs.h" +#include "album-view.h" +#include "disc-view.h" +#include "config/config.h" +#include "koto-utils.h" + +extern KotoCartographer * koto_maps; + +struct _KotoAlbumView { + GObject parent_instance; + KotoAlbum * album; + GtkWidget * main; + GtkWidget * album_tracks_box; + GtkWidget * discs; + + KotoCoverArtButton * album_cover; + + KotoAlbumInfo * album_info; + GHashTable * cd_to_disc_views; +}; + +G_DEFINE_TYPE(KotoAlbumView, koto_album_view, G_TYPE_OBJECT); + +enum { + PROP_0, + PROP_ALBUM, + N_PROPERTIES +}; + +static GParamSpec * props[N_PROPERTIES] = { + NULL, +}; +static void koto_album_view_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +); + +static void koto_album_view_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +); + +static void koto_album_view_class_init(KotoAlbumViewClass * c) { + GObjectClass * gobject_class; + + gobject_class = G_OBJECT_CLASS(c); + gobject_class->set_property = koto_album_view_set_property; + gobject_class->get_property = koto_album_view_get_property; + + props[PROP_ALBUM] = g_param_spec_object( + "album", + "Album", + "Album", + KOTO_TYPE_ALBUM, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + g_object_class_install_properties(gobject_class, N_PROPERTIES, props); +} + +static void koto_album_view_init(KotoAlbumView * self) { + self->cd_to_disc_views = g_hash_table_new(g_str_hash, g_str_equal); + self->main = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_widget_add_css_class(self->main, "album-view"); + gtk_widget_set_can_focus(self->main, FALSE); + self->album_tracks_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + self->album_info = koto_album_info_new(KOTO_ALBUM_INFO_TYPE_ALBUM); // Create an Album-type Album Info + gtk_box_prepend(GTK_BOX(self->album_tracks_box), GTK_WIDGET(self->album_info)); // Add our Album Info to the album_tracks box + + 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 + gtk_box_append(GTK_BOX(self->album_tracks_box), self->discs); // Add the discs list box to the albums tracks box + + self->album_cover = koto_cover_art_button_new(220, 220, NULL); + GtkWidget * album_cover_main = koto_cover_art_button_get_main(self->album_cover); + + gtk_widget_set_valign(album_cover_main, GTK_ALIGN_START); + + gtk_box_prepend(GTK_BOX(self->main), album_cover_main); + KotoButton * cover_art_button = koto_cover_art_button_get_button(self->album_cover); // Get the button for the cover art + koto_button_add_click_handler(cover_art_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_album_view_toggle_album_playback), self); +} + +GtkWidget * koto_album_view_get_main(KotoAlbumView * self) { + return self->main; +} + +static void koto_album_view_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +) { + KotoAlbumView * self = KOTO_ALBUM_VIEW(obj); + + switch (prop_id) { + case PROP_ALBUM: + g_value_set_object(val, self->album); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_album_view_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +) { + KotoAlbumView * self = KOTO_ALBUM_VIEW(obj); + + switch (prop_id) { + case PROP_ALBUM: + koto_album_view_set_album(self, (KotoAlbum*) g_value_get_object(val)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +void koto_album_view_add_track( + KotoAlbumView * self, + KotoTrack * track +) { + if (!KOTO_IS_ALBUM_VIEW(self)) { // Not an album view + return; + } + + if (!KOTO_IS_ALBUM(self->album)) { // Somehow don't have an album set + return; + } + + if (!KOTO_IS_TRACK(track)) { // Track doesn't exist + return; + } + + guint disc_number = koto_track_get_disc_number(track); // Get the disc number + gchar * disc_num_as_str = g_strdup_printf("%u", disc_number); + + KotoDiscView * disc_view; + + if (g_hash_table_contains(self->cd_to_disc_views, disc_num_as_str)) { // Already have this added this disc and its disc view + disc_view = g_hash_table_lookup(self->cd_to_disc_views, disc_num_as_str); // Get the disc view + } else { + disc_view = koto_disc_view_new(self->album, disc_number); // Build a new disc view + gtk_list_box_append(GTK_LIST_BOX(self->discs), GTK_WIDGET(disc_view)); // Add the Disc View to the List Box + g_hash_table_replace(self->cd_to_disc_views, disc_num_as_str, disc_view); // Add the new Disc View to our hash table + } + + if (!KOTO_IS_DISC_VIEW(disc_view)) { // If this is not a Disc View + return; + } + + koto_disc_view_add_track(disc_view, track); // Add the track to the disc view + koto_album_view_update_disc_labels(self); // Update our disc labels +} + +void koto_album_view_handle_track_added( + KotoAlbum * album, + KotoTrack * track, + gpointer user_data +) { + if (!KOTO_IS_ALBUM(album)) { // If not an album + return; + } + + if (!KOTO_IS_TRACK(track)) { // If not a track + return; + } + + KotoAlbumView * self = KOTO_ALBUM_VIEW(user_data); // Define as an album view + + if (!KOTO_IS_ALBUM_VIEW(self)) { + return; + } + + koto_album_view_add_track(self, track); // Add the track +} + +void koto_album_view_set_album( + KotoAlbumView * self, + KotoAlbum * album +) { + if (!KOTO_IS_ALBUM_VIEW(self)) { + return; + } + + if (!KOTO_IS_ALBUM(album)) { + return; + } + + self->album = album; + + gchar * album_art = koto_album_get_art(self->album); // Get the art for the album + koto_cover_art_button_set_art_path(self->album_cover, album_art); + koto_album_info_set_album_uuid(self->album_info, koto_album_get_uuid(album)); // Set the album we should be displaying info about on the Album Info + g_signal_connect(self->album, "track-added", G_CALLBACK(koto_album_view_handle_track_added), self); // Handle track added on our album +} + +int koto_album_view_sort_discs( + GtkListBoxRow * disc1, + GtkListBoxRow * disc2, + gpointer user_data +) { + (void) user_data; + KotoDiscView * disc1_item = KOTO_DISC_VIEW(gtk_list_box_row_get_child(disc1)); + KotoDiscView * disc2_item = KOTO_DISC_VIEW(gtk_list_box_row_get_child(disc2)); + + guint disc1_num; + guint disc2_num; + + g_object_get(disc1_item, "disc", &disc1_num, NULL); + g_object_get(disc2_item, "disc", &disc2_num, NULL); + + if (disc1_num == disc2_num) { // Identical positions (like reported as 0) + return 0; + } else if (disc1_num < disc2_num) { + return -1; + } else { + return 1; + } +} + +void koto_album_view_toggle_album_playback( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer data +) { + (void) gesture; + (void) n_press; + (void) x; + (void) y; + KotoAlbumView * self = data; + + koto_album_set_as_current_playlist(self->album); // Set as the current playlist +} + +void koto_album_view_update_disc_labels(KotoAlbumView * self) { + gboolean show_disc_labels = g_hash_table_size(self->cd_to_disc_views) > 1; + gpointer disc_num_ptr; + gpointer disc_view_ptr; + + GHashTableIter disc_view_iter; + g_hash_table_iter_init(&disc_view_iter, self->cd_to_disc_views); + + while (g_hash_table_iter_next(&disc_view_iter, &disc_num_ptr, &disc_view_ptr)) { + (void) disc_num_ptr; + KotoDiscView * view = (KotoDiscView*) disc_view_ptr; + if (KOTO_IS_DISC_VIEW(view)) { + koto_disc_view_set_disc_label_visible(view, show_disc_labels); + } + } +} + +KotoAlbumView * koto_album_view_new(KotoAlbum * album) { + return g_object_new(KOTO_TYPE_ALBUM_VIEW, "album", album, NULL); +} diff --git a/src/pages/music/album-view.h b/src/pages/music/album-view.h new file mode 100644 index 0000000..cc55195 --- /dev/null +++ b/src/pages/music/album-view.h @@ -0,0 +1,65 @@ +/* album-view.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 + +#define KOTO_TYPE_ALBUM_VIEW (koto_album_view_get_type()) + +G_DECLARE_FINAL_TYPE(KotoAlbumView, koto_album_view, KOTO, ALBUM_VIEW, GObject) + +KotoAlbumView* koto_album_view_new(KotoAlbum * album); +GtkWidget * koto_album_view_get_main(KotoAlbumView * self); + +void koto_album_view_add_track( + KotoAlbumView * self, + KotoTrack * track +); + +void koto_album_view_handle_track_added( + KotoAlbum * album, + KotoTrack * track, + gpointer user_data +); + +void koto_album_view_set_album( + KotoAlbumView * self, + KotoAlbum * album +); + +int koto_album_view_sort_discs( + GtkListBoxRow * track1, + GtkListBoxRow * track2, + gpointer user_data +); + +void koto_album_view_toggle_album_playback( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer data +); + +void koto_album_view_update_disc_labels(KotoAlbumView * self); + +G_END_DECLS diff --git a/src/pages/music/artist-view.c b/src/pages/music/artist-view.c new file mode 100644 index 0000000..d0c4215 --- /dev/null +++ b/src/pages/music/artist-view.c @@ -0,0 +1,336 @@ +/* artist-view.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/cover-art-button.h" +#include "../../components/track-table.h" +#include "../../db/cartographer.h" +#include "../../indexer/artist-playlist-funcs.h" +#include "../../indexer/structs.h" +#include "../../playlist/current.h" +#include "album-view.h" +#include "artist-view.h" +#include "config/config.h" +#include "koto-utils.h" + +extern KotoCurrentPlaylist * current_playlist; +extern KotoCartographer * koto_maps; + +struct _KotoArtistView { + GObject parent_instance; + KotoArtist * artist; + GtkWidget * scrolled_window; + GtkWidget * content; + GtkWidget * album_list; + + GtkWidget * no_albums_view; + GtkWidget * no_albums_artist_header; + KotoCoverArtButton * no_albums_artist_button; + GtkWidget * no_albums_artist_label; + + KotoTrackTable * table; + GHashTable * albums_to_component; +}; + +G_DEFINE_TYPE(KotoArtistView, koto_artist_view, G_TYPE_OBJECT); + +enum { + PROP_0, + PROP_ARTIST, + N_PROPERTIES +}; + +static GParamSpec * props[N_PROPERTIES] = { + NULL, +}; + +static void koto_artist_view_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +); + +static void koto_artist_view_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +); + +static void koto_artist_view_class_init(KotoArtistViewClass * c) { + GObjectClass * gobject_class; + + gobject_class = G_OBJECT_CLASS(c); + gobject_class->set_property = koto_artist_view_set_property; + gobject_class->get_property = koto_artist_view_get_property; + + props[PROP_ARTIST] = g_param_spec_object( + "artist", + "Artist", + "Artist", + KOTO_TYPE_ARTIST, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + g_object_class_install_properties(gobject_class, N_PROPERTIES, props); +} + +static void koto_artist_view_init(KotoArtistView * self) { + self->artist = NULL; + self->albums_to_component = g_hash_table_new(g_str_hash, g_str_equal); + + self->scrolled_window = gtk_scrolled_window_new(); // Create our scrolled window + self->content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); // Create our content as a GtkBox + + gtk_scrolled_window_set_propagate_natural_height(GTK_SCROLLED_WINDOW(self->scrolled_window), TRUE); + gtk_scrolled_window_set_propagate_natural_width(GTK_SCROLLED_WINDOW(self->scrolled_window), TRUE); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(self->scrolled_window), self->content); // Add the content as the widget for the scrolled window + gtk_widget_add_css_class(GTK_WIDGET(self->scrolled_window), "artist-view"); + gtk_widget_add_css_class(GTK_WIDGET(self->content), "artist-view-content"); + + self->album_list = gtk_flow_box_new(); // Create our list of our albums + 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"); + + self->no_albums_view = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_add_css_class(self->no_albums_view, "no-albums-view"); + + self->no_albums_artist_header = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_widget_add_css_class(self->no_albums_artist_header, "no-albums-view-header"), + + self->no_albums_artist_button = koto_cover_art_button_new(220, 220, NULL); + KotoButton * cover_art_button = koto_cover_art_button_get_button(self->no_albums_artist_button); // Get the button for the KotoCoverArt + koto_button_add_click_handler(cover_art_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_artist_view_toggle_playback), self); // Handle clicking the button + + self->no_albums_artist_label = gtk_label_new(NULL); // Create a label without any artist name + + gtk_box_append(GTK_BOX(self->no_albums_artist_header), koto_cover_art_button_get_main(self->no_albums_artist_button)); // Add our button to the header + gtk_box_append(GTK_BOX(self->no_albums_artist_header), self->no_albums_artist_label); + + gtk_box_append(GTK_BOX(self->no_albums_view), self->no_albums_artist_header); // Add the header to the no albums view + + self->table = koto_track_table_new(); //Create our track table + gtk_box_append(GTK_BOX(self->no_albums_view), koto_track_table_get_main(self->table)); // Add the table to the no albums view + + gtk_box_append(GTK_BOX(self->content), self->album_list); // Add the album flowbox + gtk_box_append(GTK_BOX(self->content), self->no_albums_view); // Add the no albums view just in case we do not have any albums + + gtk_widget_set_hexpand(GTK_WIDGET(self->album_list), TRUE); + gtk_widget_set_hexpand(GTK_WIDGET(self->no_albums_view), TRUE); +} + +static void koto_artist_view_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +) { + KotoArtistView * self = KOTO_ARTIST_VIEW(obj); + + switch (prop_id) { + case PROP_ARTIST: + g_value_set_object(val, self->artist); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_artist_view_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +) { + KotoArtistView * self = KOTO_ARTIST_VIEW(obj); + + switch (prop_id) { + case PROP_ARTIST: + koto_artist_view_set_artist(self, (KotoArtist*) g_value_get_object(val)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +void koto_artist_view_add_album( + KotoArtistView * self, + KotoAlbum * album +) { + if (!KOTO_IS_ARTIST_VIEW(self)) { + return; + } + + if (!KOTO_IS_ALBUM(album)) { + return; + } + + gchar * album_uuid = koto_album_get_uuid(album); // Get the album UUID + + if (g_hash_table_contains(self->albums_to_component, album_uuid)) { // Already added this album + return; + } + + KotoAlbumView * album_view = koto_album_view_new(album); // Create our new album view + GtkWidget * album_view_main = koto_album_view_get_main(album_view); + + gtk_flow_box_insert(GTK_FLOW_BOX(self->album_list), album_view_main, -1); // Append the album view to the album list + g_hash_table_replace(self->albums_to_component, album_uuid, album_view_main); + + gtk_widget_hide(self->no_albums_view); // Hide the no album view + gtk_widget_show(self->album_list); // Show the album list now that it has albums +} + +GtkWidget * koto_artist_view_get_main(KotoArtistView * self) { + return self->scrolled_window; +} + +void koto_artist_view_handle_album_added( + KotoArtist * artist, + KotoAlbum * album, + gpointer user_data +) { + KotoArtistView * self = user_data; + + if (!KOTO_IS_ARTIST_VIEW(self)) { + return; + } + + if (!KOTO_IS_ARTIST(artist)) { + return; + } + + if (!KOTO_IS_ALBUM(album)) { + return; + } + + if (g_strcmp0(koto_artist_get_uuid(self->artist), koto_artist_get_uuid(artist)) != 0) { // Not the same artist + return; + } + + koto_artist_view_add_album(self, album); // Add the album if necessary +} + +void koto_artist_view_handle_album_removed( + KotoArtist * artist, + gchar * album_uuid, + gpointer user_data +) { + KotoArtistView * self = user_data; + + if (!KOTO_IS_ARTIST_VIEW(self)) { + return; + } + + if (g_strcmp0(koto_artist_get_uuid(self->artist), koto_artist_get_uuid(artist)) != 0) { // Not the same artist + return; + } + + GtkWidget * album_view = g_hash_table_lookup(self->albums_to_component, album_uuid); // Get the album view if it exists + + if (!GTK_IS_WIDGET(album_view)) { // Not a widget + return; + } + + gtk_flow_box_remove(GTK_FLOW_BOX(self->album_list), album_view); // Remove the album + g_hash_table_remove(self->albums_to_component, album_uuid); // Remove the album from our hash table +} + +void koto_artist_view_handle_artist_name_changed( + KotoArtist * artist, + guint prop_id, + KotoArtistView * self +) { + (void) prop_id; + gtk_label_set_text(GTK_LABEL(self->no_albums_artist_label), koto_artist_get_name(artist)); // Update the label for the artist now that it has changed +} + +void koto_artist_view_handle_has_no_albums( + KotoArtist * artist, + gpointer user_data +) { + (void) artist; + KotoArtistView * self = user_data; + + if (!KOTO_IS_ARTIST_VIEW(self)) { + return; + } + + gtk_widget_hide(self->album_list); // Hide our album list flowbox + gtk_widget_show(self->no_albums_view); // Show the no albums view +} + +void koto_artist_view_set_artist( + KotoArtistView * self, + KotoArtist * artist +) { + if (!KOTO_IS_ARTIST_VIEW(self)) { // Not an Artist view + return; + } + + if (!KOTO_IS_ARTIST(artist)) { + return; + } + + self->artist = artist; + koto_track_table_set_playlist(self->table, koto_artist_get_playlist(self->artist)); // Set our track table to the artist's playlist + gtk_label_set_text(GTK_LABEL(self->no_albums_artist_label), koto_artist_get_name(self->artist)); // Update our label with the name of the artist + + g_signal_connect(artist, "album-added", G_CALLBACK(koto_artist_view_handle_album_added), self); + g_signal_connect(artist, "album-removed", G_CALLBACK(koto_artist_view_handle_album_removed), self); + g_signal_connect(artist, "has-no-albums", G_CALLBACK(koto_artist_view_handle_has_no_albums), self); + g_signal_connect(artist, "notify::name", G_CALLBACK(koto_artist_view_handle_artist_name_changed), self); +} + +void koto_artist_view_toggle_playback( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer data +) { + (void) gesture; + (void) n_press; + (void) x; + (void) y; + KotoArtistView * self = data; + + if (!KOTO_IS_ARTIST_VIEW(self)) { + return; + } + + KotoPlaylist * artist_playlist = koto_artist_get_playlist(self->artist); + + if (!KOTO_IS_PLAYLIST(artist_playlist)) { // Failed our is playlist check for the artist playlist + return; + } + + koto_current_playlist_set_playlist(current_playlist, artist_playlist, TRUE, FALSE); // Set our playlist to the one associated with the Artist and start playback immediately +} + +KotoArtistView * koto_artist_view_new(KotoArtist * artist) { + return g_object_new( + KOTO_TYPE_ARTIST_VIEW, + "artist", + artist, + NULL + ); +} diff --git a/src/pages/music/artist-view.h b/src/pages/music/artist-view.h new file mode 100644 index 0000000..e5c6baa --- /dev/null +++ b/src/pages/music/artist-view.h @@ -0,0 +1,75 @@ +/* artist-view.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 + +#define KOTO_TYPE_ARTIST_VIEW (koto_artist_view_get_type()) + +G_DECLARE_FINAL_TYPE(KotoArtistView, koto_artist_view, KOTO, ARTIST_VIEW, GObject) + +KotoArtistView* koto_artist_view_new(KotoArtist * artist); + +void koto_artist_view_add_album( + KotoArtistView * self, + KotoAlbum * album +); + +GtkWidget * koto_artist_view_get_main(KotoArtistView * self); + +void koto_artist_view_handle_album_added( + KotoArtist * artist, + KotoAlbum * album, + gpointer user_data +); + +void koto_artist_view_handle_album_removed( + KotoArtist * artist, + gchar * album_uuid, + gpointer user_data +); + +void koto_artist_view_handle_artist_name_changed( + KotoArtist * artist, + guint prop_id, + KotoArtistView * self +); + +void koto_artist_view_handle_has_no_albums( + KotoArtist * artist, + gpointer user_data +); + +void koto_artist_view_set_artist( + KotoArtistView * self, + KotoArtist * artist +); + +void koto_artist_view_toggle_playback( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer data +); + +G_END_DECLS diff --git a/src/pages/music/disc-view.c b/src/pages/music/disc-view.c new file mode 100644 index 0000000..01a8971 --- /dev/null +++ b/src/pages/music/disc-view.c @@ -0,0 +1,326 @@ +/* disc-view.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 "../../components/action-bar.h" +#include "../../components/track-item.h" +#include "../../db/cartographer.h" +#include "../../indexer/track-helpers.h" +#include "../../indexer/structs.h" +#include "../../koto-utils.h" +#include "disc-view.h" + +extern KotoActionBar * action_bar; +extern KotoCartographer * koto_maps; + +struct _KotoDiscView { + GtkBox parent_instance; + KotoAlbum * album; + GtkWidget * header; + GtkWidget * label; + GtkWidget * list; + + GHashTable * tracks_to_items; + + guint disc_number; +}; + +G_DEFINE_TYPE(KotoDiscView, koto_disc_view, GTK_TYPE_BOX); + +enum { + PROP_0, + PROP_DISC, + PROP_ALBUM, + N_PROPERTIES +}; + +static GParamSpec * props[N_PROPERTIES] = { + NULL, +}; + +static void koto_disc_view_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +); + +static void koto_disc_view_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +); + +static void koto_disc_view_class_init(KotoDiscViewClass * c) { + GObjectClass * gobject_class; + + gobject_class = G_OBJECT_CLASS(c); + gobject_class->set_property = koto_disc_view_set_property; + gobject_class->get_property = koto_disc_view_get_property; + + props[PROP_DISC] = g_param_spec_uint( + "disc", + "Disc", + "Disc", + 0, + G_MAXUINT16, + 1, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_ALBUM] = g_param_spec_object( + "album", + "Album", + "Album", + KOTO_TYPE_ALBUM, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + g_object_class_install_properties(gobject_class, N_PROPERTIES, props); +} + +static void koto_disc_view_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +) { + KotoDiscView * self = KOTO_DISC_VIEW(obj); + + switch (prop_id) { + case PROP_DISC: + g_value_set_uint(val, GPOINTER_TO_UINT(self->disc_number)); + break; + case PROP_ALBUM: + g_value_set_object(val, self->album); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_disc_view_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +) { + KotoDiscView * self = KOTO_DISC_VIEW(obj); + + switch (prop_id) { + case PROP_DISC: + koto_disc_view_set_disc_number(self, g_value_get_uint(val)); + break; + case PROP_ALBUM: + koto_disc_view_set_album(self, (KotoAlbum*) g_value_get_object(val)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_disc_view_init(KotoDiscView * self) { + self->tracks_to_items = g_hash_table_new(g_str_hash, g_str_equal); + + gtk_widget_add_css_class(GTK_WIDGET(self), "disc-view"); + gtk_widget_set_can_focus(GTK_WIDGET(self), FALSE); + self->header = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_widget_set_hexpand(self->header, TRUE); + gtk_widget_set_size_request(self->header, 16, -1); + + GtkWidget * ico = gtk_image_new_from_icon_name("drive-optical-symbolic"); + + gtk_box_prepend(GTK_BOX(self->header), ico); + + self->label = gtk_label_new(NULL); // Create an empty label + gtk_widget_set_halign(self->label, GTK_ALIGN_START); + gtk_widget_set_valign(self->label, GTK_ALIGN_CENTER); + gtk_box_append(GTK_BOX(self->header), self->label); + + gtk_box_append(GTK_BOX(self), self->header); + + 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_list_box_set_sort_func(GTK_LIST_BOX(self->list), koto_disc_view_sort_list_box_rows, self, NULL); + 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_signal_connect(self->list, "selected-rows-changed", G_CALLBACK(koto_disc_view_handle_selected_rows_changed), self); +} + +void koto_disc_view_add_track( + KotoDiscView * self, + KotoTrack * track +) { + if (!KOTO_IS_DISC_VIEW(self)) { // If this is not a disc view + return; + } + + if (!KOTO_IS_TRACK(track)) { // If this is not a track + return; + } + + if (self->disc_number != koto_track_get_disc_number(track)) { // Not the same disc + return; + } + + gchar * track_uuid = koto_track_get_uuid(track); // Get the track UUID + + if (g_hash_table_contains(self->tracks_to_items, track_uuid)) { // Already have added this track + return; + } + + KotoTrackItem * track_item = koto_track_item_new(track); // Create our new track item + + if (!KOTO_IS_TRACK_ITEM(track_item)) { // Failed to create a track item for this track + return; + } + + gtk_list_box_append(GTK_LIST_BOX(self->list), GTK_WIDGET(track_item)); // Add to our tracks list box + g_hash_table_replace(self->tracks_to_items, track_uuid, track_item); // Add to our hash table so we know we have added this +} + +void koto_disc_view_handle_selected_rows_changed( + GtkListBox * box, + gpointer user_data +) { + KotoDiscView * self = user_data; + + if (!KOTO_IS_DISC_VIEW(self)) { + return; + } + + gchar * album_uuid = koto_album_get_uuid(self->album); // Get the UUID + + if (!koto_utils_string_is_valid(album_uuid)) { // 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 KotoTracks + 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 KotoTrack 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_remove_track( + KotoDiscView * self, + gchar * track_uuid +) { + if (!KOTO_IS_DISC_VIEW(self)) { // If this is not a disc view + return; + } + + if (!koto_utils_string_is_valid(track_uuid)) { // If our UUID is not a valid + return; + } + + KotoTrackItem * track_item = g_hash_table_lookup(self->tracks_to_items, track_uuid); // Get the track item + + if (!KOTO_IS_TRACK_ITEM(track_item)) { // Track item not valid + return; + } + + gtk_list_box_remove(GTK_LIST_BOX(self->list), GTK_WIDGET(track_item)); // Remove the item from the listbox + g_hash_table_remove(self->tracks_to_items, track_uuid); // Remove the track from the hashtable +} + +void koto_disc_view_set_album( + KotoDiscView * self, + KotoAlbum * album +) { + if (album == NULL) { + return; + } + + if (self->album != NULL) { + g_free(self->album); + } + + self->album = album; +} + +void koto_disc_view_set_disc_number( + KotoDiscView * self, + guint disc_number +) { + if (disc_number == 0) { + return; + } + + self->disc_number = disc_number; + + gchar * disc_label = g_strdup_printf("Disc %u", disc_number); + + gtk_label_set_text(GTK_LABEL(self->label), disc_label); // Set the label + + g_free(disc_label); +} + +void koto_disc_view_set_disc_label_visible( + KotoDiscView * self, + gboolean visible +) { + (visible) ? gtk_widget_show(self->header) : gtk_widget_hide(self->header); +} + +gint koto_disc_view_sort_list_box_rows( + GtkListBoxRow * row1, + GtkListBoxRow * row2, + gpointer user_data +) { + KotoTrackItem * track1_item = KOTO_TRACK_ITEM(gtk_list_box_row_get_child(row1)); + KotoTrackItem * track2_item = KOTO_TRACK_ITEM(gtk_list_box_row_get_child(row2)); + return koto_track_helpers_sort_track_items(track1_item, track2_item, user_data); +} + +KotoDiscView * koto_disc_view_new( + KotoAlbum * album, + guint disc_number +) { + return g_object_new( + KOTO_TYPE_DISC_VIEW, + "disc", + disc_number, + "album", + album, + "orientation", + GTK_ORIENTATION_VERTICAL, + NULL + ); +} diff --git a/src/pages/music/disc-view.h b/src/pages/music/disc-view.h new file mode 100644 index 0000000..35e7bcd --- /dev/null +++ b/src/pages/music/disc-view.h @@ -0,0 +1,66 @@ +/* disc-view.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 + +#define KOTO_TYPE_DISC_VIEW (koto_disc_view_get_type()) + +G_DECLARE_FINAL_TYPE(KotoDiscView, koto_disc_view, KOTO, DISC_VIEW, GtkBox) + +KotoDiscView* koto_disc_view_new( + KotoAlbum * album, + guint disc +); + +void koto_disc_view_add_track( + KotoDiscView * self, + KotoTrack * track +); + +void koto_disc_view_handle_selected_rows_changed( + GtkListBox * box, + gpointer user_data +); + +void koto_disc_view_set_album( + KotoDiscView * self, + KotoAlbum * album +); + +void koto_disc_view_set_disc_label_visible( + KotoDiscView * self, + gboolean visible +); + +void koto_disc_view_set_disc_number( + KotoDiscView * self, + guint disc_number +); + +gint koto_disc_view_sort_list_box_rows( + GtkListBoxRow * row1, + GtkListBoxRow * row2, + gpointer user_data +); + +G_END_DECLS diff --git a/src/pages/music/music-local.c b/src/pages/music/music-local.c new file mode 100644 index 0000000..b979817 --- /dev/null +++ b/src/pages/music/music-local.c @@ -0,0 +1,252 @@ +/* music-local.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/button.h" +#include "../../db/cartographer.h" +#include "../../indexer/structs.h" +#include "config/config.h" +#include "../../koto-utils.h" +#include "music-local.h" + +extern KotoCartographer * koto_maps; + +struct _KotoPageMusicLocal { + GtkBox parent_instance; + GtkWidget * scrolled_window; + GtkWidget * artist_list; + GtkWidget * stack; + GHashTable * artist_name_to_buttons; + + gboolean constructed; +}; + +struct _KotoPageMusicLocalClass { + GtkBoxClass parent_class; +}; + +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_class_init(KotoPageMusicLocalClass * c) { + GObjectClass * gobject_class; + + gobject_class = G_OBJECT_CLASS(c); + gobject_class->constructed = koto_page_music_local_constructed; +} + +static void koto_page_music_local_init(KotoPageMusicLocal * self) { + self->constructed = FALSE; + self->artist_name_to_buttons = g_hash_table_new(g_str_hash, g_str_equal); + + gtk_widget_add_css_class(GTK_WIDGET(self), "page-music-local"); + gtk_widget_set_hexpand(GTK_WIDGET(self), TRUE); + gtk_widget_set_vexpand(GTK_WIDGET(self), TRUE); + + self->scrolled_window = gtk_scrolled_window_new(); + gtk_scrolled_window_set_min_content_width(GTK_SCROLLED_WINDOW(self->scrolled_window), 300); + gtk_scrolled_window_set_propagate_natural_height(GTK_SCROLLED_WINDOW(self->scrolled_window), FALSE); + gtk_widget_add_css_class(self->scrolled_window, "artist-list"); + gtk_widget_set_vexpand(self->scrolled_window, TRUE); // Expand our scrolled window + gtk_box_prepend(GTK_BOX(self), self->scrolled_window); + + self->artist_list = gtk_list_box_new(); // Create our artist list + g_signal_connect(GTK_LIST_BOX(self->artist_list), "row-activated", G_CALLBACK(koto_page_music_local_handle_artist_click), self); + gtk_list_box_set_activate_on_single_click(GTK_LIST_BOX(self->artist_list), TRUE); + gtk_list_box_set_selection_mode(GTK_LIST_BOX(self->artist_list), GTK_SELECTION_BROWSE); + gtk_list_box_set_sort_func(GTK_LIST_BOX(self->artist_list), koto_page_music_local_sort_artists, NULL, NULL); // Add our sort function + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(self->scrolled_window), self->artist_list); + + self->stack = gtk_stack_new(); // Create a new stack + gtk_stack_set_transition_duration(GTK_STACK(self->stack), 400); + gtk_stack_set_transition_type(GTK_STACK(self->stack), GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT); + gtk_widget_set_hexpand(self->stack, TRUE); + gtk_widget_set_vexpand(self->stack, TRUE); + gtk_box_append(GTK_BOX(self), self->stack); + + g_signal_connect(koto_maps, "artist-added", G_CALLBACK(koto_page_music_local_handle_artist_added), self); + g_signal_connect(koto_maps, "artist-removed", G_CALLBACK(koto_page_music_local_handle_artist_removed), self); +} + +static void koto_page_music_local_constructed(GObject * obj) { + KotoPageMusicLocal * self = KOTO_PAGE_MUSIC_LOCAL(obj); + + G_OBJECT_CLASS(koto_page_music_local_parent_class)->constructed(obj); + self->constructed = TRUE; +} + +void koto_page_music_local_add_artist( + KotoPageMusicLocal * self, + KotoArtist * artist +) { + if (!KOTO_IS_PAGE_MUSIC_LOCAL(self)) { // Not the local page + return; + } + + if (!KOTO_IS_ARTIST(artist)) { // Not an artist + return; + } + + gchar * artist_name = koto_artist_get_name(artist); // Get the artist name + + if (!GTK_IS_STACK(self->stack)) { + return; + } + + GtkWidget * stack_child = gtk_stack_get_child_by_name(GTK_STACK(self->stack), artist_name); + + if (GTK_IS_WIDGET(stack_child)) { // Already have a page for this artist + g_free(artist_name); + return; + } + + KotoButton * artist_button = koto_button_new_plain(artist_name); + gtk_list_box_prepend(GTK_LIST_BOX(self->artist_list), GTK_WIDGET(artist_button)); + g_hash_table_replace(self->artist_name_to_buttons, artist_name, artist_button); // Add the KotoButton for this artist to the hash table, that way we can reference and remove it when we remove the artist + + KotoArtistView * artist_view = koto_artist_view_new(artist); // Create our new artist view + + gtk_stack_add_named(GTK_STACK(self->stack), koto_artist_view_get_main(artist_view), artist_name); + g_free(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 +) { + KotoArtist * artist = koto_cartographer_get_artist_by_uuid(koto_maps, artist_uuid); // Get the artist + + if (!KOTO_IS_ARTIST(artist)) { // No artist for this UUID + return; + } + + gchar * artist_name = NULL; + + g_object_get( + artist, + "name", + &artist_name, + NULL + ); + + if (!koto_utils_string_is_valid(artist_name)) { // 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; + KotoButton * btn = KOTO_BUTTON(gtk_list_box_row_get_child(row)); + + gchar * artist_name; + + g_object_get(btn, "button-text", &artist_name, NULL); + koto_page_music_local_go_to_artist_by_name(self, artist_name); +} + +void koto_page_music_local_handle_artist_added( + KotoCartographer * carto, + KotoArtist * artist, + gpointer user_data +) { + (void) carto; + KotoPageMusicLocal * self = user_data; + + if (!KOTO_IS_PAGE_MUSIC_LOCAL(self)) { // Not the page + return; + } + + if (!KOTO_IS_ARTIST(artist)) { // Not an artist + return; + } + + if (koto_artist_get_lib_type(artist) != KOTO_LIBRARY_TYPE_MUSIC) { // Not in our music library + return; + } + + koto_page_music_local_add_artist(self, artist); // Add the artist if needed +} + +void koto_page_music_local_handle_artist_removed( + KotoCartographer * carto, + gchar * artist_uuid, + gchar * artist_name, + gpointer user_data +) { + (void) carto; + (void) artist_uuid; + + KotoPageMusicLocal * self = user_data; + + GtkWidget * existing_artist_page = gtk_stack_get_child_by_name(GTK_STACK(self->stack), artist_name); + + // TODO: Navigate away from artist if we are currently looking at it + if (GTK_IS_WIDGET(existing_artist_page)) { // Page exists + gtk_stack_remove(GTK_STACK(self->stack), existing_artist_page); // Remove the artist page + } + + KotoButton * btn = g_hash_table_lookup(self->artist_name_to_buttons, artist_name); + + if (KOTO_IS_BUTTON(btn)) { // If we have a button for this artist + gtk_list_box_remove(GTK_LIST_BOX(self->artist_list), GTK_WIDGET(btn)); // Remove the button + } +} + +int koto_page_music_local_sort_artists( + GtkListBoxRow * artist1, + GtkListBoxRow * artist2, + gpointer user_data +) { + (void) user_data; + KotoButton * artist1_btn = KOTO_BUTTON(gtk_list_box_row_get_child(artist1)); + KotoButton * artist2_btn = KOTO_BUTTON(gtk_list_box_row_get_child(artist2)); + + gchar * artist1_text; + gchar * artist2_text; + + g_object_get(artist1_btn, "button-text", &artist1_text, NULL); + g_object_get(artist2_btn, "button-text", &artist2_text, NULL); + + return g_utf8_collate(artist1_text, artist2_text); +} + +KotoPageMusicLocal * koto_page_music_local_new() { + return g_object_new( + KOTO_TYPE_PAGE_MUSIC_LOCAL, + "orientation", + GTK_ORIENTATION_HORIZONTAL, + NULL + ); +} diff --git a/src/pages/music/music-local.h b/src/pages/music/music-local.h new file mode 100644 index 0000000..1bef6d5 --- /dev/null +++ b/src/pages/music/music-local.h @@ -0,0 +1,72 @@ +/* music-local.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" +#include "artist-view.h" + +G_BEGIN_DECLS + +#define KOTO_TYPE_PAGE_MUSIC_LOCAL (koto_page_music_local_get_type()) + +G_DECLARE_FINAL_TYPE(KotoPageMusicLocal, koto_page_music_local, KOTO, PAGE_MUSIC_LOCAL, GtkBox) + +KotoPageMusicLocal* koto_page_music_local_new(); +void koto_page_music_local_add_artist( + KotoPageMusicLocal * self, + KotoArtist * artist +); + +void koto_page_music_local_handle_artist_click( + GtkListBox * box, + GtkListBoxRow * row, + gpointer data +); + +void koto_page_music_local_handle_artist_added( + KotoCartographer * carto, + KotoArtist * artist, + gpointer user_data +); + +void koto_page_music_local_handle_artist_removed( + KotoCartographer * carto, + gchar * artist_uuid, + gchar * artist_name, + gpointer user_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 +); + +int koto_page_music_local_sort_artists( + GtkListBoxRow * artist1, + GtkListBoxRow * artist2, + gpointer user_data +); + +G_END_DECLS diff --git a/src/pages/playlist/list.c b/src/pages/playlist/list.c new file mode 100644 index 0000000..b49b330 --- /dev/null +++ b/src/pages/playlist/list.c @@ -0,0 +1,324 @@ +/* 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/action-bar.h" +#include "../../components/button.h" +#include "../../components/cover-art-button.h" +#include "../../components/track-table.h" +#include "../../db/cartographer.h" +#include "../../indexer/structs.h" +#include "../../playlist/current.h" +#include "../../playlist/playlist.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; + + KotoTrackTable * table; +}; + +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->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->table = koto_track_table_new(); // Create our new KotoTrackTable + GtkWidget * track_main = koto_track_table_get_main(self->table); + gtk_box_append(GTK_BOX(self->content), track_main); +} + +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; + } +} + +GtkWidget * koto_playlist_page_get_main(KotoPlaylistPage * self) { + return self->main; +} + +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, TRUE, FALSE); // Switch to this playlist and start playing immediately +} + +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_playlist_modified( + KotoPlaylist * playlist, + gpointer user_data +) { + if (!KOTO_IS_PLAYLIST(playlist)) { + return; + } + + KotoPlaylistPage * self = user_data; + + if (!KOTO_IS_PLAYLIST_PAGE(self)) { + return; + } + + gchar * artwork = koto_playlist_get_artwork(playlist); // Get the artwork + + if (koto_utils_string_is_valid(artwork)) { // Have valid artwork + koto_cover_art_button_set_art_path(self->playlist_image, artwork); // Update our Koto Cover Art Button + } + + gchar * name = koto_playlist_get_name(playlist); // Get the name + + if (koto_utils_string_is_valid(name)) { // Have valid name + gtk_label_set_label(GTK_LABEL(self->name_label), name); // Update the name label + } +} + +void koto_playlist_page_set_playlist_uuid( + KotoPlaylistPage * self, + gchar * playlist_uuid +) { + if (!KOTO_IS_PLAYLIST_PAGE(self)) { + return; + } + + if (!koto_utils_string_is_valid(playlist_uuid)) { // Provided UUID string is not valid + return; + } + + if (!koto_cartographer_has_playlist_by_uuid(koto_maps, playlist_uuid)) { // If we don't have this playlist + return; + } + + 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_update_header(self); // Update our header + + g_signal_connect(playlist, "modified", G_CALLBACK(koto_playlist_page_handle_playlist_modified), self); // Handle playlist modification + koto_track_table_set_playlist(self->table, playlist); // Set our track table +} + +void koto_playlist_page_update_header(KotoPlaylistPage * self) { + if (!KOTO_IS_PLAYLIST_PAGE(self)) { + return; + } + + if (!KOTO_IS_PLAYLIST(self->playlist)) { // Not a playlist + return; + } + + 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 (koto_utils_string_is_valid(artwork)) { // Have artwork + koto_cover_art_button_set_art_path(self->playlist_image, artwork); // Update our artwork + } +} + +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..6822a26 --- /dev/null +++ b/src/pages/playlist/list.h @@ -0,0 +1,87 @@ +/* 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/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_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_playlist_modified( + KotoPlaylist * playlist, + gpointer user_data +); + +void koto_playlist_page_hide_popover(KotoPlaylistPage * self); + +void koto_playlist_page_set_playlist_uuid( + KotoPlaylistPage * self, + gchar * playlist_uuid +); + +void koto_playlist_page_show_popover(KotoPlaylistPage * self); + +void koto_playlist_page_update_header(KotoPlaylistPage * self); + +G_END_DECLS diff --git a/src/playback/engine.c b/src/playback/engine.c new file mode 100644 index 0000000..510f2bf --- /dev/null +++ b/src/playback/engine.c @@ -0,0 +1,866 @@ +/* engine.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 +#include "../config/config.h" +#include "../db/cartographer.h" +#include "../playlist/current.h" +#include "../indexer/structs.h" +#include "../koto-paths.h" +#include "../koto-utils.h" +#include "engine.h" +#include "mpris.h" + +enum { + SIGNAL_IS_PLAYING, + SIGNAL_IS_PAUSED, + SIGNAL_PLAY_STATE_CHANGE, + SIGNAL_TICK_DURATION, + SIGNAL_TICK_TRACK, + SIGNAL_TRACK_CHANGE, + SIGNAL_TRACK_REPEAT_CHANGE, + SIGNAL_TRACK_SHUFFLE_CHANGE, + N_SIGNALS +}; + +extern gchar * koto_rev_dns; + +static guint playback_engine_signals[N_SIGNALS] = { + 0 +}; + +extern KotoCartographer * koto_maps; +extern KotoConfig * config; +extern KotoCurrentPlaylist * current_playlist; + +KotoPlaybackEngine * playback_engine; + +struct _KotoPlaybackEngine { + GObject parent_class; + GstElement * player; + GstElement * playbin; + GstElement * suppress_video; + GstBus * monitor; + + GstQuery * duration_query; + GstQuery * position_query; + + KotoTrack * current_track; + + gboolean is_muted; + gboolean is_repeat_enabled; + + gboolean is_playing; + gboolean is_playing_specific_track; + gboolean is_shuffle_enabled; + + gboolean tick_duration_timer_running; + gboolean tick_track_timer_running; + gboolean tick_update_playlist_state; + + gboolean via_config_continue_on_playlist; // Pulls from our Koto Config + gboolean via_config_maintain_shuffle; // Pulls from our Koto Config + + guint playback_position; + gint requested_playback_position; + + gdouble rate; + gdouble volume; +}; + +struct _KotoPlaybackEngineClass { + GObjectClass parent_class; + + void (* is_playing) (KotoPlaybackEngine * engine); + void (* is_paused) (KotoPlaybackEngine * engine); + void (* play_state_changed) (KotoPlaybackEngine * engine); + void (* tick_duration) (KotoPlaybackEngine * engine); + void (* tick_track) (KotoPlaybackEngine * engine); + void (* track_changed) (KotoPlaybackEngine * engine); + void (* track_repeat_changed) (KotoPlaybackEngine * engine); + void (* track_shuffle_changed) (KotoPlaybackEngine * engine); +}; + +G_DEFINE_TYPE(KotoPlaybackEngine, koto_playback_engine, G_TYPE_OBJECT); + +static void koto_playback_engine_class_init(KotoPlaybackEngineClass * c) { + c->play_state_changed = NULL; + GObjectClass * gobject_class; + gobject_class = G_OBJECT_CLASS(c); + + playback_engine_signals[SIGNAL_IS_PLAYING] = g_signal_new( + "is-playing", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoPlaybackEngineClass, play_state_changed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0 + ); + + playback_engine_signals[SIGNAL_IS_PAUSED] = g_signal_new( + "is-paused", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoPlaybackEngineClass, play_state_changed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0 + ); + + playback_engine_signals[SIGNAL_PLAY_STATE_CHANGE] = g_signal_new( + "play-state-changed", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoPlaybackEngineClass, play_state_changed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0 + ); + + playback_engine_signals[SIGNAL_TICK_DURATION] = g_signal_new( + "tick-duration", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoPlaybackEngineClass, tick_duration), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0 + ); + + playback_engine_signals[SIGNAL_TICK_TRACK] = g_signal_new( + "tick-track", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoPlaybackEngineClass, tick_track), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0 + ); + + playback_engine_signals[SIGNAL_TRACK_CHANGE] = g_signal_new( + "track-changed", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoPlaybackEngineClass, track_changed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0 + ); + + playback_engine_signals[SIGNAL_TRACK_REPEAT_CHANGE] = g_signal_new( + "track-repeat-changed", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoPlaybackEngineClass, track_repeat_changed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0 + ); + + playback_engine_signals[SIGNAL_TRACK_SHUFFLE_CHANGE] = g_signal_new( + "track-shuffle-changed", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoPlaybackEngineClass, track_shuffle_changed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0 + ); +} + +static void koto_playback_engine_init(KotoPlaybackEngine * self) { + self->current_track = NULL; + current_playlist = koto_current_playlist_new(); // Ensure our global current playlist is set + + self->player = gst_pipeline_new("player"); + self->playbin = gst_element_factory_make("playbin", NULL); + self->suppress_video = gst_element_factory_make("fakesink", "suppress-video"); + + g_object_set(self->playbin, "video-sink", self->suppress_video, NULL); + self->rate = 1.0; + self->volume = 0.5; + koto_playback_engine_set_volume(self, 0.5); + + 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->is_shuffle_enabled = FALSE; + self->requested_playback_position = -1; + self->tick_duration_timer_running = FALSE; + self->tick_track_timer_running = FALSE; + self->tick_update_playlist_state = FALSE; + self->via_config_continue_on_playlist = FALSE; + self->via_config_maintain_shuffle = TRUE; + + koto_playback_engine_apply_configuration_state(NULL, 0, self); // Apply configuration immediately + + g_signal_connect(config, "notify::playback-continue-on-playlist", G_CALLBACK(koto_playback_engine_apply_configuration_state), self); // Handle changes to our config + g_signal_connect(config, "notify::last-used-volume", G_CALLBACK(koto_playback_engine_apply_configuration_state), self); // Handle changes to our config + g_signal_connect(config, "notify::maintain-shuffle", G_CALLBACK(koto_playback_engine_apply_configuration_state), self); // Handle changes to our config + + if (KOTO_IS_CURRENT_PLAYLIST(current_playlist)) { + g_signal_connect(current_playlist, "playlist-changed", G_CALLBACK(koto_playback_engine_handle_current_playlist_changed), self); + } +} + +void koto_playback_engine_apply_configuration_state( + KotoConfig * c, + guint prop_id, + KotoPlaybackEngine * self +) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { // Not a playback engine + return; + } + + (void) c; + (void) prop_id; + + gboolean config_continue_on_playlist = self->via_config_continue_on_playlist; + gdouble config_last_used_volume = 0.5; + gboolean config_maintain_shuffle = self->via_config_maintain_shuffle; + + g_object_get( + config, + "playback-continue-on-playlist", + &config_continue_on_playlist, + "playback-last-used-volume", + &config_last_used_volume, + "playback-maintain-shuffle", + &config_maintain_shuffle, + NULL + ); + + self->via_config_continue_on_playlist = config_continue_on_playlist; + self->via_config_maintain_shuffle = config_maintain_shuffle; + + if (self->volume != config_last_used_volume) { // Not the same volume + koto_playback_engine_set_volume(self, config_last_used_volume); + } +} + +void koto_playback_engine_backwards(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { // Not a playback engine + return; + } + + KotoPlaylist * playlist = koto_current_playlist_get_playlist(current_playlist); // Get the current playlist + + if (!KOTO_IS_PLAYLIST(playlist)) { // If we do not have a playlist currently + return; + } + + if (self->is_repeat_enabled || self->is_playing_specific_track) { // Repeat enabled or playing a specific track + return; + } + + koto_playback_engine_set_track_playback_position(self, 0); // Reset the current track position to 0 + koto_playback_engine_set_track_by_uuid(self, koto_playlist_go_to_previous(playlist), FALSE); +} + +void koto_playback_engine_handle_current_playlist_changed( + KotoCurrentPlaylist * current_pl, + KotoPlaylist * playlist, + gboolean play_immediately, + gboolean play_current, + gpointer user_data +) { + (void) current_pl; + KotoPlaybackEngine * self = user_data; + + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + + if (self->is_shuffle_enabled) { // If shuffle is enabled + koto_playback_engine_set_track_shuffle(self, self->via_config_maintain_shuffle); // Set to our maintain shuffle value + } + + if (play_immediately) { // Should play immediately + gchar * play_uuid = NULL; + + if (play_current) { // Play the current track + KotoTrack * track = koto_playlist_get_current_track(playlist); // Get the current track + + if (KOTO_IS_TRACK(track)) { + play_uuid = koto_track_get_uuid(track); + } + } + + if (!koto_utils_string_is_valid(play_uuid)) { + play_uuid = koto_playlist_go_to_next(playlist); + } + + koto_playback_engine_set_track_by_uuid(self, play_uuid, FALSE); // Go to "next" which is the first track + } +} + +void koto_playback_engine_forwards(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { // Not a playback engine + return; + } + + KotoPlaylist * playlist = koto_current_playlist_get_playlist(current_playlist); // Get the current playlist + + if (!KOTO_IS_PLAYLIST(playlist)) { // If we do not have a playlist currently + return; + } + + 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 { // If repeat is not enabled + if ( + (self->via_config_continue_on_playlist && self->is_playing_specific_track) || // Playing a specific track and wanting to continue on the playlist + (!self->is_playing_specific_track) // Not playing a specific track + ) { + koto_playback_engine_set_track_playback_position(self, 0); // Reset the current track position to 0 + koto_playback_engine_set_track_by_uuid(self, koto_playlist_go_to_next(playlist), FALSE); + } + } +} + +KotoTrack * koto_playback_engine_get_current_track(KotoPlaybackEngine * self) { + return KOTO_IS_PLAYBACK_ENGINE(self) ? self->current_track : NULL; +} + +gint64 koto_playback_engine_get_duration(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { // Not a playback engine + return 0; + } + + guint64 track_duration = koto_track_get_duration(self->current_track); // Get the current track's duration + + if (track_duration != 0) { // Have a duration stored + return (gint64) track_duration; // Trust what we got from the ID3 data + } + + gint64 duration = 0; + + 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; +} + +gdouble koto_playback_engine_get_progress(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { // Not a playback engine + return 0; + } + + 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 + } + + return progress; +} + +gdouble koto_playback_engine_get_sane_playback_rate(gdouble rate) { + if ((rate < 0.25) && (rate != 0)) { // 0.25 is probably the most reasonable limit + return 0.25; + } else if (rate > 2.5) { // Anything greater than 2.5 + return 2.5; + } else if (rate == 0) { // Possible conversion failure + return 1.0; + } else { + return rate; + } +} + +GstState koto_playback_engine_get_state(KotoPlaybackEngine * self) { + return GST_STATE(self->player); +} + +gboolean koto_playback_engine_get_track_repeat(KotoPlaybackEngine * self) { + return KOTO_IS_PLAYBACK_ENGINE(self) ? self->is_repeat_enabled : FALSE; +} + +gboolean koto_playback_engine_get_track_shuffle(KotoPlaybackEngine * self) { + return KOTO_IS_PLAYBACK_ENGINE(self) ? self->is_shuffle_enabled : FALSE; +} + +gdouble koto_playback_engine_get_volume(KotoPlaybackEngine * self) { + return KOTO_IS_PLAYBACK_ENGINE(self) ? self->volume : 0; +} + +void koto_playback_engine_jump_backwards(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { // Not an engine + return; + } + + if (!self->is_playing) { // Is not playing + return; + } + + gdouble cur_pos = koto_playback_engine_get_progress(self); // Get the current position + guint backwards_increment = 10; + g_object_get( + config, + "playback-jump-backwards-increment", // Get our backwards increment from our settings + &backwards_increment, + NULL + ); + + gint new_pos = (gint) cur_pos - backwards_increment; + + if (new_pos < 0) { // Before the beginning of time + new_pos = 0; + } + + koto_playback_engine_set_position(self, new_pos); +} + +void koto_playback_engine_jump_forwards(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { // Not an engine + return; + } + + if (!self->is_playing) { // Is not playing + return; + } + + gdouble cur_pos = koto_playback_engine_get_progress(self); // Get the current position + guint forwards_increment = 30; + g_object_get( + config, + "playback-jump-forwards-increment", // Get our forward increment from our settings + &forwards_increment, + NULL + ); + + gint new_pos = (gint) cur_pos + forwards_increment; + koto_playback_engine_set_position(self, new_pos); +} + +gboolean koto_playback_engine_monitor_changed( + GstBus * bus, + GstMessage * msg, + gpointer user_data +) { + (void) bus; + KotoPlaybackEngine * self = user_data; + + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return FALSE; + } + + switch (GST_MESSAGE_TYPE(msg)) { + case GST_MESSAGE_DURATION_CHANGED: { // Duration changed + koto_playback_engine_tick_duration(self); + koto_playback_engine_tick_track(self); + break; + } + case GST_MESSAGE_STATE_CHANGED: { // State changed + GstState old_state; + GstState new_state; + GstState pending_state; + gst_message_parse_state_changed(msg, &old_state, &new_state, &pending_state); + + gboolean possibly_buffered = (old_state == GST_STATE_READY) && (new_state == GST_STATE_PAUSED) && (pending_state == GST_STATE_PLAYING); + + if (possibly_buffered && (self->requested_playback_position != -1)) { // Have a requested playback position + koto_playback_engine_set_position(self, self->requested_playback_position); // Set the position + self->requested_playback_position = -1; + koto_playback_engine_play(self); // Start playback + return TRUE; + } + + if (new_state == GST_STATE_PLAYING) { // Now playing + koto_playback_engine_tick_duration(self); + g_signal_emit(self, playback_engine_signals[SIGNAL_IS_PLAYING], 0); // Emit our is playing state signal + } else if (new_state == GST_STATE_PAUSED) { // Now paused + g_signal_emit(self, playback_engine_signals[SIGNAL_IS_PAUSED], 0); // Emit our is paused state signal + } + + if (new_state != GST_STATE_VOID_PENDING) { + g_signal_emit(self, playback_engine_signals[SIGNAL_PLAY_STATE_CHANGE], 0); // Emit our play state signal + } + + break; + } + case GST_MESSAGE_EOS: { // Reached end of stream + koto_playback_engine_forwards(self); // Go to the next track + break; + } + default: + break; + } + return TRUE; +} + +void koto_playback_engine_play(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + + self->is_playing = TRUE; + gst_element_set_state(self->player, GST_STATE_PLAYING); // Set our state to play + + if (!self->tick_duration_timer_running) { + self->tick_duration_timer_running = TRUE; + g_timeout_add(1000, koto_playback_engine_tick_duration, self); // Create a 1s duration tick + } + + if (!self->tick_track_timer_running) { + self->tick_track_timer_running = TRUE; + g_timeout_add(100, koto_playback_engine_tick_track, self); // Create a 100ms track tick + } + + if (!self->tick_update_playlist_state) { + self->tick_update_playlist_state = TRUE; + g_timeout_add(60000, koto_playback_engine_tick_update_playlist_state, self); // Update the current playlist state every minute + } + + koto_update_mpris_playback_state(GST_STATE_PLAYING); +} + +void koto_playback_engine_pause(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + + self->is_playing = FALSE; + gst_element_change_state(self->player, GST_STATE_CHANGE_PLAYING_TO_PAUSED); + koto_update_mpris_playback_state(GST_STATE_PAUSED); +} + +void koto_playback_engine_set_playback_rate( + KotoPlaybackEngine * self, + gdouble rate +) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { // Not a playback engine + return; + } + + self->rate = koto_playback_engine_get_sane_playback_rate(rate); // Get a fixed rate and set our current rate to it + gst_element_set_state(self->player, GST_STATE_PAUSED); // Set our state to paused + koto_playback_engine_set_position(self, koto_playback_engine_get_progress(self)); // Basically creates a new seek event + gst_element_set_state(self->player, GST_STATE_PLAYING); // Set our state to play +} + +void koto_playback_engine_set_position( + KotoPlaybackEngine * self, + int position +) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + + gst_element_seek( + self->playbin, + self->rate, + GST_FORMAT_TIME, + GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_ACCURATE, + GST_SEEK_TYPE_SET, + position * GST_SECOND, + GST_SEEK_TYPE_NONE, + -1 + ); +} + +void koto_playback_engine_set_track_playback_position( + KotoPlaybackEngine * self, + guint64 pos +) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + + if (!KOTO_IS_TRACK(self->current_track) || self->is_repeat_enabled) { // If we do not have a track or repeating + return; + } + + koto_track_set_playback_position(self->current_track, pos); // Set the playback position of the track +} + +void koto_playback_engine_set_track_repeat( + KotoPlaybackEngine * self, + gboolean enable_repeat +) { + self->is_repeat_enabled = enable_repeat; + g_signal_emit(self, playback_engine_signals[SIGNAL_TRACK_REPEAT_CHANGE], 0); // Emit our track repeat changed event +} + +void koto_playback_engine_set_track_shuffle( + KotoPlaybackEngine * self, + gboolean enable_shuffle +) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + + KotoPlaylist * playlist = koto_current_playlist_get_playlist(current_playlist); + + if (!KOTO_IS_PLAYLIST(playlist)) { // Don't have a playlist currently + return; + } + + self->is_shuffle_enabled = enable_shuffle; + g_object_set(playlist, "is-shuffle-enabled", self->is_shuffle_enabled, NULL); // Set the is-shuffle-enabled on any existing playlist + g_signal_emit(self, playback_engine_signals[SIGNAL_TRACK_SHUFFLE_CHANGE], 0); // Emit our track shuffle changed event +} + +void koto_playback_engine_set_track_by_uuid( + KotoPlaybackEngine * self, + gchar * track_uuid, + gboolean playing_specific_track +) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + + if (!koto_utils_string_is_valid(track_uuid)) { // If this is not a valid track uuid string + return; + } + + KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, track_uuid); // Get the track from cartographer + + if (!KOTO_IS_TRACK(track)) { // Not a track + return; + } + + self->current_track = track; + + gchar * track_file_path = koto_track_get_path(self->current_track); // Get the most optimal path for the track given the libraries it is in + + koto_playback_engine_stop(self); // Stop current track + + self->is_playing_specific_track = playing_specific_track; + + gchar * gst_filename = gst_filename_to_uri(track_file_path, NULL); // Get the gst supported file naem + + g_object_set(self->playbin, "uri", gst_filename, NULL); + g_free(gst_filename); // Free the filename + + self->requested_playback_position = koto_track_get_playback_position(self->current_track); // Set our requested playback position + koto_playback_engine_play(self); // Play the new track + koto_playback_engine_set_volume(self, self->volume); // Re-enforce our volume on the updated playbin + + GVariant * metadata = koto_track_get_metadata_vardict(track); // Get the GVariantBuilder variable dict for the metadata + GVariantDict * metadata_dict = g_variant_dict_new(metadata); + + g_signal_emit(self, playback_engine_signals[SIGNAL_TRACK_CHANGE], 0); // Emit our track change signal + koto_update_mpris_info_for_track(self->current_track); + + GVariant * track_name_var = g_variant_dict_lookup_value(metadata_dict, "xesam:title", NULL); // Get the GVariant for the name of the track + const gchar * track_name = g_variant_get_string(track_name_var, NULL); // Get the string of the track name + + GVariant * album_name_var = g_variant_dict_lookup_value(metadata_dict, "xesam:album", NULL); // Get the GVariant for the album name + gchar * album_name = (album_name_var != NULL) ? g_strdup(g_variant_get_string(album_name_var, NULL)) : NULL; // Get the string for the album name + + GVariant * artist_name_var = g_variant_dict_lookup_value(metadata_dict, "playbackengine:artist", NULL); // Get the GVariant for the name of the artist + gchar * artist_name = g_strdup(g_variant_get_string(artist_name_var, NULL)); // Get the string for the artist name + + gchar * artist_album_combo = koto_utils_string_is_valid(album_name) ? g_strjoin(" - ", artist_name, album_name, NULL) : artist_name; // Join artist and album name separated by " - " + + gchar * icon_name = "audio-x-generic-symbolic"; + + if (g_variant_dict_contains(metadata_dict, "mpris:artUrl")) { // If we have artwork specified + GVariant * art_url_var = g_variant_dict_lookup_value(metadata_dict, "mpris:artUrl", NULL); // Get the GVariant for the art URL + const gchar * art_uri = g_variant_get_string(art_url_var, NULL); // Get the string for the artwork + icon_name = koto_utils_string_replace_all(g_strdup(art_uri), "file://", ""); + } + + // Super important note: We are not using libnotify directly because the synchronous nature of notify_notification_send seems to result in dbus timeouts + if (g_find_program_in_path("notify-send") != NULL) { // Have notify-send + char * argv[14] = { + "notify-send", + "-a", + "Koto", + "-i", + icon_name, + "-c", + "x-gnome.music", + "-t", + "5000", + "-h", + g_strdup_printf("string:desktop-entry:%s", koto_rev_dns), + g_strdup(track_name), + artist_album_combo, + NULL + }; + + g_spawn_async(NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, NULL); + } +} + +void koto_playback_engine_set_volume( + KotoPlaybackEngine * self, + gdouble volume +) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + + self->volume = volume; + g_object_set(self->playbin, "volume", self->volume, NULL); +} + +void koto_playback_engine_stop(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + + gst_element_set_state(self->player, GST_STATE_NULL); + GstPad * pad = gst_element_get_static_pad(self->player, "sink"); // Get the static pad of the audio element + + if (!GST_IS_PAD(pad)) { + return; + } + + gst_pad_set_offset(pad, 0); // Change offset + koto_update_mpris_playback_state(GST_STATE_NULL); +} + +void koto_playback_engine_toggle(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + + if (self->is_playing) { // Currently playing + koto_playback_engine_pause(self); // Pause + } else { + koto_playback_engine_play(self); // Play + } + + koto_current_playlist_save_playlist_state(current_playlist); // Save the playlist state during pause and play in case we are doing that remotely +} + +gboolean koto_playback_engine_tick_duration(gpointer user_data) { + KotoPlaybackEngine * self = user_data; + + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return FALSE; + } + + if (self->is_playing) { // Is playing + g_signal_emit(self, playback_engine_signals[SIGNAL_TICK_DURATION], 0); // Emit our 1s track tick + } else { // Not playing so exiting timer + self->tick_duration_timer_running = FALSE; + } + + return self->is_playing; +} + +gboolean koto_playback_engine_tick_track(gpointer user_data) { + KotoPlaybackEngine * self = user_data; + + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return FALSE; + } + + if (self->is_playing) { // Is playing + KotoPlaylist * playlist = koto_current_playlist_get_playlist(current_playlist); + + if (KOTO_IS_PLAYLIST(playlist)) { + koto_playlist_emit_modified(playlist); // Emit modified on the playlist to update UI in realtime (ish, okay? every 100ms is enough geez) + } + + g_signal_emit(self, playback_engine_signals[SIGNAL_TICK_TRACK], 0); // Emit our 100ms track tick + } else { + self->tick_track_timer_running = FALSE; + } + + koto_playback_engine_update_track_position(self); // Update track position if needed + + return self->is_playing; +} + +gboolean koto_playback_engine_tick_update_playlist_state(gpointer user_data) { + KotoPlaybackEngine * self = user_data; + + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return FALSE; + } + + if (self->is_playing) { // Is playing + koto_current_playlist_save_playlist_state(current_playlist); // Save the state of the current playlist + } else { + self->tick_update_playlist_state = FALSE; + } + + return self->is_playing; +} + +void koto_playback_engine_toggle_track_repeat(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + + koto_playback_engine_set_track_repeat(self, !self->is_repeat_enabled); +} + +void koto_playback_engine_toggle_track_shuffle(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + + koto_playback_engine_set_track_shuffle(self, !self->is_shuffle_enabled); // Invert the currently shuffling vale +} + +void koto_playback_engine_update_track_position(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + + koto_playback_engine_set_track_playback_position(self, (guint64) koto_playback_engine_get_progress(self)); // Set to current progress +} + +KotoPlaybackEngine * koto_playback_engine_new() { + return g_object_new(KOTO_TYPE_PLAYBACK_ENGINE, NULL); +} diff --git a/src/playback/engine.h b/src/playback/engine.h new file mode 100644 index 0000000..8d10a8d --- /dev/null +++ b/src/playback/engine.h @@ -0,0 +1,151 @@ +/* engine.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 +#include "../config/config.h" +#include "../playlist/current.h" + +G_BEGIN_DECLS + +/** + * Type Definition + **/ + +#define KOTO_TYPE_PLAYBACK_ENGINE (koto_playback_engine_get_type()) +#define KOTO_PLAYBACK_ENGINE(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), KOTO_TYPE_PLAYBACK_ENGINE, KotoPlaybackEngine)) +#define KOTO_IS_PLAYBACK_ENGINE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_PLAYBACK_ENGINE)) + +typedef struct _KotoPlaybackEngine KotoPlaybackEngine; +typedef struct _KotoPlaybackEngineClass KotoPlaybackEngineClass; + +GLIB_AVAILABLE_IN_ALL +GType koto_playback_engine_get_type(void) G_GNUC_CONST; + +/** + * Playback Engine Functions + **/ + +KotoPlaybackEngine * koto_playback_engine_new(); + +void koto_playback_engine_apply_configuration_state( + KotoConfig * config, + guint prop_id, + KotoPlaybackEngine * self +); + +void koto_playback_engine_backwards(KotoPlaybackEngine * self); + +void koto_playback_engine_handle_current_playlist_changed( + KotoCurrentPlaylist * current_pl, + KotoPlaylist * playlist, + gboolean play_immediately, + gboolean play_current, + gpointer user_data +); + +void koto_playback_engine_forwards(KotoPlaybackEngine * self); + +KotoTrack * koto_playback_engine_get_current_track(KotoPlaybackEngine * self); + +gint64 koto_playback_engine_get_duration(KotoPlaybackEngine * self); + +gdouble koto_playback_engine_get_progress(KotoPlaybackEngine * self); + +; + +gdouble koto_playback_engine_get_sane_playback_rate(gdouble rate); + +GstState koto_playback_engine_get_state(KotoPlaybackEngine * self); + +gboolean koto_playback_engine_get_track_repeat(KotoPlaybackEngine * self); + +gboolean koto_playback_engine_get_track_shuffle(KotoPlaybackEngine * self); + +gdouble koto_playback_engine_get_volume(KotoPlaybackEngine * self); + +void koto_playback_engine_jump_backwards(KotoPlaybackEngine * self); + +void koto_playback_engine_jump_forwards(KotoPlaybackEngine * self); + +void koto_playback_engine_mute(KotoPlaybackEngine * self); + +gboolean koto_playback_engine_monitor_changed( + GstBus * bus, + GstMessage * msg, + gpointer user_data +); + +void koto_playback_engine_pause(KotoPlaybackEngine * self); + +void koto_playback_engine_play(KotoPlaybackEngine * self); + +void koto_playback_engine_toggle(KotoPlaybackEngine * self); + +void koto_playback_engine_set_playback_rate( + KotoPlaybackEngine * self, + gdouble rate +); + +void koto_playback_engine_set_position( + KotoPlaybackEngine * self, + int position +); + +void koto_playback_engine_set_track_playback_position( + KotoPlaybackEngine * self, + guint64 pos +); + +void koto_playback_engine_set_track_repeat( + KotoPlaybackEngine * self, + gboolean enable_repeat +); + +void koto_playback_engine_set_track_shuffle( + KotoPlaybackEngine * self, + gboolean enable_shuffle +); + +void koto_playback_engine_set_track_by_uuid( + KotoPlaybackEngine * self, + gchar * track_uuid, + gboolean playing_specific_track +); + +void koto_playback_engine_set_volume( + KotoPlaybackEngine * self, + gdouble volume +); + +void koto_playback_engine_stop(KotoPlaybackEngine * self); + +void koto_playback_engine_toggle_track_repeat(KotoPlaybackEngine * self); + +void koto_playback_engine_toggle_track_shuffle(KotoPlaybackEngine * self); + +void koto_playback_engine_update_duration(KotoPlaybackEngine * self); + +void koto_playback_engine_update_track_position(KotoPlaybackEngine * self); + +gboolean koto_playback_engine_tick_duration(gpointer user_data); + +gboolean koto_playback_engine_tick_track(gpointer user_data); + +gboolean koto_playback_engine_tick_update_playlist_state(gpointer user_data); diff --git a/src/playback/media-keys.c b/src/playback/media-keys.c new file mode 100644 index 0000000..05248d9 --- /dev/null +++ b/src/playback/media-keys.c @@ -0,0 +1,199 @@ +/* media-keys.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 "engine.h" +#include "media-keys.h" +#include "../koto-paths.h" + +extern GtkWindow * main_window; +extern KotoPlaybackEngine * playback_engine; +extern gchar * koto_rev_dns; + +GDBusConnection * media_keys_dbus_conn = NULL; +GDBusProxy * media_keys_proxy = NULL; +GDBusNodeInfo * media_keys_introspection_data = NULL; + +static const gchar introspection_xml[] = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + +void grab_media_keys() { + if (media_keys_proxy == NULL) { // No connection + return; + } + + g_dbus_proxy_call( + media_keys_proxy, + "GrabMediaPlayerKeys", + g_variant_new("(su)", koto_rev_dns, 0), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + handle_media_keys_async_done, + NULL + ); +} + +void handle_media_keys_async_done( + GObject * source_object, + GAsyncResult * res, + gpointer user_data +) { + (void) user_data; + g_dbus_proxy_call_finish(G_DBUS_PROXY(source_object), res, NULL); // Ensure we finish our call +} + +void handle_media_keys_signal( + GDBusProxy * proxy, + const gchar * sender_name, + const gchar * signal_name, + GVariant * parameters, + gpointer user_data +) { + (void) proxy; + (void) sender_name; + (void) user_data; + if (g_strcmp0(signal_name, "MediaPlayerKeyPressed") != 0) { // Not MediaPlayerKeyPressed + return; + } + + gchar * application_name = NULL; + gchar * key = NULL; + + g_variant_get(parameters, "(ss)", &application_name, &key); + + if (g_strcmp0(application_name, koto_rev_dns) != 0) { // Not for Koto + return; + } + + if (g_strcmp0(key, "Play") == 0) { + koto_playback_engine_play(playback_engine); + } else if (g_strcmp0(key, "Pause") == 0) { + koto_playback_engine_pause(playback_engine); + } else if (g_strcmp0(key, "Stop") == 0) { + koto_playback_engine_stop(playback_engine); + } else if (g_strcmp0(key, "Previous") == 0) { + koto_playback_engine_backwards(playback_engine); + } else if (g_strcmp0(key, "Next") == 0) { + koto_playback_engine_forwards(playback_engine); + } else if (g_strcmp0(key, "Repeat") == 0) { + koto_playback_engine_toggle_track_repeat(playback_engine); + } else if (g_strcmp0(key, "Shuffle") == 0) { + koto_playback_engine_toggle_track_shuffle(playback_engine); + } +} + +void handle_window_enter( + GtkEventControllerFocus * controller, + gpointer user_data +) { + (void) controller; + (void) user_data; + grab_media_keys(); // Grab our media keys +} + +void handle_window_leave( + GtkEventControllerFocus * controller, + gpointer user_data +) { + (void) controller; + (void) user_data; + release_media_keys(); // Release our media keys +} + +void release_media_keys() { + if (media_keys_dbus_conn == NULL) { // No connection + return; + } + + GVariant * params = g_variant_new_string(g_strdup(koto_rev_dns)); + + g_dbus_proxy_call( + media_keys_proxy, + "ReleaseMediaPlayerKeys", + params, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + handle_media_keys_async_done, + NULL + ); +} + +void setup_mediakeys_interface() { + media_keys_introspection_data = g_dbus_node_info_new_for_xml(introspection_xml, NULL); + g_assert(media_keys_introspection_data != NULL); + + GDBusConnection * bus; + GError * error = NULL; + + bus = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error); + + if (bus == NULL) { // Failed to get session bus + g_printerr("Failed to get our session bus: %s\n", error->message); + g_error_free(error); + return; + } + + media_keys_proxy = g_dbus_proxy_new_sync( + bus, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + "org.gnome.SettingsDaemon.MediaKeys", + "/org/gnome/SettingsDaemon/MediaKeys", + "org.gnome.SettingsDaemon.MediaKeys", + NULL, + &error + ); + + if (media_keys_proxy == NULL) { + g_printerr("Failed to get a proxy to GNOME Settings Daemon Media-Keys: %s\n", error->message); + g_error_free(error); + return; + } + + g_signal_connect_object( + media_keys_proxy, + "g-signal", + G_CALLBACK(handle_media_keys_signal), + NULL, + 0 + ); + + GtkEventController * focus_controller = gtk_event_controller_focus_new(); // Create a new focus controller + + g_signal_connect(focus_controller, "enter", G_CALLBACK(handle_window_enter), NULL); + g_signal_connect(focus_controller, "leave", G_CALLBACK(handle_window_leave), NULL); + gtk_widget_add_controller(GTK_WIDGET(main_window), focus_controller); +} diff --git a/src/playback/media-keys.h b/src/playback/media-keys.h new file mode 100644 index 0000000..20eee0c --- /dev/null +++ b/src/playback/media-keys.h @@ -0,0 +1,54 @@ +/* media-keys.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 + +G_BEGIN_DECLS + +void grab_media_keys(); + +void handle_media_keys_async_done( + GObject * source_object, + GAsyncResult * res, + gpointer user_data +); + +void handle_media_keys_signal( + GDBusProxy * proxy, + const gchar * sender_name, + const gchar * signal_name, + GVariant * parameters, + gpointer user_data +); + +void handle_window_enter( + GtkEventControllerFocus * controller, + gpointer user_data +); + +void handle_window_leave( + GtkEventControllerFocus * controller, + gpointer user_data +); + +void release_media_keys(); + +void setup_mediakeys_interface(); + +G_END_DECLS diff --git a/src/playback/mimes.c b/src/playback/mimes.c new file mode 100644 index 0000000..8c3f0ea --- /dev/null +++ b/src/playback/mimes.c @@ -0,0 +1,75 @@ +/* mimes.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-utils.h" + +GHashTable * supported_mimes_hash = NULL; +GList * supported_mimes = NULL; + +gboolean koto_playback_engine_gst_caps_iter( + GstCapsFeatures * features, + GstStructure * structure, + gpointer user_data +) { + (void) features; + (void) user_data; + gchar * caps_name = (gchar*) gst_structure_get_name(structure); // Get the name, typically a mimetype + + if (g_str_has_prefix(caps_name, "unknown")) { // Is unknown + return TRUE; + } + + if (g_hash_table_contains(supported_mimes_hash, caps_name)) { // Found in list already + return TRUE; + } + + g_hash_table_add(supported_mimes_hash, g_strdup(caps_name)); + supported_mimes = g_list_prepend(supported_mimes, g_strdup(caps_name)); + supported_mimes = g_list_prepend(supported_mimes, g_strdup(koto_utils_string_replace_all(caps_name, "x-", ""))); + + return TRUE; +} + +void koto_playback_engine_gst_pad_iter( + gpointer list_data, + gpointer user_data +) { + (void) user_data; + GstStaticPadTemplate * templ = list_data; + + if (templ->direction == GST_PAD_SINK) { // Is a sink pad + GstCaps * capabilities = gst_static_pad_template_get_caps(templ); // Get the capabilities + gst_caps_foreach(capabilities, koto_playback_engine_gst_caps_iter, NULL); // Iterate over and add to mimes + gst_caps_unref(capabilities); + } +} + +void koto_playback_engine_get_supported_mimetypes() { + // Credit for code goes to https://github.com/mopidy/mopidy/issues/812#issuecomment-75868363 + // These are GstElementFactory + GList * elements = gst_element_factory_list_get_elements(GST_ELEMENT_FACTORY_TYPE_DEPAYLOADER | GST_ELEMENT_FACTORY_TYPE_DEMUXER | GST_ELEMENT_FACTORY_TYPE_PARSER | GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_AUDIO, GST_RANK_NONE); + + GList * ele; + + for (ele = elements; ele != NULL; ele = ele->next) { // For each of the elements + // GList of GstStaticPadTemplate + GList * static_pads = (GList*) gst_element_factory_get_static_pad_templates(ele->data); // Get the pads + g_list_foreach(static_pads, koto_playback_engine_gst_pad_iter, NULL); + } +} diff --git a/src/playback/mimes.h b/src/playback/mimes.h new file mode 100644 index 0000000..f5010f7 --- /dev/null +++ b/src/playback/mimes.h @@ -0,0 +1,37 @@ +/* mimes.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 + +gboolean koto_bplayback_engine_gst_caps_iter( + GstCapsFeatures * features, + GstStructure * structure, + gpointer user_data +); + +void koto_playback_engine_gst_pad_iter( + gpointer list_data, + gpointer user_data +); + +void koto_playback_engine_get_supported_mimetypes(GList * mimes); + +G_END_DECLS diff --git a/src/playback/mpris.c b/src/playback/mpris.c new file mode 100644 index 0000000..1d726c1 --- /dev/null +++ b/src/playback/mpris.c @@ -0,0 +1,404 @@ +/* mpris.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. + */ + +// Huge thanks to the GLib folks for providing an example server test + +#include +#include +#include +#include "../db/cartographer.h" +#include "../playlist/current.h" +#include "../playlist/playlist.h" +#include "../koto-paths.h" +#include "../koto-utils.h" +#include "engine.h" +#include "mimes.h" +#include "mpris.h" + +extern gchar * koto_rev_dns; +extern KotoCartographer * koto_maps; +extern KotoCurrentPlaylist * current_playlist; +extern GtkApplication * app; +extern GtkWindow * main_window; +extern KotoPlaybackEngine * playback_engine; +extern GList * supported_mimes; + +GDBusConnection * dbus_conn = NULL; +guint mpris_bus_id = 0; +GDBusNodeInfo * introspection_data = NULL; + +static const gchar introspection_xml[] = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + +void handle_method_call( + GDBusConnection * connection, + const gchar * sender, + const gchar * object_path, + const gchar * interface_name, + const gchar * method_name, + GVariant * parameters, + GDBusMethodInvocation * invocation, + gpointer user_data +) { + (void) connection; + (void) sender; + (void) object_path; + (void) parameters; + (void) invocation; + (void) user_data; + + if (g_strcmp0(interface_name, "org.mpris.MediaPlayer2") == 0) { // Root mediaplayer2 interface + if (g_strcmp0(method_name, "Raise") == 0) { // Raise the window + gtk_window_unminimize(main_window); // Ensure we unminimize the window + gtk_window_present(main_window); // Present our main window + return; + } + + if (g_strcmp0(method_name, "Quit") == 0) { // Quit the application + gtk_application_remove_window(app, main_window); // Remove the window, thereby closing the app + return; + } + } else if (g_strcmp0(interface_name, "org.mpris.MediaPlayer2.Player") == 0) { // Root mediaplayer2 interface + if (g_strcmp0(method_name, "Next") == 0) { // Next track + koto_playback_engine_forwards(playback_engine); + return; + } + + if (g_strcmp0(method_name, "Previous") == 0) { // Previous track + koto_playback_engine_backwards(playback_engine); + return; + } + + if (g_strcmp0(method_name, "Pause") == 0) { // Explicit pause + koto_playback_engine_pause(playback_engine); + return; + } + + if (g_strcmp0(method_name, "PlayPause") == 0) { // Explicit playpause + koto_playback_engine_toggle(playback_engine); + return; + } + + if (g_strcmp0(method_name, "Stop") == 0) { // Explicit stop + koto_playback_engine_stop(playback_engine); + return; + } + + if (g_strcmp0(method_name, "Play") == 0) { // Explicit play + koto_playback_engine_play(playback_engine); + return; + } + } +} + +GVariant * handle_get_property( + GDBusConnection * connection, + const gchar * sender, + const gchar * object_path, + const gchar * interface_name, + const gchar * property_name, + GError ** error, + gpointer user_data +) { + (void) connection; + (void) sender; + (void) object_path; + (void) interface_name; + (void) error; + (void) user_data; + GVariant * ret; + + ret = NULL; + + if (g_strcmp0(property_name, "CanQuit") == 0) { // If property is CanQuit + ret = g_variant_new_boolean(TRUE); // Allow quitting. You can escape Hotel California for now. + } + + if (g_strcmp0(property_name, "CanRaise") == 0) { // If property is CanRaise + ret = g_variant_new_boolean(TRUE); // Allow raising. + } + + if (g_strcmp0(property_name, "HasTrackList") == 0) { // If property is HasTrackList + KotoPlaylist * playlist = koto_current_playlist_get_playlist(current_playlist); + if (KOTO_IS_PLAYLIST(playlist)) { + ret = g_variant_new_boolean(koto_playlist_get_length(playlist) > 0); + } else { // Don't have a playlist + ret = g_variant_new_boolean(FALSE); + } + } + + if (g_strcmp0(property_name, "Identity") == 0) { // Want identity + ret = g_variant_new_string("Koto"); // Not Jason Bourne but close enough + } + + if (g_strcmp0(property_name, "DesktopEntry") == 0) { // Desktop Entry + ret = g_variant_new_string(koto_rev_dns); + } + + if (g_strcmp0(property_name, "SupportedUriSchemas") == 0) { // Supported URI Schemas + GVariantBuilder * builder = g_variant_builder_new(G_VARIANT_TYPE("as")); // Array of strings + g_variant_builder_add(builder, "s", "file"); + ret = g_variant_new("as", builder); + g_variant_builder_unref(builder); // Unref builder since we no longer need it + } + + if (g_strcmp0(property_name, "SupportedMimeTypes") == 0) { // Supported mimetypes + GVariantBuilder * builder = g_variant_builder_new(G_VARIANT_TYPE("as")); // Array of strings + GList * mimes; + mimes = NULL; + + for (mimes = supported_mimes; mimes != NULL; mimes = mimes->next) { // For each mimetype + g_variant_builder_add(builder, "s", mimes->data); // Add the mime as a string + } + + ret = g_variant_new("as", builder); + g_variant_builder_unref(builder); + g_list_free(mimes); + } + + if (g_strcmp0(property_name, "Metadata") == 0) { // Metadata + KotoTrack * current_track = koto_playback_engine_get_current_track(playback_engine); + + if (KOTO_IS_TRACK(current_track)) { // Currently playing a track + ret = koto_track_get_metadata_vardict(current_track); + } else { // No track + GVariantBuilder * builder = g_variant_builder_new(G_VARIANT_TYPE_VARDICT); // Create an empty builder + ret = g_variant_builder_end(builder); // return the vardict + } + } + + if ( + (g_strcmp0(property_name, "CanPlay") == 0) || + (g_strcmp0(property_name, "CanPause") == 0) + ) { + KotoTrack * current_track = koto_playback_engine_get_current_track(playback_engine); + ret = g_variant_new_boolean(KOTO_IS_TRACK(current_track)); + } + + if (g_strcmp0(property_name, "CanSeek") == 0) { // Can control position over mpris + ret = g_variant_new_boolean(FALSE); + } + + if (g_strcmp0(property_name, "CanGoNext") == 0) { // Can Go Next + // TODO: Add some actual logic here + ret = g_variant_new_boolean(TRUE); + } + + if (g_strcmp0(property_name, "CanGoPrevious") == 0) { // Can Go Previous + // TODO: Add some actual logic here + ret = g_variant_new_boolean(TRUE); + } + + if (g_strcmp0(property_name, "CanControl") == 0) { // Can Control + ret = g_variant_new_boolean(TRUE); + } + + if (g_strcmp0(property_name, "PlaybackStatus") == 0) { // Get our playback status + GstState current_state = koto_playback_engine_get_state(playback_engine); // Get the current state + + if (current_state == GST_STATE_PLAYING) { + ret = g_variant_new_string("Playing"); + } else if (current_state == GST_STATE_PAUSED) { + ret = g_variant_new_string("Paused"); + } else { + ret = g_variant_new_string("Stopped"); + } + } + + return ret; +} + +gboolean handle_set_property( + GDBusConnection * connection, + const gchar * sender, + const gchar * object_path, + const gchar * interface_name, + const gchar * property_name, + GVariant * value, + GError ** error, + gpointer user_data +) { + (void) connection; + (void) sender; + (void) interface_name; + (void) object_path; + (void) error; + (void) user_data; + + if (g_strcmp0(property_name, "LoopStatus") == 0) { // Changing LoopStatus + koto_playback_engine_set_track_repeat(playback_engine, g_variant_get_boolean(value)); // Set the loop status state + return TRUE; + } + + if (g_strcmp0(property_name, "Shuffle") == 0) { // Changing Shuffle + koto_playback_engine_set_track_shuffle(playback_engine, g_variant_get_boolean(value)); // Set the shuffle state + return TRUE; + } + + return FALSE; +} + +void koto_update_mpris_playback_state(GstState state) { + GVariantBuilder * builder = g_variant_builder_new(G_VARIANT_TYPE_ARRAY); + + if (state == GST_STATE_PLAYING) { + g_variant_builder_add(builder, "{sv}", "PlaybackStatus", g_variant_new_string("Playing")); + } else if (state == GST_STATE_PAUSED) { + g_variant_builder_add(builder, "{sv}", "PlaybackStatus", g_variant_new_string("Paused")); + } else { + g_variant_builder_add(builder, "{sv}", "PlaybackStatus", g_variant_new_string("Stopped")); + } + + g_dbus_connection_emit_signal( + dbus_conn, + NULL, + "/org/mpris/MediaPlayer2", + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + g_variant_new("(sa{sv}as)", "org.mpris.MediaPlayer2.Player", builder, NULL), + NULL + ); +} + +void koto_update_mpris_info_for_track(KotoTrack * track) { + if (!KOTO_IS_TRACK(track)) { + return; + } + + GVariant * metadata = koto_track_get_metadata_vardict(track); // Get the GVariantBuilder variable dict for the metadata + + koto_update_mpris_info_for_track_with_metadata(track, metadata); +} + +void koto_update_mpris_info_for_track_with_metadata( + KotoTrack * track, + GVariant * metadata +) { + if (!KOTO_IS_TRACK(track)) { + return; + } + + GVariantBuilder * builder = g_variant_builder_new(G_VARIANT_TYPE_ARRAY); + + g_variant_builder_add(builder, "{sv}", "Metadata", metadata); + + g_dbus_connection_emit_signal( + dbus_conn, + NULL, + "/org/mpris/MediaPlayer2", + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + g_variant_new("(sa{sv}as)", "org.mpris.MediaPlayer2.Player", builder, NULL), + NULL + ); +} + +static const GDBusInterfaceVTable main_mpris_interface_vtable = { + handle_method_call, + handle_get_property, + handle_set_property, + { 0 } +}; + +void on_main_mpris_bus_acquired( + GDBusConnection * connection, + const gchar * name, + gpointer user_data +) { + (void) name; + (void) user_data; + dbus_conn = connection; + g_dbus_connection_register_object( + dbus_conn, + "/org/mpris/MediaPlayer2", + introspection_data->interfaces[0], + &main_mpris_interface_vtable, + NULL, + NULL, + NULL + ); + + g_dbus_connection_register_object( + dbus_conn, + "/org/mpris/MediaPlayer2", + introspection_data->interfaces[1], + &main_mpris_interface_vtable, + NULL, + NULL, + NULL + ); +} + +void setup_mpris_interfaces() { + introspection_data = g_dbus_node_info_new_for_xml(introspection_xml, NULL); + g_assert(introspection_data != NULL); + + mpris_bus_id = g_bus_own_name( + G_BUS_TYPE_SESSION, + "org.mpris.MediaPlayer2.koto", + G_BUS_NAME_OWNER_FLAGS_NONE, + on_main_mpris_bus_acquired, + NULL, + NULL, + NULL, + NULL + ); + + g_assert(mpris_bus_id > 0); +} diff --git a/src/playback/mpris.h b/src/playback/mpris.h new file mode 100644 index 0000000..6662a7b --- /dev/null +++ b/src/playback/mpris.h @@ -0,0 +1,71 @@ +/* mpris.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 +#include +#include "../indexer/structs.h" + +void koto_update_mpris_playback_state(GstState state); + +void koto_update_mpris_info_for_track(KotoTrack * track); + +void koto_update_mpris_info_for_track_with_metadata( + KotoTrack * track, + GVariant * metadata +); + +void handle_method_call( + GDBusConnection * connection, + const gchar * sender, + const gchar * object_path, + const gchar * interface_name, + const gchar * method_name, + GVariant * parameters, + GDBusMethodInvocation * invocation, + gpointer user_data +); + +GVariant * handle_get_property( + GDBusConnection * connection, + const gchar * sender, + const gchar * object_path, + const gchar * interface_name, + const gchar * property_name, + GError ** error, + gpointer user_data +); + +gboolean handle_set_property( + GDBusConnection * connection, + const gchar * sender, + const gchar * object_path, + const gchar * interface_name, + const gchar * property_name, + GVariant * value, + GError ** error, + gpointer user_data +); + +void on_main_mpris_bus_acquired( + GDBusConnection * connection, + const gchar * name, + gpointer user_data +); + +void setup_mpris_interfaces(); diff --git a/src/playlist/add-remove-track-popover.c b/src/playlist/add-remove-track-popover.c new file mode 100644 index 0000000..01c08a6 --- /dev/null +++ b/src/playlist/add-remove-track-popover.c @@ -0,0 +1,275 @@ +/* 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 KotoTracks + KotoTrack * track = pos->data; + + if (!KOTO_TRACK(track)) { // Not a track + continue; // Skip this + } + + if (should_add) { // Should be adding + koto_playlist_add_track(playlist, track, FALSE, TRUE); // Add the track to the playlist + } else { // Should be removing + gchar * track_uuid = koto_track_get_uuid(track); // Get the track + 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 { + KotoTrack * track = g_list_nth_data(self->tracks, 0); // Get the first track + + if (KOTO_IS_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..481bcd5 --- /dev/null +++ b/src/playlist/add-remove-track-popover.h @@ -0,0 +1,80 @@ +/* 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-modify-dialog.c b/src/playlist/create-modify-dialog.c new file mode 100644 index 0000000..32c2ee9 --- /dev/null +++ b/src/playlist/create-modify-dialog.c @@ -0,0 +1,353 @@ +/* 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; + + switch (prop_id) { + case PROP_PLAYLIST_UUID: + koto_create_modify_playlist_dialog_set_playlist_uuid(self, g_strdup(g_value_get_string(val))); + 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 = koto_utils_string_is_valid(self->playlist_uuid); + + 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"); + self->playlist_uuid = NULL; // Reset playlist UUID +} + +void koto_create_modify_playlist_dialog_set_playlist_uuid( + KotoCreateModifyPlaylistDialog * self, + gchar * playlist_uuid +) { + if (!koto_utils_string_is_valid(playlist_uuid)) { // Not a valid playlist UUID string + 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 (!koto_utils_string_is_valid(art)) { // If art is not defined + 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..0d7d26b --- /dev/null +++ b/src/playlist/create-modify-dialog.h @@ -0,0 +1,72 @@ +/* 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 new file mode 100644 index 0000000..ba27fa5 --- /dev/null +++ b/src/playlist/current.c @@ -0,0 +1,133 @@ +/* current.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 "current.h" + +enum { + SIGNAL_PLAYLIST_CHANGED, + N_SIGNALS +}; + +static guint signals[N_SIGNALS] = { + 0 +}; + +KotoCurrentPlaylist * current_playlist = NULL; + +struct _KotoCurrentPlaylist { + GObject parent_class; + KotoPlaylist * current_playlist; +}; + +struct _KotoCurrentPlaylistClass { + GObjectClass parent_class; + + void (* playlist_changed) ( + KotoCurrentPlaylist * self, + KotoPlaylist * playlist, + gboolean play_immediately + ); +}; + +static void koto_current_playlist_class_init(KotoCurrentPlaylistClass * c) { + GObjectClass * gobject_class; + + gobject_class = G_OBJECT_CLASS(c); + + signals[SIGNAL_PLAYLIST_CHANGED] = g_signal_new( + "playlist-changed", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoCurrentPlaylistClass, playlist_changed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 3, + KOTO_TYPE_PLAYLIST, + G_TYPE_BOOLEAN, + G_TYPE_BOOLEAN + ); +} + +G_DEFINE_TYPE(KotoCurrentPlaylist, koto_current_playlist, G_TYPE_OBJECT); + +static void koto_current_playlist_init(KotoCurrentPlaylist * self) { + self->current_playlist = NULL; +} + +KotoPlaylist * koto_current_playlist_get_playlist(KotoCurrentPlaylist * self) { + return KOTO_IS_CURRENT_PLAYLIST(self) ? self->current_playlist : NULL; +} + +void koto_current_playlist_save_playlist_state(KotoCurrentPlaylist * self) { + if (!KOTO_IS_CURRENT_PLAYLIST(self)) { // Not a CurrentPlaylist + return; + } + + if (!KOTO_IS_PLAYLIST(self->current_playlist)) { // No playlist + return; + } + + koto_playlist_save_current_playback_state(self->current_playlist); // Save the current playlist state +} + +void koto_current_playlist_set_playlist( + KotoCurrentPlaylist * self, + KotoPlaylist * playlist, + gboolean play_immediately, + gboolean play_current +) { + if (!KOTO_IS_CURRENT_PLAYLIST(self)) { + return; + } + + if (!KOTO_IS_PLAYLIST(playlist)) { // Not a playlist + return; + } + + if (KOTO_IS_PLAYLIST(self->current_playlist)) { + koto_current_playlist_save_playlist_state(self); // Save the current playlist state if needed + + gboolean * is_temp = FALSE; + g_object_get(self->current_playlist, "ephemeral", &is_temp, NULL); // Get the current ephemeral value + + if (is_temp) { // Is a temporary playlist + koto_playlist_unmap(self->current_playlist); // Unmap the playlist if needed + } else { // Not a temporary playlist + koto_playlist_commit(self->current_playlist); // Save the current playlist + } + + g_object_unref(self->current_playlist); // Unreference + } + + self->current_playlist = playlist; + g_object_ref(playlist); // Increment the reference + g_signal_emit( + self, + signals[SIGNAL_PLAYLIST_CHANGED], + 0, + playlist, + play_immediately, + play_current + ); +} + +KotoCurrentPlaylist * koto_current_playlist_new() { + return g_object_new(KOTO_TYPE_CURRENT_PLAYLIST, NULL); +} diff --git a/src/playlist/current.h b/src/playlist/current.h new file mode 100644 index 0000000..57b8cd2 --- /dev/null +++ b/src/playlist/current.h @@ -0,0 +1,55 @@ +/* current.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 "playlist.h" + +G_BEGIN_DECLS + +/** + * Type Definition + **/ + +#define KOTO_TYPE_CURRENT_PLAYLIST koto_current_playlist_get_type() +#define KOTO_CURRENT_PLAYLIST(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), KOTO_TYPE_CURRENT_PLAYLIST, KotoCurrentPlaylist)) +#define KOTO_IS_CURRENT_PLAYLIST(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_CURRENT_PLAYLIST)) + +typedef struct _KotoCurrentPlaylist KotoCurrentPlaylist; +typedef struct _KotoCurrentPlaylistClass KotoCurrentPlaylistClass; + +GLIB_AVAILABLE_IN_ALL +GType koto_current_playlist_get_type(void) G_GNUC_CONST; + +/** + * Current Playlist Functions + **/ + +KotoCurrentPlaylist * koto_current_playlist_new(); + +KotoPlaylist * koto_current_playlist_get_playlist(KotoCurrentPlaylist * self); + +void koto_current_playlist_save_playlist_state(KotoCurrentPlaylist * self); + +void koto_current_playlist_set_playlist( + KotoCurrentPlaylist * self, + KotoPlaylist * playlist, + gboolean play_immediately, + gboolean play_current +); + +G_END_DECLS diff --git a/src/playlist/playlist.c b/src/playlist/playlist.c new file mode 100644 index 0000000..7e3bb39 --- /dev/null +++ b/src/playlist/playlist.c @@ -0,0 +1,1021 @@ +/* playlist.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 "../db/db.h" +#include "../koto-utils.h" +#include "playlist.h" + +extern KotoCartographer * koto_maps; +extern sqlite3 * koto_db; + +enum { + PROP_0, + PROP_UUID, + PROP_NAME, + PROP_ALBUM_UUID, + PROP_ART_PATH, + PROP_EPHEMERAL, + PROP_IS_SHUFFLE_ENABLED, + N_PROPERTIES, +}; + +enum { + SIGNAL_MODIFIED, + SIGNAL_TRACK_ADDED, + SIGNAL_TRACK_LOAD_FINALIZED, + SIGNAL_TRACK_REMOVED, + N_SIGNALS +}; + +struct _KotoPlaylist { + GObject parent_instance; + gchar * uuid; + gchar * name; + gchar * art_path; + gchar * album_uuid; + gint current_position; + gchar * current_uuid; + + KotoPreferredPlaylistSortType 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 (* modified) (KotoPlaylist * playlist); + 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 +); + +static void koto_playlist_class_init(KotoPlaylistClass * c) { + GObjectClass * gobject_class; + + gobject_class = G_OBJECT_CLASS(c); + gobject_class->set_property = koto_playlist_set_property; + gobject_class->get_property = koto_playlist_get_property; + + props[PROP_UUID] = g_param_spec_string( + "uuid", + "UUID of the Playlist", + "UUID of the Playlist", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_NAME] = g_param_spec_string( + "name", + "Name of the Playlist", + "Name of the Playlist", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_ALBUM_UUID] = g_param_spec_string( + "album-uuid", + "UUID of an Album associated with this Playlist", + "UUID of an Album associated with this Playlist. Useful for Audiobooks.", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_ART_PATH] = g_param_spec_string( + "art-path", + "Path to any associated artwork of the Playlist", + "Path to any associated artwork of the Playlist", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_EPHEMERAL] = g_param_spec_boolean( + "ephemeral", + "Is the playlist ephemeral (temporary)", + "Is the playlist ephemeral (temporary)", + FALSE, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_IS_SHUFFLE_ENABLED] = g_param_spec_boolean( + "is-shuffle-enabled", + "Is shuffling enabled", + "Is shuffling enabled", + FALSE, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + g_object_class_install_properties(gobject_class, N_PROPERTIES, props); + + playlist_signals[SIGNAL_MODIFIED] = g_signal_new( + "modified", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoPlaylistClass, modified), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0 + ); + + 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 +) { + KotoPlaylist * self = KOTO_PLAYLIST(obj); + + switch (prop_id) { + case PROP_UUID: + g_value_set_string(val, self->uuid); + break; + case PROP_NAME: + g_value_set_string(val, self->name); + break; + case PROP_ALBUM_UUID: + g_value_set_string(val, self->album_uuid); + break; + case PROP_ART_PATH: + g_value_set_string(val, self->art_path); + break; + case PROP_EPHEMERAL: + g_value_set_boolean(val, self->ephemeral); + break; + case PROP_IS_SHUFFLE_ENABLED: + g_value_set_boolean(val, self->is_shuffle_enabled); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_playlist_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +) { + KotoPlaylist * self = KOTO_PLAYLIST(obj); + + switch (prop_id) { + case PROP_UUID: + koto_playlist_set_uuid(self, g_value_get_string(val)); + break; + case PROP_NAME: + koto_playlist_set_name(self, g_value_get_string(val)); + break; + case PROP_ALBUM_UUID: + koto_playlist_set_album_uuid(self, g_value_get_string(val)); + break; + case PROP_ART_PATH: + koto_playlist_set_artwork(self, g_value_get_string(val)); + break; + case PROP_EPHEMERAL: + self->ephemeral = g_value_get_boolean(val); + break; + case PROP_IS_SHUFFLE_ENABLED: + self->is_shuffle_enabled = g_value_get_boolean(val); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +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_PLAYLIST_SORT_TYPE_DEFAULT; // Default to default model + self->is_shuffle_enabled = FALSE; + 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_TRACK); +} + +void koto_playlist_add_to_played_tracks( + KotoPlaylist * self, + gchar * uuid +) { + if (!KOTO_IS_PLAYLIST(self)) { + return; + } + + KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, uuid); + + if (!KOTO_IS_TRACK(track)) { + return; + } + + if (koto_playlist_get_position_of_track(self, track) != -1) { // Already added + return; + } + + g_queue_push_tail(self->played_tracks, uuid); // Add to end +} + +void koto_playlist_add_track( + KotoPlaylist * self, + KotoTrack * track, + gboolean current, + gboolean commit_to_table +) { + if (!KOTO_IS_PLAYLIST(self)) { + return; + } + + if (!KOTO_IS_TRACK(track)) { + return; + } + + gchar * track_uuid = koto_track_get_uuid(track); + + if (koto_playlist_get_position_of_track(self, track) != -1) { // Found already + return; + } + + g_queue_push_tail(self->tracks, track_uuid); // Prepend the UUID to the tracks + g_queue_push_tail(self->sorted_tracks, track_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_track_save_to_playlist(track, self->uuid); // 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 = track_uuid; // Mark this as current UUID + koto_playlist_apply_model(self, self->model); // Re-apply our model to enforce mass sort + } + + g_signal_emit( + self, + playlist_signals[SIGNAL_TRACK_ADDED], + 0, + track_uuid + ); +} + +void koto_playlist_apply_model( + KotoPlaylist * self, + KotoPreferredPlaylistSortType 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 +} + +void koto_playlist_commit(KotoPlaylist * self) { + if (!KOTO_IS_PLAYLIST(self)) { + return; + } + + gboolean album_acceptable_type = FALSE; + + if (koto_utils_string_is_valid(self->album_uuid)) { + KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, self->album_uuid); // Get the album + + if (KOTO_IS_ALBUM(album)) { // Got the album + KotoLibraryType album_type = koto_album_get_lib_type(album); + album_acceptable_type = ((album_type == KOTO_LIBRARY_TYPE_AUDIOBOOK) || (album_type == KOTO_LIBRARY_TYPE_PODCAST)); + } + } + + if (self->ephemeral && !album_acceptable_type) { // Is a temporary playlist and NOT an acceptable type + return; + } + + guint64 track_playback_pos = 0; + + if (koto_utils_string_is_valid(self->current_uuid)) { // Have a track UUID + KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, self->current_uuid); // Get the track + + if (KOTO_IS_TRACK(track)) { // Is a track + track_playback_pos = koto_track_get_playback_position(track); // Get the track playback position and set it to our track_playback_pos + } + } + + gchar * commit_op = g_strdup_printf( + "INSERT INTO playlist_meta(id, name, art_path, preferred_model, album_id, track_id, playback_position_of_track)" + "VALUES('%s', quote(\"%s\"), quote(\"%s\"), %li, '%s', '%s', '%li')" + "ON CONFLICT(id) DO UPDATE SET name=excluded.name, art_path=excluded.art_path, preferred_model=excluded.preferred_model, album_id=excluded.album_id, track_id=excluded.track_id;", + self->uuid, + koto_utils_string_get_valid(self->name), + koto_utils_string_get_valid(self->art_path), + (guint64) self->model, + koto_utils_string_get_valid(self->album_uuid), + koto_utils_string_get_valid(self->current_uuid), + track_playback_pos + ); + + new_transaction(commit_op, "Failed to save playlist", FALSE); +} + +void koto_playlist_emit_modified(KotoPlaylist * self) { + if (!KOTO_IS_PLAYLIST(self)) { + return; + } + + g_signal_emit( + self, + playlist_signals[SIGNAL_MODIFIED], + 0 + ); +} + +gchar * koto_playlist_get_artwork(KotoPlaylist * self) { + return (KOTO_IS_PLAYLIST(self) && koto_utils_string_is_valid(self->art_path)) ? g_strdup(self->art_path) : NULL; // Return a duplicate of our art path +} + +KotoPreferredPlaylistSortType koto_playlist_get_current_model(KotoPlaylist * self) { + return KOTO_IS_PLAYLIST(self) ? self->model : KOTO_PREFERRED_PLAYLIST_SORT_TYPE_DEFAULT; +} + +gint koto_playlist_get_current_position(KotoPlaylist * self) { + return KOTO_IS_PLAYLIST(self) ? self->current_position : -1; +} + +KotoTrack * koto_playlist_get_current_track(KotoPlaylist * self) { + if (!KOTO_IS_PLAYLIST(self)) { // Not a playlist + return NULL; + } + + return koto_cartographer_get_track_by_uuid(koto_maps, self->current_uuid); // Get the current track +} + +gboolean koto_playlist_get_is_finalized(KotoPlaylist * self) { + return KOTO_IS_PLAYLIST(self) ? self->finalized : FALSE; +} + +gboolean koto_playlist_get_is_hidden(KotoPlaylist * self) { + return (self->ephemeral && koto_utils_string_is_valid(self->album_uuid)); // If the playlist is ephemeral and associated with an album, it should be hidden from nav +} + +guint koto_playlist_get_length(KotoPlaylist * self) { + return KOTO_IS_PLAYLIST(self) ? g_queue_get_length(self->tracks) : 0; // Get the length of the tracks +} + +gchar * koto_playlist_get_name(KotoPlaylist * self) { + return (KOTO_IS_PLAYLIST(self) && koto_utils_string_is_valid(self->name)) ? g_strdup(self->name) : NULL; +} + +gint koto_playlist_get_position_of_track( + KotoPlaylist * self, + KotoTrack * track +) { + if (!KOTO_IS_PLAYLIST(self)) { + return -1; + } + + if (!G_IS_LIST_STORE(self->store)) { + return -1; + } + + if (!KOTO_IS_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->sorted_tracks); + + if (tracks_len == g_queue_get_length(self->played_tracks)) { // Played all tracks + 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 + guint attempt = 0; + + 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->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; + track_uuid = selected_track; + break; + } else { // Failed to get the track + if (attempt > tracks_len / 2) { + break; // Give up + } + } + } + + g_rand_free(rando_calrissian); // Free rando + } + + return track_uuid; +} + +GListStore * koto_playlist_get_store(KotoPlaylist * self) { + return self->store; +} + +GQueue * koto_playlist_get_tracks(KotoPlaylist * self) { + return self->tracks; +} + +gchar * koto_playlist_get_uuid(KotoPlaylist * self) { + return g_strdup(self->uuid); +} + +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 + self->current_uuid = random_track_uuid; // Update our current UUID + koto_playlist_add_to_played_tracks(self, random_track_uuid); + + koto_playlist_emit_modified(self); + + return random_track_uuid; + } + + if (!koto_utils_string_is_valid(self->current_uuid)) { // No valid UUID yet + self->current_position++; + } else { // Have a UUID currently + KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, self->current_uuid); + + if (!KOTO_IS_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_uuid = g_queue_peek_nth(self->sorted_tracks, self->current_position); + koto_playlist_add_to_played_tracks(self, self->current_uuid); + koto_playlist_emit_modified(self); + + return self->current_uuid; +} + +gchar * koto_playlist_go_to_previous(KotoPlaylist * self) { + if (self->is_shuffle_enabled) { // Shuffling enabled + return koto_playlist_get_random_track(self); // Get a random track + } + + if (!koto_utils_string_is_valid(self->current_uuid)) { // No valid UUID + return NULL; + } + + KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, self->current_uuid); + + if (!KOTO_IS_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); + + koto_playlist_emit_modified(self); + + return self->current_uuid; +} + +void koto_playlist_mark_as_finalized(KotoPlaylist * self) { + if (!KOTO_IS_PLAYLIST(self)) { // Not a playlist + 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 +) { + KotoTrack * first_track = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) first_item); + KotoTrack * 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 +) { + KotoTrack * first_track = (KotoTrack*) first_item; + KotoTrack * second_track = (KotoTrack*) 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 + KotoPreferredPlaylistSortType model = GPOINTER_TO_UINT(g_list_nth_data(ptr_list, 1)); // Second item in the GPtrArray is a pointer to our KotoPreferredPlaylistSortType + + if ( + (model == KOTO_PREFERRED_PLAYLIST_SORT_TYPE_DEFAULT) || // Newest first model + (model == KOTO_PREFERRED_PLAYLIST_SORT_TYPE_OLDEST_FIRST) // Oldest first + ) { + gint first_track_pos = g_queue_index(self->tracks, koto_track_get_uuid(first_track)); + gint second_track_pos = g_queue_index(self->tracks, koto_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_PLAYLIST_SORT_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_PLAYLIST_SORT_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 + } + + KotoAlbum * first_album = koto_cartographer_get_album_by_uuid(koto_maps, first_album_uuid); + KotoAlbum * 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_ALBUM(first_album) && !KOTO_IS_ALBUM(second_album)) { // Neither are valid albums + return 0; // Just consider them as equal + } + + return g_utf8_collate(koto_album_get_name(first_album), koto_album_get_name(second_album)); + } + + if (model == KOTO_PREFERRED_PLAYLIST_SORT_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 + ); + + KotoArtist * first_artist = koto_cartographer_get_artist_by_uuid(koto_maps, first_artist_uuid); + KotoArtist * 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_ARTIST(first_artist) && !KOTO_IS_ARTIST(second_artist)) { // Neither are valid artists + return 0; // Just consider them as equal + } + + return g_utf8_collate(koto_artist_get_name(first_artist), koto_artist_get_name(second_artist)); + } else if (model == KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_TRACK_NAME) { // Track name + return g_utf8_collate(koto_track_get_name(first_track), koto_track_get_name(second_track)); + } else if (model == KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_TRACK_POS) { // Sort by track position + + guint first_track_disc = koto_track_get_disc_number(first_track); + guint second_track_disc = koto_track_get_disc_number(second_track); + + if (first_track_disc < second_track_disc) { // First is in an earlier CD / Disc / Part + return -1; + } else if (first_track_disc > second_track_disc) { // First is in later CD / Disc / Part + return 1; + } // Continue on to same CD + + guint64 first_track_pos = koto_track_get_position(first_track); + guint64 second_track_pos = koto_track_get_position(second_track); + + if (first_track_pos < second_track_pos) { + return -1; + } else if (first_track_pos > second_track_pos) { + return 1; + } + } + + 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_by_uuid( + KotoPlaylist * self, + gchar * uuid +) { + 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) { // 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 + } + + KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, uuid); // Get the track + + if (!KOTO_IS_TRACK(track)) { // Is not a track + 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_track_remove_from_playlist(track, self->uuid); + + g_signal_emit( + self, + playlist_signals[SIGNAL_TRACK_REMOVED], + 0, + uuid + ); +} + +void koto_playlist_save_current_playback_state(KotoPlaylist * self) { + if (!KOTO_IS_PLAYLIST(self)) { + return; + } + + if (!koto_utils_string_is_valid(self->album_uuid)) { // No album associated with this playlist + return; + } + + KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, self->album_uuid); // Get the album + + if (!KOTO_IS_ALBUM(album)) { // Failed to get the album + return; + } + + KotoLibraryType album_type = koto_album_get_lib_type(album); + + if ((album_type == KOTO_LIBRARY_TYPE_MUSIC) || (album_type == KOTO_LIBRARY_TYPE_UNKNOWN)) { // Not an Audiobook or Podcast + return; + } + + KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, self->current_uuid); // Get the track + + if (!KOTO_IS_TRACK(track) || !koto_utils_string_is_valid(self->current_uuid)) { // If we don't have a track we are currently playing + gchar * commit_op = g_strdup_printf("UPDATE playlist_meta SET track_id='', playback_position_of_track=0 WHERE id='%s';", self->uuid); + new_transaction(commit_op, "Failed to update our playlist meta", FALSE); + return; + } + + gchar * commit_op = g_strdup_printf( + "UPDATE playlist_meta SET track_id='%s', playback_position_of_track=%li WHERE id='%s';", + koto_utils_string_get_valid(self->current_uuid), + koto_track_get_playback_position(track), + self->uuid + ); + + new_transaction(commit_op, "Failed to update our playlist state", FALSE); + koto_playlist_emit_modified(self); +} + +void koto_playlist_set_album_uuid( + KotoPlaylist * self, + const gchar * album_uuid +) { + if (!KOTO_IS_PLAYLIST(self)) { + return; + } + + if (!koto_utils_string_is_valid(album_uuid)) { + return; + } + + self->album_uuid = g_strdup(album_uuid); // Update the album UUID + + if (self->finalized) { // Has already been loaded + koto_playlist_emit_modified(self); + } +} + +void koto_playlist_set_artwork( + KotoPlaylist * self, + const gchar * path +) { + if (!KOTO_IS_PLAYLIST(self)) { + return; + } + + if (path == NULL) { + return; + } + + magic_t cookie = magic_open(MAGIC_MIME); // Create our magic cookie so we can validate if what we are setting is an image + + if (cookie == NULL) { // Failed to allocate + return; + } + + if (magic_load(cookie, NULL) != 0) { // Failed to load our cookie + goto free_cookie; + } + + const gchar * mime_type = magic_file(cookie, path); // Get the mimetype for this file + + if ((mime_type == NULL) || !g_str_has_prefix(mime_type, "image/")) { // Failed to get our mimetype or not an image + goto free_cookie; + } + + if (self->art_path != NULL) { // art path is set + g_free(self->art_path); // Free the current path + } + + self->art_path = g_strdup(path); // Update our art path to a duplicate of provided path + + if (self->finalized) { // Has already been loaded + koto_playlist_emit_modified(self); + } + +free_cookie: + magic_close(cookie); // Close and free the cookie to the cookie monster +} + +void koto_playlist_set_name( + KotoPlaylist * self, + const gchar * name +) { + if (name == NULL) { + return; + } + + if (self->name != NULL) { // Have a name allocated already + g_free(self->name); // Free the name + } + + self->name = g_strdup(name); + + if (self->finalized) { // Has already been loaded + koto_playlist_emit_modified(self); + } +} + +void koto_playlist_set_position( + KotoPlaylist * self, + gint position +) { + if (!KOTO_IS_PLAYLIST(self)) { // Not a playlist + return; + } + + self->current_position = position; + + if (self->finalized) { // Has already been loaded + koto_playlist_emit_modified(self); + } +} + +void koto_playlist_set_track_as_current( + KotoPlaylist * self, + gchar * track_uuid +) { + if (!KOTO_IS_PLAYLIST(self)) { // Not a playlist + return; + } + + KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, track_uuid); // Get the track + + if (!KOTO_IS_TRACK(track)) { // Not a track + return; + } + + gint position_of_track = koto_playlist_get_position_of_track(self, track); + + if (position_of_track == -1) { + return; + } + + self->current_uuid = track_uuid; + self->current_position = position_of_track; // Set accurate position +} + +void koto_playlist_set_uuid( + KotoPlaylist * self, + const gchar * uuid +) { + if (uuid == NULL) { // No actual UUID + return; + } + + if (self->uuid != NULL) { // Have a UUID allocated already + return; // Do not allow changing the UUID + } + + self->uuid = g_strdup(uuid); // Set the new UUID +} + +void koto_playlist_tracks_queue_push_to_store( + gpointer data, + gpointer user_data +) { + gchar * track_uuid = (gchar*) data; + KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, track_uuid); + + if (!KOTO_IS_TRACK(track)) { // Not a track + return; + } + + g_list_store_append(G_LIST_STORE(user_data), track); +} + +void koto_playlist_unmap(KotoPlaylist * self) { + koto_cartographer_remove_playlist_by_uuid(koto_maps, self->uuid); // Remove from our cartographer +} + +KotoPlaylist * koto_playlist_new() { + return g_object_new( + KOTO_TYPE_PLAYLIST, + "uuid", + g_uuid_string_random(), + NULL + ); +} + +KotoPlaylist * koto_playlist_new_with_uuid(const gchar * uuid) { + return g_object_new( + KOTO_TYPE_PLAYLIST, + "uuid", + uuid, + NULL + ); +} diff --git a/src/playlist/playlist.h b/src/playlist/playlist.h new file mode 100644 index 0000000..69f708e --- /dev/null +++ b/src/playlist/playlist.h @@ -0,0 +1,171 @@ +/* playlist.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_PREFERRED_PLAYLIST_SORT_TYPE_DEFAULT, // Considered to be newest first + KOTO_PREFERRED_PLAYLIST_SORT_TYPE_OLDEST_FIRST, + KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_ALBUM, + KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_ARTIST, + KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_TRACK_NAME, + KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_TRACK_POS +} KotoPreferredPlaylistSortType; + +/** + * Type Definition + **/ + +#define KOTO_TYPE_PLAYLIST koto_playlist_get_type() +#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 + **/ + +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, + KotoTrack * track, + gboolean current, + gboolean commit_to_table +); + +void koto_playlist_apply_model( + KotoPlaylist * self, + KotoPreferredPlaylistSortType preferred_model +); + +void koto_playlist_commit(KotoPlaylist * self); + +void koto_playlist_emit_modified(KotoPlaylist * self); + +gchar * koto_playlist_get_artwork(KotoPlaylist * self); + +KotoPreferredPlaylistSortType koto_playlist_get_current_model(KotoPlaylist * self); + +gint koto_playlist_get_current_position(KotoPlaylist * self); + +KotoTrack * koto_playlist_get_current_track(KotoPlaylist * self); + +guint koto_playlist_get_length(KotoPlaylist * self); + +gboolean koto_playlist_get_is_finalized(KotoPlaylist * self); + +gboolean koto_playlist_get_is_hidden(KotoPlaylist * self); + +gchar * koto_playlist_get_name(KotoPlaylist * self); + +gint koto_playlist_get_position_of_track( + KotoPlaylist * self, + KotoTrack * 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_by_uuid( + KotoPlaylist * self, + gchar * uuid +); + +void koto_playlist_set_album_uuid( + KotoPlaylist * self, + const gchar * album_uuid +); + +void koto_playlist_set_artwork( + KotoPlaylist * self, + const gchar * path +); + +void koto_playlist_save_current_playback_state(KotoPlaylist * self); + +void koto_playlist_set_name( + KotoPlaylist * self, + const gchar * name +); + +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 new file mode 100644 index 0000000..3622206 --- /dev/null +++ b/theme/_button.scss @@ -0,0 +1,17 @@ +@import "vars"; + +.koto-button { + border-width: 0; + + & > .button-label { + margin-left: 10px; + } + + &:not(.active) { + color: $text-color-bright; + } + + &.active > image { + color: $koto-primary-color; + } +} diff --git a/theme/_disc-view.scss b/theme/_disc-view.scss new file mode 100644 index 0000000..dc3a487 --- /dev/null +++ b/theme/_disc-view.scss @@ -0,0 +1,14 @@ +@import "vars"; + +.discs-list { + background-color: transparent; + border-color: transparent; + border-width: 0; + + .disc-view { + & > box { // Horizontal box with image and disc label + color: $text-color-faded; + margin: 10px 0; + } + } +} \ No newline at end of file diff --git a/theme/_expander.scss b/theme/_expander.scss new file mode 100644 index 0000000..c07efe7 --- /dev/null +++ b/theme/_expander.scss @@ -0,0 +1,17 @@ +@import 'vars'; + +.expander { + & > .expander-header { + & > label { + font-size: large; + } + } + + & > revealer > box { + & > .koto-button { + &, & > box { + min-height: 40px; + } + } + } +} diff --git a/theme/_main.scss b/theme/_main.scss new file mode 100644 index 0000000..ea45551 --- /dev/null +++ b/theme/_main.scss @@ -0,0 +1,52 @@ +@import 'components/album-info'; +@import 'components/audiobook-view'; +@import 'components/badge'; +@import 'components/cover-art-button'; +@import 'components/gtk-overrides'; +@import 'components/track-list'; +@import 'components/track-table'; +@import 'components/writer-page'; +@import 'pages/audiobook-library'; +@import 'pages/artist-view'; +@import 'pages/music-local'; +@import 'pages/playlist-page'; + +@import 'button'; +@import 'disc-view'; +@import 'expander'; +@import 'player-bar'; +@import 'primary-nav'; +@import 'track-item'; + +window { + color: $text-color-bright; + background-color: $bg-primary; + + & > headerbar, & > headerbar:active { + background-color: $bg-secondary; + background-image: none; + border-bottom-width: 0; // Unset default styling for headerbar + + &:active windowcontrols button { // Minimize, Maximize, Close buttons when window is active + color: $text-color-bright; + text-decoration-color: $text-color-bright; + } + + &:not(:active) windowcontrols button { // Minimize, Maximize, Close buttons when window is inactive + color: $text-color-faded; + text-decoration-color: $text-color-faded; + } + } + + .koto-dialog-container { + background-color: transparentize($bg-secondary, 0.25); + 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 + .writer-page { // Writer page in Audiobook + padding: $padding; + } +} diff --git a/theme/_player-bar.scss b/theme/_player-bar.scss new file mode 100644 index 0000000..17580e6 --- /dev/null +++ b/theme/_player-bar.scss @@ -0,0 +1,73 @@ +@import 'vars'; + +.player-bar { + background-color: $bg-secondary; + background-image: none; + padding: $halvedpadding; + + .koto-button { + &:not(.toggled) { + color: $player-bar-icon-color; + } + + &.toggled { + color: $text-color-bright; + } + } + + .playerbar-primary-controls, // Primary + .playerbar-secondary-controls { // Secondary + & > .koto-button { // Direct descendents + margin: 0 $quarterpadding; + } + } + + .playerbar-primary-controls { // Primary Controls + .playerbar-advanced-controls { // Advanced controls + margin-left: $padding; + + & > entry { // Inner GtkEntry + margin: 0 $halvedpadding; + } + } + } + + .playerbar-info { // Central info section + & > box { // Info labels + margin-left: 2ex; + + & > image { + color: $text-color-faded; + } + + & > 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 + + } + } + } + } + + .playerbar-secondary-controls { // Secondary controls + label { // Inner playback position label + font-size: large; + margin-right: $halvedpadding; + } + } +} diff --git a/theme/_primary-nav.scss b/theme/_primary-nav.scss new file mode 100644 index 0000000..7c0c054 --- /dev/null +++ b/theme/_primary-nav.scss @@ -0,0 +1,35 @@ +.primary-nav { + padding: 10px 0; + + & > viewport > box { + & > .koto-button, // Direct buttons like Home + & > .expander > .expander-header { // Expander Headers + font-size: large; + padding-top: 10px; + padding-bottom: 10px; + } + + & > .koto-button, // Direct buttons like Home + & > .expander > .expander-header, // Expander Headers + & > .expander revealer .koto-button { // Expander revealer Buttons + margin-left: 10px; + margin-right: 10px; + padding-left: 10px; + padding-right: 10px; + } + + & > .koto-button, // Direct buttons like Home + & > .expander revealer .koto-button { // Expander revealer Buttons + &.pseudoactive { // When hovering or explicit request to show as active + background-color: $primary-nav-button-active-color; + border-radius: 10px; + } + } + + & > .expander { + & > .expander-header { + color: $text-color-faded; + } + } + } +} \ No newline at end of file diff --git a/theme/_track-item.scss b/theme/_track-item.scss new file mode 100644 index 0000000..7afbbb0 --- /dev/null +++ b/theme/_track-item.scss @@ -0,0 +1,3 @@ +.track-item { + padding: 10px; +} diff --git a/theme/_vars.scss b/theme/_vars.scss new file mode 100644 index 0000000..348f9e9 --- /dev/null +++ b/theme/_vars.scss @@ -0,0 +1,9 @@ +$midnight: #1d1d1d; +$darkgrey: #666666; +$green: #60E078; +$palewhite: #cccccc; +$red : #FF4652; + +$padding: 40px; +$halvedpadding: $padding / 2; +$quarterpadding: $padding / 4; \ No newline at end of file diff --git a/theme/components/_album-info.scss b/theme/components/_album-info.scss new file mode 100644 index 0000000..f0c646c --- /dev/null +++ b/theme/components/_album-info.scss @@ -0,0 +1,30 @@ +// This file contain the styling for the Album Info section + +.album-info { + .album-description, + .album-narrator, + .album-title-year-combo, + .genres-tag-list { + margin-bottom: $quarterpadding + } + + .album-title-year-combo { + padding-top: $halvedpadding; + } + + .album-title { // Title of album + color: $text-color-faded; + font-size: 2.5em; + font-weight: bold; + } + + .album-year { + margin-left: $halvedpadding; + } + + .genres-tag-list { // Genres Tag List + & .label-badge:not(:last-child) { + margin-right: $halvedpadding; + } + } +} \ No newline at end of file diff --git a/theme/components/_audiobook-view.scss b/theme/components/_audiobook-view.scss new file mode 100644 index 0000000..dafa68a --- /dev/null +++ b/theme/components/_audiobook-view.scss @@ -0,0 +1,34 @@ +// This is the styling for the Audiobook VIew + +@import '../vars'; + +.audiobook-view { + .side-info { // Side Info + margin-right: $halvedpadding; + + button, + image { + margin-bottom: $halvedpadding; + } + + button { // Play / Continue Playback button + font-size: large; + font-weight: bold; + } + + & > label { + font-size: large; + + &:last-child { + margin-bottom: $halvedpadding; + } + } + } + + .chapters-label { // Chapters label after album info + color: $text-color-faded; + font-size: x-large; + font-weight: bold; + padding: $halvedpadding 0; // Top / bottom padding + } +} \ No newline at end of file diff --git a/theme/components/_badge.scss b/theme/components/_badge.scss new file mode 100644 index 0000000..1f28024 --- /dev/null +++ b/theme/components/_badge.scss @@ -0,0 +1,12 @@ + // This file contains the styling for our label badge + +@import '../vars'; + +.label-badge { + color: $text-color-faded; + font-size: large; + font-weight: 900; + background-color: $bg-secondary; + border-radius: 10px; + padding: 5px 20px; +} \ No newline at end of file diff --git a/theme/components/_cover-art-button.scss b/theme/components/_cover-art-button.scss new file mode 100644 index 0000000..4bd5d20 --- /dev/null +++ b/theme/components/_cover-art-button.scss @@ -0,0 +1,11 @@ +.cover-art-button { + &:hover { + & > image { + opacity: 0.75; + } + + & > revealer > box { // Inner controls + background-color: transparentize($bg-secondary, 0.25); + } + } +} \ 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..0eccc9e --- /dev/null +++ b/theme/components/_gtk-overrides.scss @@ -0,0 +1,135 @@ +@import '../vars'; + +@mixin selected-row-styling { + color: $selected-row-color-text; + background-color: $selected-row-color-bg; + 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 { + .text-button { + &:not(.destructive-action):not(.suggested-action) { // Is just a plain ol' normal text button + color: $button-normal-color-text; + background: $button-normal-color-bg; + border-color: $button-normal-color-border; + } + } + + &.destructive-action { + color: $button-destructive-color-text; + background-color: $button-destructive-color-bg; + background-image: none; + border-width: 0; + } + + &.suggested-action { // Adwaita makes it blue but we want it green + color: $button-suggested-color-text; + background-color: $button-suggested-color-bg; + background-image: none; + border-width: 0; + } +} + +checkbutton { + color: $text-color-bright; + + &:active { + background-color: transparent; + } +} + +check { + background-image: none; + + &:not(:checked) { // Not checked + color: $text-color-faded; + } + + &:active { + background-color: transparent; + } + + &:checked { // Checked but not actively pressing on it + color: $text-color-bright; + background-color: $koto-primary-color; + } +} + +entry { + color: $text-color-bright; + background: $input-background; + border-color: $border-color; + + placeholder { // Placeholder text + color: $text-color-faded; + } +} + +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; + } + } +} + +popover.background { + & > arrow, & > contents { + background-color: $bg-primary; + border-color: $border-color; + } + + & > contents { + color: $text-color-bright; + } +} + +range { + &.dragging { // Dragging a range + & > trough { + highlight { + border-color: $koto-primary-color; + } + + & > slider { + background-color: $koto-primary-color; + } + } + } +} + +scale { // Progress bar + highlight { + background-color: $koto-primary-color; + border-color: $koto-primary-color; + } + + slider { // Slider + outline-color: $koto-primary-color; + } +} + +scalebutton { + &, button, image { + color: $player-bar-icon-color; + } + + &:active, &:checked, &:hover, button:active, button:checked, button:hover { + background-color: transparent; + } +} \ No newline at end of file diff --git a/theme/components/_track-list.scss b/theme/components/_track-list.scss new file mode 100644 index 0000000..217089b --- /dev/null +++ b/theme/components/_track-list.scss @@ -0,0 +1,19 @@ +// Track List styling + +@import '../vars'; + +.track-list { + & > row { + &:not(:active):not(:selected) { // Neither active nor selected, see gtk overrides + color: $text-color-bright; + + &:nth-child(odd):not(:hover) { + background-color: $bg-primary; + } + + &:nth-child(even), &:hover { + background-color: $bg-secondary; + } + } + } +} \ No newline at end of file diff --git a/theme/components/_track-table.scss b/theme/components/_track-table.scss new file mode 100644 index 0000000..59f2d8c --- /dev/null +++ b/theme/components/_track-table.scss @@ -0,0 +1,49 @@ +.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 { + &:not(:selected) { + color: $text-color-bright; + } + + &:nth-child(odd):not(:selected) { + background-color: $bg-secondary; + } + + & > .track-list-columned-item { // Track rows + font-size: x-large; + } + } + } + + .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 diff --git a/theme/components/_writer-page.scss b/theme/components/_writer-page.scss new file mode 100644 index 0000000..442201e --- /dev/null +++ b/theme/components/_writer-page.scss @@ -0,0 +1,12 @@ +// This is the styling for the writer page + +@import '../vars'; + +.writer-page { + .writer-header { // Our writer / artist header label + color: $text-color-faded; + font-size: 4em; + font-weight: bold; + padding-bottom: $padding; + } +} \ No newline at end of file diff --git a/theme/koto-builtin-dark.scss b/theme/koto-builtin-dark.scss new file mode 100644 index 0000000..f697902 --- /dev/null +++ b/theme/koto-builtin-dark.scss @@ -0,0 +1,5 @@ +$variant : 'dark'; + +@import 'vars'; // Root vars +@import 'variants/dark/vars'; // Dark variant vars +@import 'main'; \ No newline at end of file diff --git a/theme/koto-builtin-gruvbox.scss b/theme/koto-builtin-gruvbox.scss new file mode 100644 index 0000000..f64fdf7 --- /dev/null +++ b/theme/koto-builtin-gruvbox.scss @@ -0,0 +1,5 @@ +$variant : 'gruvbox'; + +@import 'vars'; // Root vars +@import 'variants/gruvbox/vars'; // Grubox variant vars +@import 'main'; \ No newline at end of file diff --git a/theme/koto-builtin-light.scss b/theme/koto-builtin-light.scss new file mode 100644 index 0000000..8a33e8a --- /dev/null +++ b/theme/koto-builtin-light.scss @@ -0,0 +1,5 @@ +$variant : 'light'; + +@import 'vars'; // Root vars +@import 'variants/light/vars'; // Light variant vars +@import 'main'; \ No newline at end of file diff --git a/theme/meson.build b/theme/meson.build new file mode 100644 index 0000000..1d6a97a --- /dev/null +++ b/theme/meson.build @@ -0,0 +1,22 @@ +sassc = find_program('sassc', required: true) + +builtin_variants = [ + 'dark', + 'gruvbox', + 'light' +] + +themes = [] + +foreach variant: builtin_variants + themes += custom_target('@0@ theme generation'.format(variant), + input: 'koto-builtin-@0@.scss'.format(variant), + output: 'koto-builtin-@0@.css'.format(variant), + command: [ + sassc, + [ '-a', '-M', '-t', 'compact' ], + '@INPUT@', '@OUTPUT@', + ], + build_by_default: true, + ) +endforeach \ No newline at end of file diff --git a/theme/pages/_artist-view.scss b/theme/pages/_artist-view.scss new file mode 100644 index 0000000..5d07e44 --- /dev/null +++ b/theme/pages/_artist-view.scss @@ -0,0 +1,14 @@ +.artist-view { + .no-albums-view { // No Albums + .no-albums-view-header { // Header for artist when we have no albums + .cover-art-button { // Button to play all artist tracks + margin-right: 40px; + } + + & > label { // Artist Name + font-weight: 800; + font-size: 10ex; + } + } + } +} \ No newline at end of file diff --git a/theme/pages/_audiobook-library.scss b/theme/pages/_audiobook-library.scss new file mode 100644 index 0000000..987d2be --- /dev/null +++ b/theme/pages/_audiobook-library.scss @@ -0,0 +1,36 @@ +// This file contains the styling for our Audiobook Library + +@import '../vars'; + +.audiobook-library { // Library page + .genres-banner { // Banner for genres list + .large-banner { // Large banner with art for each genre + padding: $padding; + + .audiobook-genre-button { // Genre buttons + .koto-button { + font-size: 2em; + margin: 0.5em; + } + } + } + } + + .writers-button-flow { // Flowbox of buttons for writers + padding: 0 $padding; // Horizontal padding of our standard item padding + + flowboxchild { + padding: 0; + + &:nth-child(even) { + margin: 0 0.5em; + } + + .writer-button { // Writer button + color: $text-color-bright; + font-size: 1.4em; + background-color: $bg-secondary; + } + } + } +} \ No newline at end of file diff --git a/theme/pages/_music-local.scss b/theme/pages/_music-local.scss new file mode 100644 index 0000000..337d5d6 --- /dev/null +++ b/theme/pages/_music-local.scss @@ -0,0 +1,29 @@ +@import '../vars'; + +.page-music-local { + & > .artist-list { + &, & > viewport, & > viewport > list { + background-color: $artist-list-bg; + } + + & > viewport > list { + & > row { + padding: $halvedpadding; + } + } + } + + & > stack { + & > .artist-view { + & > viewport > .artist-view-content { + & > .album-list { + & > flowboxchild > .album-view { + & > overlay { + margin-right: $padding; + } + } + } + } + } + } +} diff --git a/theme/pages/_playlist-page.scss b/theme/pages/_playlist-page.scss new file mode 100644 index 0000000..9527357 --- /dev/null +++ b/theme/pages/_playlist-page.scss @@ -0,0 +1,38 @@ +@import '../vars'; + +.playlist-page { + .playlist-page-header { // Our header + & > .playlist-page-header-info { // Our info centerbox + margin-left: 40px; + + & > label { // All labels + color: $text-color-faded; + } + + & > 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 { + + } + } +} \ No newline at end of file diff --git a/theme/variants/dark/_vars.scss b/theme/variants/dark/_vars.scss new file mode 100644 index 0000000..2748469 --- /dev/null +++ b/theme/variants/dark/_vars.scss @@ -0,0 +1,27 @@ +$koto-primary-color: $green; + +$bg-primary : #2e2e2e; +$bg-secondary : $midnight; + +$border-color: black; + +$text-color-bright: white; +$text-color-faded: $palewhite; + +$button-destructive-color-bg: $red; +$button-destructive-color-text: white; +$button-suggested-color-bg: $koto-primary-color; +$button-suggested-color-text: $midnight; +$button-normal-color-bg: $midnight; +$button-normal-color-text: $text-color-bright; +$button-normal-color-border: black; + +$input-background: $bg-primary; + +$primary-nav-button-active-color: $bg-secondary; + +$artist-list-bg: $bg-secondary; +$player-bar-icon-color: $darkgrey; + +$selected-row-color-bg : $koto-primary-color; +$selected-row-color-text : $midnight; \ No newline at end of file diff --git a/theme/variants/gruvbox/_vars.scss b/theme/variants/gruvbox/_vars.scss new file mode 100644 index 0000000..db06e49 --- /dev/null +++ b/theme/variants/gruvbox/_vars.scss @@ -0,0 +1,27 @@ +$koto-primary-color: #ba5923; + +$bg-primary : #2C2826; +$bg-secondary : #211e1c; + +$border-color: black; + +$text-color-bright: white; +$text-color-faded: $palewhite; + +$button-destructive-color-bg: $red; +$button-destructive-color-text: white; +$button-suggested-color-bg: $koto-primary-color; +$button-suggested-color-text: $midnight; +$button-normal-color-bg: $midnight; +$button-normal-color-text: $text-color-bright; +$button-normal-color-border: black; + +$input-background: $bg-primary; + +$primary-nav-button-active-color: $bg-secondary; + +$artist-list-bg: $bg-secondary; +$player-bar-icon-color: #423C3A; + +$selected-row-color-bg : $koto-primary-color; +$selected-row-color-text : $midnight; \ No newline at end of file diff --git a/theme/variants/light/_vars.scss b/theme/variants/light/_vars.scss new file mode 100644 index 0000000..34fd53c --- /dev/null +++ b/theme/variants/light/_vars.scss @@ -0,0 +1,27 @@ +$koto-primary-color: $green; + +$bg-primary : #fafafa; +$bg-secondary : $palewhite; + +$border-color: $darkgrey; + +$text-color-bright: $midnight; +$text-color-faded: $darkgrey; + +$button-destructive-color-bg: $red; +$button-destructive-color-text: white; +$button-suggested-color-bg: $koto-primary-color; +$button-suggested-color-text: $midnight; +$button-normal-color-bg: white; +$button-normal-color-text: $text-color-bright; +$button-normal-color-border: $darkgrey; + +$input-background: $bg-primary; + +$primary-nav-button-active-color: $bg-secondary; + +$artist-list-bg: $bg-secondary; +$player-bar-icon-color: $darkgrey; + +$selected-row-color-bg : $koto-primary-color; +$selected-row-color-text : $midnight; \ No newline at end of file