Stemify iOS 1.1 and WebUI updates, repo restructure into desktop/ios/server/web
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
894dd5a695
commit
df5d847169
157 files changed
+3935
-742
No files matched your search
@@ -0,0 +1,192 @@
|
||||
import QtQuick
|
||||
|
||||
// File: Excel's backstage view. Info, Account (Plex sign-in) and Options.
|
||||
Rectangle {
|
||||
id: bs
|
||||
property var win: Window.window
|
||||
property string page: "info"
|
||||
color: "#FFFFFF"
|
||||
|
||||
Rectangle {
|
||||
id: side
|
||||
width: 190
|
||||
height: parent.height
|
||||
color: X.green
|
||||
Column {
|
||||
width: parent.width
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 64
|
||||
Rectangle { anchors.fill: parent; color: bm.containsMouse ? X.greenHover : "transparent" }
|
||||
Ico { x: 22; anchors.verticalCenter: parent.verticalCenter; name: "back"; ink: "#FFFFFF"; size: 30 }
|
||||
MouseArea { id: bm; anchors.fill: parent; hoverEnabled: true; onClicked: bs.win.closeBackstage() }
|
||||
}
|
||||
Repeater {
|
||||
model: [["info", "Info"], ["account", "Account"], ["options", "Options"]]
|
||||
delegate: Item {
|
||||
required property var modelData
|
||||
width: side.width
|
||||
height: 40
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: bs.page === parent.modelData[0] ? X.greenDark : im.containsMouse ? X.greenHover : "transparent"
|
||||
}
|
||||
T { x: 26; height: parent.height; text: parent.modelData[1]; color: "#FFFFFF"; font.pixelSize: 15 }
|
||||
MouseArea { id: im; anchors.fill: parent; hoverEnabled: true; onClicked: bs.page = parent.modelData[0] }
|
||||
}
|
||||
}
|
||||
Item { width: 1; height: 20 }
|
||||
Item {
|
||||
width: side.width
|
||||
height: 40
|
||||
Rectangle { anchors.fill: parent; color: qm.containsMouse ? X.greenHover : "transparent" }
|
||||
T { x: 26; height: parent.height; text: "Close"; color: "#FFFFFF"; font.pixelSize: 15 }
|
||||
MouseArea { id: qm; anchors.fill: parent; hoverEnabled: true; onClicked: bs.win.close() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
x: side.width + 40
|
||||
y: 24
|
||||
width: parent.width - x - 40
|
||||
height: parent.height - y
|
||||
|
||||
T {
|
||||
id: heading
|
||||
text: bs.page === "info" ? "Info" : bs.page === "account" ? "Account" : "Options"
|
||||
font.pixelSize: 38
|
||||
font.weight: Font.Light
|
||||
color: X.ink
|
||||
height: 56
|
||||
}
|
||||
|
||||
// ---- Info ----
|
||||
Column {
|
||||
visible: bs.page === "info"
|
||||
y: heading.height + 20
|
||||
width: parent.width
|
||||
spacing: 18
|
||||
Row {
|
||||
spacing: 40
|
||||
Column {
|
||||
width: 360
|
||||
spacing: 8
|
||||
T { text: "Now Playing"; font.pixelSize: 20; color: X.greenText; height: 30 }
|
||||
Prop { k: "Title"; v: player.track.title || "—" }
|
||||
Prop { k: "Artist"; v: player.track.artist || "—" }
|
||||
Prop { k: "Album"; v: player.track.album || "—" }
|
||||
Prop { k: "Plays from"; v: player.sourceKind === "stems" ? "Stems (" + player.stems.length + ")" : player.sourceKind === "original" ? "Original file" + (player.track.codec ? " (" + player.track.codec.toUpperCase() + ")" : "") : "—" }
|
||||
Prop { k: "Mix"; v: player.presetLabel || "—" }
|
||||
Prop { k: "Queue"; v: player.queueInfo.count ? (player.queueInfo.index + 1) + " of " + player.queueInfo.count : "Empty" }
|
||||
}
|
||||
Column {
|
||||
width: 360
|
||||
spacing: 8
|
||||
T { text: "Properties"; font.pixelSize: 20; color: X.greenText; height: 30 }
|
||||
Prop { k: "Songs"; v: library.counts.songs.toLocaleString(Qt.locale(), "f", 0) }
|
||||
Prop { k: "With stems"; v: library.counts.stems.toLocaleString(Qt.locale(), "f", 0) }
|
||||
Prop { k: "Stems index"; v: library.counts.stemIndex.toLocaleString(Qt.locale(), "f", 0) + " songs" }
|
||||
Prop { k: "Stem server"; v: (library.counts.stemOnline === true ? "Online · " : library.counts.stemOnline === false ? "Offline · " : "Checking · ") + settings.get("stemUrl") }
|
||||
Prop { k: "Plex"; v: (library.signedIn ? "Connected · " : "Not connected · ") + settings.get("plexUrl") }
|
||||
Prop { k: "Media keys"; v: bs.win.mprisOk ? "On (MPRIS)" : "Off" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Account ----
|
||||
Column {
|
||||
visible: bs.page === "account"
|
||||
y: heading.height + 20
|
||||
width: parent.width
|
||||
spacing: 14
|
||||
T { text: "Connected Services"; font.pixelSize: 20; color: X.greenText; height: 30 }
|
||||
Row {
|
||||
spacing: 14
|
||||
Ico { name: "plex"; size: 40 }
|
||||
Column {
|
||||
spacing: 4
|
||||
T { text: "Plex Media Server"; font.pixelSize: 15; font.bold: true }
|
||||
T { text: library.signedIn ? "Connected to " + settings.get("plexUrl") : "Not connected"; color: X.inkSoft }
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
visible: library.pin !== ""
|
||||
width: 420
|
||||
height: 150
|
||||
color: "#F3F3F3"
|
||||
border.color: X.ribbonLine
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 6
|
||||
T { anchors.horizontalCenter: parent.horizontalCenter; text: "Go to plex.tv/link and enter"; font.pixelSize: 14 }
|
||||
T {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: library.pin
|
||||
font.family: cellFont
|
||||
font.pixelSize: 54
|
||||
font.bold: true
|
||||
font.letterSpacing: 10
|
||||
color: X.greenText
|
||||
}
|
||||
T { anchors.horizontalCenter: parent.horizontalCenter; text: "Waiting for Plex…"; color: X.inkSoft }
|
||||
}
|
||||
}
|
||||
Row {
|
||||
spacing: 10
|
||||
OfficeButton {
|
||||
text: library.pin !== "" ? "Cancel" : library.signedIn ? "Sign in again" : "Sign in to Plex"
|
||||
primary: library.pin === ""
|
||||
onClicked: library.pin !== "" ? library.cancelSignIn() : library.signIn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Options ----
|
||||
Column {
|
||||
visible: bs.page === "options"
|
||||
y: heading.height + 20
|
||||
width: Math.min(parent.width, 640)
|
||||
spacing: 10
|
||||
T { text: "Connections"; font.pixelSize: 20; color: X.greenText; height: 30 }
|
||||
Opt { id: oPlex; label: "Plex server"; key: "plexUrl" }
|
||||
Opt { id: oStem; label: "Stem server (stemserve)"; key: "stemUrl" }
|
||||
T { text: "Folders"; font.pixelSize: 20; color: X.greenText; height: 40; verticalAlignment: Text.AlignBottom }
|
||||
Opt { id: oStems; label: "Stems share (played directly when mounted)"; key: "stemsDir" }
|
||||
Opt { id: oMusic; label: "Music share"; key: "musicDir" }
|
||||
Item { width: 1; height: 8 }
|
||||
Row {
|
||||
spacing: 10
|
||||
OfficeButton {
|
||||
text: "Save"
|
||||
primary: true
|
||||
onClicked: {
|
||||
oPlex.save(); oStem.save(); oStems.save(); oMusic.save()
|
||||
library.refresh()
|
||||
bs.win.closeBackstage()
|
||||
}
|
||||
}
|
||||
OfficeButton { text: "Cancel"; onClicked: bs.win.closeBackstage() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Prop: Row {
|
||||
property string k
|
||||
property string v
|
||||
spacing: 10
|
||||
T { width: 110; text: parent.k; color: X.inkSoft }
|
||||
T { width: 240; text: parent.v }
|
||||
}
|
||||
component Opt: Column {
|
||||
id: o
|
||||
property string label
|
||||
property string key
|
||||
function save() { settings.set(key, f.text.trim()) }
|
||||
spacing: 3
|
||||
width: parent.width
|
||||
T { text: o.label; color: X.inkSoft }
|
||||
XField { id: f; width: o.width; text: settings.get(o.key) || "" }
|
||||
Connections { target: bs; function onVisibleChanged() { if (bs.visible) f.text = settings.get(o.key) || "" } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: c
|
||||
property string icon
|
||||
property bool danger: false
|
||||
property string tip: ""
|
||||
signal clicked()
|
||||
width: icon === "pane" ? 40 : 46
|
||||
height: parent.height
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: cm.pressed ? (c.danger ? "#F1707A" : X.greenDark) : cm.containsMouse ? (c.danger ? "#E81123" : X.greenHover) : "transparent"
|
||||
}
|
||||
Ico { anchors.centerIn: parent; name: c.icon; ink: "#FFFFFF"; size: c.icon === "pane" ? 16 : 24 }
|
||||
MouseArea { id: cm; anchors.fill: parent; hoverEnabled: true; onClicked: c.clicked() }
|
||||
Tip { target: cm; text: c.tip }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import QtQuick
|
||||
|
||||
// Name box, the ✕ ✓ fx buttons, and the formula of the active cell.
|
||||
Rectangle {
|
||||
id: fb
|
||||
property var win: Window.window
|
||||
height: 30
|
||||
color: "#FFFFFF"
|
||||
Rectangle { anchors.bottom: parent.bottom; width: parent.width; height: 1; color: X.ribbonLine }
|
||||
|
||||
Rectangle {
|
||||
id: nameBox
|
||||
x: 4
|
||||
y: 4
|
||||
width: 110
|
||||
height: 22
|
||||
border.color: "#C6C6C6"
|
||||
T {
|
||||
x: 5
|
||||
height: parent.height
|
||||
width: parent.width - 20
|
||||
text: fb.win.multiSel ? (fb.win.selBottom - fb.win.selTop + 1) + "R x " + (Math.abs(fb.win.curCol - fb.win.anchorCol) + 1) + "C"
|
||||
: X.colName(fb.win.curCol) + (fb.win.curRow + 2)
|
||||
}
|
||||
Ico { anchors.right: parent.right; anchors.rightMargin: 4; anchors.verticalCenter: parent.verticalCenter; name: "dropdown"; size: 10 }
|
||||
}
|
||||
Row {
|
||||
x: nameBox.x + nameBox.width + 10
|
||||
height: parent.height
|
||||
spacing: 12
|
||||
Ico { anchors.verticalCenter: parent.verticalCenter; name: "cancel"; size: 14; opacity: 0.35 }
|
||||
Ico { anchors.verticalCenter: parent.verticalCenter; name: "check"; size: 14; opacity: 0.35 }
|
||||
Ico { anchors.verticalCenter: parent.verticalCenter; name: "function"; size: 18 }
|
||||
}
|
||||
Rectangle {
|
||||
x: nameBox.x + nameBox.width + 92
|
||||
y: 4
|
||||
width: parent.width - x - 6
|
||||
height: 22
|
||||
border.color: "#C6C6C6"
|
||||
T {
|
||||
x: 6
|
||||
width: parent.width - 12
|
||||
height: parent.height
|
||||
font.family: cellFont
|
||||
font.pixelSize: 14
|
||||
color: "#000000"
|
||||
text: fb.win.formulaText
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import QtQuick
|
||||
|
||||
// image://icon/<name>[/<hex ink>], rendered at device pixels.
|
||||
Image {
|
||||
property string name: "grid"
|
||||
property string ink: ""
|
||||
property int size: 16
|
||||
width: size
|
||||
height: size
|
||||
source: "image://icon/" + name + (ink ? "/" + ink.replace("#", "") : "")
|
||||
sourceSize.width: size * Screen.devicePixelRatio
|
||||
sourceSize.height: size * Screen.devicePixelRatio
|
||||
smooth: true
|
||||
asynchronous: false
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
Window {
|
||||
id: win
|
||||
width: 1440
|
||||
height: 900
|
||||
minimumWidth: 960
|
||||
minimumHeight: 600
|
||||
visible: true
|
||||
title: (player.track.title ? player.track.title + " - " + player.track.artist : "Book1") + ".xlsx - Excel"
|
||||
color: "#FFFFFF"
|
||||
flags: Qt.Window | Qt.FramelessWindowHint
|
||||
|
||||
readonly property bool maximized: visibility === Window.Maximized || visibility === Window.FullScreen
|
||||
function toggleMaximized() { maximized ? showNormal() : showMaximized() }
|
||||
|
||||
// ---- selection: the active cell and the anchor of the range ----
|
||||
property int curRow: 0
|
||||
property int curCol: 0
|
||||
property int anchorRow: 0
|
||||
property int anchorCol: 0
|
||||
readonly property int selTop: Math.min(curRow, anchorRow)
|
||||
readonly property int selBottom: Math.max(curRow, anchorRow)
|
||||
readonly property bool multiSel: curRow !== anchorRow || curCol !== anchorCol
|
||||
property string formulaText: ""
|
||||
property var selStats: ({})
|
||||
function refreshCell() {
|
||||
formulaText = library.formula(curRow, curCol)
|
||||
selStats = multiSel ? library.stats(anchorRow, anchorCol, curRow, curCol) : ({})
|
||||
}
|
||||
onCurRowChanged: cellTimer.restart()
|
||||
onCurColChanged: cellTimer.restart()
|
||||
onAnchorRowChanged: cellTimer.restart()
|
||||
Timer { id: cellTimer; interval: 0; onTriggered: win.refreshCell() }
|
||||
Connections {
|
||||
target: library
|
||||
function onSheetSwitched() { win.curRow = win.anchorRow = 0; win.curCol = win.anchorCol = 0; win.refreshCell() }
|
||||
function onSheetChanged() { cellTimer.restart() }
|
||||
function onToast(msg) { messages.show(msg) }
|
||||
}
|
||||
Connections { target: player; function onToast(msg) { messages.show(msg) } }
|
||||
Connections {
|
||||
target: sheetModel
|
||||
function onModelReset() {
|
||||
// new data under the same sheet: keep the selection, inside the rows that exist
|
||||
var last = Math.max(0, grid.rows - 1)
|
||||
if (win.curRow > last) win.curRow = last
|
||||
if (win.anchorRow > last) win.anchorRow = last
|
||||
cellTimer.restart()
|
||||
}
|
||||
}
|
||||
Component.onCompleted: refreshCell()
|
||||
|
||||
// ---- view options ----
|
||||
property bool showMixer: settings.get("showMixer") !== false
|
||||
property bool gridlines: settings.get("gridlines") !== false
|
||||
property bool headings: settings.get("headings") !== false
|
||||
property int zoom: settings.get("zoom") || 100
|
||||
property bool mprisOk: false
|
||||
readonly property bool inPlaylist: library.sheetId.indexOf("playlist:") === 0
|
||||
function toggleMixer() { showMixer = !showMixer; settings.set("showMixer", showMixer) }
|
||||
function setOption(k, v) { win[k] = v; settings.set(k, v) }
|
||||
function setZoom(z) { zoom = Math.max(50, Math.min(200, z)); settings.set("zoom", zoom) }
|
||||
|
||||
function openBackstage(page) { backstage.page = page; backstage.visible = true }
|
||||
function closeBackstage() { backstage.visible = false; focusGrid() }
|
||||
function focusGrid() { grid.forceActiveFocus() }
|
||||
function focusSearch() { ribbon.focusSearch() }
|
||||
function selectAll() { anchorRow = 0; anchorCol = 0; curRow = Math.max(0, grid.rows - 1); curCol = Math.max(0, library.columns.length - 1) }
|
||||
function copySelection() { library.copyCells(anchorRow, anchorCol, curRow, curCol); messages.show("Copied " + (selBottom - selTop + 1) + " rows") }
|
||||
function sortCurrent(desc) { library.sortTo(curCol, desc) }
|
||||
function sortColumn(c, desc) { library.sortTo(c, desc) }
|
||||
function newPlaylist() { plDialog.open() }
|
||||
function addToPlaylist(name) { library.addToPlaylist(name, selTop, selBottom) }
|
||||
|
||||
// ---- playhead, read once per frame while playing ----
|
||||
property real playPos: 0
|
||||
property real levelL: 0
|
||||
property real levelR: 0
|
||||
function seekTo(s) { player.seek(s); playPos = s }
|
||||
FrameAnimation {
|
||||
running: engine.playing
|
||||
onTriggered: {
|
||||
var s = engine.snapshot()
|
||||
win.playPos = s[0]
|
||||
// meters fall back smoothly between peaks
|
||||
var k = Math.exp(-frameTime / 0.12)
|
||||
win.levelL = Math.max(s[1], win.levelL * k)
|
||||
win.levelR = Math.max(s[2], win.levelR * k)
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: engine
|
||||
function onPlayingChanged() { var s = engine.snapshot(); win.playPos = s[0]; if (!engine.playing) { win.levelL = 0; win.levelR = 0 } }
|
||||
function onDurationChanged() { win.playPos = engine.snapshot()[0] }
|
||||
}
|
||||
Timer { interval: 500; running: !engine.playing; repeat: true; onTriggered: win.playPos = engine.snapshot()[0] }
|
||||
|
||||
// dev hook: --eval runs JavaScript against the window
|
||||
property string devEval: ""
|
||||
onDevEvalChanged: if (devEval) { try { console.log("eval:", eval(devEval)) } catch (e) { console.log("eval error:", e) } }
|
||||
|
||||
Shortcut { sequence: "Ctrl+F"; onActivated: win.focusSearch() }
|
||||
Shortcut { sequence: "Ctrl+Q"; onActivated: win.close() }
|
||||
Shortcut { sequence: "Media Play"; onActivated: player.toggle() }
|
||||
Shortcut { sequence: "Ctrl+Right"; context: Qt.ApplicationShortcut; enabled: !grid.activeFocus; onActivated: player.next() }
|
||||
Shortcut { sequence: "Ctrl+Left"; context: Qt.ApplicationShortcut; enabled: !grid.activeFocus; onActivated: player.previous() }
|
||||
Shortcut { sequence: "Alt+Left"; onActivated: library.back() }
|
||||
Shortcut { sequence: "F1"; onActivated: win.openBackstage("info") }
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
TitleBar { width: parent.width }
|
||||
Ribbon { id: ribbon; width: parent.width }
|
||||
MessageBar { id: messages; width: parent.width }
|
||||
FormulaBar { width: parent.width }
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - y - tabs.height - status.height
|
||||
Sheet {
|
||||
id: grid
|
||||
width: parent.width - (pane.visible ? pane.width : 0)
|
||||
height: parent.height
|
||||
focus: true
|
||||
}
|
||||
TaskPane {
|
||||
id: pane
|
||||
visible: win.showMixer
|
||||
anchors.right: parent.right
|
||||
width: 330
|
||||
height: parent.height
|
||||
}
|
||||
}
|
||||
SheetTabs { id: tabs; width: parent.width }
|
||||
StatusBar { id: status; width: parent.width }
|
||||
}
|
||||
|
||||
Backstage {
|
||||
id: backstage
|
||||
visible: false
|
||||
y: 32
|
||||
width: parent.width
|
||||
height: parent.height - y
|
||||
}
|
||||
|
||||
// New Playlist: an Office dialog
|
||||
Popup {
|
||||
id: plDialog
|
||||
modal: true
|
||||
anchors.centerIn: parent
|
||||
width: 380
|
||||
padding: 0
|
||||
onOpened: { plName.text = ""; plName.forceActiveFocus() }
|
||||
onClosed: win.focusGrid()
|
||||
background: Rectangle { color: "#FFFFFF"; border.color: "#7A7A7A" }
|
||||
Overlay.modal: Rectangle { color: Qt.rgba(0, 0, 0, 0.12) }
|
||||
contentItem: Column {
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 30
|
||||
color: "#FFFFFF"
|
||||
T { x: 10; height: parent.height; text: "New Playlist" }
|
||||
Ico { anchors.right: parent.right; anchors.rightMargin: 8; anchors.verticalCenter: parent.verticalCenter; name: "close"; size: 18
|
||||
MouseArea { anchors.fill: parent; onClicked: plDialog.close() } }
|
||||
}
|
||||
Column {
|
||||
x: 14
|
||||
width: parent.width - 28
|
||||
spacing: 8
|
||||
T { text: (win.selBottom - win.selTop + 1) + " selected row(s) go into a new Plex playlist."; color: X.inkSoft }
|
||||
T { text: "Name:" }
|
||||
XField {
|
||||
id: plName
|
||||
width: parent.width
|
||||
onAccepted: okBtn.clicked()
|
||||
}
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
spacing: 8
|
||||
bottomPadding: 14
|
||||
topPadding: 6
|
||||
OfficeButton {
|
||||
id: okBtn
|
||||
text: "OK"
|
||||
primary: true
|
||||
onClicked: { library.newPlaylist(plName.text, win.selTop, win.selBottom); plDialog.close() }
|
||||
}
|
||||
OfficeButton { text: "Cancel"; onClicked: plDialog.close() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// frameless: resize from the edges
|
||||
ResizeEdge { visible: !win.maximized; edges: Qt.LeftEdge; x: 0; y: 6; width: 5; height: parent.height - 12 }
|
||||
ResizeEdge { visible: !win.maximized; edges: Qt.RightEdge; x: parent.width - 5; y: 6; width: 5; height: parent.height - 12 }
|
||||
ResizeEdge { visible: !win.maximized; edges: Qt.TopEdge; x: 6; y: 0; width: parent.width - 12; height: 4 }
|
||||
ResizeEdge { visible: !win.maximized; edges: Qt.BottomEdge; x: 6; y: parent.height - 4; width: parent.width - 12; height: 4 }
|
||||
ResizeEdge { visible: !win.maximized; edges: Qt.LeftEdge | Qt.TopEdge; width: 8; height: 8 }
|
||||
ResizeEdge { visible: !win.maximized; edges: Qt.RightEdge | Qt.TopEdge; x: parent.width - 8; width: 8; height: 8 }
|
||||
ResizeEdge { visible: !win.maximized; edges: Qt.LeftEdge | Qt.BottomEdge; y: parent.height - 8; width: 8; height: 8 }
|
||||
ResizeEdge { visible: !win.maximized; edges: Qt.RightEdge | Qt.BottomEdge; x: parent.width - 8; y: parent.height - 8; width: 8; height: 8 }
|
||||
Rectangle { anchors.fill: parent; color: "transparent"; border.color: X.green; border.width: win.maximized ? 0 : 1 }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import QtQuick
|
||||
|
||||
// Excel's yellow message bar, for notices.
|
||||
Rectangle {
|
||||
id: mb
|
||||
property string text: ""
|
||||
height: text ? 32 : 0
|
||||
visible: text !== ""
|
||||
color: "#FFF4CE"
|
||||
Rectangle { anchors.bottom: parent.bottom; width: parent.width; height: 1; color: "#E6D9A8" }
|
||||
Behavior on height { NumberAnimation { duration: X.easeMs } }
|
||||
function show(t) { text = t; hide.restart() }
|
||||
Timer { id: hide; interval: 6000; onTriggered: mb.text = "" }
|
||||
Row {
|
||||
x: 12
|
||||
height: parent.height
|
||||
spacing: 10
|
||||
Ico { anchors.verticalCenter: parent.verticalCenter; name: "info"; size: 16 }
|
||||
T { height: parent.height; text: "NOTICE"; font.bold: true }
|
||||
T { height: parent.height; text: mb.text }
|
||||
}
|
||||
Item {
|
||||
anchors { right: parent.right; rightMargin: 8; verticalCenter: parent.verticalCenter }
|
||||
width: 22
|
||||
height: 22
|
||||
Rectangle { anchors.fill: parent; color: xm.containsMouse ? "#F2E2A6" : "transparent" }
|
||||
Ico { anchors.centerIn: parent; name: "close"; size: 18 }
|
||||
MouseArea { id: xm; anchors.fill: parent; hoverEnabled: true; onClicked: mb.text = "" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import QtQuick
|
||||
|
||||
// Dialog button: gray with a border; the default one outlined in green.
|
||||
Rectangle {
|
||||
id: ob
|
||||
property string text
|
||||
property bool primary: false
|
||||
signal clicked()
|
||||
implicitWidth: Math.max(80, t.implicitWidth + 24)
|
||||
implicitHeight: 26
|
||||
color: m.pressed ? "#CCE4F7" : m.containsMouse ? "#E5F1FB" : "#FDFDFD"
|
||||
border.color: m.containsMouse ? "#0078D7" : primary ? X.green : "#ADADAD"
|
||||
border.width: primary ? 2 : 1
|
||||
T { id: t; anchors.centerIn: parent; text: ob.text }
|
||||
MouseArea { id: m; anchors.fill: parent; hoverEnabled: true; onClicked: ob.clicked() }
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import QtQuick
|
||||
|
||||
// A ribbon command: large (icon over a label) or small (icon beside a label).
|
||||
Item {
|
||||
id: b
|
||||
property string icon: ""
|
||||
property string text: ""
|
||||
property bool big: false
|
||||
property bool checkable: false
|
||||
property bool checked: false
|
||||
property bool dropdown: false
|
||||
property string tip: ""
|
||||
signal clicked()
|
||||
implicitWidth: big ? Math.max(44, lbl.implicitWidth + 12) : (icon ? 22 : 6) + (text ? lbl.implicitWidth + 8 : 0) + (dropdown ? 12 : 0)
|
||||
implicitHeight: big ? 66 : 22
|
||||
opacity: enabled ? 1 : 0.4
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: b.checked ? X.checked : ma.pressed ? X.pressed : ma.containsMouse ? X.hover : "transparent"
|
||||
border.color: b.checked ? X.checkedBorder : "transparent"
|
||||
}
|
||||
Ico {
|
||||
id: ic
|
||||
visible: b.icon !== ""
|
||||
name: b.icon
|
||||
size: b.big ? 32 : 16
|
||||
x: b.big ? (parent.width - width) / 2 : 3
|
||||
y: b.big ? 4 : 3
|
||||
}
|
||||
T {
|
||||
id: lbl
|
||||
text: b.text
|
||||
font.pixelSize: X.fsUi
|
||||
x: b.big ? (parent.width - width) / 2 : (b.icon ? 23 : 4)
|
||||
y: b.big ? 38 : 0
|
||||
height: b.big ? implicitHeight : parent.height
|
||||
width: b.big ? Math.min(implicitWidth, 90) : implicitWidth
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: b.big ? Text.WordWrap : Text.NoWrap
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideNone
|
||||
lineHeight: 0.95
|
||||
}
|
||||
Ico {
|
||||
visible: b.dropdown
|
||||
name: "dropdown"
|
||||
size: 10
|
||||
x: b.big ? (parent.width - width) / 2 : parent.width - 12
|
||||
y: b.big ? parent.height - 13 : 6
|
||||
}
|
||||
MouseArea {
|
||||
id: ma
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: b.clicked()
|
||||
}
|
||||
Tip { target: ma; text: b.tip || b.text }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import QtQuick
|
||||
|
||||
// A ribbon group: its commands, a caption along the bottom, a rule on the right.
|
||||
Item {
|
||||
id: g
|
||||
property string title: ""
|
||||
default property alias content: row.data
|
||||
implicitWidth: Math.max(row.implicitWidth, cap.implicitWidth) + 12
|
||||
height: parent ? parent.height : 92
|
||||
Row {
|
||||
id: row
|
||||
x: 6
|
||||
y: 3
|
||||
height: 68
|
||||
spacing: 2
|
||||
}
|
||||
T {
|
||||
id: cap
|
||||
text: g.title
|
||||
font.pixelSize: X.fsSmall
|
||||
color: X.inkSoft
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: parent.height - 18
|
||||
}
|
||||
Rectangle {
|
||||
anchors { right: parent.right; top: parent.top; bottom: parent.bottom; topMargin: 4; bottomMargin: 4 }
|
||||
width: 1
|
||||
color: X.ribbonLine
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import QtQuick
|
||||
|
||||
// Invisible edge/corner grip that hands resizing to the compositor.
|
||||
MouseArea {
|
||||
property int edges: 0
|
||||
property var win: Window.window
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton
|
||||
cursorShape: edges === (Qt.LeftEdge | Qt.TopEdge) || edges === (Qt.RightEdge | Qt.BottomEdge) ? Qt.SizeFDiagCursor
|
||||
: edges === (Qt.RightEdge | Qt.TopEdge) || edges === (Qt.LeftEdge | Qt.BottomEdge) ? Qt.SizeBDiagCursor
|
||||
: (edges & (Qt.LeftEdge | Qt.RightEdge)) ? Qt.SizeHorCursor : Qt.SizeVerCursor
|
||||
onPressed: win.startSystemResize(edges)
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
// Tabs on the green strip, commands on the gray ribbon below.
|
||||
Column {
|
||||
id: rib
|
||||
property var win: Window.window
|
||||
property int tab: 1 // 0 = File (backstage), 1 Home, 2 Insert, 3 Data, 4 View
|
||||
readonly property var tabs: ["File", "Home", "Insert", "Data", "View"]
|
||||
function focusSearch() { tell.startEditing() }
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 28
|
||||
color: X.green
|
||||
Row {
|
||||
x: 0
|
||||
height: parent.height
|
||||
Repeater {
|
||||
model: rib.tabs
|
||||
delegate: Item {
|
||||
id: tabItem
|
||||
required property int index
|
||||
required property string modelData
|
||||
readonly property bool file: index === 0
|
||||
readonly property bool active: rib.tab === index && !file
|
||||
width: tl.implicitWidth + (file ? 30 : 22)
|
||||
height: 28
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: tabItem.active ? 2 : 0
|
||||
color: tabItem.active ? X.ribbon : tabItem.file ? (tm.containsMouse ? "#0E4B2A" : X.greenDark)
|
||||
: tm.containsMouse ? X.greenHover : "transparent"
|
||||
}
|
||||
T {
|
||||
id: tl
|
||||
anchors.centerIn: parent
|
||||
text: tabItem.modelData
|
||||
color: tabItem.active ? X.greenText : "#FFFFFF"
|
||||
}
|
||||
MouseArea {
|
||||
id: tm
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: tabItem.file ? rib.win.openBackstage("info") : rib.tab = tabItem.index
|
||||
}
|
||||
}
|
||||
}
|
||||
// "Tell me what you want to do": here it searches the library.
|
||||
Item {
|
||||
id: tell
|
||||
width: 260
|
||||
height: 28
|
||||
function startEditing() { field.visible = true; field.forceActiveFocus(); field.selectAll() }
|
||||
Ico { x: 12; anchors.verticalCenter: parent.verticalCenter; name: "lightbulb"; ink: "#FFFFFF"; size: 16 }
|
||||
T {
|
||||
x: 34
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: !field.visible
|
||||
color: "#FFFFFF"
|
||||
text: "Tell me what you want to listen to..."
|
||||
}
|
||||
MouseArea { anchors.fill: parent; cursorShape: Qt.IBeamCursor; onClicked: tell.startEditing() }
|
||||
XField {
|
||||
id: field
|
||||
visible: false
|
||||
x: 32
|
||||
width: 220
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
placeholderText: "Song, artist or album"
|
||||
onAccepted: { library.search(text); visible = false; rib.win.focusGrid() }
|
||||
onActiveFocusChanged: if (!activeFocus && text === "") visible = false
|
||||
Keys.onEscapePressed: { text = ""; visible = false; rib.win.focusGrid() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 94
|
||||
color: X.ribbon
|
||||
Rectangle { anchors.bottom: parent.bottom; width: parent.width; height: 1; color: X.ribbonLine }
|
||||
|
||||
// ---- Home ----
|
||||
Row {
|
||||
visible: rib.tab === 1
|
||||
height: parent.height - 2
|
||||
RGroup {
|
||||
title: "Playback"
|
||||
RButton {
|
||||
big: true
|
||||
icon: player.status === "playing" ? "pause" : "play"
|
||||
text: player.status === "playing" ? "Pause" : "Play"
|
||||
tip: "Play or pause (Space)"
|
||||
onClicked: player.toggle()
|
||||
}
|
||||
Column {
|
||||
RButton { icon: "previous"; text: "Previous"; tip: "Previous song (Ctrl+Left)"; onClicked: player.previous() }
|
||||
RButton { icon: "next"; text: "Next"; tip: "Next song (Ctrl+Right)"; onClicked: player.next() }
|
||||
RButton { icon: "stop"; text: "Stop"; onClicked: { engine.pause(); player.seek(0) } }
|
||||
}
|
||||
Column {
|
||||
RButton { icon: "shuffle"; text: "Shuffle"; checked: player.shuffleOn; onClicked: player.toggleShuffle() }
|
||||
RButton {
|
||||
icon: player.repeatMode === "one" ? "repeat_one" : "repeat"
|
||||
text: player.repeatMode === "one" ? "Repeat One" : "Repeat"
|
||||
checked: player.repeatMode !== "off"
|
||||
onClicked: player.cycleRepeat()
|
||||
}
|
||||
}
|
||||
}
|
||||
RGroup {
|
||||
title: "Karaoke"
|
||||
RButton {
|
||||
big: true
|
||||
enabled: player.sourceKind === "stems"
|
||||
icon: player.presetLabel === "Karaoke" ? "vocals_off" : "vocals"
|
||||
text: player.presetLabel === "Karaoke" ? "Vocals\nOff" : "Vocals\nOn"
|
||||
checked: player.presetLabel === "Karaoke"
|
||||
tip: "Mute the vocals (V)"
|
||||
onClicked: player.toggleVocals()
|
||||
}
|
||||
}
|
||||
RGroup {
|
||||
title: "Mix Styles"
|
||||
Grid {
|
||||
columns: 4
|
||||
rowSpacing: 3
|
||||
columnSpacing: 3
|
||||
y: 4
|
||||
Repeater {
|
||||
model: player.presets
|
||||
delegate: Rectangle {
|
||||
id: sty
|
||||
required property int index
|
||||
required property var modelData
|
||||
readonly property var s: X.styles[index]
|
||||
width: 82
|
||||
height: 28
|
||||
opacity: modelData.usable ? 1 : 0.35
|
||||
color: s.fill
|
||||
border.width: modelData.active ? 2 : 1
|
||||
border.color: modelData.active ? X.green : (sm.containsMouse ? "#8A8A8A" : s.border)
|
||||
T {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 5
|
||||
font.family: cellFont
|
||||
font.pixelSize: 14
|
||||
font.bold: sty.s.bold
|
||||
color: sty.s.text
|
||||
text: sty.modelData.label
|
||||
}
|
||||
MouseArea {
|
||||
id: sm
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: sty.modelData.usable
|
||||
onClicked: player.applyPreset(sty.index)
|
||||
}
|
||||
Tip { target: sm; text: "Style: " + sty.s.name + " — " + sty.modelData.label }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RGroup {
|
||||
title: "Cells"
|
||||
RButton { big: true; icon: "play_next"; text: "Play\nNext"; tip: "Play the selection after this song"; onClicked: library.playNextRows(rib.win.selTop, rib.win.selBottom) }
|
||||
RButton { big: true; icon: "queue_add"; text: "Add to\nQueue"; tip: "Add the selection to the end of the queue"; onClicked: library.queueRows(rib.win.selTop, rib.win.selBottom) }
|
||||
RButton {
|
||||
big: true
|
||||
icon: "delete_rows"
|
||||
text: "Delete\nRows"
|
||||
enabled: library.layout === "queue" || library.sheetTitle !== "" && rib.win.inPlaylist
|
||||
tip: "Remove the selected rows from the queue or playlist (Delete)"
|
||||
onClicked: library.removeRows(rib.win.selTop, rib.win.selBottom)
|
||||
}
|
||||
}
|
||||
RGroup {
|
||||
title: "Editing"
|
||||
Column {
|
||||
RButton { icon: "sigma"; text: "AutoShuffle"; tip: "Shuffle everything on this sheet"; onClicked: library.shuffleSheet() }
|
||||
RButton { icon: "sort_az"; text: "Sort & Filter"; dropdown: true; onClicked: sortMenu.popup() }
|
||||
RButton { icon: "search"; text: "Find & Select"; tip: "Search the library (Ctrl+F)"; onClicked: rib.focusSearch() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Insert ----
|
||||
Row {
|
||||
visible: rib.tab === 2
|
||||
height: parent.height - 2
|
||||
RGroup {
|
||||
title: "Playlists"
|
||||
RButton { big: true; icon: "playlist_new"; text: "New\nPlaylist"; tip: "A Plex playlist from the selected rows"; onClicked: rib.win.newPlaylist() }
|
||||
RButton { big: true; icon: "playlist_add"; text: "Add to\nPlaylist"; dropdown: true; onClicked: plMenu.popup() }
|
||||
}
|
||||
RGroup {
|
||||
title: "Queue"
|
||||
RButton { big: true; icon: "play_next"; text: "Play\nNext"; onClicked: library.playNextRows(rib.win.selTop, rib.win.selBottom) }
|
||||
RButton { big: true; icon: "queue_add"; text: "Add to\nQueue"; onClicked: library.queueRows(rib.win.selTop, rib.win.selBottom) }
|
||||
RButton { big: true; icon: "play"; text: "Play\nSelection"; onClicked: library.playRows(rib.win.selTop, rib.win.selBottom) }
|
||||
}
|
||||
RGroup {
|
||||
title: "Charts"
|
||||
RButton { big: true; icon: "chart"; text: "Stem\nChart"; checked: rib.win.showMixer; tip: "Show the stem mixer pane"; onClicked: rib.win.toggleMixer() }
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Data ----
|
||||
Row {
|
||||
visible: rib.tab === 3
|
||||
height: parent.height - 2
|
||||
RGroup {
|
||||
title: "Get External Data"
|
||||
RButton { big: true; icon: "plex"; text: library.signedIn ? "Plex\nAccount" : "Connect\nto Plex"; onClicked: rib.win.openBackstage("account") }
|
||||
RButton { big: true; icon: "server"; text: "Connections"; tip: "Server addresses and folders"; onClicked: rib.win.openBackstage("options") }
|
||||
}
|
||||
RGroup {
|
||||
title: "Connections"
|
||||
RButton { big: true; icon: "refresh"; text: "Refresh\nAll"; tip: "Reload the Plex library and the stems index"; onClicked: library.refresh() }
|
||||
Column {
|
||||
y: 4
|
||||
spacing: 2
|
||||
T { text: "Songs: " + library.counts.songs.toLocaleString(Qt.locale(), "f", 0); height: 20 }
|
||||
T { text: "With stems: " + library.counts.stems.toLocaleString(Qt.locale(), "f", 0); height: 20 }
|
||||
T {
|
||||
text: "Stem server: " + (library.counts.stemOnline === true ? "Online" : library.counts.stemOnline === false ? "Offline" : "Checking…")
|
||||
color: library.counts.stemOnline === true ? X.goodText : library.counts.stemOnline === false ? X.badText : X.inkSoft
|
||||
height: 20
|
||||
}
|
||||
}
|
||||
}
|
||||
RGroup {
|
||||
title: "Sort & Filter"
|
||||
Column {
|
||||
RButton { icon: "sort_az"; text: "Sort A to Z"; onClicked: rib.win.sortCurrent(false) }
|
||||
RButton { icon: "sort_za"; text: "Sort Z to A"; onClicked: rib.win.sortCurrent(true) }
|
||||
RButton { icon: "filter"; text: "Stems Only"; tip: "Songs that have stems"; onClicked: library.current = 4 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- View ----
|
||||
Row {
|
||||
visible: rib.tab === 4
|
||||
height: parent.height - 2
|
||||
RGroup {
|
||||
title: "Show"
|
||||
Column {
|
||||
y: 2
|
||||
XCheck { text: "Gridlines"; checked: rib.win.gridlines; onToggled: rib.win.setOption("gridlines", !rib.win.gridlines) }
|
||||
XCheck { text: "Headings"; checked: rib.win.headings; onToggled: rib.win.setOption("headings", !rib.win.headings) }
|
||||
XCheck { text: "Mixer Pane"; checked: rib.win.showMixer; onToggled: rib.win.toggleMixer() }
|
||||
}
|
||||
}
|
||||
RGroup {
|
||||
title: "Zoom"
|
||||
RButton { big: true; icon: "zoom"; text: "Zoom"; tip: "Ctrl+scroll on the grid"; onClicked: rib.win.setZoom(rib.win.zoom >= 150 ? 100 : rib.win.zoom + 25) }
|
||||
RButton { big: true; icon: "grid"; text: "100%"; onClicked: rib.win.setZoom(100) }
|
||||
}
|
||||
RGroup {
|
||||
title: "Window"
|
||||
RButton { big: true; icon: "freeze"; text: "Freeze\nPanes"; checked: true; tip: "The header row is always frozen" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
XMenu {
|
||||
id: sortMenu
|
||||
Action { text: "Sort A to Z"; icon.name: "sort_az"; onTriggered: rib.win.sortCurrent(false) }
|
||||
Action { text: "Sort Z to A"; icon.name: "sort_za"; onTriggered: rib.win.sortCurrent(true) }
|
||||
MenuSeparator {}
|
||||
Action { text: "Filter: Stems Only"; icon.name: "filter"; onTriggered: library.current = 4 }
|
||||
}
|
||||
XMenu {
|
||||
id: plMenu
|
||||
Instantiator {
|
||||
model: library.playlistNames
|
||||
delegate: MenuItem {
|
||||
required property int index
|
||||
required property string modelData
|
||||
text: modelData
|
||||
onTriggered: rib.win.addToPlaylist(modelData)
|
||||
}
|
||||
onObjectAdded: (i, o) => plMenu.insertItem(i, o)
|
||||
onObjectRemoved: (i, o) => plMenu.removeItem(o)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
// The worksheet. Row 1 is the frozen table header; data starts on row 2. Past the data the
|
||||
// sheet carries on as empty gridlines, as Excel's does.
|
||||
FocusScope {
|
||||
id: g
|
||||
property var win: Window.window
|
||||
readonly property real z100: win.zoom / 100
|
||||
readonly property int rowH: Math.round(20 * z100)
|
||||
readonly property int headW: Math.max(30, Math.round(rowNumW.advanceWidth + 12))
|
||||
readonly property int colHeadH: 20
|
||||
readonly property var cols: library.columns
|
||||
readonly property int rows: table.rows
|
||||
readonly property int padCols: 26
|
||||
property real extraColW: 64 * z100
|
||||
|
||||
function colX(c) {
|
||||
var x = 0
|
||||
for (var i = 0; i < c && i < cols.length; i++) x += cols[i].width * z100
|
||||
if (c > cols.length) x += (c - cols.length) * extraColW
|
||||
return x
|
||||
}
|
||||
function colW(c) { return c < cols.length ? cols[c].width * z100 : extraColW }
|
||||
function colAt(x) {
|
||||
var acc = 0
|
||||
for (var i = 0; i < cols.length; i++) {
|
||||
acc += cols[i].width * z100
|
||||
if (x < acc) return i
|
||||
}
|
||||
return cols.length + Math.floor((x - acc) / extraColW)
|
||||
}
|
||||
function ensureVisible(r, c) {
|
||||
var y = r * rowH
|
||||
if (y < table.contentY) table.contentY = y
|
||||
else if (y + rowH > table.contentY + table.height) table.contentY = y + rowH - table.height
|
||||
var x = colX(c), w = colW(c)
|
||||
if (x < table.contentX) table.contentX = x
|
||||
else if (x + w > table.contentX + table.width) table.contentX = Math.min(x + w - table.width, Math.max(0, table.contentWidth - table.width))
|
||||
}
|
||||
function moveTo(r, c, extend) {
|
||||
r = Math.max(0, Math.min(r, Math.max(0, rows - 1)))
|
||||
c = Math.max(0, Math.min(c, cols.length - 1))
|
||||
win.curRow = r
|
||||
win.curCol = c
|
||||
if (!extend) { win.anchorRow = r; win.anchorCol = c }
|
||||
ensureVisible(r, c)
|
||||
}
|
||||
|
||||
TextMetrics { id: rowNumW; font.family: uiFont; font.pixelSize: X.fsSmall; text: String(g.rows + 40) }
|
||||
|
||||
// ---- column letters ----
|
||||
Rectangle {
|
||||
id: colHead
|
||||
visible: win.headings
|
||||
x: g.headW
|
||||
width: parent.width - x
|
||||
height: visible ? g.colHeadH : 0
|
||||
color: X.header
|
||||
clip: true
|
||||
Rectangle { anchors.bottom: parent.bottom; width: parent.width; height: 1; color: X.headerLine }
|
||||
Repeater {
|
||||
model: g.cols.length + g.padCols
|
||||
delegate: Rectangle {
|
||||
required property int index
|
||||
readonly property bool sel: index >= Math.min(win.curCol, win.anchorCol) && index <= Math.max(win.curCol, win.anchorCol)
|
||||
x: g.colX(index) - table.contentX
|
||||
width: g.colW(index)
|
||||
height: g.colHeadH
|
||||
visible: x + width > 0 && x < colHead.width
|
||||
color: sel ? X.headerSel : X.header
|
||||
T {
|
||||
anchors.centerIn: parent
|
||||
text: X.colName(index)
|
||||
font.pixelSize: X.fsSmall
|
||||
color: parent.sel ? X.greenText : X.ink
|
||||
}
|
||||
Rectangle { anchors.right: parent.right; width: 1; height: parent.height; color: X.headerLine }
|
||||
Rectangle { visible: parent.sel; anchors.bottom: parent.bottom; width: parent.width; height: 2; color: X.green }
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: if (index < g.cols.length) { win.anchorRow = 0; win.curRow = Math.max(0, g.rows - 1); win.curCol = win.anchorCol = index }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// the select-all corner
|
||||
Rectangle {
|
||||
visible: win.headings
|
||||
width: g.headW
|
||||
height: g.colHeadH
|
||||
color: X.header
|
||||
Rectangle { anchors.right: parent.right; width: 1; height: parent.height; color: X.headerLine }
|
||||
Rectangle { anchors.bottom: parent.bottom; width: parent.width; height: 1; color: X.headerLine }
|
||||
Canvas {
|
||||
anchors { right: parent.right; bottom: parent.bottom; margins: 3 }
|
||||
width: 10
|
||||
height: 10
|
||||
onPaint: {
|
||||
var c = getContext("2d")
|
||||
c.fillStyle = "#B4B4B4"
|
||||
c.beginPath(); c.moveTo(10, 0); c.lineTo(10, 10); c.lineTo(0, 10); c.closePath(); c.fill()
|
||||
}
|
||||
}
|
||||
MouseArea { anchors.fill: parent; onClicked: win.selectAll() }
|
||||
}
|
||||
|
||||
// ---- row numbers ----
|
||||
Rectangle {
|
||||
id: rowHead
|
||||
visible: win.headings
|
||||
y: colHead.height
|
||||
width: visible ? g.headW : 0
|
||||
height: parent.height - y
|
||||
color: X.header
|
||||
clip: true
|
||||
Rectangle { anchors.right: parent.right; width: 1; height: parent.height; color: X.headerLine }
|
||||
// row 1: the frozen table header
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: g.rowH
|
||||
color: X.header
|
||||
T { anchors.centerIn: parent; text: "1"; font.pixelSize: X.fsSmall }
|
||||
Rectangle { anchors.bottom: parent.bottom; width: parent.width; height: 1; color: X.headerLine }
|
||||
Rectangle { anchors.right: parent.right; width: 1; height: parent.height; color: X.headerLine }
|
||||
}
|
||||
Item {
|
||||
y: g.rowH
|
||||
width: parent.width
|
||||
height: parent.height - y
|
||||
clip: true
|
||||
Repeater {
|
||||
model: Math.max(0, Math.ceil(parent.height / g.rowH) + 1)
|
||||
delegate: Rectangle {
|
||||
required property int index
|
||||
readonly property int r: Math.floor(table.contentY / g.rowH) + index
|
||||
readonly property bool sel: r >= win.selTop && r <= win.selBottom
|
||||
y: r * g.rowH - table.contentY
|
||||
width: rowHead.width
|
||||
height: g.rowH
|
||||
color: sel ? X.headerSel : X.header
|
||||
T {
|
||||
anchors.centerIn: parent
|
||||
text: parent.r + 2
|
||||
font.pixelSize: X.fsSmall
|
||||
color: parent.sel ? X.greenText : X.ink
|
||||
}
|
||||
Rectangle { anchors.bottom: parent.bottom; width: parent.width; height: 1; color: X.headerLine }
|
||||
Rectangle { anchors.right: parent.right; width: parent.sel ? 2 : 1; height: parent.height; color: parent.sel ? X.green : X.headerLine }
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: (m) => { if (parent.r < g.rows) { g.moveTo(parent.r, 0, m.modifiers & Qt.ShiftModifier); if (!(m.modifiers & Qt.ShiftModifier)) win.anchorCol = 0; win.curCol = g.cols.length - 1 } }
|
||||
onDoubleClicked: if (parent.r < g.rows) library.activate(parent.r, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- sheet ----
|
||||
Item {
|
||||
id: sheet
|
||||
x: rowHead.width
|
||||
y: colHead.height
|
||||
width: parent.width - x
|
||||
height: parent.height - y
|
||||
clip: true
|
||||
|
||||
Rectangle { anchors.fill: parent; color: "#FFFFFF" }
|
||||
|
||||
// the empty sheet: gridlines everywhere, under the cells
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: win.gridlines
|
||||
Repeater {
|
||||
model: Math.max(0, Math.ceil(sheet.height / g.rowH) + 2)
|
||||
delegate: Rectangle {
|
||||
required property int index
|
||||
y: g.rowH + (index + 1) * g.rowH - (table.contentY % g.rowH) - 1
|
||||
width: sheet.width
|
||||
height: 1
|
||||
color: X.grid
|
||||
}
|
||||
}
|
||||
Repeater {
|
||||
model: g.cols.length + g.padCols
|
||||
delegate: Rectangle {
|
||||
required property int index
|
||||
x: g.colX(index + 1) - table.contentX - 1
|
||||
width: 1
|
||||
height: sheet.height
|
||||
color: X.grid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// row 1, frozen: the table header with its filter buttons
|
||||
Item {
|
||||
id: headRow
|
||||
width: parent.width
|
||||
height: g.rowH
|
||||
z: 2
|
||||
Repeater {
|
||||
model: g.cols
|
||||
delegate: Rectangle {
|
||||
id: hc
|
||||
required property int index
|
||||
required property var modelData
|
||||
x: g.colX(index) - table.contentX
|
||||
width: g.colW(index)
|
||||
height: g.rowH
|
||||
color: X.tableHead
|
||||
Rectangle { anchors.right: parent.right; width: 1; height: parent.height; color: "#FFFFFF"; opacity: 0.5 }
|
||||
T {
|
||||
x: 4
|
||||
width: parent.width - 26
|
||||
height: parent.height
|
||||
font.family: cellFont
|
||||
font.pixelSize: Math.round(X.fsCell * g.z100)
|
||||
font.bold: true
|
||||
color: "#FFFFFF"
|
||||
text: hc.modelData.title
|
||||
horizontalAlignment: hc.modelData.kind === "text" || hc.modelData.kind === "stems" ? Text.AlignLeft : Text.AlignRight
|
||||
rightPadding: hc.modelData.kind === "text" || hc.modelData.kind === "stems" ? 0 : 2
|
||||
}
|
||||
// the AutoFilter button: sorts this column
|
||||
Rectangle {
|
||||
anchors { right: parent.right; rightMargin: 3; verticalCenter: parent.verticalCenter }
|
||||
width: Math.round(15 * g.z100)
|
||||
height: width
|
||||
color: fm.containsMouse ? "#F0F0F0" : "#FFFFFF"
|
||||
border.color: "#A0A0A0"
|
||||
Ico {
|
||||
anchors.centerIn: parent
|
||||
size: Math.round(12 * g.z100)
|
||||
name: library.sortColumn === hc.index ? (library.sortDescending ? "sort_down" : "sort_up") : "dropdown"
|
||||
}
|
||||
MouseArea {
|
||||
id: fm
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: { g.win.curCol = g.win.anchorCol = hc.index; filterMenu.col = hc.index; filterMenu.popup() }
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
anchors { fill: parent; rightMargin: 20 }
|
||||
onClicked: library.sortBy(hc.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TableView {
|
||||
id: table
|
||||
y: g.rowH
|
||||
width: parent.width
|
||||
height: parent.height - y
|
||||
model: sheetModel
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
reuseItems: true
|
||||
columnWidthProvider: (c) => g.colW(c)
|
||||
rowHeightProvider: (r) => g.rowH
|
||||
interactive: false // the wheel and the scrollbars move it; dragging selects
|
||||
ScrollBar.vertical: XScrollBar { id: vbar }
|
||||
ScrollBar.horizontal: XScrollBar { id: hbar }
|
||||
Connections {
|
||||
target: g.win
|
||||
function onZoomChanged() { Qt.callLater(table.forceLayout) }
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
id: cell
|
||||
required property int row
|
||||
required property int column
|
||||
required property string display
|
||||
required property string align
|
||||
required property string cellStyle
|
||||
required property bool playing
|
||||
readonly property bool bar: cellStyle.indexOf("bar:") === 0
|
||||
color: cellStyle === "good" ? X.good : (row % 2 === 1 ? X.tableBand : "#FFFFFF")
|
||||
Rectangle { // data bar
|
||||
visible: cell.bar
|
||||
x: 2
|
||||
y: 2
|
||||
height: parent.height - 4
|
||||
width: cell.bar ? (parent.width - 4) * parseFloat(cell.cellStyle.slice(4)) : 0
|
||||
color: X.dataBar
|
||||
opacity: 0.85
|
||||
}
|
||||
Rectangle { visible: g.win.gridlines; anchors.right: parent.right; width: 1; height: parent.height; color: X.grid }
|
||||
Rectangle { visible: g.win.gridlines; anchors.bottom: parent.bottom; width: parent.width; height: 1; color: X.grid }
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
leftPadding: 3
|
||||
rightPadding: 4
|
||||
verticalAlignment: Text.AlignBottom
|
||||
bottomPadding: Math.round(2 * g.z100)
|
||||
horizontalAlignment: cell.align === "right" ? Text.AlignRight : Text.AlignLeft
|
||||
font.family: cellFont
|
||||
font.pixelSize: Math.round(X.fsCell * g.z100)
|
||||
font.bold: cell.playing
|
||||
color: cell.cellStyle === "good" ? X.goodText : X.cellText
|
||||
text: cell.cellStyle === "good" ? cell.display + " stems" : cell.display
|
||||
elide: cell.align === "right" ? Text.ElideLeft : Text.ElideRight
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
// Excel's green error triangle, here marking the song that's playing
|
||||
Canvas {
|
||||
visible: cell.playing && cell.column === 0
|
||||
width: 7
|
||||
height: 7
|
||||
onPaint: {
|
||||
var c = getContext("2d")
|
||||
c.fillStyle = "#217346"
|
||||
c.beginPath(); c.moveTo(0, 0); c.lineTo(7, 0); c.lineTo(0, 7); c.closePath(); c.fill()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// selection: range fill, green border, fill handle
|
||||
Rectangle {
|
||||
id: selBox
|
||||
parent: table.contentItem
|
||||
z: 50
|
||||
visible: g.rows > 0
|
||||
readonly property bool range: g.win.selTop !== g.win.selBottom || g.win.curCol !== g.win.anchorCol
|
||||
x: g.colX(Math.min(g.win.curCol, g.win.anchorCol)) - 1
|
||||
y: g.win.selTop * g.rowH - 1
|
||||
width: g.colX(Math.max(g.win.curCol, g.win.anchorCol) + 1) - x + 1
|
||||
height: (g.win.selBottom - g.win.selTop + 1) * g.rowH + 1
|
||||
color: range ? X.selFill : "transparent"
|
||||
border.color: X.green
|
||||
border.width: 2
|
||||
Rectangle {
|
||||
// the active cell stays clear inside a range
|
||||
visible: selBox.range
|
||||
x: g.colX(g.win.curCol) - parent.x
|
||||
y: g.win.curRow * g.rowH - parent.y
|
||||
width: g.colW(g.win.curCol)
|
||||
height: g.rowH
|
||||
color: "#FFFFFF"
|
||||
opacity: 0.6
|
||||
}
|
||||
Rectangle {
|
||||
x: parent.width - 4
|
||||
y: parent.height - 4
|
||||
width: 7
|
||||
height: 7
|
||||
color: X.green
|
||||
border.color: "#FFFFFF"
|
||||
border.width: 1
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
parent: table.contentItem
|
||||
z: 60
|
||||
width: Math.max(table.contentWidth, table.width)
|
||||
height: Math.max(table.contentHeight, table.height)
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
property bool dragging: false
|
||||
function cellAt(m) { return [Math.floor(m.y / g.rowH), g.colAt(m.x)] }
|
||||
onPressed: (m) => {
|
||||
g.forceActiveFocus()
|
||||
var rc = cellAt(m)
|
||||
if (rc[0] >= g.rows) return
|
||||
if (m.button === Qt.RightButton) {
|
||||
if (rc[0] < g.win.selTop || rc[0] > g.win.selBottom) g.moveTo(rc[0], rc[1], false)
|
||||
ctxMenu.popup()
|
||||
return
|
||||
}
|
||||
g.moveTo(rc[0], rc[1], m.modifiers & Qt.ShiftModifier)
|
||||
dragging = true
|
||||
}
|
||||
onPositionChanged: (m) => {
|
||||
if (!dragging) return
|
||||
var rc = cellAt(m)
|
||||
g.moveTo(rc[0], rc[1], true)
|
||||
}
|
||||
onReleased: dragging = false
|
||||
onDoubleClicked: (m) => { var rc = cellAt(m); if (rc[0] < g.rows) library.activate(rc[0], 0) }
|
||||
onWheel: (w) => {
|
||||
if (w.modifiers & Qt.ControlModifier) {
|
||||
g.win.setZoom(g.win.zoom + (w.angleDelta.y > 0 ? 10 : -10))
|
||||
return
|
||||
}
|
||||
var dy = w.pixelDelta.y !== 0 ? w.pixelDelta.y : w.angleDelta.y / 120 * 3 * g.rowH
|
||||
var dx = w.pixelDelta.x !== 0 ? w.pixelDelta.x : w.angleDelta.x / 120 * 3 * g.rowH
|
||||
if (w.modifiers & Qt.ShiftModifier) { dx = dy; dy = 0 }
|
||||
table.contentY = Math.max(0, Math.min(table.contentY - dy, table.contentHeight - table.height))
|
||||
table.contentX = Math.max(0, Math.min(table.contentX - dx, Math.max(0, table.contentWidth - table.width)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #GETTING_DATA: what Excel shows while a query runs
|
||||
T {
|
||||
visible: library.loading
|
||||
x: 4
|
||||
y: g.rowH
|
||||
height: g.rowH
|
||||
font.family: cellFont
|
||||
font.pixelSize: Math.round(X.fsCell * g.z100)
|
||||
color: X.cellText
|
||||
text: "#GETTING_DATA"
|
||||
}
|
||||
// a cell comment when there's nothing to show
|
||||
Rectangle {
|
||||
visible: g.rows === 0 && !library.loading
|
||||
x: g.colX(1) + 30
|
||||
y: g.rowH * 2
|
||||
width: 300
|
||||
height: note.implicitHeight + 16
|
||||
color: X.note
|
||||
border.color: "#B2B2B2"
|
||||
Rectangle { x: -30; y: 6; width: 30; height: 1; color: "#000000" }
|
||||
Text {
|
||||
id: note
|
||||
x: 8
|
||||
y: 8
|
||||
width: parent.width - 16
|
||||
wrapMode: Text.WordWrap
|
||||
font.family: uiFont
|
||||
font.pixelSize: X.fsUi
|
||||
textFormat: Text.StyledText
|
||||
text: !library.signedIn ? "<b>Stemify:</b><br>This workbook isn't connected yet. Go to <b>Data → Connect to Plex</b> and sign in with the code."
|
||||
: library.layout === "queue" ? "<b>Stemify:</b><br>The queue is empty. Double-click a song on any sheet to play it."
|
||||
: library.status !== "" ? "<b>Stemify:</b><br>Loading the library from Plex…"
|
||||
: "<b>Stemify:</b><br>Nothing on this sheet."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: (e) => {
|
||||
var ext = e.modifiers & Qt.ShiftModifier
|
||||
var ctrl = e.modifiers & Qt.ControlModifier
|
||||
var page = Math.max(1, Math.floor(table.height / g.rowH) - 1)
|
||||
switch (e.key) {
|
||||
case Qt.Key_Down: g.moveTo(ctrl ? g.rows - 1 : win.curRow + 1, win.curCol, ext); break
|
||||
case Qt.Key_Up: g.moveTo(ctrl ? 0 : win.curRow - 1, win.curCol, ext); break
|
||||
case Qt.Key_Right: if (ctrl) { player.next(); break } g.moveTo(win.curRow, win.curCol + 1, ext); break
|
||||
case Qt.Key_Left: if (ctrl) { player.previous(); break } g.moveTo(win.curRow, win.curCol - 1, ext); break
|
||||
case Qt.Key_Tab: g.moveTo(win.curRow, win.curCol + 1, false); break
|
||||
case Qt.Key_PageDown: g.moveTo(win.curRow + page, win.curCol, ext); break
|
||||
case Qt.Key_PageUp: g.moveTo(win.curRow - page, win.curCol, ext); break
|
||||
case Qt.Key_Home: g.moveTo(ctrl ? 0 : win.curRow, 0, ext); break
|
||||
case Qt.Key_End: g.moveTo(ctrl ? g.rows - 1 : win.curRow, g.cols.length - 1, ext); break
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter: library.activate(win.curRow, 0); break
|
||||
case Qt.Key_Space: player.toggle(); break
|
||||
case Qt.Key_Delete: library.removeRows(win.selTop, win.selBottom); break
|
||||
case Qt.Key_Backspace: library.back(); break
|
||||
case Qt.Key_A: if (ctrl) win.selectAll(); else return; break
|
||||
case Qt.Key_C: if (ctrl) win.copySelection(); else return; break
|
||||
case Qt.Key_V: if (!ctrl) player.toggleVocals(); else return; break
|
||||
default: return
|
||||
}
|
||||
e.accepted = true
|
||||
}
|
||||
|
||||
// A new sheet starts at the top; fresh data for the same sheet keeps its scroll position.
|
||||
property real keptY: 0
|
||||
property bool switching: false
|
||||
Connections {
|
||||
target: library
|
||||
function onSheetSwitched() { g.switching = true; table.contentY = 0; table.contentX = 0 }
|
||||
}
|
||||
Connections {
|
||||
target: sheetModel
|
||||
function onModelAboutToBeReset() { g.keptY = table.contentY }
|
||||
function onModelReset() {
|
||||
if (g.switching) { g.switching = false; return }
|
||||
Qt.callLater(() => { table.contentY = Math.min(g.keptY, Math.max(0, table.contentHeight - table.height)) })
|
||||
}
|
||||
}
|
||||
|
||||
XMenu {
|
||||
id: ctxMenu
|
||||
Action { text: "Play"; icon.name: "play"; onTriggered: library.playRows(g.win.selTop, g.win.selBottom) }
|
||||
Action { text: "Play Next"; icon.name: "play_next"; onTriggered: library.playNextRows(g.win.selTop, g.win.selBottom) }
|
||||
Action { text: "Add to Queue"; icon.name: "queue_add"; onTriggered: library.queueRows(g.win.selTop, g.win.selBottom) }
|
||||
MenuSeparator {}
|
||||
Action { text: "Open"; icon.name: "right"; onTriggered: library.activate(g.win.curRow, 0) }
|
||||
Action { text: "New Playlist from Selection…"; icon.name: "playlist_new"; onTriggered: g.win.newPlaylist() }
|
||||
Action { text: "Copy"; onTriggered: g.win.copySelection() }
|
||||
MenuSeparator {}
|
||||
Action {
|
||||
text: "Delete Rows"
|
||||
icon.name: "delete_rows"
|
||||
enabled: library.layout === "queue" || g.win.inPlaylist
|
||||
onTriggered: library.removeRows(g.win.selTop, g.win.selBottom)
|
||||
}
|
||||
}
|
||||
XMenu {
|
||||
id: filterMenu
|
||||
property int col: 0
|
||||
Action { text: "Sort A to Z"; icon.name: "sort_az"; onTriggered: g.win.sortColumn(filterMenu.col, false) }
|
||||
Action { text: "Sort Z to A"; icon.name: "sort_za"; onTriggered: g.win.sortColumn(filterMenu.col, true) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import QtQuick
|
||||
|
||||
// Worksheet tabs. The first six are fixed; opened artists, albums, playlists and searches follow.
|
||||
Rectangle {
|
||||
id: st
|
||||
property var win: Window.window
|
||||
height: 26
|
||||
color: X.ribbon
|
||||
Rectangle { width: parent.width; height: 1; color: X.ribbonLine }
|
||||
|
||||
Row {
|
||||
id: nav
|
||||
x: 6
|
||||
height: parent.height
|
||||
spacing: 2
|
||||
Repeater {
|
||||
model: ["left", "right"]
|
||||
delegate: Item {
|
||||
required property int index
|
||||
required property string modelData
|
||||
width: 18
|
||||
height: parent.height
|
||||
Ico { anchors.centerIn: parent; name: parent.modelData; size: 12; opacity: na.containsMouse ? 1 : 0.6 }
|
||||
MouseArea {
|
||||
id: na
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: library.current = Math.max(0, Math.min(library.tabs.length - 1, library.current + (parent.index ? 1 : -1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: flick
|
||||
x: nav.x + nav.width + 8
|
||||
width: parent.width - x - 8
|
||||
height: parent.height
|
||||
contentWidth: tabRow.width + 40
|
||||
clip: true
|
||||
flickableDirection: Flickable.HorizontalFlick
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
Row {
|
||||
id: tabRow
|
||||
height: parent.height
|
||||
Repeater {
|
||||
model: library.tabs
|
||||
delegate: Item {
|
||||
id: tb
|
||||
required property int index
|
||||
required property var modelData
|
||||
readonly property bool active: library.current === index
|
||||
width: lbl.implicitWidth + 28
|
||||
height: st.height
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 0
|
||||
color: tb.active ? "#FFFFFF" : tm.containsMouse ? "#E5E5E5" : "transparent"
|
||||
}
|
||||
Rectangle { visible: !tb.active; anchors.right: parent.right; y: 6; width: 1; height: parent.height - 12; color: "#C8C8C8" }
|
||||
Rectangle { visible: tb.active; anchors.bottom: parent.bottom; width: parent.width; height: 3; color: X.green }
|
||||
T {
|
||||
id: lbl
|
||||
anchors.centerIn: parent
|
||||
text: tb.modelData.title
|
||||
color: tb.active ? X.greenText : X.ink
|
||||
font.bold: tb.active
|
||||
}
|
||||
MouseArea {
|
||||
id: tm
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
|
||||
onClicked: (m) => m.button === Qt.MiddleButton ? library.closeTab(tb.index) : (library.current = tb.index)
|
||||
}
|
||||
Tip { target: tm; text: tb.modelData.base ? "" : "Middle-click or Backspace to close" }
|
||||
}
|
||||
}
|
||||
// ⊕ new sheet: here, the search box
|
||||
Item {
|
||||
width: 30
|
||||
height: st.height
|
||||
Ico { anchors.centerIn: parent; name: "plus"; size: 16; opacity: pm.containsMouse ? 1 : 0.6 }
|
||||
MouseArea { id: pm; anchors.fill: parent; hoverEnabled: true; onClicked: st.win.focusSearch() }
|
||||
Tip { target: pm; text: "New sheet from a search" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import QtQuick
|
||||
|
||||
// Ready / Calculating on the left; Average, Count, Sum; view buttons; the zoom slider is the volume.
|
||||
Rectangle {
|
||||
id: sb
|
||||
property var win: Window.window
|
||||
height: 24
|
||||
color: X.green
|
||||
|
||||
T {
|
||||
x: 10
|
||||
height: parent.height
|
||||
color: "#FFFFFF"
|
||||
text: library.status !== "" ? library.status + (library.progress >= 0 ? ": " + Math.round(library.progress * 100) + "%" : "")
|
||||
: player.status === "loading" ? "Calculating..."
|
||||
: player.status === "buffering" ? "Buffering..."
|
||||
: player.status === "playing" ? "Playing" + (player.presetLabel && player.presetLabel !== "Full Mix" ? " · " + player.presetLabel : "")
|
||||
: "Ready"
|
||||
}
|
||||
Rectangle { // progress for Calculating, like Excel's
|
||||
visible: library.progress >= 0
|
||||
x: 190
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 120
|
||||
height: 8
|
||||
color: "#FFFFFF"
|
||||
opacity: 0.9
|
||||
Rectangle { width: parent.width * Math.max(0, library.progress); height: parent.height; color: "#9FD2B4" }
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 10
|
||||
height: parent.height
|
||||
spacing: 18
|
||||
|
||||
Row {
|
||||
height: parent.height
|
||||
spacing: 18
|
||||
visible: !!sb.win.selStats.count
|
||||
T { color: "#FFFFFF"; height: parent.height; visible: !!sb.win.selStats.average; text: "Average: " + (sb.win.selStats.average || "") }
|
||||
T { color: "#FFFFFF"; height: parent.height; text: "Count: " + (sb.win.selStats.count || "") }
|
||||
T { color: "#FFFFFF"; height: parent.height; visible: !!sb.win.selStats.sum; text: "Sum: " + (sb.win.selStats.sum || "") }
|
||||
}
|
||||
Row {
|
||||
height: parent.height
|
||||
spacing: 2
|
||||
Repeater {
|
||||
model: ["normal_view", "page_view", "break_view"]
|
||||
delegate: Item {
|
||||
required property int index
|
||||
required property string modelData
|
||||
width: 24
|
||||
height: sb.height
|
||||
Rectangle { anchors.fill: parent; anchors.margins: 2; color: index === 0 ? X.greenDark : "transparent" }
|
||||
Ico { anchors.centerIn: parent; name: parent.modelData; ink: "#FFFFFF"; size: 16 }
|
||||
}
|
||||
}
|
||||
}
|
||||
// the zoom slider, wired to the master volume
|
||||
Row {
|
||||
height: parent.height
|
||||
spacing: 6
|
||||
T { color: "#FFFFFF"; height: parent.height; text: "−"; font.pixelSize: 16
|
||||
MouseArea { anchors.fill: parent; anchors.margins: -4; onClicked: engine.volume = Math.max(0, engine.volume - 0.05) } }
|
||||
Item {
|
||||
id: track
|
||||
width: 110
|
||||
height: parent.height
|
||||
Rectangle { anchors.verticalCenter: parent.verticalCenter; width: parent.width; height: 2; color: "#FFFFFF"; opacity: 0.8 }
|
||||
Rectangle { x: parent.width / 2 - 1; anchors.verticalCenter: parent.verticalCenter; width: 2; height: 8; color: "#FFFFFF"; opacity: 0.8 }
|
||||
Rectangle {
|
||||
x: engine.volume * (track.width - width)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 6
|
||||
height: 13
|
||||
color: vm.pressed ? "#E0E0E0" : "#FFFFFF"
|
||||
}
|
||||
MouseArea {
|
||||
id: vm
|
||||
anchors.fill: parent
|
||||
anchors.margins: -3
|
||||
hoverEnabled: true
|
||||
function set(m) { engine.volume = Math.max(0, Math.min(1, (m.x - 3) / (track.width))) }
|
||||
onPressed: (m) => set(m)
|
||||
onPositionChanged: (m) => { if (pressed) set(m) }
|
||||
onWheel: (w) => engine.volume = Math.max(0, Math.min(1, engine.volume + (w.angleDelta.y > 0 ? 0.03 : -0.03)))
|
||||
}
|
||||
Tip { target: vm; text: "Volume" }
|
||||
}
|
||||
T { color: "#FFFFFF"; height: parent.height; text: "+"; font.pixelSize: 15
|
||||
MouseArea { anchors.fill: parent; anchors.margins: -4; onClicked: engine.volume = Math.min(1, engine.volume + 0.05) } }
|
||||
T { color: "#FFFFFF"; height: parent.height; width: 40; horizontalAlignment: Text.AlignRight; text: Math.round(engine.volume * 100) + "%" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import QtQuick
|
||||
|
||||
// The stem faders as an Excel column chart. Drag a column to set its level; double-click
|
||||
// for 0 dB. Below each category: M(ute) and S(olo).
|
||||
Rectangle {
|
||||
id: ch
|
||||
color: "#FFFFFF"
|
||||
border.color: "#D9D9D9"
|
||||
readonly property var stems: player.stems
|
||||
readonly property int n: stems.length
|
||||
readonly property real unity: 0.8
|
||||
|
||||
T {
|
||||
id: title
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: 6
|
||||
text: "Stem Mix"
|
||||
font.family: cellFont
|
||||
font.pixelSize: 17
|
||||
color: X.chartText
|
||||
}
|
||||
|
||||
Item {
|
||||
id: plot
|
||||
x: 34
|
||||
y: title.y + title.height + 18
|
||||
width: parent.width - x - 8
|
||||
height: parent.height - y - 52
|
||||
|
||||
// value axis: fader position, 0 dB marked
|
||||
Repeater {
|
||||
model: [0, 0.25, 0.5, 0.8, 1.0]
|
||||
delegate: Item {
|
||||
required property var modelData
|
||||
y: plot.height * (1 - modelData)
|
||||
width: plot.width
|
||||
Rectangle { width: parent.width; height: 1; color: modelData === 0.8 ? "#A6A6A6" : X.chartLine }
|
||||
T {
|
||||
x: -32
|
||||
width: 28
|
||||
y: -7
|
||||
height: 14
|
||||
horizontalAlignment: Text.AlignRight
|
||||
font.family: cellFont
|
||||
font.pixelSize: 11
|
||||
color: X.chartText
|
||||
text: modelData === 0 ? "-∞" : modelData === 0.8 ? "0 dB" : modelData === 1 ? "+4" : (40 * Math.log(modelData / 0.8) / Math.LN10).toFixed(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ch.stems
|
||||
delegate: Item {
|
||||
id: cat
|
||||
required property int index
|
||||
required property var modelData
|
||||
readonly property real cw: plot.width / Math.max(1, ch.n)
|
||||
x: index * cw
|
||||
width: cw
|
||||
height: plot.height
|
||||
|
||||
Rectangle {
|
||||
id: bar
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: cat.cw * 0.62
|
||||
height: Math.max(1, plot.height * cat.modelData.level)
|
||||
y: plot.height - height
|
||||
color: cat.modelData.color
|
||||
opacity: cat.modelData.audible ? 1 : 0.22
|
||||
border.color: dm.containsMouse || dm.pressed ? "#404040" : "transparent"
|
||||
Behavior on height { enabled: !dm.pressed; NumberAnimation { duration: X.easeMs; easing.type: Easing.OutCubic } }
|
||||
}
|
||||
T { // data label
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: bar.y - 16
|
||||
font.family: cellFont
|
||||
font.pixelSize: 11
|
||||
color: X.chartText
|
||||
text: cat.modelData.muted ? "Muted" : !cat.modelData.audible ? "—" : cat.modelData.level <= 0.001 ? "-∞" : (cat.modelData.db >= 0 ? "+" : "") + cat.modelData.db.toFixed(1)
|
||||
}
|
||||
MouseArea {
|
||||
id: dm
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.SizeVerCursor
|
||||
function set(m) { player.setLevel(cat.index, Math.max(0, Math.min(1, 1 - m.y / plot.height))) }
|
||||
onPressed: (m) => set(m)
|
||||
onPositionChanged: (m) => { if (pressed) set(m) }
|
||||
onDoubleClicked: player.setLevel(cat.index, ch.unity)
|
||||
onWheel: (w) => player.setLevel(cat.index, Math.max(0, Math.min(1, cat.modelData.level + (w.angleDelta.y > 0 ? 0.02 : -0.02))))
|
||||
}
|
||||
Tip { target: dm; text: cat.modelData.label + ": drag to set, double-click for 0 dB" }
|
||||
|
||||
// category axis label and the M / S toggles
|
||||
Column {
|
||||
y: plot.height + 4
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 3
|
||||
T {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
font.family: cellFont
|
||||
font.pixelSize: 12
|
||||
color: X.chartText
|
||||
text: cat.modelData.label
|
||||
width: Math.min(implicitWidth, cat.cw)
|
||||
}
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 2
|
||||
Toggle { label: "M"; on: cat.modelData.muted; onFill: X.bad; onText: X.badText; tip: "Mute " + cat.modelData.label; onClicked: player.toggleMute(cat.index) }
|
||||
Toggle { label: "S"; on: cat.modelData.solo; onFill: X.neutral; onText: X.neutralText; tip: "Solo " + cat.modelData.label; onClicked: player.toggleSolo(cat.index) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle { y: plot.height; width: plot.width; height: 1; color: "#BFBFBF" }
|
||||
}
|
||||
|
||||
component Toggle: Rectangle {
|
||||
id: tg
|
||||
property string label
|
||||
property bool on: false
|
||||
property color onFill
|
||||
property color onText
|
||||
property string tip
|
||||
signal clicked()
|
||||
width: 18
|
||||
height: 18
|
||||
color: on ? onFill : tgm.containsMouse ? "#F2F2F2" : "#FFFFFF"
|
||||
border.color: on ? onText : "#BFBFBF"
|
||||
T { anchors.centerIn: parent; text: tg.label; font.pixelSize: 11; font.bold: tg.on; color: tg.on ? tg.onText : X.inkSoft }
|
||||
MouseArea { id: tgm; anchors.fill: parent; hoverEnabled: true; onClicked: tg.clicked() }
|
||||
Tip { target: tgm; text: tg.tip }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import QtQuick
|
||||
|
||||
Text {
|
||||
font.family: uiFont
|
||||
font.pixelSize: X.fsUi
|
||||
color: X.ink
|
||||
renderType: Text.NativeRendering
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
// The "Format Mix" task pane: what's playing, a sparkline to seek with, and the stems as a chart.
|
||||
Rectangle {
|
||||
id: pane
|
||||
property var win: Window.window
|
||||
color: "#FFFFFF"
|
||||
Rectangle { width: 1; height: parent.height; color: X.ribbonLine }
|
||||
|
||||
Item {
|
||||
id: head
|
||||
x: 1
|
||||
width: parent.width - 1
|
||||
height: 40
|
||||
T { x: 14; height: parent.height; text: "Format Mix"; font.pixelSize: 17; color: X.ink }
|
||||
Ico { anchors.right: parent.right; anchors.rightMargin: 34; anchors.verticalCenter: parent.verticalCenter; name: "dropdown"; size: 12 }
|
||||
Item {
|
||||
anchors { right: parent.right; rightMargin: 6; verticalCenter: parent.verticalCenter }
|
||||
width: 24
|
||||
height: 24
|
||||
Rectangle { anchors.fill: parent; color: cm.containsMouse ? X.hover : "transparent" }
|
||||
Ico { anchors.centerIn: parent; name: "close"; size: 20 }
|
||||
MouseArea { id: cm; anchors.fill: parent; hoverEnabled: true; onClicked: pane.win.toggleMixer() }
|
||||
}
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: flick
|
||||
x: 1
|
||||
y: head.height
|
||||
width: parent.width - 1
|
||||
height: parent.height - y
|
||||
contentHeight: col.height + 20
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
ScrollBar.vertical: XScrollBar { policy: flick.contentHeight > flick.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff }
|
||||
|
||||
Column {
|
||||
id: col
|
||||
x: 14
|
||||
width: flick.width - 34
|
||||
spacing: 10
|
||||
|
||||
// ---- now playing ----
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
Rectangle {
|
||||
width: 128
|
||||
height: 128
|
||||
color: "#F2F2F2"
|
||||
border.color: "#D9D9D9"
|
||||
Image {
|
||||
id: art
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
source: player.track.art || ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
cache: true
|
||||
sourceSize.width: 256
|
||||
sourceSize.height: 256
|
||||
}
|
||||
Ico { visible: art.status !== Image.Ready; anchors.centerIn: parent; name: "excel"; size: 48; opacity: 0.25 }
|
||||
}
|
||||
Column {
|
||||
width: parent.width - 140
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 3
|
||||
Text {
|
||||
width: parent.width
|
||||
text: player.track.title || "Nothing playing"
|
||||
font.family: uiFont
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
color: "#222222"
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 3
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
T { width: parent.width; text: player.track.artist || "Double-click a song to play it"; font.pixelSize: 13; color: X.inkSoft }
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: !!player.track.album
|
||||
text: (player.track.album || "") + (player.track.year ? " · " + player.track.year : "")
|
||||
font.family: uiFont
|
||||
font.pixelSize: 12
|
||||
color: X.inkFaint
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- the sparkline seek bar ----
|
||||
Item {
|
||||
id: spark
|
||||
width: parent.width
|
||||
height: 46
|
||||
readonly property real dur: engine.duration > 0 ? engine.duration : (player.track.durationMs || 0) / 1000
|
||||
readonly property real frac: dur > 0 ? Math.min(1, pane.win.playPos / dur) : 0
|
||||
readonly property var bars: player.wave.length ? player.wave : []
|
||||
readonly property int n: bars.length ? bars.length : 80
|
||||
property real hoverFrac: -1
|
||||
Rectangle { anchors.fill: parent; color: "#FFFFFF"; border.color: "#D9D9D9" }
|
||||
Canvas {
|
||||
id: sparkCanvas
|
||||
anchors.fill: parent
|
||||
anchors.margins: 3
|
||||
property real frac: spark.frac
|
||||
property var bars: spark.bars
|
||||
onFracChanged: requestPaint()
|
||||
onBarsChanged: requestPaint()
|
||||
onWidthChanged: requestPaint()
|
||||
onPaint: {
|
||||
var c = getContext("2d")
|
||||
c.reset()
|
||||
var n = spark.n, w = width / n, h = height
|
||||
for (var i = 0; i < n; i++) {
|
||||
var v = bars.length ? bars[i] : 0.12
|
||||
var bh = Math.max(2, v * h)
|
||||
c.fillStyle = (i + 0.5) / n <= frac ? "#217346" : "#A9D08E"
|
||||
c.fillRect(i * w + w * 0.15, h - bh, Math.max(1, w * 0.7), bh)
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle { // playhead
|
||||
visible: spark.dur > 0
|
||||
x: 3 + spark.frac * (parent.width - 6) - 1
|
||||
y: 2
|
||||
width: 2
|
||||
height: parent.height - 4
|
||||
color: "#C00000"
|
||||
}
|
||||
Rectangle {
|
||||
visible: spark.hoverFrac >= 0
|
||||
x: Math.max(0, Math.min(parent.width - width, spark.hoverFrac * parent.width - width / 2))
|
||||
y: -22
|
||||
width: hv.implicitWidth + 10
|
||||
height: 18
|
||||
color: "#FFFFFF"
|
||||
border.color: "#BEBEBE"
|
||||
T { id: hv; anchors.centerIn: parent; font.pixelSize: X.fsSmall; text: X.time(spark.hoverFrac * spark.dur) }
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
function f(m) { return Math.max(0, Math.min(1, (m.x - 3) / (width - 6))) }
|
||||
onPositionChanged: (m) => { spark.hoverFrac = f(m); if (pressed) pane.win.seekTo(f(m) * spark.dur) }
|
||||
onExited: spark.hoverFrac = -1
|
||||
onPressed: (m) => pane.win.seekTo(f(m) * spark.dur)
|
||||
}
|
||||
}
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 14
|
||||
y: -6
|
||||
T { font.pixelSize: X.fsSmall; color: X.inkSoft; text: X.time(pane.win.playPos) }
|
||||
T {
|
||||
anchors.right: parent.right
|
||||
font.pixelSize: X.fsSmall
|
||||
color: X.inkSoft
|
||||
text: spark.dur > 0 ? "-" + X.time(spark.dur - pane.win.playPos) : ""
|
||||
}
|
||||
}
|
||||
|
||||
// ---- transport ----
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 6
|
||||
PaneButton { icon: "shuffle"; checked: player.shuffleOn; tip: "Shuffle"; onClicked: player.toggleShuffle() }
|
||||
PaneButton { icon: "previous"; tip: "Previous"; onClicked: player.previous() }
|
||||
PaneButton {
|
||||
icon: player.status === "playing" ? "pause" : "play"
|
||||
size: 40
|
||||
tip: "Play / Pause (Space)"
|
||||
onClicked: player.toggle()
|
||||
}
|
||||
PaneButton { icon: "next"; tip: "Next"; onClicked: player.next() }
|
||||
PaneButton {
|
||||
icon: player.repeatMode === "one" ? "repeat_one" : "repeat"
|
||||
checked: player.repeatMode !== "off"
|
||||
tip: player.repeatMode === "off" ? "Repeat" : player.repeatMode === "all" ? "Repeat all" : "Repeat one"
|
||||
onClicked: player.cycleRepeat()
|
||||
}
|
||||
}
|
||||
|
||||
// output meter, as a pair of data bars
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 2
|
||||
Repeater {
|
||||
model: 2
|
||||
delegate: Rectangle {
|
||||
required property int index
|
||||
width: parent.width
|
||||
height: 5
|
||||
color: "#EDEDED"
|
||||
Rectangle {
|
||||
readonly property real lv: index === 0 ? pane.win.levelL : pane.win.levelR
|
||||
readonly property real db: 20 * Math.log(Math.max(lv, 1e-5)) / Math.LN10
|
||||
width: parent.width * Math.max(0, Math.min(1, (db + 48) / 48))
|
||||
height: parent.height
|
||||
color: db > -1 ? "#C00000" : db > -9 ? "#FFC000" : X.dataBar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- section: Stem Mix ----
|
||||
Row {
|
||||
spacing: 6
|
||||
height: 24
|
||||
Ico { anchors.verticalCenter: parent.verticalCenter; name: "dropdown"; size: 10 }
|
||||
T { anchors.verticalCenter: parent.verticalCenter; text: "Stem Mix"; font.bold: true; font.pixelSize: 13 }
|
||||
}
|
||||
|
||||
Rectangle { // "comment" for songs without stems
|
||||
visible: player.sourceKind === "original"
|
||||
width: parent.width
|
||||
height: noteTxt.implicitHeight + 16
|
||||
color: X.note
|
||||
border.color: "#B2B2B2"
|
||||
Text {
|
||||
id: noteTxt
|
||||
x: 8
|
||||
y: 8
|
||||
width: parent.width - 16
|
||||
wrapMode: Text.WordWrap
|
||||
font.family: uiFont
|
||||
font.pixelSize: X.fsUi
|
||||
textFormat: Text.StyledText
|
||||
text: "<b>Stemify:</b><br>This song hasn't been separated yet, so it plays the original file. The server's stemd will get to it."
|
||||
}
|
||||
}
|
||||
|
||||
StemChart {
|
||||
visible: player.sourceKind === "stems"
|
||||
width: parent.width
|
||||
height: 250
|
||||
}
|
||||
|
||||
Row {
|
||||
visible: player.sourceKind === "stems"
|
||||
spacing: 8
|
||||
T { height: 22; text: "Mix style:" }
|
||||
Rectangle {
|
||||
width: 150
|
||||
height: 22
|
||||
border.color: pm.containsMouse ? X.green : "#ABABAB"
|
||||
T { x: 6; height: parent.height; width: parent.width - 24; text: player.presetLabel }
|
||||
Ico { anchors.right: parent.right; anchors.rightMargin: 5; anchors.verticalCenter: parent.verticalCenter; name: "dropdown"; size: 10 }
|
||||
MouseArea { id: pm; anchors.fill: parent; hoverEnabled: true; onClicked: presetMenu.popup() }
|
||||
}
|
||||
}
|
||||
XCheck {
|
||||
text: "Keep the mix for the next song"
|
||||
checked: player.keepMix
|
||||
onToggled: player.setKeepMix(!player.keepMix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
XMenu {
|
||||
id: presetMenu
|
||||
Instantiator {
|
||||
model: player.presets
|
||||
delegate: MenuItem {
|
||||
required property int index
|
||||
required property var modelData
|
||||
text: modelData.label
|
||||
enabled: modelData.usable
|
||||
checkable: true
|
||||
checked: modelData.active
|
||||
onTriggered: player.applyPreset(index)
|
||||
}
|
||||
onObjectAdded: (i, o) => presetMenu.insertItem(i, o)
|
||||
onObjectRemoved: (i, o) => presetMenu.removeItem(o)
|
||||
}
|
||||
}
|
||||
|
||||
component PaneButton: Item {
|
||||
id: pb
|
||||
property string icon
|
||||
property bool checked: false
|
||||
property int size: 32
|
||||
property string tip: ""
|
||||
signal clicked()
|
||||
width: size
|
||||
height: size
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: pb.size / 2
|
||||
color: pb.checked ? X.checked : pbm.pressed ? X.pressed : pbm.containsMouse ? X.hover : "transparent"
|
||||
}
|
||||
Ico { anchors.centerIn: parent; name: pb.icon; size: Math.round(pb.size * 0.55) }
|
||||
MouseArea { id: pbm; anchors.fill: parent; hoverEnabled: true; onClicked: pb.clicked() }
|
||||
Tip { target: pbm; text: pb.tip }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
// Office-style tooltip: white box, gray border, after a short hover.
|
||||
ToolTip {
|
||||
id: tt
|
||||
property var target
|
||||
visible: target && target.containsMouse && text !== "" && hoverTimer.done
|
||||
delay: 0
|
||||
timeout: 6000
|
||||
padding: 6
|
||||
Timer {
|
||||
id: hoverTimer
|
||||
property bool done: false
|
||||
interval: 600
|
||||
running: tt.target && tt.target.containsMouse
|
||||
onRunningChanged: if (!running) done = false
|
||||
onTriggered: done = true
|
||||
}
|
||||
contentItem: T { text: tt.text; font.pixelSize: X.fsSmall; elide: Text.ElideNone }
|
||||
background: Rectangle { color: "#FFFFFF"; border.color: "#BEBEBE" }
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import QtQuick
|
||||
|
||||
// Excel's green title bar: Quick Access Toolbar, workbook name, account, caption buttons.
|
||||
Rectangle {
|
||||
id: bar
|
||||
property var win: Window.window
|
||||
height: 32
|
||||
color: X.green
|
||||
|
||||
DragHandler {
|
||||
target: null
|
||||
grabPermissions: PointerHandler.CanTakeOverFromAnything
|
||||
onActiveChanged: if (active) bar.win.startSystemMove()
|
||||
}
|
||||
TapHandler { onDoubleTapped: bar.win.toggleMaximized() }
|
||||
|
||||
Row {
|
||||
x: 6
|
||||
height: parent.height
|
||||
spacing: 0
|
||||
Ico { name: "excel"; size: 20; anchors.verticalCenter: parent.verticalCenter }
|
||||
Item { width: 10; height: 1 }
|
||||
QatButton { icon: player.status === "playing" ? "pause" : "play"; tip: player.status === "playing" ? "Pause (Space)" : "Play (Space)"; onClicked: player.toggle() }
|
||||
QatButton { icon: "undo"; tip: "Undo — previous song (Ctrl+Left)"; onClicked: player.previous() }
|
||||
QatButton { icon: "redo"; tip: "Redo — next song (Ctrl+Right)"; onClicked: player.next() }
|
||||
QatButton { icon: "dropdown"; tip: "Customize Quick Access Toolbar"; width: 16 }
|
||||
}
|
||||
|
||||
T {
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(implicitWidth, parent.width - 560)
|
||||
color: "#FFFFFF"
|
||||
font.pixelSize: X.fsUi
|
||||
text: (player.track.title ? player.track.title + " - " + player.track.artist : "Book1") + ".xlsx - Excel"
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
height: parent.height
|
||||
T {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: "#FFFFFF"
|
||||
text: library.signedIn ? (settings.get("plexUser") || "Plex") : "Sign in"
|
||||
rightPadding: 14
|
||||
MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: bar.win.openBackstage("account") }
|
||||
}
|
||||
CaptionButton { icon: "pane"; tip: "Show or hide the mixer pane"; onClicked: bar.win.toggleMixer() }
|
||||
CaptionButton { icon: "minimize"; onClicked: bar.win.showMinimized() }
|
||||
CaptionButton { icon: bar.win.maximized ? "restore" : "maximize"; onClicked: bar.win.toggleMaximized() }
|
||||
CaptionButton { icon: "close"; danger: true; onClicked: bar.win.close() }
|
||||
}
|
||||
|
||||
component QatButton: Item {
|
||||
id: q
|
||||
property string icon
|
||||
property string tip
|
||||
signal clicked()
|
||||
width: 26
|
||||
height: parent.height
|
||||
Rectangle { anchors.fill: parent; color: qm.pressed ? X.greenDark : qm.containsMouse ? X.greenHover : "transparent" }
|
||||
Ico { anchors.centerIn: parent; name: q.icon; ink: "#FFFFFF"; size: q.icon === "dropdown" ? 10 : 16 }
|
||||
MouseArea { id: qm; anchors.fill: parent; hoverEnabled: true; onClicked: q.clicked() }
|
||||
Tip { target: qm; text: q.tip }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
|
||||
// Excel 2016, "Colorful" Office theme.
|
||||
QtObject {
|
||||
readonly property color green: "#217346"
|
||||
readonly property color greenDark: "#185C37"
|
||||
readonly property color greenHover: "#2A8754"
|
||||
readonly property color greenText: "#217346"
|
||||
readonly property color ribbon: "#F3F3F3"
|
||||
readonly property color ribbonLine: "#D5D5D5"
|
||||
readonly property color hover: "#DCDCDC"
|
||||
readonly property color pressed: "#C6C6C6"
|
||||
readonly property color checked: "#C6C6C6"
|
||||
readonly property color checkedBorder: "#A5A5A5"
|
||||
readonly property color ink: "#444444"
|
||||
readonly property color inkSoft: "#666666"
|
||||
readonly property color inkFaint: "#9A9A9A"
|
||||
readonly property color white: "#FFFFFF"
|
||||
readonly property color grid: "#D4D4D4"
|
||||
readonly property color header: "#F3F3F3"
|
||||
readonly property color headerSel: "#D2D2D2"
|
||||
readonly property color headerLine: "#BDBDBD"
|
||||
readonly property color selFill: Qt.rgba(0, 0, 0, 0.09)
|
||||
readonly property color cellText: "#000000"
|
||||
readonly property color tableHead: "#70AD47"
|
||||
readonly property color tableBand: "#E2EFDA"
|
||||
readonly property color good: "#C6EFCE"
|
||||
readonly property color goodText: "#006100"
|
||||
readonly property color bad: "#FFC7CE"
|
||||
readonly property color badText: "#9C0006"
|
||||
readonly property color neutral: "#FFEB9C"
|
||||
readonly property color neutralText: "#9C5700"
|
||||
readonly property color dataBar: "#63BE7B"
|
||||
readonly property color note: "#FFFFE1"
|
||||
readonly property color menuBorder: "#C6C6C6"
|
||||
readonly property color chartLine: "#D9D9D9"
|
||||
readonly property color chartText: "#595959"
|
||||
|
||||
// Excel's cell styles, used for the mix presets.
|
||||
readonly property var styles: [
|
||||
{ name: "Normal", fill: "#FFFFFF", text: "#000000", border: "#D4D4D4", bold: false },
|
||||
{ name: "Bad", fill: "#FFC7CE", text: "#9C0006", border: "#FFC7CE", bold: false },
|
||||
{ name: "Good", fill: "#C6EFCE", text: "#006100", border: "#C6EFCE", bold: false },
|
||||
{ name: "Neutral", fill: "#FFEB9C", text: "#9C5700", border: "#FFEB9C", bold: false },
|
||||
{ name: "Calculation", fill: "#F2F2F2", text: "#FA7D00", border: "#7F7F7F", bold: true },
|
||||
{ name: "Check Cell", fill: "#A5A5A5", text: "#FFFFFF", border: "#3F3F3F", bold: true },
|
||||
{ name: "Note", fill: "#FFFFCC", text: "#000000", border: "#B2B2B2", bold: false }
|
||||
]
|
||||
|
||||
readonly property int fsSmall: 11
|
||||
readonly property int fsUi: 12
|
||||
readonly property int fsCell: 15 // Calibri 11 pt
|
||||
readonly property int easeMs: 120
|
||||
|
||||
function colName(i) {
|
||||
var s = ""
|
||||
i += 1
|
||||
while (i > 0) {
|
||||
var m = (i - 1) % 26
|
||||
s = String.fromCharCode(65 + m) + s
|
||||
i = Math.floor((i - 1) / 26)
|
||||
}
|
||||
return s
|
||||
}
|
||||
function time(sec) {
|
||||
if (!(sec > 0)) sec = 0
|
||||
var s = Math.floor(sec), m = Math.floor(s / 60)
|
||||
return m + ":" + ("0" + (s % 60)).slice(-2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import QtQuick
|
||||
|
||||
// Office checkbox: square box, green tick.
|
||||
Item {
|
||||
id: c
|
||||
property bool checked: false
|
||||
property string text: ""
|
||||
signal toggled()
|
||||
implicitWidth: 18 + lbl.implicitWidth + 4
|
||||
implicitHeight: 20
|
||||
opacity: enabled ? 1 : 0.4
|
||||
Rectangle {
|
||||
id: box
|
||||
width: 13
|
||||
height: 13
|
||||
y: (parent.height - height) / 2
|
||||
x: 2
|
||||
color: "#FFFFFF"
|
||||
border.color: ma.containsMouse ? X.green : "#7A7A7A"
|
||||
Ico { visible: c.checked; anchors.centerIn: parent; name: "check"; size: 12 }
|
||||
}
|
||||
T { id: lbl; x: 20; height: parent.height; text: c.text }
|
||||
MouseArea { id: ma; anchors.fill: parent; hoverEnabled: true; onClicked: c.toggled() }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
TextField {
|
||||
font.family: uiFont
|
||||
font.pixelSize: X.fsUi
|
||||
color: X.ink
|
||||
selectByMouse: true
|
||||
selectionColor: "#B7D7C4"
|
||||
selectedTextColor: "#000000"
|
||||
leftPadding: 5
|
||||
rightPadding: 5
|
||||
topPadding: 2
|
||||
bottomPadding: 2
|
||||
implicitHeight: 22
|
||||
background: Rectangle { color: "#FFFFFF"; border.color: parent.activeFocus ? X.green : "#ABABAB" }
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
// Office context menu: white, gray border, icon gutter on the left.
|
||||
Menu {
|
||||
id: m
|
||||
padding: 2
|
||||
delegate: MenuItem {
|
||||
id: mi
|
||||
implicitWidth: Math.max(200, lab.implicitWidth + 56)
|
||||
implicitHeight: mi.text === "" ? 7 : 26
|
||||
enabled: mi.action ? mi.action.enabled : true
|
||||
contentItem: Item {
|
||||
T {
|
||||
id: lab
|
||||
x: 28
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: mi.text
|
||||
color: mi.enabled ? X.ink : X.inkFaint
|
||||
}
|
||||
Ico {
|
||||
visible: mi.action && mi.action.icon.name !== ""
|
||||
name: mi.action ? mi.action.icon.name : ""
|
||||
size: 16
|
||||
x: 4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Ico {
|
||||
visible: mi.checkable && mi.checked
|
||||
name: "check"
|
||||
size: 14
|
||||
x: 5
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
background: Rectangle {
|
||||
color: mi.highlighted && mi.enabled ? "#E0E0E0" : "transparent"
|
||||
}
|
||||
arrow: null
|
||||
indicator: null
|
||||
}
|
||||
background: Rectangle {
|
||||
implicitWidth: 200
|
||||
color: "#FFFFFF"
|
||||
border.color: X.menuBorder
|
||||
Rectangle { x: 25; y: 1; width: 1; height: parent.height - 2; color: "#E7E7E7" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
// Office scrollbar: flat gray thumb on a light track.
|
||||
ScrollBar {
|
||||
id: sb
|
||||
policy: ScrollBar.AlwaysOn
|
||||
minimumSize: 0.04
|
||||
padding: 2
|
||||
implicitWidth: 15
|
||||
implicitHeight: 15
|
||||
contentItem: Rectangle {
|
||||
implicitWidth: 9
|
||||
implicitHeight: 9
|
||||
color: sb.pressed ? "#7A7A7A" : sb.hovered ? "#A6A6A6" : "#C2C2C2"
|
||||
}
|
||||
background: Rectangle { color: "#F0F0F0"; border.color: "#E0E0E0" }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
module StemPlayer
|
||||
singleton X 1.0 X.qml
|
||||
Backstage 1.0 Backstage.qml
|
||||
CaptionButton 1.0 CaptionButton.qml
|
||||
FormulaBar 1.0 FormulaBar.qml
|
||||
Ico 1.0 Ico.qml
|
||||
Main 1.0 Main.qml
|
||||
MessageBar 1.0 MessageBar.qml
|
||||
OfficeButton 1.0 OfficeButton.qml
|
||||
RButton 1.0 RButton.qml
|
||||
ResizeEdge 1.0 ResizeEdge.qml
|
||||
RGroup 1.0 RGroup.qml
|
||||
Ribbon 1.0 Ribbon.qml
|
||||
Sheet 1.0 Sheet.qml
|
||||
SheetTabs 1.0 SheetTabs.qml
|
||||
StatusBar 1.0 StatusBar.qml
|
||||
StemChart 1.0 StemChart.qml
|
||||
TaskPane 1.0 TaskPane.qml
|
||||
Tip 1.0 Tip.qml
|
||||
TitleBar 1.0 TitleBar.qml
|
||||
T 1.0 T.qml
|
||||
XCheck 1.0 XCheck.qml
|
||||
XField 1.0 XField.qml
|
||||
XMenu 1.0 XMenu.qml
|
||||
XScrollBar 1.0 XScrollBar.qml
|
||||
Reference in new issue
Block a user