Compare commits
No commits in common. "master" and "qt6" have entirely different histories.
258
.clang-format
Normal file
|
@ -0,0 +1,258 @@
|
|||
---
|
||||
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: '^<ext/.*\.h>'
|
||||
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
|
||||
---
|
2
.clangd
Normal file
|
@ -0,0 +1,2 @@
|
|||
CompileFlags:
|
||||
Add: [-std=c++20]
|
12
.github/FUNDING.yml
vendored
|
@ -1,12 +0,0 @@
|
|||
# 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']
|
BIN
.github/koto-alpha-image.png
vendored
Before Width: | Height: | Size: 696 KiB |
94
.gitignore
vendored
|
@ -1,4 +1,90 @@
|
|||
DesiredSettings.md
|
||||
builddir
|
||||
.buildconfig
|
||||
src/theme/style.css
|
||||
# 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
|
||||
|
|
17
.vscode/c_cpp_properties.json
vendored
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Linux",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/**",
|
||||
"/usr/include/**"
|
||||
],
|
||||
"defines": [],
|
||||
"compilerPath": "/usr/bin/gcc",
|
||||
"cStandard": "gnu17",
|
||||
"cppStandard": "c++20",
|
||||
"intelliSenseMode": "linux-gcc-x64"
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
50
.vscode/launch.json
vendored
|
@ -1,50 +0,0 @@
|
|||
{
|
||||
// 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"
|
||||
},
|
||||
]
|
||||
}
|
22
.vscode/settings.json
vendored
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
92
.vscode/tasks.json
vendored
|
@ -1,92 +0,0 @@
|
|||
{
|
||||
// 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": []
|
||||
}
|
||||
]
|
||||
}
|
8
CMakeLists.txt
Normal file
|
@ -0,0 +1,8 @@
|
|||
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)
|
509
COPYING
|
@ -1,202 +1,373 @@
|
|||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1. Definitions.
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
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.
|
||||
|
||||
"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.
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
(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.
|
||||
|
||||
"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.
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
"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).
|
||||
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.
|
||||
|
||||
"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.
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
"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."
|
||||
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.
|
||||
|
||||
"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.
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
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.
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
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.
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
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:
|
||||
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.
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
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.
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
(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
|
||||
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.
|
||||
|
||||
(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.
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
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.
|
||||
2.1. Grants
|
||||
|
||||
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.
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
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.
|
||||
(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
|
||||
|
||||
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.
|
||||
(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.
|
||||
|
||||
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.
|
||||
2.2. Effective Date
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
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:
|
||||
|
||||
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.
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
(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
|
||||
|
||||
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
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
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).
|
||||
|
||||
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.
|
||||
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.
|
||||
|
|
29
README.md
|
@ -1,26 +1,13 @@
|
|||
# Koto
|
||||
# Koto Qt
|
||||
|
||||

|
||||
Work in progress implementation of Koto in Qt6, Kirigami, and C++.
|
||||
|
||||
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**.
|
||||
To clone this repository on [Radicle](https://radicle.xyz), simple run:
|
||||
|
||||
## Blog
|
||||
```
|
||||
rad clone rad://z2RuqdobvbGje3mWhtMQ9cUVNfizp
|
||||
```
|
||||
|
||||
- [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/)
|
||||
## LICENSE
|
||||
|
||||
## License
|
||||
|
||||
Koto is licensed under the Apache 2.0 license.
|
||||
Licensed under the Mozilla Public License 2.0 (MPL-2.0).
|
||||
|
|
17
Taskfile.yml
Normal file
|
@ -0,0 +1,17 @@
|
|||
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
|
|
@ -1,14 +0,0 @@
|
|||
#!/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')])
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
{
|
||||
"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" : { }
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop">
|
||||
<id>com.github.joshstrobl.koto.desktop</id>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>Apache-2.0</project_license>
|
||||
<name>Koto</name>
|
||||
<summary>Koto is an in-development audiobook, music, and podcast manager.</summary>
|
||||
<description>
|
||||
<p>Koto is an in-development audiobook, music, and podcast manager that is designed for and caters to a modern desktop Linux experience.</p>
|
||||
</description>
|
||||
<launchable type="desktop-id">com.github.joshstrobl.koto.desktop</launchable>
|
||||
<url type="homepage">https://github.com/JoshStrobl/koto</url>
|
||||
<url type="bugtracker">https://github.com/JoshStrobl/koto/issues</url>
|
||||
<url type="donation">https://patreon.com/joshuastrobl</url>
|
||||
<url type="donation">https://liberapay.com/joshuastrobl</url>
|
||||
<developer_name>Joshua Strobl</developer_name>
|
||||
<update_contact>joshua.strobl_AT_outlook.com</update_contact>
|
||||
<content_rating type="oars-1.0">
|
||||
<content_attribute id="language-humor">mild</content_attribute>
|
||||
</content_rating>
|
||||
<provides>
|
||||
<binary>com.github.joshstrobl.koto</binary>
|
||||
</provides>
|
||||
<recommends>
|
||||
<control>pointing</control>
|
||||
<control>touch</control>
|
||||
<display_length compare="ge">1600</display_length>
|
||||
</recommends>
|
||||
<requires>
|
||||
<control>keyboard</control>
|
||||
<display_length compare="ge">1366</display_length>
|
||||
</requires>
|
||||
</component>
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<schemalist gettext-domain="koto">
|
||||
<schema id="com.github.joshstrobl.koto" path="/com/github/joshstrobl/koto/">
|
||||
</schema>
|
||||
</schemalist>
|
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 6 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 9.1 KiB |
|
@ -1,41 +0,0 @@
|
|||
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
|
|
@ -1,181 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="520"
|
||||
height="240"
|
||||
version="1.1"
|
||||
viewBox="0 0 137.58 63.5"
|
||||
id="svg49"
|
||||
sodipodi:docname="business-and-personal-finance.svg"
|
||||
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview51"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="3.5480769"
|
||||
inkscape:cx="308.75881"
|
||||
inkscape:cy="167.13279"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2087"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg49" />
|
||||
<defs
|
||||
id="defs15">
|
||||
<linearGradient
|
||||
id="linearGradient295062"
|
||||
x1="-134.86"
|
||||
x2="-134.86"
|
||||
y1="-123.4"
|
||||
y2="40.627"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
stop-color="#5f38ad"
|
||||
offset="0"
|
||||
id="stop7" />
|
||||
<stop
|
||||
stop-color="#874ff5"
|
||||
offset="1"
|
||||
id="stop9" />
|
||||
</linearGradient>
|
||||
<clipPath
|
||||
id="clipPath294187-9">
|
||||
<rect
|
||||
x="-218.13"
|
||||
y="-82.027"
|
||||
width="39.22"
|
||||
height="24.317"
|
||||
rx="5.2917"
|
||||
ry="5.2917"
|
||||
fill="#e8cc4c"
|
||||
fill-rule="evenodd"
|
||||
id="rect12" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g
|
||||
id="g1680"
|
||||
transform="scale(1.0000242,1)"
|
||||
style="stroke-width:0.999988">
|
||||
<g
|
||||
id="g1664"
|
||||
style="stroke-width:0.999988">
|
||||
<rect
|
||||
transform="translate(0,-11.25)"
|
||||
x="2.7931001e-06"
|
||||
y="11.25"
|
||||
width="137.58"
|
||||
height="63.5"
|
||||
fill="#ffffff"
|
||||
stroke-opacity="0.82206"
|
||||
stroke-width="1.05829"
|
||||
style="paint-order:stroke fill markers"
|
||||
id="rect17" />
|
||||
<g
|
||||
transform="matrix(0.14144,0,0,0.14144,136.46,21.232)"
|
||||
stroke-width="7.06981"
|
||||
id="g39">
|
||||
<rect
|
||||
x="-267.14999"
|
||||
y="-123.41"
|
||||
width="264.57999"
|
||||
height="164.03999"
|
||||
fill="url(#linearGradient295062)"
|
||||
fill-rule="evenodd"
|
||||
id="rect19"
|
||||
style="fill:url(#linearGradient295062);stroke-width:7.06973" />
|
||||
<g
|
||||
transform="translate(-22.74,-0.10437)"
|
||||
stroke-width="7.06981"
|
||||
id="g25">
|
||||
<rect
|
||||
x="-218.13"
|
||||
y="-82.027"
|
||||
width="39.220001"
|
||||
height="24.316999"
|
||||
rx="5.2916999"
|
||||
ry="5.2916999"
|
||||
fill="#e8cc4c"
|
||||
fill-rule="evenodd"
|
||||
id="rect21"
|
||||
style="stroke-width:7.06973" />
|
||||
<path
|
||||
d="m -267.18,-101.62 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m -134.94,-63.5 h 137.58 m -137.58,7.9375 h 137.58 m -137.58,7.9375 h 137.58 m -137.58,7.9375 h 137.58 m -137.58,7.9375 h 137.58 m -137.58,7.9375 h 137.58 m -137.58,7.9375 h 137.58 m -137.58,7.9375 h 137.58 m -137.58,7.9375 h 137.58"
|
||||
clip-path="url(#clipPath294187-9)"
|
||||
fill="none"
|
||||
stroke="#665c0c"
|
||||
stroke-width="3.74115px"
|
||||
id="path23" />
|
||||
</g>
|
||||
<g
|
||||
fill="#ffffff"
|
||||
fill-rule="evenodd"
|
||||
id="g37"
|
||||
style="stroke-width:7.06973">
|
||||
<rect
|
||||
x="-240.87"
|
||||
y="8.8177996"
|
||||
width="151.45"
|
||||
height="15.875"
|
||||
rx="5.2916999"
|
||||
ry="5.2916999"
|
||||
id="rect27"
|
||||
style="stroke-width:7.06981" />
|
||||
<rect
|
||||
x="-240.87"
|
||||
y="-35.048"
|
||||
width="40.146999"
|
||||
height="15.875"
|
||||
rx="5.2916999"
|
||||
ry="5.2916999"
|
||||
id="rect29"
|
||||
style="stroke-width:7.06981" />
|
||||
<rect
|
||||
x="-187.03999"
|
||||
y="-35.048"
|
||||
width="40.146999"
|
||||
height="15.875"
|
||||
rx="5.2916999"
|
||||
ry="5.2916999"
|
||||
id="rect31"
|
||||
style="stroke-width:7.06981" />
|
||||
<rect
|
||||
x="-133.21001"
|
||||
y="-35.048"
|
||||
width="40.146999"
|
||||
height="15.875"
|
||||
rx="5.2916999"
|
||||
ry="5.2916999"
|
||||
id="rect33"
|
||||
style="stroke-width:7.06981" />
|
||||
<rect
|
||||
x="-79.375"
|
||||
y="-35.048"
|
||||
width="40.146999"
|
||||
height="15.875"
|
||||
rx="5.2916999"
|
||||
ry="5.2916999"
|
||||
id="rect35"
|
||||
style="stroke-width:7.06981" />
|
||||
</g>
|
||||
</g>
|
||||
<rect
|
||||
x="2.7931001e-06"
|
||||
y="1.1538e-07"
|
||||
width="137.58"
|
||||
height="63.5"
|
||||
fill="#ffffff"
|
||||
fill-opacity="0.46872"
|
||||
style="stroke-width:0.999976;paint-order:stroke fill markers"
|
||||
id="rect41" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 5.4 KiB |
|
@ -1,153 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="520"
|
||||
height="240"
|
||||
version="1.1"
|
||||
viewBox="0 0 137.58 63.5"
|
||||
id="svg65"
|
||||
sodipodi:docname="foreign-languages.svg"
|
||||
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview67"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2.2583333"
|
||||
inkscape:cx="136.16236"
|
||||
inkscape:cy="120"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2087"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg65" />
|
||||
<defs
|
||||
id="defs29">
|
||||
<clipPath
|
||||
id="clipPath301434">
|
||||
<rect
|
||||
y="-3.9777e-7"
|
||||
width="137.58"
|
||||
height="63.5"
|
||||
fill="#a0a0ff"
|
||||
fill-opacity=".66627"
|
||||
fill-rule="evenodd"
|
||||
id="rect26" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g
|
||||
id="g1521"
|
||||
transform="scale(1.0000242,1)"
|
||||
style="stroke-width:0.999988"
|
||||
inkscape:export-filename="/home/joshua/Code/Personal/Koto/data/genres/foreign-language.png"
|
||||
inkscape:export-xdpi="96.0084"
|
||||
inkscape:export-ydpi="96.0084">
|
||||
<rect
|
||||
y="0"
|
||||
width="137.58"
|
||||
height="63.5"
|
||||
fill="#a0a0ff"
|
||||
fill-rule="evenodd"
|
||||
id="rect31"
|
||||
x="0"
|
||||
style="stroke-width:0.999976" />
|
||||
<g
|
||||
clip-path="url(#clipPath301434)"
|
||||
id="g53"
|
||||
style="stroke-width:0.999988">
|
||||
<g
|
||||
id="g43"
|
||||
style="stroke-width:0.999988">
|
||||
<path
|
||||
d="m 141.34,38.614 h -38.032 c -1.9369,0 -3.5071,1.5702 -3.5071,3.5071 v 18.824 c 0,1.9369 1.5702,3.5066 3.5071,3.5066 h 22.416 l 6.1064,11.291 v -11.291 h 9.5088 c 1.9369,0 3.5071,-1.5697 3.5071,-3.5066 v -18.824 c 0,-1.9369 -1.5702,-3.5071 -3.5071,-3.5071"
|
||||
fill="#f99746"
|
||||
id="path33"
|
||||
style="stroke-width:0.999976" />
|
||||
<path
|
||||
d="m 101.58,0.73638 h 34.001 c 1.7314,0 3.1353,1.6392 3.1353,2.9966 v 11.868 c 0,2.0377 -1.4039,3.2468 -3.1353,3.2468 h -20.04 l -5.4593,7.9149 v -7.9149 h -8.5011 c -1.7314,0 -3.1353,-1.1001 -3.1353,-2.4582 v -12.051 c 0,-2.2834 1.4038,-3.6022 3.1353,-3.6022"
|
||||
fill="#c0d8fb"
|
||||
id="path35"
|
||||
style="stroke-width:0.999976" />
|
||||
<path
|
||||
d="M 37.569,1.5124 H 7.089 c -1.5522,0 -2.8106,1.2578 -2.8106,2.8104 v 15.087 c 0,1.5518 1.2584,2.8104 2.8106,2.8104 h 17.965 l 4.8943,9.0492 v -9.0492 h 7.6204 c 1.5526,0 2.8104,-1.2586 2.8104,-2.8104 V 4.3228 c 0,-1.5526 -1.2578,-2.8104 -2.8104,-2.8104"
|
||||
fill="#ebf3fa"
|
||||
id="path37"
|
||||
style="stroke-width:0.999976" />
|
||||
<path
|
||||
d="m 21.158,14.314 c -0.20383,-1.0087 -0.82632,-2.5543 -1.4703,-3.7031 l 0.63324,-0.22511 c 0.6761,1.1487 1.3093,2.6618 1.5239,3.6706 z m -3.8958,-3.6172 c -0.03218,0.0758 -0.10737,0.11835 -0.23609,0.11835 -0.34339,1.3205 -0.9337,2.7261 -1.6099,3.5847 -0.13947,-0.11835 -0.39707,-0.27926 -0.56888,-0.36512 0.65467,-0.81534 1.2129,-2.2 1.5455,-3.5739 z m 3.4344,-0.35352 c 0.08578,-0.39761 0.19316,-0.99868 0.28978,-1.5781 h -2.1358 v 6.2674 c 0,0.7728 -0.27903,0.91281 -1.7815,0.91281 -0.04293,-0.21505 -0.1503,-0.52602 -0.26835,-0.7403 0.26835,0.01002 0.52595,0.01002 0.72978,0.01002 h 0.42941 c 0.13939,0 0.18241,-0.04254 0.18241,-0.18256 v -6.2674 h -1.728 c -0.32188,0.78362 -0.69753,1.4922 -1.1054,2.0283 -0.12881,-0.12841 -0.37564,-0.34346 -0.54745,-0.44016 0.77272,-0.9979 1.406,-2.6827 1.7709,-4.3676 l 0.89084,0.23594 c -0.02143,0.075032 -0.09655,0.11835 -0.23617,0.10753 -0.13947,0.59023 -0.31121,1.1913 -0.51512,1.7599 h 4.7438 l 0.37557,0.010777 c 0,0.18256 -0.2576,1.5881 -0.48301,2.3609 z m -7.3517,-0.2576 c -0.35422,0.62195 -0.72986,1.1913 -1.1163,1.674 -0.09662,-0.15007 -0.32188,-0.42933 -0.46151,-0.5794 1.0089,-1.2021 1.9748,-3.2412 2.5866,-5.227 l 0.89084,0.26843 c -0.04301,0.10753 -0.13964,0.12841 -0.2576,0.11835 -0.28986,0.85866 -0.63324,1.7274 -1.0196,2.5435 l 0.31128,0.085872 c -0.02158,0.064217 -0.0752,0.11758 -0.21474,0.12841 v 6.826 h -0.71896 v -5.8381"
|
||||
fill="#2b478b"
|
||||
opacity="0.54345"
|
||||
id="path39"
|
||||
style="stroke-width:0.999976" />
|
||||
<path
|
||||
d="m 33.617,10.751 v 0.69776 h -2.586 v 3.5522 c 0,0.82617 -0.32258,0.95535 -2.2217,0.93369 -0.04332,-0.19339 -0.1609,-0.50437 -0.26843,-0.71942 0.5152,0.01078 0.98707,0.02166 1.288,0.02166 0.36512,0 0.47187,0 0.47187,-0.22511 v -3.563 h -2.6294 v -0.69776 h 2.6294 v -1.8248 l 0.35429,0.021655 c 0.55852,-0.47265 1.1487,-1.148 1.5881,-1.749 h -4.1316 v -0.67688 h 4.6793 l 0.13924,-0.042537 0.56934,0.35429 c -0.03248,0.053389 -0.10753,0.085872 -0.17173,0.10753 -0.55852,0.79368 -1.4814,1.8132 -2.2967,2.4785 v 1.3313 z M 25.4284,8.9053 c -0.22534,0.9979 -0.47218,2.0066 -0.6976,2.887 0.45068,0.24677 0.91227,0.54691 1.3629,0.84783 0.42964,-0.99868 0.74062,-2.2433 0.89069,-3.7348 z m 1.932,-0.70859 0.40767,0.10753 -0.06422,0.15007 c -0.15084,1.8991 -0.49353,3.4238 -1.0304,4.6151 0.52602,0.40767 0.97624,0.80451 1.2663,1.1696 l -0.44016,0.59023 c -0.26842,-0.34346 -0.67609,-0.73025 -1.148,-1.1163 -0.54761,0.9979 -1.2666,1.7281 -2.1253,2.2433 -0.09662,-0.18256 -0.30053,-0.44016 -0.45068,-0.59023 0.8049,-0.42933 1.481,-1.1271 2.0176,-2.0933 -0.39707,-0.30014 -0.81565,-0.5794 -1.2235,-0.82617 l -0.12879,0.47188 -0.61174,-0.32181 c 0.26828,-0.94452 0.60099,-2.3076 0.91227,-3.6915 h -1.0733 V 8.22824 h 1.2235 c 0.17173,-0.80451 0.32188,-1.5773 0.45068,-2.2642 l 0.88009,0.10753 c -0.01078,0.075032 -0.07503,0.11758 -0.21459,0.12841 -0.11804,0.61189 -0.26827,1.3096 -0.42925,2.0283 h 1.6525 l 0.12919,-0.031709"
|
||||
fill="#2b478b"
|
||||
opacity="0.54345"
|
||||
id="path41"
|
||||
style="stroke-width:0.999976" />
|
||||
</g>
|
||||
<text
|
||||
x="107.10843"
|
||||
y="11.536377"
|
||||
fill="#333333"
|
||||
font-family="sans-serif"
|
||||
font-size="6.7091px"
|
||||
letter-spacing="0px"
|
||||
opacity="0.53934"
|
||||
stroke-width="0.264577px"
|
||||
word-spacing="0px"
|
||||
style="line-height:4.19317px"
|
||||
xml:space="preserve"
|
||||
id="text47"><tspan
|
||||
x="107.10843"
|
||||
y="11.536377"
|
||||
fill="#333333"
|
||||
font-family="'Noto Sans'"
|
||||
font-size="6.7091px"
|
||||
font-stretch="condensed"
|
||||
font-weight="bold"
|
||||
stroke-width="0.264577px"
|
||||
id="tspan45">Bonjour</tspan></text>
|
||||
<text
|
||||
x="111.31922"
|
||||
y="55.259583"
|
||||
fill="#5a5a5a"
|
||||
font-family="sans-serif"
|
||||
font-size="9.9404px"
|
||||
letter-spacing="0px"
|
||||
opacity="0.48424"
|
||||
stroke-width="0.264577px"
|
||||
word-spacing="0px"
|
||||
style="line-height:6.21273px"
|
||||
xml:space="preserve"
|
||||
id="text51"><tspan
|
||||
x="111.31922"
|
||||
y="55.259583"
|
||||
fill="#5a5a5a"
|
||||
font-family="'Noto Sans'"
|
||||
font-size="9.9404px"
|
||||
font-stretch="condensed"
|
||||
font-weight="bold"
|
||||
stroke-width="0.264577px"
|
||||
id="tspan49">Hello</tspan></text>
|
||||
</g>
|
||||
<rect
|
||||
y="0"
|
||||
width="137.58"
|
||||
height="63.5"
|
||||
fill="#a0a0ff"
|
||||
fill-opacity="0.66627"
|
||||
fill-rule="evenodd"
|
||||
id="rect55"
|
||||
x="0"
|
||||
style="stroke-width:0.999976" />
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 7.5 KiB |
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" version="1.1" viewBox="0 0 6.35 6.35" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(0 -11.25)">
|
||||
<g transform="translate(-312.3 -265.79)" opacity=".996">
|
||||
<path d="m312.49 277.23-0.18709 2.4322 2.4322-0.1871-0.74834-0.74833a2.1167 2.1167 0 0 1 2.9934-1e-5 2.1167 2.1167 0 0 1 0 2.9934 2.1167 2.1167 0 0 1-2.9934 2e-5l-0.74836 0.74835a3.175 3.175 0 0 0 4.4901-3e-5 3.175 3.175 0 0 0 2e-5 -4.4901 3.175 3.175 0 0 0-4.4902-2e-5z" fill="#666" stroke-width=".26458"/>
|
||||
<path d="m315.46 279.29v1.1199s0.43968 0.4382 0.54774 0.54626" fill="none" stroke="#666" stroke-linecap="round" stroke-width=".79375"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 695 B |
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" version="1.1" viewBox="0 0 6.35 6.35" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(0 -11.25)">
|
||||
<g transform="matrix(.99913 0 0 1 -320.37 -265.83)" opacity=".996">
|
||||
<path d="m326.82 277.26 0.18709 2.4322-2.4322-0.18709 0.74834-0.74833a2.1167 2.1167 0 0 0-2.9934-1e-5 2.1167 2.1167 0 0 0 0 2.9934 2.1167 2.1167 0 0 0 2.9934 1e-5l0.74836 0.74835a3.175 3.175 0 0 1-4.4901-3e-5 3.175 3.175 0 0 1-2e-5 -4.4901 3.175 3.175 0 0 1 4.4902-1e-5z" fill="#666" opacity=".998" stroke-width=".2647"/>
|
||||
<path d="m323.85 279.32v1.12s-0.43968 0.4382-0.54774 0.54625" fill="none" opacity=".998" stroke="#666" stroke-linecap="round" stroke-width=".7941"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 733 B |
|
@ -1,572 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="520"
|
||||
height="240"
|
||||
version="1.1"
|
||||
viewBox="0 0 137.58 63.5"
|
||||
id="svg296"
|
||||
sodipodi:docname="mystery-and-thriller.svg"
|
||||
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview298"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2.2583333"
|
||||
inkscape:cx="136.16236"
|
||||
inkscape:cy="120"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2087"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg296" />
|
||||
<defs
|
||||
id="defs72">
|
||||
<clipPath
|
||||
id="clipPath302672">
|
||||
<rect
|
||||
x="15.037"
|
||||
y="6.529"
|
||||
width="114.27"
|
||||
height="52.739"
|
||||
fill="#333"
|
||||
fill-rule="evenodd"
|
||||
opacity=".56765"
|
||||
id="rect69" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g
|
||||
id="g1377"
|
||||
transform="scale(1.000023,0.99999772)"
|
||||
style="stroke-width:0.999989"
|
||||
inkscape:export-filename="/home/joshua/Code/Personal/Koto/data/genres/mystery-and-thriller.png"
|
||||
inkscape:export-xdpi="96.0084"
|
||||
inkscape:export-ydpi="96.0084">
|
||||
<rect
|
||||
x="0.0054583"
|
||||
y="0.065999985"
|
||||
width="137.58"
|
||||
height="63.5"
|
||||
fill="#333333"
|
||||
fill-rule="evenodd"
|
||||
id="rect74"
|
||||
style="stroke-width:0.999978" />
|
||||
<g
|
||||
transform="matrix(1.204,0,0,1.204,-18.1,-7.7951)"
|
||||
clip-path="url(#clipPath302672)"
|
||||
stroke-width="0.830531"
|
||||
id="g284">
|
||||
<g
|
||||
stroke-width="0.830531"
|
||||
id="g266">
|
||||
<path
|
||||
d="m 130.09,41.568 c 0.14098,0.10843 2.3979,1.8472 2.5041,2.1724 0.10831,0.32528 -0.8044,3.3497 -1.3399,4.2255 -0.0152,0.02595 -0.0325,0.05201 -0.0476,0.07379 -1.7019,2.6623 -4.438,4.8715 -7.8071,6.3197 1.6563,-1.8731 2.5127,-4.9604 2.5127,-4.9604 l -1.0428,-3.6921 2.387,-0.55303 0.49429,-2.6926 c -0.53762,-1.4808 -1.7778,-2.5084 -1.7778,-2.5084 -0.26226,-0.35552 -0.55499,-0.70895 -0.86939,-1.0341 0,0 6.0706,2.4368 7.9914,3.6401 h 0.002 c -0.0108,0.15615 -0.0282,0.30791 -0.0476,0.46174 l -2.9594,-1.4525"
|
||||
fill="#2c2c2c"
|
||||
id="path76"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 130.09,41.568 2.9594,1.4525 c -0.21246,1.7584 -0.83476,3.4257 -1.7952,4.9454 0.53554,-0.87588 1.4482,-3.9003 1.3399,-4.2255 -0.10623,-0.32517 -2.3632,-2.0639 -2.5041,-2.1724"
|
||||
fill="#878787"
|
||||
id="path78"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 128.21,21.038 c 1.4786,0.45097 3.2932,1.7345 3.2196,3.1805 0,0 -0.35992,-0.73281 -1.3161,-1.7345 -0.89534,-0.93878 -1.7951,-1.3918 -1.9035,-1.446"
|
||||
fill="#1e1e1e"
|
||||
id="path80"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 131.43,24.219 c 0.0737,-1.446 -1.741,-2.7296 -3.2196,-3.1805 -0.004,-0.0021 -0.007,-0.0021 -0.0109,-0.0043 0,0 0.004,0.0022 0.0109,0.0043 0.10842,0.05421 1.0082,0.50727 1.9035,1.446 0.95615,1.0017 1.3161,1.7345 1.3161,1.7345 z m -2.0163,2.8184 c -0.16044,0.07588 -5.3876,2.5193 -15.395,2.5193 v -4.2298 c 3.3345,0 6.4196,-0.48781 8.3427,-1.2857 1.8385,-0.76305 2.7209,-1.4655 2.736,-1.7344 0.0152,-0.27107 -1.2314,-0.48352 -1.2314,-0.48352 l -0.0196,0.0022 c -0.0672,-0.53333 -0.15824,-1.1078 -0.25798,-1.704 -0.013,-0.07159 -0.0259,-0.14527 -0.039,-0.21906 l 0.28185,-0.0065 c 8.219,0.54202 9.0712,4.1302 7.367,6.2375 -1.006,1.2445 -2.6992,2.4888 -6.3437,3.2997 -0.0888,0.01946 -0.1799,0.03904 -0.27096,0.0585 -0.50089,0.10634 -1.0363,0.20388 -1.6109,0.29273 -2.3003,0.35552 -5.2206,0.56588 -8.9541,0.56588 v -0.38158 c 9.0993,0 13.108,-1.4049 15.395,-2.9312"
|
||||
fill="#2c2c2c"
|
||||
id="path82"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 126.82,44.722 h 0.002 c 0.38586,-1.3984 0.60052,-2.3502 0.60052,-2.3502 0,0 -0.5616,-1.2076 -1.4591,-2.4196 0,0 1.2402,1.0276 1.7778,2.5084 l -0.4943,2.6926 -2.387,0.55303 1.0428,3.6921 c 0,0 -0.85641,3.0873 -2.5127,4.9604 -0.27964,0.12141 -0.56368,0.2363 -0.85202,0.34472 1.1577,-1.3072 2.1874,-3.6185 2.9983,-5.8883 -0.0152,-0.03695 -1.3246,-3.2955 -1.3246,-3.2955 l 0.76745,-0.23423 1.8407,-0.56357"
|
||||
fill="#1e1e1e"
|
||||
id="path84"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 125.1,48.82 c -0.0411,0.02815 -0.68508,0.43788 -2.1875,-0.34254 -1.5935,-0.82826 -1.5935,-4.49 -1.5935,-4.49 0.0305,3.1805 -0.78911,8.8651 -1.8428,11.642 -0.0607,0.01516 -0.1236,0.02815 -0.18639,0.04114 0.54423,-2.3284 1.4027,-9.1577 1.6326,-14.329 0.078,-1.728 0.0846,-3.2695 -0.0152,-4.3968 0.45306,-0.63087 0.77394,-1.1707 0.86059,-1.496 1.2272,1.6737 3.3345,3.4689 3.3345,3.4689 0.31439,0.32517 0.60712,0.6786 0.86939,1.0341 0.89753,1.2119 1.4591,2.4196 1.4591,2.4196 0,0 -0.21465,0.95175 -0.60052,2.3502 h -0.002 l -1.8407,0.56357 -2.9745,-4.2991 c 0.0824,0.14526 1.4113,2.4822 2.207,4.5333 0,0 1.3095,3.2585 1.3246,3.2955 -0.81089,2.2698 -1.8406,4.5811 -2.9983,5.8883 l -0.002,0.0021 c -0.17561,0.06733 -0.35563,0.13244 -0.53553,0.1952 0.9865,-1.3896 2.1528,-3.3908 3.0916,-6.0813"
|
||||
fill="#2c2c2c"
|
||||
id="path86"
|
||||
style="stroke-width:0.830522" />
|
||||
<g
|
||||
fill="#1e1e1e"
|
||||
id="g94"
|
||||
style="stroke-width:0.830522">
|
||||
<path
|
||||
d="m 114.02,29.557 c 10.008,0 15.235,-2.4434 15.395,-2.5193 -2.2873,1.5263 -6.296,2.9312 -15.395,2.9312 v -0.41193"
|
||||
id="path88"
|
||||
style="stroke-width:0.830531" />
|
||||
<path
|
||||
d="m 123.87,21.823 c 0,0 1.2466,0.21245 1.2314,0.48352 -0.0152,0.26887 -0.89753,0.97133 -2.736,1.7344 -1.9231,0.79791 -5.0082,1.2857 -8.3427,1.2857 v -0.40544 c 7.625,0 9.9123,-2.3307 9.9123,-2.3307 -0.0217,-0.24489 -0.0498,-0.50078 -0.0846,-0.76525 l 0.0196,-0.0022"
|
||||
id="path90"
|
||||
style="stroke-width:0.830531" />
|
||||
<path
|
||||
d="m 122.01,40.987 2.9745,4.2991 -0.76745,0.23423 c -0.79571,-2.0511 -2.1246,-4.3881 -2.207,-4.5333"
|
||||
id="path92"
|
||||
style="stroke-width:0.830531" />
|
||||
</g>
|
||||
<path
|
||||
d="m 124.83,29.633 c -0.16693,1.1642 -0.71764,1.9599 -2.0358,2.244 0,0 1.0082,-0.78054 1.2423,-1.0776 0.54643,-0.69377 0.54643,-1.3074 0.54643,-1.3074 0.0911,-0.01946 0.1821,-0.03904 0.27095,-0.0585 -0.006,0.06719 -0.0152,0.13449 -0.0239,0.19948"
|
||||
fill="#878787"
|
||||
id="path96"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 122.91,48.477 c 1.5025,0.78042 2.1464,0.37069 2.1875,0.34254 -0.93877,2.6905 -2.1051,4.6917 -3.0916,6.0813 -0.81309,0.28626 -1.6607,0.53125 -2.5323,0.72854 1.0537,-2.7773 1.8733,-8.4619 1.8428,-11.642 0,0 0,3.6617 1.5935,4.49"
|
||||
fill="#2c2c2c"
|
||||
id="path98"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 124.59,29.492 c 0,0 0,0.61361 -0.54643,1.3074 -0.23411,0.29702 -1.2423,1.0776 -1.2423,1.0776 -0.0824,0.01726 -0.16692,0.03464 -0.25577,0.04761 0.15823,-0.75876 0.29481,-1.4374 0.36849,-1.8189 0.0412,-0.20388 0.0651,-0.32088 0.0651,-0.32088 0.57457,-0.08885 1.11,-0.18639 1.6109,-0.29273"
|
||||
fill="#1e1e1e"
|
||||
id="path100"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 115.13,21.015 c 4.774,-0.16264 7.5469,-1.342 8.3167,-1.7193 0.0237,0.13229 0.0476,0.26226 0.0715,0.39235 -0.85201,0.51827 -2.0987,0.89117 -3.6032,1.1838 0,0 1.5848,0.34254 3.6791,-0.75007 0.0997,0.59624 0.19079,1.1707 0.25798,1.704 0.0348,0.26446 0.0629,0.52036 0.0846,0.76525 0,0 -2.2873,2.3307 -9.9123,2.3307 v -3.8873 c 0.3795,0 0.74799,-0.0065 1.1057,-0.01946"
|
||||
fill="#4d4d4d"
|
||||
id="path102"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 123.55,19.902 c 0.0131,0.07379 0.0261,0.14747 0.039,0.21906 -2.0943,1.0926 -3.6791,0.75007 -3.6791,0.75007 1.5046,-0.29262 2.7512,-0.66551 3.6032,-1.1838 0.013,0.07159 0.0239,0.14318 0.0368,0.21465"
|
||||
fill="#2c2c2c"
|
||||
id="path104"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 123.35,18.777 c 0.0325,0.17341 0.0651,0.34683 0.0955,0.51816 -0.76977,0.3773 -3.5427,1.5567 -8.3167,1.7193 5.0733,-0.51827 6.9551,-1.548 8.2212,-2.2375"
|
||||
fill="#1e1e1e"
|
||||
id="path106"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 122.3,12.542 c 0.15604,1.7301 0.63516,4.0499 1.0428,6.2353 -1.2661,0.68949 -3.148,1.7192 -8.2212,2.2375 -0.35772,0.01297 -0.72621,0.01946 -1.1057,0.01946 v -8.5594 c 3.432,0 3.677,-2.0185 3.677,-2.0185 -0.71973,0.51388 -1.4786,0.96484 -3.677,0.96484 v -0.26887 c 0.45538,0.02386 1.1405,0.0065 1.9275,-0.22554 0.89973,-0.26226 1.548,-0.68926 1.9294,-0.98639 0.63967,-0.023872 1.4375,0.0044 2.9897,1.1209 0.68301,0.49221 1.1405,1.0472 1.4375,1.4808 z m -2.1139,-1.2791 c 0.0629,0.0563 1.0602,0.94955 1.2987,1.8818 0.245,0.96473 0.9192,4.284 0.9192,4.284 l -0.40325,-4.529 c 0,0 -0.33814,-0.85201 -1.8146,-1.6368"
|
||||
fill="#2c2c2c"
|
||||
id="path108"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 122.97,29.784 c 0,0 -0.0239,0.117 -0.0651,0.32088 0,0 -0.53114,1.3138 -0.92789,2.5669 -0.4943,0.22554 -0.99728,0.24292 -2.4,0.26887 -1.8169,0.03255 -2.3328,0.39235 -3.0504,0.41413 -0.71764,0.02155 -1.5025,-0.83047 -2.5106,-0.83047 v -2.1745 c 3.7334,0 6.6538,-0.21037 8.9541,-0.56588"
|
||||
fill="#1e1e1e"
|
||||
id="path110"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 122.91,30.105 c -0.0737,0.38158 -0.21026,1.0602 -0.36849,1.8189 -0.0326,0.16056 -0.0672,0.32517 -0.10195,0.49001 -0.16692,0.10843 -0.30999,0.19299 -0.45745,0.25798 0.39675,-1.2532 0.92789,-2.5669 0.92789,-2.5669"
|
||||
fill="#878787"
|
||||
id="path112"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 122.01,12.9 0.40325,4.529 c 0,0 -0.6742,-3.3193 -0.9192,-4.284 -0.23852,-0.93229 -1.2358,-1.8255 -1.2987,-1.8818 1.4765,0.78482 1.8146,1.6368 1.8146,1.6368"
|
||||
fill="#4d4d4d"
|
||||
id="path114"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 121.98,32.672 c 0.14746,-0.06499 0.29053,-0.14955 0.45745,-0.25798 -0.24928,1.1946 -0.52035,2.4629 -0.67431,3.0353 -0.0866,0.32528 -0.40753,0.8651 -0.86058,1.496 -0.30791,0.42919 -0.67651,0.89974 -1.0732,1.3745 -0.74799,0.89117 -1.5979,1.7974 -2.3459,2.4607 0.12361,-0.12349 1.2769,-1.2834 2.2764,-2.851 1.0581,-1.6563 1.5935,-2.5322 1.8624,-3.9241 0.0737,-0.38587 0.20597,-0.8541 0.35772,-1.3333"
|
||||
fill="#fbfbfb"
|
||||
id="path116"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 116.18,34.357 c 0.69389,0.17341 1.5134,1.3268 1.6283,2.2375 0.013,0.16913 0.013,0.27536 0.013,0.27536 0.004,-0.08897 0,-0.18002 -0.013,-0.27536 -0.0217,-0.33826 -0.0803,-0.92569 -0.24071,-1.3853 -0.20377,-0.57886 -1.11,-0.79792 -1.3876,-0.85213 z m -0.63296,7.5643 c -0.60492,0.14306 -1.2336,0.16693 -1.5242,0.16693 v -3.1415 c 0.81089,0 1.7756,-0.58755 1.8363,-0.62438 -0.0454,0.0044 -0.60481,0.07159 -1.8363,0.07159 v -0.88236 c 1.6305,0 3.1848,-0.22334 3.1848,-0.22334 -0.83684,-0.01517 -2.168,-0.35992 -2.3782,-0.34254 -0.20817,0.01726 -0.80661,0.41622 -0.80661,0.41622 v -1.7474 c 0.6808,0 0.98871,-0.27107 1.1275,-0.52256 0.8541,-0.1821 0.94736,-0.75019 0.94736,-0.75019 -0.14735,0.20168 -0.33595,0.34903 -0.83464,0.32957 0,0 -0.44877,0.62879 -1.2402,0.62879 v -2.7752 c 1.0082,0 1.793,0.85201 2.5106,0.83047 0.71764,-0.02178 1.2336,-0.38158 3.0504,-0.41413 1.4027,-0.02595 1.9057,-0.04333 2.4,-0.26887 -0.15175,0.47924 -0.28405,0.94747 -0.35772,1.3333 -0.26887,1.3919 -0.80429,2.2678 -1.8624,3.9241 -0.99948,1.5676 -2.1528,2.7275 -2.2764,2.851 -0.006,0.0044 -0.0109,0.0088 -0.0109,0.0088 -0.4812,0.42699 -0.917,0.75227 -1.2553,0.90831 -0.20596,0.09545 -0.43788,0.16913 -0.67419,0.22334"
|
||||
fill="#878787"
|
||||
id="path118"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 120.9,36.945 c 0.0997,1.1273 0.0931,2.6688 0.0152,4.3968 -0.0846,0.56148 -0.19079,1.097 -0.30362,1.5869 -0.37498,1.6305 -0.82375,2.7774 -0.82375,2.7774 -0.84344,-1.173 -2.0489,-2.2028 -2.9616,-2.8922 -0.34034,-0.25589 -0.63955,-0.46614 -0.8629,-0.6157 1.6738,-0.52685 2.6189,-1.8364 3.8635,-3.8787 0.39664,-0.47472 0.76525,-0.94526 1.0732,-1.3745"
|
||||
fill="#fbfbfb"
|
||||
id="path120"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 120.62,42.929 c 0.11283,-0.48989 0.21906,-1.0254 0.30362,-1.5869 -0.22983,5.1708 -1.0883,12 -1.6326,14.329 -0.34903,0.07597 -0.70246,0.14525 -1.0581,0.20593 1.1123,-3.0938 2.3502,-12.648 2.387,-12.948"
|
||||
fill="#878787"
|
||||
id="path122"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 119.79,45.706 c 0,0 0.44877,-1.1469 0.82374,-2.7774 -0.0368,0.29922 -1.2747,9.8538 -2.387,12.948 -0.36849,0.0629 -0.7393,0.11933 -1.1165,0.16485 0,0 -0.49001,-4.9084 -0.64605,-6.7621 -0.15835,-1.8537 -1.0407,-4.2321 -1.0407,-4.2321 l 0.81089,-1.524 0.22983,-0.27315 c 0,0 2.0401,1.2531 3.3258,2.4564"
|
||||
fill="#fbfbfb"
|
||||
id="path124"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 117.48,40.78 c 0.74799,-0.66331 1.5979,-1.5695 2.3459,-2.4607 -1.2446,2.0423 -2.1898,3.3519 -3.8635,3.8787 -0.24709,-0.16704 -0.40104,-0.26458 -0.42271,-0.27756 0.23632,-0.05421 0.46823,-0.12789 0.6742,-0.22334 0.33825,-0.15604 0.77405,-0.48132 1.2552,-0.90831 0,0 0.004,-0.0044 0.0109,-0.0088"
|
||||
fill="#2c2c2c"
|
||||
id="path126"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 117.56,35.209 c 0.16043,0.45966 0.21905,1.0471 0.24071,1.3853 -0.11491,-0.91063 -0.93437,-2.0641 -1.6283,-2.2375 0.27755,0.05421 1.1838,0.27327 1.3876,0.85213"
|
||||
fill="#2c2c2c"
|
||||
id="path128"
|
||||
style="stroke-width:0.830522" />
|
||||
<g
|
||||
fill="#1e1e1e"
|
||||
id="g136"
|
||||
style="stroke-width:0.830522">
|
||||
<path
|
||||
d="m 119.79,45.706 c -1.2857,-1.2034 -3.3258,-2.4564 -3.3258,-2.4564 l 0.36421,-0.4358 c 0.91271,0.68937 2.1182,1.7192 2.9616,2.8922"
|
||||
id="path130"
|
||||
style="stroke-width:0.830531" />
|
||||
<path
|
||||
d="m 117.7,10.456 c 0,0 -0.24501,2.0185 -3.677,2.0185 v -1.0537 c 2.1984,0 2.9573,-0.45097 3.677,-0.96484"
|
||||
id="path132"
|
||||
style="stroke-width:0.830531" />
|
||||
<path
|
||||
d="m 117.11,56.041 c -0.19079,0.02388 -0.38158,0.04542 -0.57457,0.06498 0,0 -0.24489,-5.2576 -0.54411,-7.3496 -0.29922,-2.0943 -0.9865,-3.768 -0.9865,-3.768 l 1.2293,-1.4656 -0.81089,1.524 c 0,0 0.88237,2.3785 1.0407,4.2321 0.15604,1.8537 0.64605,6.7621 0.64605,6.7621"
|
||||
id="path134"
|
||||
style="stroke-width:0.830531" />
|
||||
</g>
|
||||
<g
|
||||
fill="#2c2c2c"
|
||||
id="g144"
|
||||
style="stroke-width:0.830522">
|
||||
<path
|
||||
d="m 116.83,42.814 -0.36421,0.4358 -0.22983,0.27315 -1.2293,1.4656 h -0.98651 v -2.528 c 0.75459,0 1.392,-0.08885 1.947,-0.26226 0.22334,0.14955 0.52256,0.3598 0.8629,0.6157"
|
||||
id="path138"
|
||||
style="stroke-width:0.830531" />
|
||||
<path
|
||||
d="m 116.54,56.106 c -0.14086,0.0152 -0.28184,0.02815 -0.42271,0.03696 l -0.002,-0.03696 c 0.12141,-2.1768 -0.34034,-6.9616 -0.34034,-6.9616 0,0 -0.0477,5.8733 -0.87379,7.0721 -0.29041,0.01085 -0.58314,0.01516 -0.87807,0.01516 v -11.243 h 0.9865 c 0,0 0.68728,1.6737 0.9865,3.768 0.29922,2.0921 0.54411,7.3496 0.54411,7.3496"
|
||||
id="path140"
|
||||
style="stroke-width:0.830531" />
|
||||
<path
|
||||
d="m 117.21,37.288 c 0,0 -1.5544,0.22334 -3.1848,0.22334 v -0.14967 c 0,0 0.59844,-0.39896 0.80661,-0.41622 0.21025,-0.01738 1.5414,0.32737 2.3782,0.34254"
|
||||
id="path142"
|
||||
style="stroke-width:0.830531" />
|
||||
</g>
|
||||
<path
|
||||
d="m 115.77,49.145 c 0,0 0.46174,4.7848 0.34034,6.9616 l 0.002,0.03696 c -0.40104,0.03453 -0.80649,0.0606 -1.2163,0.07358 0.82606,-1.1989 0.87379,-7.0721 0.87379,-7.0721"
|
||||
fill="#878787"
|
||||
id="path146"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 116.1,34.341 c 0,0 -0.0933,0.56809 -0.94736,0.75019 0.11921,-0.21906 0.11272,-0.42062 0.11272,-0.42062 0.49869,0.01946 0.68729,-0.12789 0.83464,-0.32957"
|
||||
fill="#1e1e1e"
|
||||
id="path148"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 115.54,41.921 c 0.0217,0.01297 0.17562,0.11051 0.42271,0.27756 -0.555,0.17341 -1.1924,0.26226 -1.947,0.26226 v -0.37289 c 0.29065,0 0.91932,-0.02386 1.5242,-0.16693"
|
||||
fill="#1e1e1e"
|
||||
id="path150"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 114.02,38.394 c 1.2315,0 1.7909,-0.06719 1.8363,-0.07159 -0.0607,0.03684 -1.0254,0.62438 -1.8363,0.62438 v -0.5528"
|
||||
fill="#878787"
|
||||
id="path152"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 115.15,35.092 c -0.13877,0.25149 -0.44668,0.52256 -1.1275,0.52256 v -0.31439 c 0.79143,0 1.2402,-0.62879 1.2402,-0.62879 0,0 0.007,0.20156 -0.11272,0.42062"
|
||||
fill="#1e1e1e"
|
||||
id="path154"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 97.954,41.568 c -0.14086,0.10843 -2.3978,1.8472 -2.504,2.1724 -0.10838,0.32528 0.80437,3.3497 1.3398,4.2255 0.01517,0.02595 0.03254,0.05201 0.04773,0.07379 1.7019,2.6623 4.438,4.8715 7.8071,6.3197 -1.6564,-1.8731 -2.5128,-4.9604 -2.5128,-4.9604 l 1.0429,-3.6921 -2.387,-0.55303 -0.49429,-2.6926 c 0.53762,-1.4808 1.7777,-2.5084 1.7777,-2.5084 0.26239,-0.35552 0.555,-0.70895 0.86939,-1.0341 0,0 -6.0705,2.4368 -7.9914,3.6401 h -0.002 c 0.01087,0.15615 0.02814,0.30791 0.04769,0.46174 l 2.9593,-1.4525"
|
||||
fill="#2c2c2c"
|
||||
id="path156"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 97.954,41.568 -2.9593,1.4525 c 0.2125,1.7584 0.83469,3.4257 1.7951,4.9454 -0.53548,-0.87588 -1.4482,-3.9003 -1.3398,-4.2255 0.10625,-0.32517 2.3632,-2.0639 2.504,-2.1724"
|
||||
fill="#878787"
|
||||
id="path158"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 99.828,21.038 c -1.4786,0.45097 -3.2933,1.7345 -3.2196,3.1805 0,0 0.35992,-0.73281 1.316,-1.7345 0.89545,-0.93878 1.7952,-1.3918 1.9036,-1.446"
|
||||
fill="#1e1e1e"
|
||||
id="path160"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 96.608,24.219 c -0.07372,-1.446 1.741,-2.7296 3.2196,-3.1805 0.0043,-0.0021 0.0065,-0.0021 0.0109,-0.0043 0,0 -0.0044,0.0022 -0.0109,0.0043 -0.10843,0.05421 -1.0082,0.50727 -1.9036,1.446 -0.95604,1.0017 -1.316,1.7345 -1.316,1.7345 z m 7.8419,-4.0975 c 0.013,-0.07159 0.0259,-0.14527 0.0389,-0.21906 l -0.28184,-0.0065 c -8.219,0.54202 -9.071,4.1302 -7.3669,6.2375 1.006,1.2445 2.6992,2.4888 6.3436,3.2997 0.089,0.01946 0.18002,0.03904 0.27107,0.0585 0.50078,0.10634 1.0363,0.20388 1.6109,0.29273 2.3003,0.35552 5.2206,0.56588 8.954,0.56588 v -0.38158 c -9.0992,0 -13.108,-1.4049 -15.395,-2.9312 0.16032,0.07588 5.3875,2.5193 15.395,2.5193 v -4.2298 c -3.3345,0 -6.4196,-0.48781 -8.3427,-1.2857 -1.8384,-0.76305 -2.7209,-1.4655 -2.7361,-1.7344 -0.0152,-0.27107 1.2315,-0.48352 1.2315,-0.48352 l 0.0195,0.0022 c 0.0672,-0.53333 0.15823,-1.1078 0.25809,-1.704"
|
||||
fill="#2c2c2c"
|
||||
id="path162"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 101.22,44.722 h -0.002 c -0.38587,-1.3984 -0.60052,-2.3502 -0.60052,-2.3502 0,0 0.56148,-1.2076 1.459,-2.4196 0,0 -1.2401,1.0276 -1.7777,2.5084 l 0.49429,2.6926 2.387,0.55303 -1.0429,3.6921 c 0,0 0.85641,3.0873 2.5128,4.9604 0.27964,0.12141 0.56368,0.2363 0.85201,0.34472 -1.1577,-1.3072 -2.1876,-3.6185 -2.9984,-5.8883 0.0152,-0.03695 1.3246,-3.2955 1.3246,-3.2955 l -0.76745,-0.23423 -1.8406,-0.56357"
|
||||
fill="#1e1e1e"
|
||||
id="path164"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 106.03,40.987 c -0.0824,0.14526 -1.4114,2.4822 -2.2071,4.5333 0,0 -1.3095,3.2585 -1.3246,3.2955 0.81089,2.2698 1.8407,4.5811 2.9984,5.8883 l 0.002,0.0021 c 0.17561,0.06733 0.35551,0.13244 0.53542,0.1952 -0.98639,-1.3896 -2.1528,-3.3908 -3.0916,-6.0813 0.0412,0.02815 0.68509,0.43788 2.1876,-0.34254 1.5935,-0.82826 1.5935,-4.49 1.5935,-4.49 -0.0303,3.1805 0.78911,8.8651 1.8428,11.642 0.0607,0.01516 0.1236,0.02815 0.1865,0.04113 -0.54422,-2.3284 -1.4027,-9.1577 -1.6326,-14.329 -0.0781,-1.728 -0.0846,-3.2695 0.0152,-4.3968 -0.45317,-0.63087 -0.77405,-1.1707 -0.8607,-1.496 -1.2271,1.6737 -3.3345,3.4689 -3.3345,3.4689 -0.3144,0.32517 -0.60701,0.6786 -0.86939,1.0341 -0.89754,1.2119 -1.459,2.4196 -1.459,2.4196 0,0 0.21465,0.95175 0.60052,2.3502 h 0.002 l 1.8406,0.56357 2.9746,-4.2991"
|
||||
fill="#2c2c2c"
|
||||
id="path166"
|
||||
style="stroke-width:0.830522" />
|
||||
<g
|
||||
fill="#1e1e1e"
|
||||
id="g174"
|
||||
style="stroke-width:0.830522">
|
||||
<path
|
||||
d="m 114.02,29.557 c -10.008,0 -15.235,-2.4434 -15.395,-2.5193 2.2873,1.5263 6.296,2.9312 15.395,2.9312 v -0.41193"
|
||||
id="path168"
|
||||
style="stroke-width:0.830531" />
|
||||
<path
|
||||
d="m 104.17,21.823 c 0,0 -1.2467,0.21245 -1.2315,0.48352 0.0152,0.26887 0.89766,0.97133 2.7361,1.7344 1.9231,0.79791 5.0082,1.2857 8.3427,1.2857 v -0.40544 c -7.625,0 -9.9123,-2.3307 -9.9123,-2.3307 0.0218,-0.24489 0.0499,-0.50078 0.0846,-0.76525 l -0.0195,-0.0022"
|
||||
id="path170"
|
||||
style="stroke-width:0.830531" />
|
||||
<path
|
||||
d="m 106.03,40.987 -2.9746,4.2991 0.76745,0.23423 c 0.79571,-2.0511 2.1248,-4.3881 2.2071,-4.5333"
|
||||
id="path172"
|
||||
style="stroke-width:0.830531" />
|
||||
</g>
|
||||
<path
|
||||
d="m 103.21,29.633 c 0.16681,1.1642 0.71752,1.9599 2.0357,2.244 0,0 -1.008,-0.78054 -1.2423,-1.0776 -0.5463,-0.69377 -0.5463,-1.3074 -0.5463,-1.3074 -0.091,-0.01946 -0.1821,-0.03904 -0.27107,-0.0585 0.007,0.06719 0.0152,0.13449 0.024,0.19948"
|
||||
fill="#878787"
|
||||
id="path176"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 105.13,48.477 c -1.5025,0.78042 -2.1463,0.37069 -2.1876,0.34254 0.93879,2.6905 2.1052,4.6917 3.0916,6.0813 0.81309,0.28626 1.6608,0.53125 2.5323,0.72854 -1.0537,-2.7773 -1.8732,-8.4619 -1.8428,-11.642 0,0 0,3.6617 -1.5935,4.49"
|
||||
fill="#2c2c2c"
|
||||
id="path178"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 103.45,29.492 c 0,0 0,0.61361 0.5463,1.3074 0.23424,0.29702 1.2423,1.0776 1.2423,1.0776 0.0825,0.01726 0.16693,0.03464 0.25589,0.04761 -0.15824,-0.75876 -0.29493,-1.4374 -0.36861,-1.8189 -0.0411,-0.20388 -0.065,-0.32088 -0.065,-0.32088 -0.57458,-0.08885 -1.1101,-0.18639 -1.6109,-0.29273"
|
||||
fill="#1e1e1e"
|
||||
id="path180"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 112.91,21.015 c -4.774,-0.16264 -7.5469,-1.342 -8.3166,-1.7193 -0.0239,0.13229 -0.0477,0.26226 -0.0716,0.39235 0.85213,0.51827 2.0987,0.89117 3.6034,1.1838 0,0 -1.5848,0.34254 -3.6791,-0.75007 -0.0999,0.59624 -0.19091,1.1707 -0.25809,1.704 -0.0346,0.26446 -0.0628,0.52036 -0.0846,0.76525 0,0 2.2873,2.3307 9.9123,2.3307 v -3.8873 c -0.37938,0 -0.74799,-0.0065 -1.1057,-0.01946"
|
||||
fill="#4d4d4d"
|
||||
id="path182"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 104.49,19.902 c -0.013,0.07379 -0.0259,0.14747 -0.0389,0.21906 2.0943,1.0926 3.6791,0.75007 3.6791,0.75007 -1.5047,-0.29262 -2.7512,-0.66551 -3.6034,-1.1838 -0.013,0.07159 -0.0239,0.14318 -0.0368,0.21465"
|
||||
fill="#2c2c2c"
|
||||
id="path184"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 104.69,18.777 c -0.0324,0.17341 -0.065,0.34683 -0.0953,0.51816 0.76965,0.3773 3.5425,1.5567 8.3166,1.7193 -5.0732,-0.51827 -6.955,-1.548 -8.2212,-2.2375"
|
||||
fill="#1e1e1e"
|
||||
id="path186"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 105.74,12.542 c -0.15616,1.7301 -0.63528,4.0499 -1.0429,6.2353 1.2663,0.68949 3.1481,1.7192 8.2212,2.2375 0.35772,0.01297 0.72633,0.01946 1.1057,0.01946 v -8.5594 c -3.432,0 -3.677,-2.0185 -3.677,-2.0185 0.71984,0.51388 1.4786,0.96484 3.677,0.96484 v -0.26887 c -0.45526,0.02386 -1.1404,0.0065 -1.9274,-0.22554 -0.89974,-0.26226 -1.548,-0.68926 -1.9296,-0.98639 -0.63956,-0.023872 -1.4375,0.0044 -2.9898,1.1209 -0.68288,0.49221 -1.1403,1.0472 -1.4374,1.4808 z m 2.1138,-1.2791 c -0.0628,0.0563 -1.0602,0.94955 -1.2986,1.8818 -0.245,0.96473 -0.91931,4.284 -0.91931,4.284 l 0.40324,-4.529 c 0,0 0.33826,-0.85201 1.8146,-1.6368"
|
||||
fill="#2c2c2c"
|
||||
id="path188"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 105.07,29.784 c 0,0 0.0239,0.117 0.065,0.32088 0,0 0.53125,1.3138 0.92789,2.5669 0.4943,0.22554 0.9974,0.24292 2.4001,0.26887 1.8167,0.03255 2.3327,0.39235 3.0504,0.41413 0.71751,0.02155 1.5023,-0.83047 2.5105,-0.83047 v -2.1745 c -3.7333,0 -6.6537,-0.21037 -8.954,-0.56588"
|
||||
fill="#1e1e1e"
|
||||
id="path190"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 105.13,30.105 c 0.0737,0.38158 0.21037,1.0602 0.36861,1.8189 0.0325,0.16056 0.0672,0.32517 0.10194,0.49001 0.16693,0.10843 0.30999,0.19299 0.45734,0.25798 -0.39664,-1.2532 -0.92789,-2.5669 -0.92789,-2.5669"
|
||||
fill="#878787"
|
||||
id="path192"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 106.03,12.9 -0.40324,4.529 c 0,0 0.67431,-3.3193 0.91931,-4.284 0.2384,-0.93229 1.2358,-1.8255 1.2986,-1.8818 -1.4764,0.78482 -1.8146,1.6368 -1.8146,1.6368"
|
||||
fill="#4d4d4d"
|
||||
id="path194"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 106.06,32.672 c -0.14735,-0.06499 -0.29041,-0.14955 -0.45734,-0.25798 0.24918,1.1946 0.52024,2.4629 0.6742,3.0353 0.0866,0.32528 0.40753,0.8651 0.8607,1.496 0.3079,0.42919 0.67639,0.89974 1.0732,1.3745 0.74799,0.89117 1.5979,1.7974 2.3458,2.4607 -0.12349,-0.12349 -1.2769,-1.2834 -2.2764,-2.851 -1.058,-1.6563 -1.5935,-2.5322 -1.8624,-3.9241 -0.0737,-0.38587 -0.20597,-0.8541 -0.35772,-1.3333"
|
||||
fill="#fbfbfb"
|
||||
id="path196"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 112.5,41.921 c 0.60481,0.14306 1.2336,0.16693 1.5241,0.16693 v -3.1415 c -0.81089,0 -1.7756,-0.58755 -1.8363,-0.62438 0.0455,0.0044 0.60492,0.07159 1.8363,0.07159 v -0.88236 c -1.6304,0 -3.1848,-0.22334 -3.1848,-0.22334 0.83684,-0.01517 2.168,-0.35992 2.3783,-0.34254 0.20805,0.01726 0.80649,0.41622 0.80649,0.41622 v -1.7474 c -0.68068,0 -0.98859,-0.27107 -1.1274,-0.52256 -0.85421,-0.1821 -0.94746,-0.75019 -0.94746,-0.75019 0.14746,0.20168 0.33605,0.34903 0.83475,0.32957 0,0 0.44876,0.62879 1.2401,0.62879 v -2.7752 c -1.0082,0 -1.793,0.85201 -2.5105,0.83047 -0.71776,-0.02178 -1.2337,-0.38158 -3.0504,-0.41413 -1.4027,-0.02595 -1.9058,-0.04333 -2.4001,-0.26887 0.15175,0.47924 0.28404,0.94747 0.35772,1.3333 0.26887,1.3919 0.8044,2.2678 1.8624,3.9241 0.99948,1.5676 2.1529,2.7275 2.2764,2.851 0.007,0.0044 0.0109,0.0088 0.0109,0.0088 0.48132,0.42699 0.91711,0.75227 1.2552,0.90831 0.20597,0.09545 0.438,0.16913 0.67432,0.22334 z m -0.63307,-7.5643 c -0.69377,0.17341 -1.5134,1.3268 -1.6282,2.2375 -0.0131,0.16913 -0.0131,0.27536 -0.0131,0.27536 -0.004,-0.08897 0,-0.18002 0.0131,-0.27536 0.0217,-0.33826 0.0802,-0.92569 0.2406,-1.3853 0.20377,-0.57886 1.11,-0.79792 1.3876,-0.85213"
|
||||
fill="#878787"
|
||||
id="path198"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 107.14,36.945 c -0.0997,1.1273 -0.0933,2.6688 -0.0152,4.3968 0.0846,0.56148 0.19079,1.097 0.3035,1.5869 0.3751,1.6305 0.82387,2.7774 0.82387,2.7774 0.84332,-1.173 2.0488,-2.2028 2.9616,-2.8922 0.34035,-0.25589 0.63956,-0.46614 0.86279,-0.6157 -1.6737,-0.52685 -2.6189,-1.8364 -3.8634,-3.8787 -0.39676,-0.47472 -0.76525,-0.94526 -1.0732,-1.3745"
|
||||
fill="#fbfbfb"
|
||||
id="path200"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 107.42,42.929 c -0.11272,-0.48989 -0.21894,-1.0254 -0.30351,-1.5869 0.22983,5.1708 1.0883,12 1.6326,14.329 0.34903,0.07597 0.70247,0.14525 1.058,0.20593 -1.1122,-3.0938 -2.3502,-12.648 -2.387,-12.948"
|
||||
fill="#878787"
|
||||
id="path202"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 108.25,45.706 c 0,0 -0.44877,-1.1469 -0.82386,-2.7774 0.0368,0.29922 1.2748,9.8538 2.387,12.948 0.36849,0.0629 0.7393,0.11933 1.1166,0.16485 0,0 0.48989,-4.9084 0.64604,-6.7621 0.15824,-1.8537 1.0406,-4.2321 1.0406,-4.2321 l -0.81077,-1.524 -0.22983,-0.27315 c 0,0 -2.0402,1.2531 -3.3258,2.4564"
|
||||
fill="#fbfbfb"
|
||||
id="path204"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 110.55,40.78 c -0.74787,-0.66331 -1.5978,-1.5695 -2.3458,-2.4607 1.2445,2.0423 2.1898,3.3519 3.8634,3.8787 0.24721,-0.16704 0.40116,-0.26458 0.42282,-0.27756 -0.23631,-0.05421 -0.46834,-0.12789 -0.67431,-0.22334 -0.33814,-0.15604 -0.77394,-0.48132 -1.2553,-0.90831 0,0 -0.004,-0.0044 -0.0109,-0.0088"
|
||||
fill="#2c2c2c"
|
||||
id="path206"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 110.47,35.209 c -0.16044,0.45966 -0.21894,1.0471 -0.2406,1.3853 0.1148,-0.91063 0.93438,-2.0641 1.6282,-2.2375 -0.27755,0.05421 -1.1838,0.27327 -1.3876,0.85213"
|
||||
fill="#2c2c2c"
|
||||
id="path208"
|
||||
style="stroke-width:0.830522" />
|
||||
<g
|
||||
fill="#1e1e1e"
|
||||
id="g216"
|
||||
style="stroke-width:0.830522">
|
||||
<path
|
||||
d="m 108.25,45.706 c 1.2856,-1.2034 3.3258,-2.4564 3.3258,-2.4564 l -0.3642,-0.4358 c -0.91283,0.68937 -2.1183,1.7192 -2.9616,2.8922"
|
||||
id="path210"
|
||||
style="stroke-width:0.830531" />
|
||||
<path
|
||||
d="m 110.34,10.456 c 0,0 0.24501,2.0185 3.677,2.0185 v -1.0537 c -2.1984,0 -2.9572,-0.45097 -3.677,-0.96484"
|
||||
id="path212"
|
||||
style="stroke-width:0.830531" />
|
||||
<path
|
||||
d="m 110.93,56.041 c 0.19067,0.02388 0.38146,0.04542 0.57445,0.06498 0,0 0.24501,-5.2576 0.54422,-7.3496 0.29922,-2.0943 0.98639,-3.768 0.98639,-3.768 l -1.2292,-1.4656 0.81077,1.524 c 0,0 -0.88236,2.3785 -1.0406,4.2321 -0.15615,1.8537 -0.64604,6.7621 -0.64604,6.7621"
|
||||
id="path214"
|
||||
style="stroke-width:0.830531" />
|
||||
</g>
|
||||
<g
|
||||
fill="#2c2c2c"
|
||||
id="g224"
|
||||
style="stroke-width:0.830522">
|
||||
<path
|
||||
d="m 111.21,42.814 0.3642,0.4358 0.22983,0.27315 1.2292,1.4656 h 0.98651 v -2.528 c -0.75448,0 -1.392,-0.08885 -1.947,-0.26226 -0.22322,0.14955 -0.52244,0.3598 -0.86278,0.6157"
|
||||
id="path218"
|
||||
style="stroke-width:0.830531" />
|
||||
<path
|
||||
d="m 111.5,56.106 c 0.14098,0.0152 0.28184,0.02815 0.42282,0.03696 l 0.002,-0.03696 c -0.12128,-2.1768 0.34046,-6.9616 0.34046,-6.9616 0,0 0.0477,5.8733 0.87367,7.0721 0.29054,0.01085 0.58327,0.01516 0.87808,0.01516 v -11.243 h -0.9865 c 0,0 -0.68717,1.6737 -0.98639,3.768 -0.29922,2.0921 -0.54422,7.3496 -0.54422,7.3496"
|
||||
id="path220"
|
||||
style="stroke-width:0.830531" />
|
||||
<path
|
||||
d="m 110.83,37.288 c 0,0 1.5545,0.22334 3.1848,0.22334 v -0.14967 c 0,0 -0.59844,-0.39896 -0.80649,-0.41622 -0.21037,-0.01738 -1.5415,0.32737 -2.3783,0.34254"
|
||||
id="path222"
|
||||
style="stroke-width:0.830531" />
|
||||
</g>
|
||||
<g
|
||||
fill="#1e1e1e"
|
||||
id="g232"
|
||||
style="stroke-width:0.830522">
|
||||
<path
|
||||
d="m 112.27,49.145 c 0,0 -0.46174,4.7848 -0.34045,6.9616 l -0.002,0.03696 c 0.40104,0.03453 0.80649,0.0606 1.2162,0.07358 -0.82594,-1.1989 -0.87367,-7.0721 -0.87367,-7.0721"
|
||||
id="path226"
|
||||
style="stroke-width:0.830531" />
|
||||
<path
|
||||
d="m 111.94,34.341 c 0,0 0.0932,0.56809 0.94746,0.75019 -0.1192,-0.21906 -0.11271,-0.42062 -0.11271,-0.42062 -0.4987,0.01946 -0.68729,-0.12789 -0.83475,-0.32957"
|
||||
id="path228"
|
||||
style="stroke-width:0.830531" />
|
||||
<path
|
||||
d="m 112.5,41.921 c -0.0217,0.01297 -0.17561,0.11051 -0.42282,0.27756 0.555,0.17341 1.1925,0.26226 1.947,0.26226 v -0.37289 c -0.29053,0 -0.91932,-0.02386 -1.5241,-0.16693"
|
||||
id="path230"
|
||||
style="stroke-width:0.830531" />
|
||||
</g>
|
||||
<path
|
||||
d="m 114.02,38.394 c -1.2314,0 -1.7908,-0.06719 -1.8363,-0.07159 0.0607,0.03684 1.0254,0.62438 1.8363,0.62438 v -0.5528"
|
||||
fill="#878787"
|
||||
id="path234"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 112.89,35.092 c 0.13878,0.25149 0.44669,0.52256 1.1274,0.52256 v -0.31439 c -0.79132,0 -1.2401,-0.62879 -1.2401,-0.62879 0,0 -0.007,0.20156 0.11271,0.42062"
|
||||
fill="#1e1e1e"
|
||||
id="path236"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 111.05,37.405 c 0.4,0.18685 0.28775,0.51862 1.1027,0.51723 0.81494,-0.0014 1.6597,-0.12604 1.6597,-0.12604 l 1.9737,-0.22299 1.2146,-0.23956 -0.0483,-0.01842 c 0,0 -1.4287,0.19566 -2.931,0.19566 -1.5024,0 -3.1545,-0.21801 -3.1545,-0.21801 l 0.18315,0.11213"
|
||||
fill="#fbfbfb"
|
||||
id="path238"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 115.39,37.619 c -0.34034,-2.31e-4 -0.79119,-0.0027 -1.2459,-0.01065 -0.91932,-0.01657 -1.628,-0.04564 -3.1612,-0.30061 -0.003,-3.61e-4 -0.006,-6.89e-4 -0.008,-0.0011 h -0.0834 l 0.16183,0.09904 c 0.39988,0.18685 0.28764,0.51862 1.1026,0.51723 0.81494,-0.0014 1.6597,-0.12604 1.6597,-0.12604 l 1.5749,-0.17793"
|
||||
fill="#1e1e1e"
|
||||
id="path240"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 114.76,33.811 c 0.19843,-0.02491 0.30258,0.79872 0.0262,0.79826 -0.27651,-3.61e-4 -0.30327,-0.76339 -0.0262,-0.79826"
|
||||
fill="#fbfbfb"
|
||||
id="path242"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 115.61,37.779 c 0.18801,-0.20689 0.68856,-0.07842 0.48237,0.05618 -0.2062,0.13449 -0.63516,0.11202 -0.48237,-0.05618"
|
||||
fill="#fbfbfb"
|
||||
id="path244"
|
||||
style="stroke-width:0.830522" />
|
||||
<g
|
||||
fill="#1e1e1e"
|
||||
id="g254"
|
||||
style="stroke-width:0.830522">
|
||||
<path
|
||||
d="m 114.56,36.174 c 0.25334,-0.26493 0.75134,-0.17944 1.2784,0.0673 0.52708,0.24674 1.5588,0.62798 1.5139,0.74011 -0.0219,0.05479 -0.28022,0.01529 -0.3203,0.01251 -0.23759,-0.0168 -0.47448,-0.04645 -0.71045,-0.07854 -0.27524,-0.0373 -0.5506,-0.0746 -0.82398,-0.12349 -0.22172,-0.03962 -0.46719,-0.06893 -0.67594,-0.15615 -0.14098,-0.05896 -0.42317,-0.29273 -0.26168,-0.46174"
|
||||
id="path246"
|
||||
style="stroke-width:0.830531" />
|
||||
<path
|
||||
d="m 113.47,36.174 c -0.25334,-0.26493 -0.75135,-0.17944 -1.2784,0.0673 -0.52708,0.24674 -1.5586,0.62798 -1.5138,0.74011 0.0219,0.05479 0.28022,0.01529 0.32019,0.01251 0.23771,-0.0168 0.4746,-0.04645 0.71057,-0.07854 0.27524,-0.0373 0.55048,-0.0746 0.82398,-0.12349 0.2216,-0.03962 0.46719,-0.06893 0.67593,-0.15615 0.14086,-0.05896 0.42317,-0.29273 0.26157,-0.46174"
|
||||
id="path248"
|
||||
style="stroke-width:0.830531" />
|
||||
<path
|
||||
d="m 112.5,37.797 c 0.78796,0.06754 3.7608,-0.27478 4.7098,-0.50866 0,0 -0.82514,0.3532 -2.0576,0.41842 -1.2323,0.06533 -2.8111,0.44448 -2.8111,0.44448 l 0.15893,-0.35424"
|
||||
id="path250"
|
||||
style="stroke-width:0.830531" />
|
||||
<path
|
||||
d="m 110.83,37.288 c 0,0 0.30084,0.27616 0.69783,0.57133 l 0.16044,-0.12001 -0.72088,-0.4329 L 110.83,37.288"
|
||||
id="path252"
|
||||
style="stroke-width:0.830531" />
|
||||
</g>
|
||||
<path
|
||||
d="m 113.19,38.811 c 0.72887,-0.06742 2.3548,-0.25798 2.3941,-0.12337 0.0393,0.13449 -0.96994,0.56067 -1.5307,0.56067 -0.56067,0 -0.86336,-0.4373 -0.86336,-0.4373"
|
||||
fill="#2c2c2c"
|
||||
id="path256"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 116.74,37.258 c 0.0924,-0.04935 0.45097,-0.03024 0.51828,0.02502 0.0673,0.05537 0.40058,0.45375 0.19785,0.47958 C 117.37193,37.77337 116.74,37.258 116.74,37.258"
|
||||
fill="#1e1e1e"
|
||||
id="path258"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 111.3,37.258 c -0.0924,-0.04935 -0.45108,-0.03024 -0.51827,0.02502 -0.0673,0.05537 -0.40069,0.45375 -0.19797,0.47958 0.0843,0.01077 0.71624,-0.5046 0.71624,-0.5046"
|
||||
fill="#1e1e1e"
|
||||
id="path260"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 125.14,48.41 -2.0562,-4.8164 c 0,0 0.9419,3.1908 1.3009,3.7345 0.35877,0.54353 0.75529,1.082 0.75529,1.082"
|
||||
fill="#878787"
|
||||
id="path262"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 101.2,42.088 c 0,0 1.5377,-2.5348 1.7444,-2.7826 0.20678,-0.24778 -0.33153,0.77614 -0.64546,1.4752 -0.31393,0.69922 -1.099,1.3074 -1.099,1.3074"
|
||||
fill="#878787"
|
||||
id="path264"
|
||||
style="stroke-width:0.830522" />
|
||||
</g>
|
||||
<path
|
||||
d="m 112.5,37.797 -3.3092,2.7712 c 0,0 0.0879,0.13519 0.0716,0.29146 l 3.2376,-2.8745 0.10044,-0.18222 -0.10044,-0.0059"
|
||||
fill="#1f1f1f"
|
||||
id="path268"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 112.51,37.938 c -0.0941,0.26006 -2.6772,2.5031 -3.3217,3.131 v 0.0028 c -0.16311,0.15766 -0.26273,0.25439 -0.26273,0.25439 -0.011,0 -0.49511,-0.0027 -0.82155,-0.4563 -0.33188,-0.45642 -0.12719,-0.68601 -0.12719,-0.68601 l 0.177,-0.12731 h 0.003 l 3.6755,-2.7496 c 0.37069,-0.12441 0.79131,0.31022 0.6779,0.6311"
|
||||
fill="#2d2d2d"
|
||||
id="path270"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 108.55,39.952 3.2466,-2.4411 -2.9334,2.5915 z"
|
||||
fill="#4e4e4e"
|
||||
id="path272"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 109.19,41.069 v 0.0028 c -0.16311,0.15766 -0.26273,0.25439 -0.26273,0.25439 -0.011,0 -0.49511,-0.0027 -0.82155,-0.4563 -0.30964,-0.42606 -0.15198,-0.65288 -0.12997,-0.68312 l 0.003,-0.0029 0.17701,-0.12731 h 0.003 c 0.35691,-0.07727 1.0036,0.63215 1.0317,1.0125"
|
||||
fill="#fbfdfe"
|
||||
id="path274"
|
||||
style="stroke-width:0.830522" />
|
||||
<path
|
||||
d="m 108.92,41.326 c -0.011,0 -0.49511,-0.0027 -0.82155,-0.4563 -0.30964,-0.42606 -0.15198,-0.65288 -0.12997,-0.68312 l 0.003,-0.0029 c 0.11619,-0.08294 0.49233,0.11063 0.6888,0.38992 0.19357,0.28219 0.34845,0.61697 0.25994,0.75239"
|
||||
fill="#ef2e2e"
|
||||
id="path276"
|
||||
style="stroke-width:0.830522" />
|
||||
<g
|
||||
fill="#fbfafa"
|
||||
stroke-width="0.830531"
|
||||
id="g282">
|
||||
<path
|
||||
d="m 106.07,38.86 c -0.55696,-0.45398 -1.0123,-1.0714 -1.1242,-1.7814 -0.12858,-0.80904 0.19427,-1.6117 0.36919,-2.4117 0.17492,-0.80139 0.15048,-1.7636 -0.48364,-2.2831 -0.7576,-0.61998 -2.0464,-0.3254 -2.6831,-1.0689 -0.44379,-0.51966 -0.34347,-1.3107 -0.0952,-1.9474 0.24953,-0.63805 0.62381,-1.2477 0.66111,-1.9307 0.0463,-0.84634 -0.45665,-1.6528 -1.1306,-2.1673 -0.67524,-0.51457 -1.5023,-0.78216 -2.3242,-0.99566 -0.62902,-0.16334 -1.2876,-0.30999 -1.8008,-0.70999 -0.13692,-0.10657 -0.25462,-0.23215 -0.35714,-0.36919 0.15546,0.37672 0.39722,0.7159 0.68763,1.0006 0.31347,0.30744 0.67733,0.56357 1.0726,0.75494 0.44518,0.21558 0.92627,0.3481 1.3756,0.55476 0.44924,0.20666 0.88167,0.50634 1.1044,0.94804 0.27038,0.53623 0.1835,1.191 -0.0485,1.7448 0.0356,-0.43742 -0.21489,-0.85885 -0.55153,-1.1403 -0.7408,-0.61952 -1.9171,-0.94133 -2.8468,-1.1157 -0.41738,-0.07831 -0.85074,-0.12673 -1.2261,-0.3254 -0.37533,-0.19867 -0.68511,-0.59716 -0.62755,-1.0179 -0.20092,0.8117 -0.03264,1.6839 0.33462,2.4352 0.23976,0.49059 0.56087,0.93635 0.91708,1.3486 0.34185,0.3956 0.64477,0.58558 1.0887,0.83464 0.78425,0.44008 1.7538,0.74683 2.1923,1.6013 0.19936,0.38981 0.25462,0.85155 0.51966,1.2001 0.27397,0.36142 0.70872,0.52488 1.1396,0.69713 0.29447,0.08873 0.56716,0.22126 0.80382,0.41298 -0.23411,-0.18141 -0.51827,-0.29841 -0.80382,-0.41298 -0.54156,-0.16588 -1.15,-0.18905 -1.7223,-0.18256 -0.9609,0.01031 -2.0027,0.03857 -2.7796,-0.52743 -0.71138,-0.51839 -0.99821,-1.3776 -1.33,-2.2175 -0.27522,-0.46302 -0.69328,-0.83985 -1.2026,-1.0032 -0.67916,-0.21871 -1.5049,0 -1.9102,0.5865 -0.27395,0.39618 -0.33698,0.90032 -0.32924,1.3814 0.0077,0.49916 0.08747,1.0071 0.32802,1.4445 0.34215,0.62253 0.97882,1.0405 1.6528,1.2657 1.3094,0.4373 2.9533,0.48618 4.3231,0.44379 0.36143,-0.0117 0.72424,-0.03858 1.0857,-0.04773 0.74729,-0.01934 1.6284,-0.02827 2.2895,0.37058 0.94793,0.56982 0.56079,1.8959 0.57886,2.8092 0.0116,0.62635 0.10809,1.2721 0.47843,1.7917 0.70362,0.98523 2.0298,1.5217 3.2067,1.5936 0.4168,0.02583 0.88618,-0.07449 1.0097,-0.50414 -0.64304,-0.30234 -1.2914,-0.61106 -1.8419,-1.0599"
|
||||
id="path278"
|
||||
style="stroke-width:0.830531" />
|
||||
<path
|
||||
d="m 101.23,34.598 c 0.0467,0.0065 0.0932,0.01402 0.13994,0.02259 0.25056,0.04622 0.50472,0.13125 0.68647,0.30988 0.26644,0.26168 0.30999,0.64257 0.33386,0.99612 0.0273,0.40626 0.0195,0.83753 0.22809,1.1873 -0.057,-0.09545 -0.18859,-0.16982 -0.25485,-0.27095 -0.0778,-0.11874 -0.13322,-0.25056 -0.18315,-0.38286 -0.1,-0.26516 -0.16797,-0.54596 -0.30848,-0.79421 -0.35031,-0.61894 -1.1386,-0.65995 -1.7705,-0.53982 -0.53831,0.1024 -1.0681,0.28057 -1.5987,0.41147 -0.56924,0.1404 -1.1599,0.21674 -1.7445,0.25647 -0.27778,0.01877 -0.57598,0.02247 -0.81042,-0.12766 -0.19427,-0.12441 -0.31884,-0.35077 -0.31997,-0.58141 -0.0026,-0.5075 0.30223,-0.5572 0.6908,-0.48595 0.67532,0.12407 1.3504,0.10785 2.0336,0.08595 0.49545,-0.01564 0.99068,-0.04031 1.4852,-0.07391 0.46174,-0.03151 0.93159,-0.08816 1.3926,-0.01297"
|
||||
id="path280"
|
||||
style="stroke-width:0.830531" />
|
||||
</g>
|
||||
</g>
|
||||
<rect
|
||||
x="0.0054588001"
|
||||
y="0.066145003"
|
||||
width="137.58"
|
||||
height="63.5"
|
||||
fill="#333333"
|
||||
fill-rule="evenodd"
|
||||
opacity="0.56765"
|
||||
id="rect286"
|
||||
style="stroke-width:0.999978" />
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 368 KiB |
Before Width: | Height: | Size: 57 KiB |
49
desktop/CMakeLists.txt
Normal file
|
@ -0,0 +1,49 @@
|
|||
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)
|
98
desktop/config/config.cpp
Normal file
|
@ -0,0 +1,98 @@
|
|||
#include "config.hpp"
|
||||
|
||||
#include <QDir>
|
||||
#include <QStandardPaths>
|
||||
#include <QTextStream>
|
||||
#include <filesystem>
|
||||
|
||||
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<KotoLibraryConfig*>();
|
||||
|
||||
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<KotoLibraryConfig*> KotoConfig::getLibraries() {
|
||||
return this->i_libraries;
|
||||
}
|
||||
|
||||
void KotoConfig::parseConfigFile(std::string filePath) {
|
||||
auto data = toml::parse(filePath);
|
||||
std::optional<toml::value> 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<std::vector<toml::value>>(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();
|
||||
}
|
||||
}
|
28
desktop/config/config.hpp
Normal file
|
@ -0,0 +1,28 @@
|
|||
#pragma once
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
#include "library.hpp"
|
||||
#include "ui_prefs.hpp"
|
||||
|
||||
class KotoConfig {
|
||||
public:
|
||||
KotoConfig();
|
||||
static KotoConfig& instance();
|
||||
static KotoConfig* create() { return &instance(); }
|
||||
void save();
|
||||
|
||||
QString getConfigDirPath();
|
||||
QList<KotoLibraryConfig*> getLibraries();
|
||||
KotoUiPreferences* getUiPreferences();
|
||||
|
||||
private:
|
||||
void bootstrap();
|
||||
void parseConfigFile(std::string filePath);
|
||||
|
||||
QString i_configDirPath;
|
||||
QString i_configPath;
|
||||
QList<KotoLibraryConfig*> i_libraries;
|
||||
KotoUiPreferences* i_uiPreferences;
|
||||
};
|
60
desktop/config/library.cpp
Normal file
|
@ -0,0 +1,60 @@
|
|||
#include "library.hpp"
|
||||
|
||||
#include <QDebug>
|
||||
#include <string>
|
||||
|
||||
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<std::string>(v, "name");
|
||||
this->i_path = toml::find<std::string>(v, "path");
|
||||
this->i_type = libraryTypeFromString(toml::find<std::string>(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);
|
||||
}
|
32
desktop/config/library.hpp
Normal file
|
@ -0,0 +1,32 @@
|
|||
#pragma once
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
#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;
|
||||
};
|
71
desktop/config/ui_prefs.cpp
Normal file
|
@ -0,0 +1,71 @@
|
|||
#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<toml::value> v) {
|
||||
// No UI prefs provided
|
||||
if (!v.has_value()) return;
|
||||
toml::value& uiPrefs = v.value();
|
||||
auto showDescription = toml::find_or<bool>(uiPrefs, "album_info_show_description", false);
|
||||
auto showGenre = toml::find_or<bool>(uiPrefs, "album_info_show_genre", false);
|
||||
auto showNarrator = toml::find_or<bool>(uiPrefs, "album_info_show_narrator", false);
|
||||
auto showYear = toml::find_or<bool>(uiPrefs, "album_info_show_year", false);
|
||||
auto lastUsedVolume = toml::find_or<float>(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;
|
||||
}
|
35
desktop/config/ui_prefs.hpp
Normal file
|
@ -0,0 +1,35 @@
|
|||
#pragma once
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "includes/toml.hpp"
|
||||
|
||||
class KotoUiPreferences {
|
||||
public:
|
||||
KotoUiPreferences();
|
||||
KotoUiPreferences(std::optional<toml::value> 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;
|
||||
};
|
119
desktop/datalake/album.cpp
Normal file
|
@ -0,0 +1,119 @@
|
|||
#include <iostream>
|
||||
|
||||
#include "database.hpp"
|
||||
#include "structs.hpp"
|
||||
|
||||
KotoAlbum::KotoAlbum() {
|
||||
this->uuid = QUuid::createUuid();
|
||||
this->tracks = QList<KotoTrack*>();
|
||||
}
|
||||
|
||||
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<QString> 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<KotoTrack*> KotoAlbum::getTracks() {
|
||||
return QList {this->tracks};
|
||||
}
|
||||
|
||||
std::optional<int> 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<QString> 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;
|
||||
}
|
91
desktop/datalake/artist.cpp
Normal file
|
@ -0,0 +1,91 @@
|
|||
#include <QSqlQuery>
|
||||
|
||||
#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<KotoAlbum*> KotoArtist::getAlbums() {
|
||||
return QList {this->albums};
|
||||
}
|
||||
|
||||
std::optional<KotoAlbum*> 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<KotoTrack*> 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};
|
||||
}
|
63
desktop/datalake/cartographer.cpp
Normal file
|
@ -0,0 +1,63 @@
|
|||
#include "cartographer.hpp"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
Cartographer::Cartographer(QObject* parent)
|
||||
: QObject(parent),
|
||||
i_albums(QHash<QUuid, KotoAlbum*>()),
|
||||
i_artists_model(new KotoArtistModel(QList<KotoArtist*>())),
|
||||
i_artists_by_name(QHash<QString, KotoArtist*>()),
|
||||
i_tracks(QHash<QUuid, KotoTrack*>()) {}
|
||||
|
||||
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<KotoAlbum*> Cartographer::getAlbum(QUuid uuid) {
|
||||
auto album = this->i_albums.value(uuid, nullptr);
|
||||
return album ? std::optional {album} : std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<KotoArtist*> Cartographer::getArtist(QUuid uuid) {
|
||||
for (auto artist : this->i_artists_model->getArtists()) {
|
||||
if (artist->uuid == uuid) { return std::optional {artist}; }
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QList<KotoArtist*> 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<KotoArtist*> Cartographer::getArtist(QString name) {
|
||||
auto artist = this->i_artists_by_name.value(name, nullptr);
|
||||
return artist ? std::optional {artist} : std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<KotoTrack*> Cartographer::getTrack(QUuid uuid) {
|
||||
auto track = this->i_tracks.value(uuid, nullptr);
|
||||
return track ? std::optional {track} : std::nullopt;
|
||||
}
|
||||
|
||||
QList<KotoTrack*> Cartographer::getTracks() {
|
||||
return this->i_tracks.values();
|
||||
}
|
51
desktop/datalake/cartographer.hpp
Normal file
|
@ -0,0 +1,51 @@
|
|||
#pragma once
|
||||
|
||||
#include <QtQml/qqmlregistration.h>
|
||||
|
||||
#include <QHash>
|
||||
#include <QQmlEngine>
|
||||
#include <QQmlListProperty>
|
||||
#include <QString>
|
||||
#include <QUuid>
|
||||
#include <optional>
|
||||
|
||||
#include "structs.hpp"
|
||||
|
||||
class Cartographer : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
// Q_PROPERTY(QQmlListProperty<KotoAlbum*> albums READ getAlbumsQml)
|
||||
Q_PROPERTY(KotoArtistModel* artists READ getArtistsModel CONSTANT)
|
||||
// Q_PROPERTY(QQmlListProperty<KotoTrack*> 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<KotoAlbum*> getAlbumsQml();
|
||||
KotoArtistModel* getArtistsModel();
|
||||
// QQmlListProperty<KotoTrack*> getTracksQml();
|
||||
|
||||
std::optional<KotoAlbum*> getAlbum(QUuid uuid);
|
||||
QList<KotoAlbum*> getAlbums();
|
||||
std::optional<KotoArtist*> getArtist(QUuid uuid);
|
||||
QList<KotoArtist*> getArtists();
|
||||
std::optional<KotoArtist*> getArtist(QString name);
|
||||
std::optional<KotoTrack*> getTrack(QUuid uuid);
|
||||
QList<KotoTrack*> getTracks();
|
||||
|
||||
private:
|
||||
QHash<QUuid, KotoAlbum*> i_albums;
|
||||
KotoArtistModel* i_artists_model;
|
||||
QHash<QString, KotoArtist*> i_artists_by_name;
|
||||
QHash<QUuid, KotoTrack*> i_tracks;
|
||||
};
|
102
desktop/datalake/database.cpp
Normal file
|
@ -0,0 +1,102 @@
|
|||
#include "database.hpp"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QSqlQuery>
|
||||
#include <iostream>
|
||||
|
||||
#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);
|
||||
}
|
||||
}
|
21
desktop/datalake/database.hpp
Normal file
|
@ -0,0 +1,21 @@
|
|||
#pragma once
|
||||
|
||||
#include <QSqlDatabase>
|
||||
|
||||
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;
|
||||
};
|
88
desktop/datalake/indexer.cpp
Normal file
|
@ -0,0 +1,88 @@
|
|||
#include "indexer.hpp"
|
||||
|
||||
#include <KFileMetaData/ExtractorCollection>
|
||||
#include <QDebug>
|
||||
#include <QDirIterator>
|
||||
#include <QMimeDatabase>
|
||||
#include <iostream>
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
26
desktop/datalake/indexer.hpp
Normal file
|
@ -0,0 +1,26 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "cartographer.hpp"
|
||||
#include "config/config.hpp"
|
||||
#include "structs.hpp"
|
||||
|
||||
class FileIndexer {
|
||||
public:
|
||||
FileIndexer(KotoLibraryConfig* config);
|
||||
~FileIndexer();
|
||||
|
||||
QList<KotoArtist*> getArtists();
|
||||
QList<KotoTrack*> getFiles();
|
||||
QString getRoot();
|
||||
|
||||
void index();
|
||||
|
||||
protected:
|
||||
QList<KotoArtist*> i_artists;
|
||||
QList<KotoTrack*> i_tracks;
|
||||
QString i_root;
|
||||
};
|
||||
|
||||
void indexAllLibraries();
|
49
desktop/datalake/models.cpp
Normal file
|
@ -0,0 +1,49 @@
|
|||
#include "structs.hpp"
|
||||
|
||||
KotoArtistModel::KotoArtistModel(const QList<KotoArtist*>& 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<KotoArtist*> KotoArtistModel::getArtists() {
|
||||
return this->m_artists;
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> KotoArtistModel::roleNames() const {
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[NameRole] = QByteArrayLiteral("name");
|
||||
roles[PathRole] = QByteArrayLiteral("path");
|
||||
roles[UuidRole] = QByteArrayLiteral("uuid");
|
||||
return roles;
|
||||
}
|
227
desktop/datalake/structs.hpp
Normal file
|
@ -0,0 +1,227 @@
|
|||
#pragma once
|
||||
#include <QtQml/qqmlregistration.h>
|
||||
|
||||
#include <KFileMetaData/SimpleExtractionResult>
|
||||
#include <QAbstractListModel>
|
||||
#include <QFileInfo>
|
||||
#include <QList>
|
||||
#include <QSqlQuery>
|
||||
#include <QSqlRecord>
|
||||
#include <QString>
|
||||
#include <QUuid>
|
||||
|
||||
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<KotoAlbum*> albums READ getAlbums NOTIFY albumsChanged)
|
||||
Q_PROPERTY(QList<KotoTrack*> 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<KotoAlbum*> getAlbums();
|
||||
std::optional<KotoAlbum*> getAlbumByName(QString name);
|
||||
QString getName();
|
||||
QString getPath();
|
||||
QList<KotoTrack*> 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<KotoAlbum*> albums;
|
||||
QList<KotoTrack*> tracks;
|
||||
};
|
||||
|
||||
class KotoArtistModel : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit KotoArtistModel(const QList<KotoArtist*>& 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<int, QByteArray> roleNames() const override;
|
||||
virtual ~KotoArtistModel();
|
||||
|
||||
QList<KotoArtist*> getArtists();
|
||||
|
||||
enum KotoArtistRoles {
|
||||
NameRole = Qt::UserRole + 1,
|
||||
PathRole,
|
||||
UuidRole,
|
||||
};
|
||||
|
||||
private:
|
||||
QList<KotoArtist*> 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<QString> 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<KotoTrack*> 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<QString> getGenres();
|
||||
QString getNarrator();
|
||||
QString getPath();
|
||||
QString getTitle();
|
||||
QList<KotoTrack*> getTracks();
|
||||
std::optional<int> getYear();
|
||||
int getYearQml();
|
||||
|
||||
void addTrack(KotoTrack* track);
|
||||
void removeTrack(KotoTrack* track);
|
||||
void setAlbumArtPath(QString str);
|
||||
void setDescription(QString str);
|
||||
void setGenres(QList<QString> 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<int> year;
|
||||
|
||||
QList<QString> genres;
|
||||
QList<KotoTrack*> 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<QUuid> 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<QString> 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;
|
||||
};
|
198
desktop/datalake/track.cpp
Normal file
|
@ -0,0 +1,198 @@
|
|||
#include <QFileInfo>
|
||||
#include <iostream>
|
||||
|
||||
#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<KotoArtist*>();
|
||||
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<QString> 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<QString> 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;
|
||||
}
|
11
desktop/example-config.toml
Normal file
|
@ -0,0 +1,11 @@
|
|||
["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"
|
17240
desktop/includes/toml.hpp
Normal file
51
desktop/main.cpp
Normal file
|
@ -0,0 +1,51 @@
|
|||
#include <QGuiApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQuickStyle>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
#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();
|
||||
}
|
24
desktop/qml/HomePage.qml
Normal file
|
@ -0,0 +1,24 @@
|
|||
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
|
||||
}
|
||||
}
|
20
desktop/qml/Main.qml
Normal file
|
@ -0,0 +1,20 @@
|
|||
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 {
|
||||
}
|
||||
}
|
74
desktop/qml/PlayerBar/PlayerBar.qml
Normal file
|
@ -0,0 +1,74 @@
|
|||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
105
desktop/qml/PrimaryNavigation.qml
Normal file
|
@ -0,0 +1,105 @@
|
|||
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));
|
||||
}
|
||||
}
|
24
desktop/qml/Root.qml
Normal file
|
@ -0,0 +1,24 @@
|
|||
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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
meson.build
|
@ -1,35 +0,0 @@
|
|||
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')
|
|
@ -1,7 +0,0 @@
|
|||
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
|
||||
|
|
@ -1 +0,0 @@
|
|||
i18n.gettext('koto', preset: 'glib')
|
|
@ -1,423 +0,0 @@
|
|||
/* 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 <glib-2.0/glib.h>
|
||||
#include <gtk-4.0/gtk/gtk.h>
|
||||
#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
|
||||
);
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
/* 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 <glib-2.0/glib.h>
|
||||
#include <gtk-4.0/gtk/gtk.h>
|
||||
#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
|
|
@ -1,289 +0,0 @@
|
|||
/* 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 <glib-2.0/glib.h>
|
||||
#include <gtk-4.0/gtk/gtk.h>
|
||||
#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
|
||||
);
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
/* 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 <glib-2.0/glib-object.h>
|
||||
#include <gtk-4.0/gtk/gtk.h>
|
||||
#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);
|
|
@ -1,818 +0,0 @@
|
|||
/* 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 <gtk-4.0/gtk/gtk.h>
|
||||
#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
|
||||
);
|
||||
}
|
|
@ -1,174 +0,0 @@
|
|||
/* 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 <glib-2.0/glib-object.h>
|
||||
#include <gdk-pixbuf-2.0/gdk-pixbuf/gdk-pixbuf.h>
|
||||
#include <gtk-4.0/gtk/gtk.h>
|
||||
|
||||
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
|
|
@ -1,269 +0,0 @@
|
|||
/* 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 <glib-2.0/glib.h>
|
||||
#include <gtk-4.0/gtk/gtk.h>
|
||||
#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
|
||||
);
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
/* 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 <glib-2.0/glib.h>
|
||||
#include <gtk-4.0/gtk/gtk.h>
|
||||
#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
|
|
@ -1,158 +0,0 @@
|
|||
/* 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 <gtk-4.0/gtk/gtk.h>
|
||||
#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
|
||||
);
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
/* 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 <glib-2.0/glib-object.h>
|
||||
#include <gtk-4.0/gtk/gtk.h>
|
||||
#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
|
|
@ -1,540 +0,0 @@
|
|||
/* 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 <glib-2.0/glib.h>
|
||||
#include <gtk-4.0/gtk/gtk.h>
|
||||
#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
|
||||
);
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
/* 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 <glib-2.0/glib-object.h>
|
||||
#include <gtk-4.0/gtk/gtk.h>
|
||||
#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
|
|
@ -1,768 +0,0 @@
|
|||
/* 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 <glib-2.0/glib.h>
|
||||
#include <glib-2.0/gio/gio.h>
|
||||
#include <errno.h>
|
||||
#include <toml.h>
|
||||
|
||||
#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);
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
/* 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 <glib-2.0/glib.h>
|
||||
#include <glib-2.0/gio/gio.h>
|
||||
#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
|
|
@ -1,910 +0,0 @@
|
|||
/* 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 <glib-2.0/glib.h>
|
||||
#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);
|
||||
}
|
|
@ -1,221 +0,0 @@
|
|||
/* 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 <glib-2.0/glib-object.h>
|
||||
#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
|
110
src/db/db.c
|
@ -1,110 +0,0 @@
|
|||
/* 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 <glib-2.0/glib.h>
|
||||
#include <sqlite3.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
#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;
|
||||
}
|
41
src/db/db.h
|
@ -1,41 +0,0 @@
|
|||
/* 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 <glib-2.0/glib.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
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();
|
392
src/db/loaders.c
|
@ -1,392 +0,0 @@
|
|||
/* 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;
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
/* 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();
|
|
@ -1,25 +0,0 @@
|
|||
/* 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
|
|
@ -1,851 +0,0 @@
|
|||
/* 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 <glib-2.0/glib.h>
|
||||
#include <magic.h>
|
||||
#include <sqlite3.h>
|
||||
#include <stdio.h>
|
||||
#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
|
||||
);
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
/* 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
|
|
@ -1,606 +0,0 @@
|
|||
/* 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 <glib-2.0/glib.h>
|
||||
#include <sqlite3.h>
|
||||
#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
|
||||
);
|
||||
}
|
|
@ -1,239 +0,0 @@
|
|||
/* 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 <dirent.h>
|
||||
#include <magic.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
#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
|
||||
}
|
||||
}
|
|
@ -1,511 +0,0 @@
|
|||
/* 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");
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
/* 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;
|
|
@ -1,425 +0,0 @@
|
|||
/* 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 <glib-2.0/glib.h>
|
||||
#include <glib-2.0/gio/gio.h>
|
||||
#include <magic.h>
|
||||
#include <toml.h>
|
||||
#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
|
|
@ -1,243 +0,0 @@
|
|||
/* 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 <glib-2.0/glib.h>
|
||||
#include <taglib/tag_c.h>
|
||||
#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);
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
/* 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 <glib-2.0/glib.h>
|
||||
|
||||
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
|
||||
);
|
|
@ -1,879 +0,0 @@
|
|||
/* 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 <glib-2.0/glib.h>
|
||||
#include <sqlite3.h>
|
||||
#include <taglib/tag_c.h>
|
||||
#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
|
||||
);
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
/* 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 <glib-2.0/glib-object.h>
|
||||
#include <gtk-4.0/gtk/gtk.h>
|
||||
#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
|
||||
);
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
/* 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 <glib-2.0/glib-object.h>
|
||||
#include <gtk-4.0/gtk/gtk.h>
|
||||
|
||||
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
|
|
@ -1,323 +0,0 @@
|
|||
/* 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 <glib/gi18n.h>
|
||||
#include <gtk-4.0/gtk/gtk.h>
|
||||
#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
|
||||
);
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
/* 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 <gtk-4.0/gtk/gtk.h>
|
||||
#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
|
|
@ -1,40 +0,0 @@
|
|||
?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.24"/>
|
||||
<object class="GtkImage" id="audioHeadphonesMenuImage">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">audio-headphones</property>
|
||||
<property name="icon_size">3</property>
|
||||
</object>
|
||||
<template class="GtkHeaderBar" parent="GtkHeaderBar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="show-close-button">True</property>
|
||||
<property name="decoration-layout">appmenu:minimize,maximize,close</property>
|
||||
<child type="title">
|
||||
<object class="GtkSearchEntry" id="search">
|
||||
<property name="width-request">400</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="primary-icon-name">edit-find-symbolic</property>
|
||||
<property name="primary-icon-activatable">False</property>
|
||||
<property name="primary-icon-sensitive">False</property>
|
||||
<property name="placeholder-text" translatable="yes">Search...</property>
|
||||
<property name="input-hints">GTK_INPUT_HINT_NO_EMOJI | GTK_INPUT_HINT_NONE</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="menu_button">
|
||||
<property name="name">menu_button</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="image">audioHeadphonesMenuImage</property>
|
||||
<style>
|
||||
<class name="flat"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
318
src/koto-nav.c
|
@ -1,318 +0,0 @@
|
|||
/* 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 <gtk-4.0/gtk/gtk.h>
|
||||
#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);
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
/* 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 <gtk-4.0/gtk/gtk.h>
|
||||
#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
|