Merge branch 'ciderapp:develop' into develop

This commit is contained in:
Gabriel Davila 2022-05-06 21:03:03 -03:00 committed by GitHub
commit 4105f84e6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 872 additions and 433 deletions

View file

@ -42,7 +42,7 @@
"airtunes2": "git+https://github.com/vapormusic/node_airtunes2.git#hap",
"castv2-client": "^1.2.0",
"chokidar": "^3.5.3",
"discord-rpc": "^4.0.1",
"discord-auto-rpc": "^1.0.16",
"dns-js": "git+https://github.com/ciderapp/node-dns-js.git",
"ejs": "^3.1.6",
"electron-fetch": "^1.7.4",
@ -82,7 +82,7 @@
"electron-builder-notarize-pkg": "^1.2.0",
"electron-webpack": "^2.8.2",
"musickit-typescript": "^1.2.4",
"typescript": "^4.6.3",
"typescript": "^4.6.4",
"vue-devtools": "^5.1.4",
"webpack": "~5.72.0"
},
@ -109,9 +109,9 @@
}
],
"build": {
"electronVersion": "18.2.0",
"electronVersion": "18.2.1",
"electronDownload": {
"version": "18.2.0+wvcus",
"version": "18.2.1+wvcus",
"mirror": "https://github.com/castlabs/electron-releases/releases/download/v"
},
"appId": "cider",

View file

@ -428,6 +428,11 @@
"settings.header.visual.theme.github.page": "Themes from GitHub",
"settings.option.visual.theme.github.install.confirm": "Are you sure you want to install {{ repo }}?",
"settings.prompt.visual.theme.github.URL": "Enter the URL of the theme you want to install",
"settings.prompt.visual.theme.uninstallTheme": "Are you sure you want to uninstall {{ theme }}?",
"settings.option.visual.theme.checkForUpdates": "Check for updates",
"settings.option.visual.theme.manageStyles": "Manage Styles",
"settings.option.visual.theme.uninstall": "Uninstall",
"settings.option.visual.theme.viewInfo": "View Info",
"settings.notyf.visual.theme.install.success": "Theme installed successfully",
"settings.notyf.visual.theme.install.error": "Theme installation failed",
"settings.header.visual.plugin": "Plugin",

View file

@ -428,6 +428,11 @@
"settings.header.visual.theme.github.page": "Themes from GitHub",
"settings.option.visual.theme.github.install.confirm": "Are you sure you want to install {{ repo }}?",
"settings.prompt.visual.theme.github.URL": "Enter the URL of the theme you want to install",
"settings.prompt.visual.theme.uninstallTheme": "Are you sure you want to uninstall {{ theme }}?",
"settings.option.visual.theme.checkForUpdates": "Check for updates",
"settings.option.visual.theme.manageStyles": "Manage Styles",
"settings.option.visual.theme.uninstall": "Uninstall",
"settings.option.visual.theme.viewInfo": "View Info",
"settings.notyf.visual.theme.install.success": "Theme installed successfully",
"settings.notyf.visual.theme.install.error": "Theme installation failed",
"settings.header.visual.plugin": "Plugin",

View file

@ -4,7 +4,18 @@ import * as windowStateKeeper from "electron-window-state";
import * as express from "express";
import * as getPort from "get-port";
import {search} from "youtube-search-without-api-key";
import {existsSync, rmSync, mkdirSync, readdirSync, readFileSync, writeFileSync, statSync} from "fs";
import {
existsSync,
rmSync,
mkdirSync,
readdirSync,
readFileSync,
writeFileSync,
statSync,
unlinkSync,
rmdirSync,
lstatSync
} from "fs";
import {Stream} from "stream";
import {networkInterfaces} from "os";
import * as mm from 'music-metadata';
@ -46,6 +57,7 @@ export class BrowserWindow {
"pages/library-artists",
"pages/browse",
"pages/settings",
"pages/installed-themes",
"pages/listen_now",
"pages/home",
"pages/artist-feed",
@ -178,6 +190,10 @@ export class BrowserWindow {
page: "settings",
component: `<cider-settings></cider-settings>`,
condition: `page == 'settings'`
}, {
page: "installed-themes",
component: `<installed-themes></installed-themes>`,
condition: `page == 'installed-themes'`
}, {
page: "search",
component: `<cider-search :search="search"></cider-search>`,
@ -256,8 +272,10 @@ export class BrowserWindow {
},
};
public static watcher: any;
StartWatcher(path: string) {
const watcher = watch(path, {
BrowserWindow.watcher = watch(path, {
ignored: /[\/\\]\./,
persistent: true
});
@ -267,7 +285,7 @@ export class BrowserWindow {
}
// Declare the listeners of the watcher
watcher
BrowserWindow.watcher
.on('add', function (path: string) {
// console.log('File', path, 'has been added');
})
@ -294,6 +312,10 @@ export class BrowserWindow {
});
}
async StopWatcher() {
await BrowserWindow.watcher.close();
}
/**
* Creates the browser window
* @generator
@ -698,6 +720,50 @@ export class BrowserWindow {
};
})
ipcMain.handle("uninstall-theme", async (event, path) => {
await this.StopWatcher()
const themesDir = utils.getPath("themes")
// validate the path is in the themes directory
try {
if (path.startsWith(themesDir)) {
// get last dir in path, can be either / or \ and may have a trailing slash
const themeName = path.split(/[\\\/]/).pop()
if (themeName == "Themes" || themeName == "themes") {
BrowserWindow.win.webContents.send("theme-uninstalled", {
path: path,
status: 3
});
return
}
// if path is directory, delete it
if (lstatSync(path).isDirectory()) {
await rmdirSync(path, {recursive: true});
} else {
// if path is file, delete it
await unlinkSync(path);
}
// return the path
BrowserWindow.win.webContents.send("theme-uninstalled", {
path: path,
status: 0
});
} else {
BrowserWindow.win.webContents.send("theme-uninstalled", {
path: path,
status: 1
});
}
} catch (e: any) {
BrowserWindow.win.webContents.send("theme-uninstalled", {
path: path,
message: e.message,
status: 2
});
}
this.StartWatcher(utils.getPath('themes'))
})
ipcMain.handle("reinstall-widevine-cdm", () => {
// remove WidevineCDM from appdata folder
const widevineCdmPath = join(app.getPath("userData"), "./WidevineCdm");
@ -813,7 +879,7 @@ export class BrowserWindow {
} else if (statSync(join(utils.getPath("themes"), file)).isDirectory()) {
let subFiles = readdirSync(join(utils.getPath("themes"), file));
for (let subFile of subFiles) {
if (subFile.endsWith(".less")) {
if (subFile.endsWith("index.less")) {
themes.push(join(file, subFile));
}
}
@ -832,15 +898,20 @@ export class BrowserWindow {
themePath = themePath.slice(0, -10);
}
if (existsSync(join(themePath, "theme.json"))) {
let themeJson = JSON.parse(readFileSync(join(themePath, "theme.json"), "utf8"));
themeObjects.push({
name: themeJson.name || themeName,
description: themeJson.description || themeDescription,
path: themePath,
file: theme,
github_repo: themeJson.github_repo || "",
commit: themeJson.commit || ""
});
try {
let themeJson = JSON.parse(readFileSync(join(themePath, "theme.json"), "utf8"));
themeObjects.push({
name: themeJson.name || themeName,
description: themeJson.description || themeDescription,
path: themePath,
file: theme,
github_repo: themeJson.github_repo || "",
commit: themeJson.commit || "",
pack: themeJson.pack || false,
});
} catch (e) {
console.error(e);
}
} else {
themeObjects.push({
name: themeName,
@ -848,7 +919,8 @@ export class BrowserWindow {
path: themePath,
file: theme,
github_repo: "",
commit: ""
commit: "",
pack: false
});
}
}
@ -971,6 +1043,11 @@ export class BrowserWindow {
BrowserWindow.win.setResizable(!lock);
});
// Move window
ipcMain.on("windowmove", (_event, x, y) => {
BrowserWindow.win.setBounds({x, y});
});
//Fullscreen
ipcMain.on('setFullScreen', (_event, flag) => {
BrowserWindow.win.setFullScreen(flag)
@ -1215,7 +1292,7 @@ export class BrowserWindow {
shell.openPath(app.getPath('userData'));
});
//#region Cider Connect
ipcMain.on('cc-auth', (_event) => {
shell.openExternal(String(utils.getStoreValue('cc_authURL')));
@ -1279,7 +1356,7 @@ export class BrowserWindow {
BrowserWindow.win.webContents.executeJavaScript(`
window.localStorage.setItem("currentTrack", JSON.stringify(app.mk.nowPlayingItem));
window.localStorage.setItem("currentTime", JSON.stringify(app.mk.currentPlaybackTime));
window.localStorage.setItem("currentQueue", JSON.stringify(app.mk.queue.items));
window.localStorage.setItem("currentQueue", JSON.stringify(app.mk.queue._unplayedQueueItems));
ipcRenderer.send('stopGCast','');`)
BrowserWindow.win.destroy();
}

View file

@ -52,11 +52,11 @@ export class Store {
"keybindings": {
"search": [
process.platform == "darwin" ? "Command" : "Control",
"S"
"F"
],
"albums": [
process.platform == "darwin" ? "Command" : "Control",
"F"
"S"
],
"artists": [
process.platform == "darwin" ? "Command" : "Control",
@ -123,6 +123,8 @@ export class Store {
"quality": "HIGH",
"seamless_audio": true,
"normalization": false,
"dBSPL": false,
"dBSPLcalibration": 90,
"maikiwiAudio": {
"ciderPPE": false,
"ciderPPE_value": "MAIKIWI",

View file

@ -1,30 +1,29 @@
import * as RPC from 'discord-rpc'
import {AutoClient} from 'discord-auto-rpc'
import {ipcMain} from "electron";
import fetch from 'electron-fetch'
export default class DiscordRPC {
/**
* Private variables for interaction in plugins
*/
private _utils: any;
private _app: any;
private _attributes: any;
private _connection: boolean = false;
/**
* Base Plugin Details (Eventually implemented into a GUI in settings)
*/
public name: string = 'Discord Rich Presence';
public description: string = 'Discord RPC plugin for Cider';
public version: string = '1.0.0';
public version: string = '1.1.0';
public author: string = 'vapormusic/Core (Cider Collective)';
/**
* Private variables for interaction in plugins
*/
private _utils: any;
private _attributes: any;
private ready: boolean = false;
/**
* Plugin Initialization
*/
private _client: any = null;
private _activity: RPC.Presence = {
private _activityCache: any = {
details: '',
state: '',
largeImageKey: '',
@ -34,15 +33,72 @@ export default class DiscordRPC {
instance: false
};
private _activityCache: RPC.Presence = {
details: '',
state: '',
largeImageKey: '',
largeImageText: '',
smallImageKey: '',
smallImageText: '',
instance: false
};
/*******************************************************************************************
* Public Methods
* ****************************************************************************************/
/**
* Runs on plugin load (Currently run on application start)
*/
constructor(utils: any) {
this._utils = utils;
console.debug(`[Plugin][${this.name}] Loading Complete.`);
}
/**
* Runs on app ready
*/
onReady(_win: any): void {
const self = this
this.connect();
console.debug(`[Plugin][${this.name}] Ready.`);
ipcMain.on('updateRPCImage', (_event, imageurl) => {
if (!this._utils.getStoreValue("general.privateEnabled")) {
fetch('https://api.cider.sh/v1/images', {
method: 'POST',
body: JSON.stringify({url: imageurl}),
headers: {
'Content-Type': 'application/json',
'User-Agent': _win.webContents.getUserAgent()
},
})
.then(res => res.json())
.then(function (json) {
self._attributes["artwork"]["url"] = json.url
self.setActivity(self._attributes)
})
}
})
}
/**
* Runs on app stop
*/
onBeforeQuit(): void {
console.debug(`[Plugin][${this.name}] Stopped.`);
}
/**
* Runs on playback State Change
* @param attributes Music Attributes (attributes.status = current state)
*/
onPlaybackStateDidChange(attributes: object): void {
this._attributes = attributes
this.setActivity(attributes)
}
/**
* Runs on song change
* @param attributes Music Attributes
*/
onNowPlayingItemDidChange(attributes: object): void {
this._attributes = attributes
this.setActivity(attributes)
}
/*******************************************************************************************
* Private Methods
@ -58,58 +114,86 @@ export default class DiscordRPC {
}
const clientId = this._utils.getStoreValue("general.discordrpc.client") === "Cider" ? '911790844204437504' : '886578863147192350';
// Apparently needed for ask to join, join, spectate etc.
RPC.register(clientId)
// Create the client
this._client = new RPC.Client({transport: "ipc"});
this._client = new AutoClient({transport: "ipc"});
// Runs on Ready
this._client.on('ready', () => {
this._client.once('ready', () => {
console.info(`[DiscordRPC][connect] Successfully Connected to Discord. Authed for user: ${this._client.user.id}.`);
if (this._activityCache && this._activityCache.details && this._activityCache.state) {
console.info(`[DiscordRPC][connect] Restoring activity cache.`);
this._client.setActivity(this._activityCache)
}
})
// Handles Errors
this._client.on('error', (err: any) => {
console.error(`[DiscordRPC] ${err}`);
this.disconnect()
});
// If Discord is closed, allow reconnecting
this._client.transport.once('close', () => {
console.info(`[DiscordRPC] Connection closed`);
this.disconnect()
});
// Login to Discord
this._client.login({clientId})
this._client.endlessLogin({clientId: clientId})
.then(() => {
this._connection = true;
this.ready = true
})
.catch((e: any) => console.error(`[DiscordRPC][connect] ${e}`));
}
/**
* Disconnects from Discord RPC
* Sets the activity
* @param attributes Music Attributes
*/
private disconnect() {
private setActivity(attributes: any) {
if (!this._client) {
return
}
this._client.destroy().then(() => {
this._connection = false;
console.log('[DiscordRPC][disconnect] Disconnected from discord.')
}).catch((e: any) => console.error(`[DiscordRPC][disconnect] ${e}`));
// Check if show buttons is (true) or (false)
let activity: Object = {
details: this._utils.getStoreValue("general.discordrpc.details_format"),
state: this._utils.getStoreValue("general.discordrpc.state_format"),
largeImageKey: attributes?.artwork?.url?.replace('{w}', '1024').replace('{h}', '1024'),
largeImageText: attributes.albumName,
instance: false // Whether the activity is in a game session
}
// Clean up, allow creating a new connection
this._client = null;
// Filter the activity
activity = this.filterActivity(activity, attributes)
if (!this.ready) {
this._activityCache = activity
return
}
// Set the activity
if (!attributes.status && this._utils.getStoreValue("general.discordrpc.clear_on_pause")) {
this._client.clearActivity()
} else if (activity && this._activityCache !== activity) {
this._client.setActivity(activity)
}
this._activityCache = activity;
}
/**
* Filter the Discord activity object
*/
private static filterActivity(activity: any, attributes: any): Object {
private filterActivity(activity: any, attributes: any): Object {
// Add the buttons if people want them
if (!this._utils.getStoreValue("general.discordrpc.hide_buttons")) {
activity.buttons = [
{label: 'Listen on Cider', url: attributes.url.cider},
{label: 'View on Apple Music', url: attributes.url.appleMusic}
] //To change attributes.url => preload/cider-preload.js
}
// Add the timestamp if its playing
if (attributes.status) {
activity.startTimestamp = Date.now() - (attributes?.durationInMillis - attributes?.remainingTime)
activity.endTimestamp = attributes.endTime
}
// If the user wants to keep the activity when paused
if (!this._utils.getStoreValue("general.discordrpc.clear_on_pause")) {
activity.smallImageKey = attributes.status ? 'play' : 'pause';
activity.smallImageText = attributes.status ? 'Playing' : 'Paused';
}
/**
* Works with:
@ -173,138 +257,4 @@ export default class DiscordRPC {
}
return activity
}
/**
* Sets the activity
* @param {activity} activity
*/
private setActivity(activity: any) {
if (!this._connection || !this._client || !activity) {
return
}
// Filter the activity
activity = DiscordRPC.filterActivity(activity, this._attributes)
// Set the activity
if (!this._attributes.status && this._utils.getStoreValue("general.discordrpc.clear_on_pause")) {
this._client.clearActivity()
} else if (this._activity && this._activityCache !== this._activity && this._activity.details) {
this._client.setActivity(activity)
this._activityCache = this._activity;
}
}
/**
* Sets the activity of the client
* @param {object} attributes
*/
private updateActivity(attributes: any) {
if (!this._utils.getStoreValue("general.discordrpc.enabled") || this._utils.getStoreValue("general.privateEnabled")) {
return
} else if (!this._client || !this._connection) {
this.connect()
}
// Check if show buttons is (true) or (false)
this._activity = {
details: this._utils.getStoreValue("general.discordrpc.details_format"),
state: this._utils.getStoreValue("general.discordrpc.state_format"),
largeImageKey: attributes?.artwork?.url?.replace('{w}', '1024').replace('{h}', '1024'),
largeImageText: attributes.albumName,
instance: false // Whether the activity is in a game session
}
// Add the buttons if people want them
if (!this._utils.getStoreValue("general.discordrpc.hide_buttons")) {
this._activity.buttons = [
{label: 'Listen on Cider', url: attributes.url.cider},
{label: 'View on Apple Music', url: attributes.url.appleMusic}
] //To change attributes.url => preload/cider-preload.js
}
// Add the timestamp if its playing
if (attributes.status) {
this._activity.startTimestamp = Date.now() - (attributes?.durationInMillis - attributes?.remainingTime)
this._activity.endTimestamp = attributes.endTime
}
// If the user wants to keep the activity when paused
if (!this._utils.getStoreValue("general.discordrpc.clear_on_pause")) {
this._activity.smallImageKey = attributes.status ? 'play' : 'pause';
this._activity.smallImageText = attributes.status ? 'Playing' : 'Paused';
}
this.setActivity(this._activity)
}
/*******************************************************************************************
* Public Methods
* ****************************************************************************************/
/**
* Runs on plugin load (Currently run on application start)
*/
constructor(utils: { getStore: () => any; getApp: () => any; }) {
this._utils = utils;
console.debug(`[Plugin][${this.name}] Loading Complete.`);
this._app = utils.getApp();
}
/**
* Runs on app ready
*/
onReady(_win: any): void {
let self = this
this.connect();
console.debug(`[Plugin][${this.name}] Ready.`);
ipcMain.on('updateRPCImage', (_event, imageurl) => {
if (!this._utils.getStoreValue("general.privateEnabled")) {
fetch('https://api.cider.sh/v1/images', {
method: 'POST',
body: JSON.stringify({url: imageurl}),
headers: {
'Content-Type': 'application/json',
'User-Agent': _win.webContents.getUserAgent()
},
})
.then(res => res.json())
.then(function (json) {
self._attributes["artwork"]["url"] = json.url
self.updateActivity(self._attributes)
})
}
})
}
/**
* Runs on app stop
*/
onBeforeQuit(): void {
if (this._client) {
this.disconnect()
}
console.debug(`[Plugin][${this.name}] Stopped.`);
}
/**
* Runs on playback State Change
* @param attributes Music Attributes (attributes.status = current state)
*/
onPlaybackStateDidChange(attributes: object): void {
this._attributes = attributes
this.updateActivity(attributes)
}
/**
* Runs on song change
* @param attributes Music Attributes
*/
onNowPlayingItemDidChange(attributes: object): void {
this._attributes = attributes
this.updateActivity(attributes)
}
}

View file

@ -181,7 +181,7 @@ export default class RAOP {
this.portairplay = ipport;
this.device = this.airtunes.add(ipv4, {
port: ipport,
volume: 60,
volume: 50,
password: sepassword,
txt: txt
});

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-heart"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-heart"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>

Before

Width:  |  Height:  |  Size: 379 B

After

Width:  |  Height:  |  Size: 373 B

Before After
Before After

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-heart"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-heart"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>

Before

Width:  |  Height:  |  Size: 371 B

After

Width:  |  Height:  |  Size: 364 B

Before After
Before After

View file

@ -364,6 +364,21 @@
align-items: center;
justify-content: center;
}
.cd-mediaitem-list-item .heart-unfilled {
background-image: url("assets/feather/heart.svg");
height: 12px;
width: 36px;
filter: contrast(0);
background-repeat: no-repeat;
}
.cd-mediaitem-list-item .heart-filled {
background-image: url("assets/feather/heart-fill.svg");
height: 12px;
width: 36px;
filter: contrast(0);
background-repeat: no-repeat;
}
.cd-mediaitem-list-item .explicit-icon {
background-image: url("assets/explicit.svg");
height: 12px;
@ -372,9 +387,7 @@
background-repeat: no-repeat;
}
.heart-icon {
position: absolute;
filter: contrast(0);
background-repeat: no-repeat;
display: flex
}
@keyframes load-bar {
10% {

View file

@ -359,7 +359,11 @@
align-items: center;
border-radius: var(--mediaItemRadius);
position: relative;
&:hover{
.heart-icon{
display: none;
}
}
.popular {
background-image: url(assets/star.svg);
background-repeat: no-repeat;
@ -448,6 +452,22 @@
justify-content: center;
}
.heart-unfilled {
-webkit-mask-image: url("assets/feather/heart.svg");
height: 12px;
width: 12px;
background-repeat: no-repeat;
background-color: #999;
}
.heart-filled {
-webkit-mask-image: url("assets/feather/heart-fill.svg");
height: 12px;
width: 12px;
background-repeat: no-repeat;
background-color: #999;
}
.explicit-icon {
background-image: url("./assets/explicit.svg");
height: 12px;
@ -457,10 +477,9 @@
}
.heart-icon {
display: flex;
position: absolute;
right:0;
filter: contrast(0);
background-repeat: no-repeat;
left: 20px;
}
/* CSS.gg

View file

@ -353,7 +353,7 @@
&:hover {
&::before {
transition: transform .1s ease-in, opacity .1s ease-in;
transition: transform 0s ease-in, opacity 0s ease-in;
opacity : 1;
transform : scale(1);
}

View file

@ -488,63 +488,79 @@
/* Album / Playlist Page */
.playlist-page {
--bgColor : transparent;
padding : 0px;
--bgColor : transparent;
padding : 0px;
//background: linear-gradient(180deg, var(--bgColor) 32px, var(--bgColor) 18px, transparent 60px, transparent 100%);
top : 0;
padding-top : var(--navigationBarHeight);
display:flex;
top : 0;
padding-top : var(--navigationBarHeight);
display : flex;
flex-direction: column;
height: 100%;
overflow: hidden;
height : 100%;
overflow : hidden;
.cd-mediaitem-list-item {
&:hover {
.heart-icon {
display: flex;
}
}
.heart-icon {
left: -25px;
}
}
.editTracksBtn {
position: absolute;
top: 20px;
right: 20px;
z-index: 1;
top : 20px;
right : 20px;
z-index : 1;
>span {
display: flex;
gap: 8px;
gap : 8px;
}
}
.mediaContainer {
transition: width 0.5s ease-in-out, height 0.5s ease-in-out;
width: 260px;height:260px;
width : 260px;
height : 260px;
}
.playlist-body {
padding : 32px;
padding : 32px;
// margin-top: -75px;
overflow-y:overlay;
height:100%;
padding:0px;
overflow-y : overlay;
height : 100%;
padding : 0px;
background-color: var(--color1);
&.scrollbody {
.tabs {
display: flex;
display : flex;
flex-flow: column;
height: 100%;
height : 100%;
.nav-link {
text-transform:capitalize;
text-transform: capitalize;
}
.tab-content {
height: 100%;
height : 100%;
overflow: hidden;
margin:0px;
margin : 0px;
.tab-pane {
height: 100%;
overflow-y: overlay;
overflow-x:hidden;
padding: var(--contentInnerPadding);
height : 100%;
overflow-y : overlay;
overflow-x : hidden;
padding : var(--contentInnerPadding);
padding-inline : 40px;
-webkit-mask-image: linear-gradient(180deg, transparent, white 20px);
.well {
margin:0px;
margin: 0px;
}
}
}
@ -563,7 +579,7 @@
background : rgba(0, 0, 0, 0.25);
top : var(--navigationBarHeight);
transition : opacity 0.1s var(--appleEase);
display: none;
display : none;
}
.playlist-display {
@ -649,14 +665,14 @@
}
.playlist-desc {
transition: height .2s ease-in-out, opacity .2s ease-in-out;
transition : height .2s ease-in-out, opacity .2s ease-in-out;
box-sizing : border-box;
font-size : 14px;
flex-shrink : unset;
margin-right: 5px;
max-height : 100px;
position : relative;
height : 4vh;
height : 4vh;
.content {
height : 4vh;
@ -750,11 +766,11 @@
}
.playlist-time {
font-size: 0.9em;
margin : 6px;
opacity : 0.7;
font-size : 0.9em;
margin : 6px;
opacity : 0.7;
transition: height .2s ease-in-out, opacity .2s ease-in-out;
height: 0.9em;
height : 0.9em;
}
&.inline-playlist {
@ -802,8 +818,8 @@
.pilldim {
.nav-pills {
width: max-content;
margin: 0 auto;
width : max-content;
margin : 0 auto;
margin-top: 16px;
}
}
@ -813,26 +829,24 @@
transition: min-height 0.5s ease-in-out;
min-height: 200px;
.playlistInfo {
}
.playlistInfo {}
.mediaContainer {
transition: width 0.5s ease-in-out, height 0.5s ease-in-out;
width: 128px!important;
height: 128px!important;
width : 128px !important;
height : 128px !important;
}
.playlist-time {
transition: height .2s ease-in-out, opacity .2s ease-in-out;
height: 0px;
opacity: 0;
height : 0px;
opacity : 0;
}
.playlist-desc {
transition: height .2s ease-in-out, opacity .2s ease-in-out;
height: 0px!important;
opacity: 0;
height : 0px !important;
opacity : 0;
}
}
}
@ -920,7 +934,7 @@
pointer-events : none;
.header-content {
z-index : 1;
z-index : 1;
// margin-top: -16px;
}
@ -1112,9 +1126,56 @@
/* Artist Page End */
// Settings page
.settings-page {
padding: 0px;
.installed-themes-page {
.themeContextMenu {
background: transparent;
color : var(--keyColor);
border : 0px;
}
.list-group-item {
&.addon {
background: rgb(86 86 86 / 20%);
}
&.applied {
background: var(--keyColor-disabled);
pointer-events: none;
}
}
.repo-header {
font-size : 16px;
position : sticky;
top : 0;
left : 0;
right : 0;
width : 100%;
height : 50px;
z-index : 1;
background : rgba(36, 36, 36, 0.5);
display : flex;
justify-content: center;
align-items : center;
backdrop-filter: var(--glassFilter);
overflow : hidden;
border-bottom : 1px solid rgb(0 0 0 / 18%);
border-top : 1px solid rgb(135 135 135 / 18%);
}
.style-editor-container {
height : 100%;
flex : 1;
background: var(--color2);
padding : 0px;
overflow-y: overlay;
.list-group-item {
border-radius: 0px;
}
}
.stylestack-editor {
width: 100%;
@ -1125,17 +1186,34 @@
}
.themeLabel {
display:flex;
display : flex;
align-items: center;
}
.handle {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.list-group-item {
&:hover {
cursor: grab;
}
&:active {
cursor: grabbing;
}
}
.removeItem {
border: 0px;
background: transparent;
height: 32px;
border : 0px;
background : transparent;
height : 32px;
font-weight: bold;
color: var(--textColor);
cursor: pointer;
color : var(--textColor);
cursor : pointer;
}
.stylesDropdown {
@ -1145,7 +1223,11 @@
}
}
}
}
// Settings page
.settings-page {
padding: 0px;
.nav {
width : 90%;
@ -1163,8 +1245,9 @@
.settings-option-body-webview {
height: 100%;
width: 100%;
width : 100%;
}
.settings-option-body {
margin: 16px;
}

View file

@ -148,6 +148,10 @@ const app = new Vue({
},
tmpHeight: '',
tmpWidth: '',
tmpX: '',
tmpY: '',
miniTmpX: '',
miniTmpY: '',
tmpVar: [],
notification: false,
chrome: {
@ -284,6 +288,9 @@ const app = new Vue({
}
}
},
formatVolumeTooltip() {
return this.cfg.audio.dBSPL ? (Number(this.cfg.audio.dBSPLcalibration) + (Math.log10(this.mk.volume) * 20)).toFixed(2) + ' dB SPL' : (Math.log10(this.mk.volume) * 20).toFixed(2) + ' dBFS'
},
mainMenuVisibility(val) {
if (val) {
(this.mk.isAuthorized) ? this.chrome.menuOpened = !this.chrome.menuOpened : false;
@ -593,9 +600,7 @@ const app = new Vue({
},
async init() {
let self = this
if (this.cfg.visual.theme != "default.less" && this.cfg.visual.theme != "") {
this.setTheme(this.cfg.visual.theme)
}
if (this.cfg.visual.styles.length != 0) {
await this.reloadStyles()
}
@ -703,6 +708,7 @@ const app = new Vue({
let lastItem = window.localStorage.getItem("currentTrack")
let time = window.localStorage.getItem("currentTime")
let queue = window.localStorage.getItem("currentQueue")
app.mk.queue.position = 0; // Reset queue position.
if (lastItem != null) {
lastItem = JSON.parse(lastItem)
let kind = lastItem.attributes.playParams.kind;
@ -722,7 +728,7 @@ const app = new Vue({
if (queue != null) {
queue = JSON.parse(queue)
if (queue && queue.length > 0) {
let ids = queue.map(e => (e.playParams ? e.playParams.id : (e.attributes.playParams ? e.attributes.playParams.id : '')))
let ids = queue.map(e => (e.playParams ? e.playParams.id : (e.item.attributes.playParams ? e.item.attributes.playParams.id : '')))
let i = 0;
if (ids.length > 0) {
for (let id of ids) {
@ -833,6 +839,14 @@ const app = new Vue({
ipcRenderer.send('wsapi-updatePlaybackState', wsapi.getAttributes());
})
this.mk.addEventListener(MusicKit.Events.queueItemsDidChange, ()=>{
if (self.$refs.queue) {
setTimeout(()=>{
self.$refs.queue.updateQueue();
}, 100)
}
})
this.mk.addEventListener(MusicKit.Events.nowPlayingItemDidChange, (a) => {
if (self.$refs.queue) {
self.$refs.queue.updateQueue();
@ -1148,8 +1162,10 @@ const app = new Vue({
async function deepScan(parent = "p.playlistsroot") {
console.debug(`scanning ${parent}`)
const playlistData = await app.mk.api.v3.music(`/v1/me/library/playlist-folders/${parent}/children/`)
await asyncForEach(playlistData.data.data, async (playlist) => {
// const playlistData = await app.mk.api.v3.music(`/v1/me/library/playlist-folders/${parent}/children/`)
const playlistData = await MusicKitTools.v3Continuous({href: `/v1/me/library/playlist-folders/${parent}/children/`})
console.log(playlistData)
await asyncForEach(playlistData, async (playlist) => {
playlist.parent = parent
if (
playlist.type != "library-playlist-folders" &&
@ -4107,13 +4123,19 @@ const app = new Vue({
if (flag) {
this.tmpWidth = window.innerWidth;
this.tmpHeight = window.innerHeight;
this.tmpX = window.screenX;
this.tmpY = window.screenY;
ipcRenderer.send('unmaximize');
ipcRenderer.send('windowmin', 250, 250)
if (this.miniTmpX !== '' && this.miniTmpY !== '') ipcRenderer.send('windowmove', this.miniTmpX, this.miniTmpY)
ipcRenderer.send('windowresize', 300, 300, false)
app.appMode = 'mini';
} else {
this.miniTmpX = window.screenX;
this.miniTmpY = window.screenY;
ipcRenderer.send('windowmin', 844, 410)
ipcRenderer.send('windowresize', this.tmpWidth, this.tmpHeight, false)
ipcRenderer.send('windowmove', this.tmpX, this.tmpY)
ipcRenderer.send('windowontop', false)
//this.cfg.visual.miniplayer_top_toggle = true;
app.appMode = 'player';

View file

@ -12944,6 +12944,7 @@ body[platform='darwin'] #window-controls-container {
}
body[platform='darwin'] .app-chrome .app-chrome-item > .app-mainmenu {
opacity: 0;
width: 52px;
pointer-events: none;
-webkit-app-region: drag;
}

View file

@ -3289,6 +3289,7 @@ body[platform='darwin'] {
.app-chrome .app-chrome-item > .app-mainmenu {
opacity: 0;
width: 52px;
pointer-events: none;
-webkit-app-region: drag;
}

View file

@ -101,7 +101,7 @@
:class="{'active': this.cfg.audio.volume == 0}"></button>
<input type="range" @wheel="volumeWheel" :step="cfg.audio.volumeStep" min="0" :max="cfg.audio.maxVolume"
v-model="mk.volume" v-if="typeof mk.volume != 'undefined'" @change="checkMuteChange()"
v-b-tooltip.hover :title="`${(Math.log10(mk.volume) * 20).toFixed(2)} dB`">
v-b-tooltip.hover :title="formatVolumeTooltip()">
</div>
<div class="app-chrome-item generic">
<button class="playback-button--small miniplayer"

View file

@ -134,7 +134,7 @@
:class="{'active': this.cfg.audio.volume == 0}"></button>
<input type="range" @wheel="volumeWheel" :step="cfg.audio.volumeStep" min="0" :max="cfg.audio.maxVolume"
v-model="mk.volume" v-if="typeof mk.volume != 'undefined'" @change="checkMuteChange()"
v-b-tooltip.hover :title="`${(Math.log10(mk.volume) * 20).toFixed(2)} dB`">
v-b-tooltip.hover :title="formatVolumeTooltip()">
</div>
<div class="app-chrome-item generic">
<button class="playback-button--small miniplayer"

View file

@ -216,7 +216,7 @@
<input type="range" class="" @wheel="volumeWheel" :step="cfg.audio.volumeStep" min="0"
:max="cfg.audio.maxVolume" v-model="mk.volume" v-if="typeof mk.volume != 'undefined'"
@change="checkMuteChange()" v-b-tooltip.hover
:title="`${(Math.log10(mk.volume) * 20).toFixed(2)} dB`">
:title="formatVolumeTooltip()">
</div>
</div>
</div>

View file

@ -14,7 +14,7 @@
<div class="md-option-segment md-option-segment_auto percent">
<input type="number"
style="width: 100%; text-align: center; margin-right: 5px;" min="0"
step="5" v-model="volume"/>
step="2" v-model="volume"/>
</div>
</div>
<div class="md-option-line">

View file

@ -82,7 +82,7 @@
<button class="volume-button--small volume" @click="app.muteButtonPressed()" :class="{'active': app.cfg.audio.volume == 0}"></button>
<input type="range" class="slider" @wheel="app.volumeWheel" :step="app.cfg.audio.volumeStep" min="0" :max="app.cfg.audio.maxVolume" v-model="app.mk.volume"
v-if="typeof app.mk.volume != 'undefined'" @change="app.checkMuteChange()"
v-b-tooltip.hover :title="`${(Math.log10(app.mk.volume) * 20).toFixed(2)} dB`">
v-b-tooltip.hover :title="$root.formatVolumeTooltip()">
</div>
</div>
</div>

View file

@ -65,8 +65,9 @@
</template>
</div>
</div>
<div class="heart-icon" v-if="isLoved">
<div class="svg-icon" :style="{'--url': 'url(./assets/feather/heart-fill.svg)'}"></div>
<div class="heart-icon" v-if="!(app.mk.isPlaying && (((app.mk.nowPlayingItem._songId ?? (app.mk.nowPlayingItem.songId ?? app.mk.nowPlayingItem.id )) == itemId) || (app.mk.nowPlayingItem.id == item.id)))">
<!-- <div class="heart-unfilled" v-if="isLoved == false" :style="{'--url': 'url(./assets/feather/heart.svg)'}" /> -->
<div class="heart-filled" v-if="isLoved == true" :style="{'--url': 'url(./assets/feather/heart-fill.svg)'}" />
</div>
<div class="explicit-icon" v-if="item.attributes && item.attributes.contentRating == 'explicit'"></div>
<template v-if="showMetaData == true" @dblclick="route()">

View file

@ -0,0 +1,368 @@
<script type="text/x-template" id="installed-themes">
<div class="content-inner github-themes-page installed-themes-page">
<div class="gh-header">
<div class="row">
<div class="col nopadding">
<h1 class="header-text">
{{ $root.getLz("settings.option.visual.theme.manageStyles") }}
</h1>
</div>
<div class="col-auto nopadding flex-center">
<button class="md-btn md-btn-small md-btn-block" @click="$root.appRoute('themes-github')">
{{$root.getLz('settings.option.visual.theme.github.explore')}}
</button>
</div>
<div class="col-auto flex-center">
<button class="md-btn md-btn-small md-btn-block" @click="$root.checkForThemeUpdates()">
{{ $root.getLz('settings.option.visual.theme.checkForUpdates') }}
</button>
</div>
<div class="col-auto nopadding flex-center">
<button class="md-btn md-btn-small md-btn-block" @click="openThemesFolder()">
{{$root.getLz('settings.option.visual.theme.github.openfolder')}}
</button>
</div>
</div>
</div>
<div class="gh-content">
<div class="repos-list">
<div class="repo-header">
<h4>Available</h4>
</div>
<ul class="list-group list-group-flush">
<template v-for="theme in themes">
<li @click="addStyle(theme.file)"
@contextmenu="contextMenu($event, theme)"
class="list-group-item list-group-item-dark"
:class="{'applied': $root.cfg.visual.styles.includes(theme.file)}">
<b-row>
<b-col class="themeLabel">{{theme.name}}</b-col>
<template v-if="$root.cfg.visual.styles.includes(theme.file)">
<b-col sm="auto" v-if="theme.pack">
<button class="themeContextMenu codicon codicon-package"></button>
</b-col>
<b-col sm="auto">
<button class="themeContextMenu codicon codicon-check"></button>
</b-col>
</template>
<template v-else>
<b-col sm="auto" v-if="theme.pack">
<button class="themeContextMenu codicon codicon-package"></button>
</b-col>
<b-col sm="auto">
<button @click.stop="contextMenu($event, theme)" class="themeContextMenu codicon codicon-list-unordered"></button>
</b-col>
</template>
</b-row>
</li>
<li @click="addStyle(packEntry.file)"
@contextmenu="contextMenu($event, theme)"
class="list-group-item list-group-item-dark addon"
v-for="packEntry in theme.pack"
:class="{'applied': $root.cfg.visual.styles.includes(packEntry.file)}"
v-if="theme.pack">
<b-row>
<b-col class="themeLabel">{{packEntry.name}}</b-col>
<template v-if="$root.cfg.visual.styles.includes(packEntry.file)">
<b-col sm="auto">
<button class="themeContextMenu codicon codicon-check"></button>
</b-col>
</template>
<template v-else>
<b-col sm="auto">
<button class="themeContextMenu codicon codicon-diff-added"></button>
</b-col>
</template>
</b-row>
</li>
</template>
</ul>
</div>
<div class="style-editor-container">
<div class="repo-header">
<h4>Applied</h4>
</div>
<stylestack-editor ref="stackEditor" v-if="themes.length != 0" :themes="themes"/>
</div>
</div>
</div>
</script>
<script>
// do not translate
Vue.component('stylestack-editor', {
/*html*/
template: `
<div class="stylestack-editor" >
<draggable class="list-group" v-model="$root.cfg.visual.styles" @end="$root.reloadStyles()">
<b-list-group-item variant="dark" v-for="theme in $root.cfg.visual.styles" :key="theme">
<b-row>
<b-col sm="auto">
<div class="handle codicon codicon-grabber"></div>
</b-col>
<b-col class="themeLabel">{{getThemeName(theme)}}</b-col>
<b-col sm="auto">
<button class="removeItem codicon codicon-close" @click="remove(theme)"></button>
</b-col>
</b-row>
</b-list-group-item>
</draggable>
</div>
`,
props: {
themes: {
type: Array,
default: [],
required: true
}
},
data: function () {
return {
selected: null,
newTheme: null,
themeList: []
}
},
mounted() {
console.log(this.themes)
this.themeList = [...this.themes]
this.themeList.forEach(theme => {
if (theme.pack) {
theme.pack.forEach(packEntry => {
packEntry.file = theme.file.replace('index.less', '') + packEntry.file
this.themeList.push(packEntry)
})
}
})
},
methods: {
gitHubExplore() {
this.$root.appRoute("themes-github")
},
getThemeName(filename) {
try {
return this.themeList.find(theme => theme.file === filename).name;
} catch (e) {
return filename;
}
},
moveUp() {
const styles = this.$root.cfg.visual.styles
const index = styles.indexOf(this.selected)
if (index > 0) {
styles.splice(index, 1)
styles.splice(index - 1, 0, this.selected)
}
this.$root.reloadStyles()
},
moveDown() {
const styles = this.$root.cfg.visual.styles
const index = styles.indexOf(this.selected)
if (index < styles.length - 1) {
styles.splice(index, 1)
styles.splice(index + 1, 0, this.selected)
}
this.$root.reloadStyles()
},
remove(style) {
const styles = this.$root.cfg.visual.styles
const index = styles.indexOf(style)
styles.splice(index, 1)
this.$root.reloadStyles()
},
addStyle(style) {
const styles = this.$root.cfg.visual.styles
styles.push(style)
this.$root.reloadStyles()
}
}
})
</script>
<script>
Vue.component('installed-themes', {
template: "#installed-themes",
props: [],
data: function () {
return {
repos: [],
openRepo: {
id: -1,
name: '',
description: '',
html_url: '',
stargazers_count: 0,
owner: {
avatar_url: ''
},
readme: ""
},
themesInstalled: [],
themes: []
}
},
mounted() {
this.getThemesList();
},
methods: {
getThemesList() {
let self = this
let themes = ipcRenderer.sendSync("get-themes")
themes.unshift({
name: "Acrylic Grain",
file: "grain.less"
})
themes.unshift({
name: "Sweetener",
file: "sweetener.less"
})
themes.unshift({
name: "Reduce Visuals",
file: "reduce_visuals.less"
})
themes.unshift({
name: "Inline Drawer",
file: "inline_drawer.less"
})
themes.unshift({
name: "Dark",
file: "dark.less"
})
this.themes = themes
},
contextMenu(event, theme) {
let self = this
let menu = {
items: {
"uninstall": {
name: app.getLz("settings.option.visual.theme.uninstall"),
disabled: true,
action: () => {
bootbox.confirm(app.stringTemplateParser(app.getLz("settings.prompt.visual.theme.uninstallTheme"), {
theme: theme.name ?? theme.file
}), (res) => {
if (res) {
console.debug(theme)
ipcRenderer.once("theme-uninstalled", (event, args) => {
console.debug(event, args)
self.getThemesList()
})
ipcRenderer.invoke("uninstall-theme", theme.path)
}
})
}
},
"viewInfo": {
name: app.getLz("settings.option.visual.theme.viewInfo"),
disabled: true,
action: () => {
}
}
}
}
if (theme.path) {
menu.items.uninstall.disabled = false
}
this.$root.showMenuPanel(menu, event)
},
openThemesFolder() {
ipcRenderer.invoke("open-path", "themes")
},
getInstalledThemes() {
let self = this
const themes = ipcRenderer.sendSync("get-themes")
// for each theme, get the github_repo property and push it to the themesInstalled array, if not blank
themes.forEach(theme => {
if (theme.github_repo !== "" && typeof theme.commit != "") {
self.themesInstalled.push(theme.github_repo.toLowerCase())
}
})
},
addStyle(filename) {
this.$refs.stackEditor.addStyle(filename)
},
showRepo(repo) {
const self = this
const readmeUrl = `https://raw.githubusercontent.com/${repo.full_name}/main/README.md`;
var requestOptions = {
method: 'GET',
redirect: 'follow'
};
fetch(readmeUrl, requestOptions)
.then(response => response.text())
.then(result => {
self.openRepo = repo
self.openRepo.readme = self.convertReadMe(result);
})
.catch(error => {
self.openRepo = repo
self.openRepo.readme = `This repository doesn't have a README.md file.`;
console.log('error', error)
});
},
convertReadMe(text) {
return marked.parse(text)
},
installThemeRepo(repo) {
let self = this
let msg = app.stringTemplateParser(app.getLz('settings.option.visual.theme.github.install.confirm'), {
repo: repo.full_name
});
bootbox.confirm(msg, (res) => {
if (res) {
ipcRenderer.once("theme-installed", (event, arg) => {
if (arg.success) {
self.themes = ipcRenderer.sendSync("get-themes")
self.getInstalledThemes()
notyf.success(app.getLz('settings.notyf.visual.theme.install.success'));
} else {
notyf.error(app.getLz('settings.notyf.visual.theme.install.error'));
}
});
ipcRenderer.invoke("get-github-theme", repo.html_url)
}
})
},
installThemeURL() {
let self = this
bootbox.prompt(app.getLz('settings.prompt.visual.theme.github.URL'), (result) => {
if (result) {
ipcRenderer.once("theme-installed", (event, arg) => {
if (arg.success) {
self.themes = ipcRenderer.sendSync("get-themes")
notyf.success(app.getLz('settings.notyf.visual.theme.install.success'));
} else {
notyf.error(app.getLz('settings.notyf.visual.theme.install.error'));
}
});
ipcRenderer.invoke("get-github-theme", result)
}
});
},
getRepos() {
let self = this
var requestOptions = {
method: 'GET',
redirect: 'follow'
};
fetch("https://api.github.com/search/repositories?q=topic:cidermusictheme fork:true", requestOptions)
.then(response => response.text())
.then(result => {
let items = JSON.parse(result).items
self.repos = items
})
.catch(error => console.log('error', error));
}
}
})
</script>

View file

@ -22,7 +22,7 @@
<hr>
<div class="row">
<div class="col">
<h4>{{ loaded.attributes.listenTimeInMinutes }} {{$root.getLz('term.time.minutes')}}</h4>
<h4>{{ convertToHours(loaded.attributes.listenTimeInMinutes) }} {{$root.getLz('term.time.hours')}}</h4>
<h4>{{ loaded.attributes.uniqueAlbumCount }} {{$root.getLz('term.uniqueAlbums')}}</h4>
<h4>{{ loaded.attributes.uniqueArtistCount }} {{$root.getLz('term.uniqueArtists')}}</h4>
<h4>{{ loaded.attributes.uniqueSongCount }} {{$root.getLz('term.uniqueSongs')}}</h4>
@ -40,7 +40,7 @@
<mediaitem-square :item="artistData.relationships.artist.data[0]"></mediaitem-square>
</div>
<div class="card-footer">
{{ artistData.attributes.listenTimeInMinutes }} {{$root.getLz('term.time.minutes', {'count': artistData.attributes.listenTimeInMinutes})}}<br>
{{ convertToHours(artistData.attributes.listenTimeInMinutes) }} {{$root.getLz('term.time.hours', {'count': convertToHours(artistData.attributes.listenTimeInMinutes) })}}<br>
{{$root.getLz('term.listenedTo')}} {{ artistData.attributes.playCount }} {{$root.getLz('term.times')}}
</div>
</div>
@ -55,7 +55,7 @@
<mediaitem-square :item="albumData.relationships.album.data[0]"></mediaitem-square>
</div>
<div class="card-footer">
{{ albumData.attributes.listenTimeInMinutes }} {{$root.getLz('term.time.minutes', {'count': albumData.attributes.listenTimeInMinutes})}}<br>
{{ convertToHours(albumData.attributes.listenTimeInMinutes) }} {{$root.getLz('term.time.hours', {'count': convertToHours(albumData.attributes.listenTimeInMinutes)})}}<br>
{{ albumData.attributes.playCount }} {{$root.getLz('term.plays')}}
</div>
</div>
@ -151,6 +151,9 @@
let playlist = await app.mk.api.v3.music(replayData.relationships.playlist.data[0].href, {extend: "editorialArtwork,editorialVideo"})
replayData.playlist = playlist.data.data[0]
this.loaded = replayData
},
convertToHours(minutes) {
return Math.floor(minutes / 60)
}
}
});

View file

@ -441,6 +441,30 @@
</label>
</div>
</div>
<div class="md-option-line" v-show="app.cfg.advanced.AudioContext && app.cfg.audio.normalization">
<div class="md-option-segment">
dB SPL Display
<br>
<small>(Advanced users only) Display dB SPL instead of dBFS on the volume slider.</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<label>
<input type="checkbox" v-model="app.cfg.audio.dBSPL" switch/>
</label>
</div>
</div>
<div class="md-option-line" v-show="app.cfg.audio.dBSPL">
<div class="md-option-segment">
0 dBFS Calibration
<br>
<small>Enter the peak Z-weighted dB SPL when Cider is at 0 dBFS.</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<label>
<input type="number" v-model="app.cfg.audio.dBSPLcalibration"/>
</label>
</div>
</div>
</div>
</div>
</b-tab>
@ -456,20 +480,8 @@
{{$root.getLz('settings.header.visual.theme')}}
</div>
<div class="md-option-segment md-option-segment_auto">
<label>
<select class="md-select" @change="$root.setTheme($root.cfg.visual.theme)"
v-model="$root.cfg.visual.theme">
<option value="default.less">
{{$root.getLz('settings.option.visual.theme.default')}}
</option>
<option value="dark.less">{{$root.getLz('settings.option.visual.theme.dark')}}
</option>
<option v-for="theme in themes" :value="theme.file">{{ theme.name }}</option>
</select>
</label>
<button class="md-btn md-btn-small md-btn-block" @click="gitHubExplore()"
style="margin-top: 8px">
{{$root.getLz('settings.option.visual.theme.github.explore')}}
<button class="md-btn md-btn-block" @click="$root.appRoute('installed-themes')">
{{$root.getLz('settings.option.visual.theme.manageStyles')}}
</button>
</div>
</div>
@ -1192,19 +1204,6 @@
</button>
</div>
</div>
<div class="md-option-line">
<!-- Do not translate -->
<div class="md-option-segment">
Style Editor<br>
<small>Mix and match various theme components to get Cider looking exactly how you
want.</small>
</div>
<div class="md-option-segment">
<stylestack-editor :themes="themes"/>
</div>
</div>
<div class="md-option-line">
<div class="md-option-segment">
{{$root.getLz('settings.option.experimental.unknownPlugin')}}
@ -1412,112 +1411,6 @@
</div>
</script>
<script>
// do not translate
Vue.component('stylestack-editor', {
/*html*/
template: `
<div class="stylestack-editor">
<draggable class="list-group" v-model="$root.cfg.visual.styles" @end="$root.reloadStyles()">
<b-list-group-item variant="dark" v-for="theme in $root.cfg.visual.styles" :key="theme">
<b-row>
<b-col class="themeLabel">{{getThemeName(theme)}}</b-col>
<b-col sm="auto">
<button class="removeItem codicon codicon-close" @click="remove(theme)"></button>
</b-col>
</b-row>
</b-list-group-item>
<b-list-group-item slot="footer" style="-webkit-user-drag: none" variant="dark">
<b-row>
<b-col>
<b-dropdown class="stylesDropdown" variant="primary" text="Add Style...">
<b-dropdown-item v-for="theme in themeList" @click="addStyle(theme.file)">{{theme.name}}</b-dropdown-item>
</b-dropdown>
</b-col>
<b-col>
<b-btn @click="gitHubExplore()">{{$root.getLz('settings.option.visual.theme.github.explore')}}</b-btn>
</b-col>
</b-row>
</b-list-group-item>
</draggable>
</div>
`,
props: {
themes: {
type: Array,
default: []
}
},
data: function () {
return {
selected: null,
newTheme: null,
themeList: []
}
},
mounted() {
this.themeList = [...this.themes]
this.themeList.unshift({
name: "Acrylic Grain",
file: "grain.less"
})
this.themeList.unshift({
name: "Sweetener",
file: "sweetener.less"
})
this.themeList.unshift({
name: "Reduce Visuals",
file: "reduce_visuals.less"
})
this.themeList.unshift({
name: "Inline Drawer",
file: "inline_drawer.less"
})
},
methods: {
gitHubExplore() {
this.$root.appRoute("themes-github")
},
getThemeName(filename) {
try {
return this.themeList.find(theme => theme.file === filename).name;
} catch (e) {
return filename;
}
},
moveUp() {
const styles = this.$root.cfg.visual.styles
const index = styles.indexOf(this.selected)
if (index > 0) {
styles.splice(index, 1)
styles.splice(index - 1, 0, this.selected)
}
this.$root.reloadStyles()
},
moveDown() {
const styles = this.$root.cfg.visual.styles
const index = styles.indexOf(this.selected)
if (index < styles.length - 1) {
styles.splice(index, 1)
styles.splice(index + 1, 0, this.selected)
}
this.$root.reloadStyles()
},
remove(style) {
const styles = this.$root.cfg.visual.styles
const index = styles.indexOf(style)
styles.splice(index, 1)
this.$root.reloadStyles()
},
addStyle(style) {
const styles = this.$root.cfg.visual.styles
styles.push(style)
this.$root.reloadStyles()
}
}
})
</script>
<script>
Vue.component('cider-settings', {
template: "#cider-settings",
@ -1629,10 +1522,10 @@
app.cfg.general.keybindings.browse = [app.platform == "darwin" ? "Command" : "Control", "B"];
app.cfg.general.keybindings.togglePrivateSession = [app.platform == "darwin" ? "Command" : "Control", "P"];
app.cfg.general.keybindings.webRemote = [app.platform == "darwin" ? "Command" : "Control", "W"];
app.cfg.general.keybindings.audioSettings = [app.platform == "darwin" ? "Option" : "Shift", "A"];
app.cfg.general.keybindings.pluginMenu = [app.platform == "darwin" ? "Option" : "Shift", "P"];
app.cfg.general.keybindings.castToDevices = [app.platform == "darwin" ? "Option" : "Shift", "C"];
app.cfg.general.keybindings.settings = [app.platform == "darwin" ? "Option" : "Shift", "S"];
app.cfg.general.keybindings.audioSettings = [app.platform == "darwin" ? "Option" : "Alt", "A"];
app.cfg.general.keybindings.pluginMenu = [app.platform == "darwin" ? "Option" : "Alt", "P"];
app.cfg.general.keybindings.castToDevices = [app.platform == "darwin" ? "Option" : "Alt", "C"];
app.cfg.general.keybindings.settings = [app.platform == "darwin" ? "Option" : "Alt", "S"];
app.cfg.general.keybindings.openDeveloperTools = [app.platform == "darwin" ? "Command" : "Control", app.platform == "darwin" ? "Option" : "Shift", "I"];
notyf.success(app.getLz('settings.notyf.general.keybindings.update.success'));
bootbox.confirm(app.getLz("settings.prompt.general.keybindings.update.success"), (ok) => {

View file

@ -5,17 +5,14 @@
<div class="col nopadding">
<h1 class="header-text">{{$root.getLz('settings.header.visual.theme.github.page')}}</h1>
</div>
<div class="col-auto flex-center">
<select class="md-select" @change="$root.setTheme($root.cfg.visual.theme)"
v-model="$root.cfg.visual.theme">
<option value="default.less">{{$root.getLz('settings.option.visual.theme.default')}}</option>
<option value="dark.less">{{$root.getLz('settings.option.visual.theme.dark')}}</option>
<option v-for="theme in themes" :value="theme.file">{{ theme.name }}</option>
</select>
<div class="col-auto nopadding flex-center">
<button class="md-btn md-btn-small md-btn-block" @click="$root.appRoute('installed-themes')">
{{$root.getLz('settings.option.visual.theme.manageStyles')}}
</button>
</div>
<div class="col-auto flex-center">
<button class="md-btn md-btn-small md-btn-block" @click="openThemesFolder()">
{{$root.getLz('settings.option.visual.theme.github.openfolder')}}
<button class="md-btn md-btn-small md-btn-block" @click="$root.checkForThemeUpdates()">
Check for updates
</button>
</div>
<div class="col-auto nopadding flex-center">
@ -106,7 +103,6 @@
this.themes = ipcRenderer.sendSync("get-themes")
this.getRepos();
this.getInstalledThemes();
app.checkForThemeUpdates()
},
methods: {
openThemesFolder() {