diff --git a/.gitignore b/.gitignore index e1bcb518..e2efe76a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ dist yarn* package-lock.json .yarnclean +build # Misc .idea @@ -307,4 +308,10 @@ GitHub.sublime-settings #Service Worker mappings src/renderer/sw.js.map -src/renderer/workbox-962786f2.js.map \ No newline at end of file +src/renderer/workbox-962786f2.js.map +/src/renderer/musickit-dev.js + +#Mac certs +*.p12 +keys.sh + diff --git a/Assets/Cider with text.svg b/Assets/Cider with text.svg new file mode 100644 index 00000000..81052080 --- /dev/null +++ b/Assets/Cider with text.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + Cider + + + diff --git a/Assets/Release.svg b/Assets/Release.svg new file mode 100644 index 00000000..f27dc45d --- /dev/null +++ b/Assets/Release.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Assets/Sources/Cider with text.afdesign b/Assets/Sources/Cider with text.afdesign new file mode 100644 index 00000000..ff9e4ee5 Binary files /dev/null and b/Assets/Sources/Cider with text.afdesign differ diff --git a/Assets/Sources/Release.afdesign b/Assets/Sources/Release.afdesign new file mode 100644 index 00000000..b6484806 Binary files /dev/null and b/Assets/Sources/Release.afdesign differ diff --git a/Assets/Sources/cider-vinyl no raster.afdesign b/Assets/Sources/cider-vinyl no raster.afdesign new file mode 100644 index 00000000..64d895bd Binary files /dev/null and b/Assets/Sources/cider-vinyl no raster.afdesign differ diff --git a/Assets/Sources/cider-vinyl-no raster 2.afdesign b/Assets/Sources/cider-vinyl-no raster 2.afdesign new file mode 100644 index 00000000..dbc8db8f Binary files /dev/null and b/Assets/Sources/cider-vinyl-no raster 2.afdesign differ diff --git a/Assets/Sources/cider-vinyl.afdesign b/Assets/Sources/cider-vinyl.afdesign new file mode 100644 index 00000000..ba1f0da0 Binary files /dev/null and b/Assets/Sources/cider-vinyl.afdesign differ diff --git a/Assets/Vinyl Logo/cider-vinyl no raster.svg b/Assets/Vinyl Logo/cider-vinyl no raster.svg new file mode 100644 index 00000000..5077ad35 --- /dev/null +++ b/Assets/Vinyl Logo/cider-vinyl no raster.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets/Vinyl Logo/cider-vinyl textured.svg b/Assets/Vinyl Logo/cider-vinyl textured.svg new file mode 100644 index 00000000..f115c926 --- /dev/null +++ b/Assets/Vinyl Logo/cider-vinyl textured.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/index.js b/index.js deleted file mode 100644 index 2d02b3a0..00000000 --- a/index.js +++ /dev/null @@ -1,262 +0,0 @@ -require('v8-compile-cache'); -const { app, components } = require('electron'), { resolve, join } = require("path"), - CiderBase = require('./src/main/cider-base'); -const customProtocols = require('./package.json').fileAssociations[0].protocols -console.log(customProtocols) -const comps = components; - - -// Analytics for debugging. -const ElectronSentry = require("@sentry/electron"); -ElectronSentry.init({ dsn: "https://68c422bfaaf44dea880b86aad5a820d2@o954055.ingest.sentry.io/6112214" }); - -const configDefaults = { - "general": { - "close_behavior": 0, // 0 = close, 1 = minimize, 2 = minimize to tray - "startup_behavior": 0, // 0 = nothing, 1 = open on startup - "discord_rpc": 1, // 0 = disabled, 1 = enabled as Cider, 2 = enabled as Apple Music - "discordClearActivityOnPause": 1, // 0 = disabled, 1 = enabled - "volume": 1 - }, - "home": { - "followedArtists": [], - "favoriteItems": [] - }, - "libraryPrefs": { - "songs": { - "sort": "name", - "sortOrder": "asc", - "size": "normal" - } - }, - "audio": { - "quality": "990", - "seamless_audio": true, - "normalization": false, - "spatial": false, - "spatial_properties": { - "presets": [], - "gain": 0.8, - "listener_position": [0, 0, 0], - "audio_position": [0, 0, 0], - "room_dimensions": { - "width": 32, - "height": 12, - "depth": 32 - }, - "room_materials": { - "left": 'metal', - "right": 'metal', - "front": 'brick-bare', - "back": 'brick-bare', - "down": 'acoustic-ceiling-tiles', - "up": 'acoustic-ceiling-tiles', - } - } - }, - "visual": { - "theme": "", - "scrollbars": 0, // 0 = show on hover, 2 = always hide, 3 = always show - "refresh_rate": 0, - "animated_artwork": "limited", // 0 = always, 1 = limited, 2 = never - "animated_artwork_qualityLevel": 1, - "bg_artwork_rotation": false, - "hw_acceleration": "default", // default, webgpu, disabled - "videoRes": 720 - }, - "lyrics": { - "enable_mxm": false, - "mxm_karaoke": false, - "mxm_language": "en", - "enable_yt": false, - }, - "lastfm": { - "enabled": false, - "scrobble_after": 50, - "auth_token": "", - "enabledRemoveFeaturingArtists": true, - "NowPlaying": "true" - }, - "advanced": { - "AudioContext": false, - "experiments": [] - } -} - -const merge = (target, source) => { - // Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties - for (const key of Object.keys(source)) { - if (source[key] instanceof Object) Object.assign(source[key], merge(target[key], source[key])) - } - // Join `target` and modified `source` - Object.assign(target || {}, source) - return target -} - - -const Store = require("electron-store"); -app.cfg = new Store({ - defaults: configDefaults -}); -let currentCfg = app.cfg.get() -app.cfg.set(merge(configDefaults, currentCfg)) - -app.paths = { - ciderCache: resolve(app.getPath("userData"), "CiderCache"), - themes: resolve(app.getPath("userData"), "Themes"), - plugins: resolve(app.getPath("userData"), "Plugins"), -} - -switch (app.cfg.get("visual.hw_acceleration")) { - default: - case "default": - app.commandLine.appendSwitch('enable-accelerated-mjpeg-decode') - app.commandLine.appendSwitch('enable-accelerated-video') - app.commandLine.appendSwitch('disable-gpu-driver-bug-workarounds') - app.commandLine.appendSwitch('ignore-gpu-blacklist') - app.commandLine.appendSwitch('enable-native-gpu-memory-buffers') - app.commandLine.appendSwitch('enable-accelerated-video-decode'); - app.commandLine.appendSwitch('enable-gpu-rasterization'); - app.commandLine.appendSwitch('enable-native-gpu-memory-buffers'); - app.commandLine.appendSwitch('enable-oop-rasterization'); - break; - case "webgpu": - console.info("WebGPU is enabled."); - app.commandLine.appendSwitch('enable-unsafe-webgpu') - break; - case "disabled": - console.info("Hardware acceleration is disabled."); - app.commandLine.appendSwitch('disable-gpu') - break; -} - -// Creating the Application Window and Calling all the Functions -function CreateWindow() { - if (app.isQuiting) { - app.quit(); - return; - } - - /** CIDER **/ - const ciderwin = require("./src/main/cider-base") - app.win = ciderwin - app.win.Start() - /** CIDER **/ -} - -if (process.platform === "linux") { - app.commandLine.appendSwitch('disable-features', 'MediaSessionService'); -} - -app.commandLine.appendSwitch('no-sandbox'); -// app.commandLine.appendSwitch('js-flags', '--max-old-space-size=1024') - -/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - * App Event Handlers - * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ - -app.whenReady().then(async() => { - if (process.platform === "win32") { - app.commandLine.appendSwitch('high-dpi-support', 'true') - app.commandLine.appendSwitch('force-device-scale-factor', '1') - app.commandLine.appendSwitch('disable-pinch'); - } - if (comps == null) { - app.on("widevine-ready", () => { - console.log('[Cider] Application is Ready. Creating Window.') - if (!app.isPackaged) { - console.info('[Cider] Running in development mode.') - require('vue-devtools').install() - } - CreateWindow() - }) - return - } - await comps.whenReady(); - console.log('components ready:', comps.status()); - - console.log('[Cider] Application is Ready. Creating Window.') - if (!app.isPackaged) { - console.info('[Cider] Running in development mode.') - require('vue-devtools').install() - } - CreateWindow() -}) - - -app.on('before-quit', () => { - console.warn(`${app.getName()} exited.`); -}); - -// Widevine Stuff -app.on('widevine-ready', (version, lastVersion) => { - if (null !== lastVersion) { - console.log('[Cider][Widevine] Widevine ' + version + ', upgraded from ' + lastVersion + ', is ready to be used!') - } else { - console.log('[Cider][Widevine] Widevine ' + version + ' is ready to be used!') - } -}) - -app.on('widevine-update-pending', (currentVersion, pendingVersion) => { - console.log('[Cider][Widevine] Widevine ' + currentVersion + ' is ready to be upgraded to ' + pendingVersion + '!') -}) - -app.on('widevine-error', (error) => { - console.log('[Cider][Widevine] Widevine installation encountered an error: ' + error) - app.exit() -}) - -if (process.defaultApp) { - if (process.argv.length >= 2) { - customProtocols.forEach((customProtocol) => { - app.setAsDefaultProtocolClient(customProtocol, process.execPath, [resolve(process.argv[1])]) - }) - } -} else { - /* - * cider - Custom Cider Protocol - * ame - Custom AME Protocol (Backwards Compat.) - * itms - iTunes HTTP Protocol - * itmss - iTunes HTTPS Protocol - * musics - macOS Client Protocol - * music - macOS Client Protocol - */ - customProtocols.forEach((customProtocol) => { - app.setAsDefaultProtocolClient(customProtocol) - }) - -} - -app.on('open-url', (event, url) => { - event.preventDefault() - if (customProtocols.some(protocol => url.includes(protocol))) { - CiderBase.LinkHandler(url) - } -}) - -app.on('second-instance', (_e, argv) => { - console.warn(`[InstanceHandler][SecondInstanceHandler] Second Instance Started with args: [${argv.join(', ')}]`) - - // Checks if first instance is authorized and if second instance has protocol args - argv.forEach((value) => { - if (customProtocols.some(protocol => value.includes(protocol))) { - CiderBase.LinkHandler(value); - } - }) - - if (argv.includes("--force-quit")) { - console.warn('[InstanceHandler][SecondInstanceHandler] Force Quit found. Quitting App.'); - // app.isQuiting = true - app.quit() - } else if (CiderBase.win && true) { // If a Second Instance has Been Started - console.warn('[InstanceHandler][SecondInstanceHandler] Showing window.'); - app.win.show() - app.win.focus() - } -}) - -if (!app.requestSingleInstanceLock() && true) { - console.warn("[InstanceHandler] Existing Instance is Blocking Second Instance."); - app.quit(); - // app.isQuiting = true -} \ No newline at end of file diff --git a/package.json b/package.json index 2f0d7fa1..2ffa4a1d 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,9 @@ "productName": "Cider", "version": "1.0.0", "description": "A new look into listening and enjoying music in style and performance.", - "license": "MIT", - "author": "Cider Collective (https://cider.sh)", + "license": "AGPL-3.0", + "main": "./build/index.js", + "author": "Cider Collective (https://cider.sh)", "repository": "https://github.com/ciderapp/Cider.git", "bugs": { "url": "https://github.com/ciderapp/Cider/issues?q=is%3Aopen+is%3Aissue+label%3Abug" @@ -13,10 +14,14 @@ "homepage": "https://cider.sh/", "buildResources": "resources", "scripts": { - "init": "yarn install --force", - "start": "electron . --enable-accelerated-mjpeg-decode --enable-accelerated-video --disable-gpu-driver-bug-workarounds --ignore-gpu-blacklist --enable-native-gpu-memory-buffers", + "build": "tsc", + "watch": "tsc --watch", + "start": "run-script-os", + "start:win32": "npm run build && set ELECTRON_ENABLE_LOGGING=true && electron ./build/index.js --enable-accelerated-mjpeg-decode --enable-accelerated-video --disable-gpu-driver-bug-workarounds --ignore-gpu-blacklist --enable-native-gpu-memory-buffers", + "start:linux": "npm run build && export ELECTRON_ENABLE_LOGGING=true && electron ./build/index.js --enable-accelerated-mjpeg-decode --enable-accelerated-video --disable-gpu-driver-bug-workarounds --ignore-gpu-blacklist --enable-native-gpu-memory-buffers", + "start:darwin": "npm run build && export ELECTRON_ENABLE_LOGGING=true && electron ./build/index.js --enable-accelerated-mjpeg-decode --enable-accelerated-video --disable-gpu-driver-bug-workarounds --ignore-gpu-blacklist --enable-native-gpu-memory-buffers", "pack": "electron-builder --dir", - "dist": "electron-builder", + "dist": "npm run build && electron-builder", "msft": "electron-builder -c msft-package.json", "postinstall": "electron-builder install-app-deps" }, @@ -25,29 +30,39 @@ "discord-rpc": "^4.0.1", "ejs": "^3.1.6", "electron-fetch": "^1.7.4", - "electron-log": "^4.4.3", + "electron-log": "^4.4.4", + "electron-notarize": "^1.1.1", + "electron-packager": "^15.4.0", "electron-store": "^8.0.1", "electron-updater": "^4.6.1", "electron-window-state": "^5.0.3", "express": "^4.17.2", "get-port": "^5.1.1", + "jsonc": "^2.0.0", "lastfmapi": "^0.1.1", + "mdns-js": "github:bitfocus/node-mdns-js", "mpris-service": "^2.1.2", "music-metadata": "^7.11.4", + "qrcode": "^1.5.0", "qrcode-terminal": "^0.12.0", "react": "^17.0.2", "react-dom": "^17.0.2", + "run-script-os": "^1.1.6", "source-map-support": "^0.5.21", "v8-compile-cache": "^2.3.0", - "ws": "^8.3.0", + "ws": "^8.4.2", "xml2js": "^0.4.23", "youtube-search-without-api-key": "^1.0.7" }, "devDependencies": { + "@types/discord-rpc": "^4.0.0", + "@types/express": "^4.17.13", + "@types/ws": "^8.2.2", "electron": "https://github.com/castlabs/electron-releases.git", "electron-builder": "^22.14.5", "electron-webpack": "^2.8.2", "musickit-typescript": "^1.2.4", + "typescript": "^4.5.4", "vue-devtools": "^5.1.4", "webpack": "~5.65.0" }, @@ -74,12 +89,14 @@ } ], "build": { - "electronVersion": "16.0.6", + "electronVersion": "16.0.7", "electronDownload": { - "version": "16.0.6+wvcus", + "version": "16.0.7+wvcus", "mirror": "https://github.com/castlabs/electron-releases/releases/download/v" }, "appId": "cider", + "afterPack": "./resources/afterPack.js", + "afterSign": "./resources/notarize.js", "protocols": [ { "name": "Cider", @@ -95,9 +112,9 @@ ], "extends": null, "files": [ - "**/*", - "./src/**/*", - "./resources/icons/icon.*" + "./build/**/*", + "./resources/icons/icon.*", + "./src/**/*" ], "linux": { "target": [ @@ -119,6 +136,13 @@ "backgroundColor": "transparent", "setBuildNumber": true }, + "nsis": { + "oneClick": false, + "perMachine": false, + "allowToChangeInstallationDirectory": true, + "license": "LICENSE", + "deleteAppDataOnUninstall": true + }, "win": { "target": [ "nsis" @@ -126,13 +150,20 @@ "icon": "resources/icons/icon.ico" }, "directories": { - "buildResources": "." + "buildResources": ".", + "output": "dist" }, "mac": { + "hardenedRuntime": true, + "gatekeeperAssess": false, "icon": "./resources/icons/icon.icns", "category": "public.app-category.music", - "entitlements": "resources/entitlements.mac.plist", - "darkModeSupport": true + "entitlements": "./resources/entitlements.mac.plist", + "entitlementsInherit": "./resources/entitlements.mac.plist", + "darkModeSupport": true, + "target": [ + "dmg" + ] } } } diff --git a/resources/afterPack.js b/resources/afterPack.js new file mode 100644 index 00000000..6c677c49 --- /dev/null +++ b/resources/afterPack.js @@ -0,0 +1,18 @@ +exports.default = function(context) { + const { execSync } = require('child_process') + + if (process.platform !== 'darwin') + return + + console.log('Castlabs-evs update start') + execSync('python3 -m pip install --upgrade castlabs-evs') + console.log('Castlabs-evs update complete') + + + + console.log('VMP signing start') + + execSync('python3 -m castlabs_evs.vmp -n sign-pkg dist/mac',{stdio: 'inherit'}) + + console.log('VMP signing complete') +} \ No newline at end of file diff --git a/resources/entitlements.mac.plist b/resources/entitlements.mac.plist index 8fff4988..fcb8d4a3 100644 --- a/resources/entitlements.mac.plist +++ b/resources/entitlements.mac.plist @@ -2,11 +2,13 @@ + + com.apple.security.cs.allow-jit + com.apple.security.cs.allow-unsigned-executable-memory + com.apple.security.cs.disable-library-validation - com.apple.security.cs.allow-dyld-environment-variables - - + \ No newline at end of file diff --git a/resources/icons/icon-test.ico b/resources/icons/icon-test.ico new file mode 100644 index 00000000..d6fd03df Binary files /dev/null and b/resources/icons/icon-test.ico differ diff --git a/resources/icons/icon.ico b/resources/icons/icon.ico index f3f78e8a..d4a7449e 100644 Binary files a/resources/icons/icon.ico and b/resources/icons/icon.ico differ diff --git a/resources/notarize.js b/resources/notarize.js new file mode 100644 index 00000000..9e98e18c --- /dev/null +++ b/resources/notarize.js @@ -0,0 +1,20 @@ + + +require('dotenv').config(); + const { notarize } = require('electron-notarize'); + +exports.default = async function notarizing(context) { + const { electronPlatformName, appOutDir } = context; + if (electronPlatformName !== 'darwin') { + return; + } + + const appName = context.packager.appInfo.productFilename; + + return await notarize({ + appBundleId: 'com.ciderapp.cider', + appPath: `${appOutDir}/${appName}.app`, + appleId: process.env.APPLEID, + appleIdPassword: process.env.APPLEIDPASS, + }); +}; \ No newline at end of file diff --git a/src/i18n/README.md b/src/i18n/README.md new file mode 100644 index 00000000..fa5b5a67 --- /dev/null +++ b/src/i18n/README.md @@ -0,0 +1,8 @@ +# Cider i18n +Some notes about Cider's i18n support. +* Localization files are stored in jsonc format aka "JSON with Comments" +* The default language is English. +* The default language is used for messages that are not translated. +* Try when possible to keep the messages the similar in length to the English ones. +* Most of the strings in the content area are provided and translated by Apple themselves, and do not need to be translated. + * The language Apple Music uses are dependent on the storefront region. \ No newline at end of file diff --git a/src/i18n/el_GR.jsonc b/src/i18n/el_GR.jsonc new file mode 100644 index 00000000..783c1d10 --- /dev/null +++ b/src/i18n/el_GR.jsonc @@ -0,0 +1,279 @@ +{ + + // i18n Info + "i18n.languageName": "Ελληνικά", + "i18n.languageNameEnglish": "Greek", + "i18n.category": "main", + "i18n.authors": "@down-bad", + + // App info + "app.name": "Cider", + + "date.format": "${d} ${m}, ${y}", + + // Dialogs + "dialog.cancel": "Ακύρωση", + "dialog.ok": "ΟΚ", + + // Notification + "notification.updatingLibrarySongs": "Ενημέρωση βιβλιοθήκης τραγουδιών...", + "notification.updatingLibraryAlbums": "Ενημέρωση βιβλιοθήκης άλμπουμ...", + "notification.updatingLibraryArtists": "Ενημέρωση βιβλιοθήκης καλλιτεχνών...", + // Terms + "term.appleInc": "Apple Inc.", + "term.appleMusic": "Apple Music", + "term.applePodcasts": "Apple Podcasts", + "term.itunes": "iTunes", + "term.github": "GitHub", + "term.discord": "Discord", + "term.learnMore": "Μάθετε περισσότερα", + "term.accountSettings": "Ρυθμίσεις λογαριασμού", + "term.logout": "Αποσύνδεση", + "term.login": "Σύνδεση", + "term.about": "Σχετικά με", + "term.privateSession": "Ιδιωτική περίοδος λειτουργίας", + "term.queue": "Ουρά", + "term.search": "Εύρεση", + "term.library": "Βιβλιοθήκη", + "term.listenNow": "Ακρόαση", + "term.browse": "Περιήγηση", + "term.radio": "Ράδιο", + "term.recentlyAdded": "Πρόσφατες προσθήκες", + "term.songs": "Τραγούδια", + "term.albums": "Άλμπουμ", + "term.artists": "Καλλιτέχνες", + "term.podcasts": "Podcast", + "term.playlists": "Λίστες αναπαραγωγής", + "term.playlist": "Λίστα αναπαραγωγής", + "term.play": "Αναπαραγωγή", + "term.pause": "Παύση", + "term.previous": "Προηγούμενο", + "term.next": "Επόμενο", + "term.shuffle": "Τυχαία σειρά", + "term.repeat": "Επανάληψη", + "term.volume": "Ένταση", + "term.mute": "Σίγαση", + "term.unmute": "Κατάργηση σίγασης", + "term.share": "Κοινή Χρήση", + "term.settings": "Ρυθμίσεις", + "term.seeAll": "Προβολή όλων", + "term.sortBy": "Ταξινόμηση κατά", + "term.sortBy.album": "Άλμπουμ", + "term.sortBy.artist": "Καλλιτέχνη", + "term.sortBy.name": "Όνομα", + "term.sortBy.genre": "Είδος", + "term.sortBy.releaseDate": "Ημερομηνία κυκλοφορίας", + "term.sortBy.duration": "Διάρκεια", + "term.sortOrder": "Α-Ω", + "term.sortOrder.ascending": "Αύξουσα", + "term.sortOrder.descending": "Φθίνουσα", + "term.viewAs": "Προβολή ως", + "term.viewAs.coverArt": "Εξώφυλλο", + "term.viewAs.list": "Λίστα", + "term.size": "Μέγεθος", + "term.size.normal": "Κανονικό", + "term.size.compact": "Συμπαγή", + "term.enable": "Ενεργοποίηση", + "term.disable": "Απενεργοποίηση", + "term.enabled": "Ενεργοποιημένο", + "term.disabled": "Απενεργοποιημένο", + "term.connect": "Σύνδεση", + "term.connecting": "Γίνεται σύνδεση", + "term.disconnect": "Αποσύνδεση", + "term.authed": "Επικυρωμένο", + "term.confirm": "Σίγουρα;", + "term.more": "Περισσότερα", + "term.less": "Λιγότερα", + "term.showMore": "Εμφάνιση περισσότερων", + "term.showLess": "Εμφάνιση λιγότερων", + "term.topSongs" : "Κορυφαία τραγούδια", + "term.latestReleases": "Τελευταίες κυκλοφορίες", + "term.time.added": "Προστέθηκε", + "term.time.released": "Κυκλοφόρησε", + "term.time.updated": "Ενημερώθηκε", + "term.fullscreenView": "Πλήρης οθόνη", + "term.defaultView": "Κανονική οθόνη", + "term.spacializedAudioSetting": "Χωρική ρύθμιση ήχου", + "term.clearAll": "Εκκαθάριση όλων", + "term.recentStations": "Πρόσφατοι σταθμοί", + "term.language": "Γλώσσα", + "term.noLyrics": "Φόρτωση... / Δεν βρέθηκαν στίχοι. / Ορχηστικό.", + "term.copyright": "Copyright", + "term.rightsReserved": "Όλα τα δικαιώματα διατηρούνται.", + "term.sponsor": "Χορήγησε αυτό το έργο", + "term.ciderTeam": "Ομάδα Cider", + "term.developer": "Προγραμματιστής", + "term.socialTeam": "Κοινωνική Ομάδα", + "term.contributors": "Συνεισφέροντες", + "term.equalizer": "Ισοσταθμιστής", + "term.reset": "Επαναφορά", + "term.tracks": "τραγούδια", // Assume x amount of tracks. e.g. 50 tracks + + + // Home + "home.title": "Αρχική", + "home.recentlyPlayed": "Έπαιξαν πρόσφατα", + "home.recentlyAdded": "Πρόσφατες προσθήκες", + "home.artistsFeed": "Ροή των καλλιτεχνών σου", + "home.artistsFeed.noArtist": "Ακολούθησε μερικούς καλλιτέχνες πρώτα και οι τελευταίες κυκλοφορίες τους θα εμφανίζονται εδώ", + "home.madeForYou": "Δημιουργήθηκε για εσάς", + "home.friendsListeningTo": "Οι φίλοι σου ακούν", + "home.followedArtists": "Καλλιτέχνες που ακολουθείτε", + // Errors + "error.appleMusicSubRequired": "Το Apple Music απαιτεί μια συνδρομή.", + "error.connectionError": "Δεν είναι δυνατή η σύνδεση με το Apple Music.", + "error.noResults": "Κανένα αποτέλεσμα.", + "error.noResults.description": "Δοκιμάστε μια νέα αναζήτηση.", + + //Podcasts + "podcast.followOnCider": "Ακολούθηση στο Cider", + "podcast.followedOnCider": "Ακολουθείτε στο Cider", + "podcast.subscribeOnItunes": "Συνδρομή στο iTunes", + "podcast.subscribedOnItunes": "Συνδρομητής στο iTunes", + "podcast.itunesStore": "iTunes Store", + "podcast.episodes": "Επεισόδια", + "podcast.playEpisode": "Αναπαραγωγή επεισοδίου", + "podcast.website": "Ιστότοπος Podcast", + + // Actions + "action.addToLibrary": "Προσθήκη στη βιβλιοθήκη", + "action.addToLibrary.success": "Προστέθηκε στη βιβλιοθήκη", + "action.addToLibrary.error": "Σφάλμα Προσθήκης στη βιβλιοθήκης", + "action.removeFromLibrary": "Αφαίρεση από τη βιβλιοθήκη", + "action.removeFromLibrary.success": "Αφαιρέθηκε από τη βιβλιοθήκη", + "action.addToQueue": "Προσθήκη στην ουρά", + "action.addToQueue.success": "Προστέθηκε στην ουρά", + "action.addToQueue.error": "Προστέθηκε στην ουρά", + "action.removeFromQueue": "Αφαίρεση από την ουρά", + "action.removeFromQueue.success": "Αφαιρέθηκε από την ουρά", + "action.removeFromQueue.error": "Σφάλμα Αφαίρεσης από την ουρά", + "action.addToPlaylist": "Προσθήκη σε λίστα", + "action.removeFromPlaylist": "Αφαίρεση από λίστα", + "action.addToFavorites": "Προσθήκη στα αγαπημένα", + "action.follow": "Ακολούθηση", + "action.follow.success": "Ακολουθήθηκε", + "action.follow.error": "Σφάλμα ακολούθησης", + "action.unfollow": "Διακοπή ακολούθησης", + "action.unfollow.success": "Έγινε διακοπή ακολούθησης", + "action.unfollow.error": "Σφάλμα διακοπής ακολούθησης ", + "action.playNext": "Αναπαραγωγή ως επόμενου", + "action.playLater": "Αναπαραγωγή αργότερα", + "action.startRadio": "Έναρξη ραδιοφώνου", + "action.goToArtist": "Μετάβαση σε καλλιτέχνη", + "action.goToAlbum": "Μετάβαση σε άλμπουμ", + "action.moveToTop": "Μετακίνηση στη κορυφή", + "action.share": "Κοινή χρήση", + "action.rename": "Μετονομασία", + "action.love": "Μου αρέσει πολύ", + "action.unlove": "Αναίρεση \"Μου αρέσει\"", + "action.dislike": "Δεν μου αρέσει", + "action.undoDislike": "Αναίρεση \"Δεν μου αρέσει\"", + "action.showWebRemoteQR": "Εμφάνιση Web Remote QR", + "action.playTracksNext": "Αναπαραγωγή ${app.selectedMediaItems.length} τραγουδιών ως επόμενων", + "action.playTracksLater": "Αναπαραγωγή ${app.selectedMediaItems.length} τραγουδιών αργότερα", + "action.removeTracks": "Αφαίρεση ${self.selectedItems.length} τραγουδιών από την ουρά", + + // Settings - Audio + "settings.header.audio": "Ήχος", + "settings.header.audio.description": "Προσαρμογή ρυθμίσεων ήχου για το Cider.", + "settings.option.audio.quality": "Ποιότητα Ήχου", // Dropdown + "settings.header.audio.quality.high": "Υψηλή", + "settings.header.audio.quality.low": "Χαμηλή", + "settings.header.audio.quality.auto": "Αυτόματη", + "settings.option.audio.seamlessTransition": "Αδιάκοπη Μετάβαση Ήχου", // Toggle + "settings.option.audio.enableAdvancedFunctionality": "Ενεργοποίηση Προηγμένης Λειτουργικότητας", // Toggle + "settings.option.audio.enableAdvancedFunctionality.description": "Ενεργοποιώντας τη λειτουργικότητα AudioContext θα επιτρέψει σε επεκταμένες δυνατότητες ήχου όπως Κανονικοποίηση Έντασης Ήχου, Ισοσταθμιστές και Οπτικοποιητές, ωστόσο σε κάποια συστήματα μπορεί να προκαλέσει τραύλισμα ήχου.", + "settings.option.audio.enableAdvancedFunctionality.audioNormalization": "Κανονικοποίηση Έντασης Ήχου", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioNormalization.description": "Κανονικοποιεί την ένταση για μεμονωμένα κομμάτια για μια πιο ομοιόμορφη εμπειρία ακρόασης.", + "settings.option.audio.enableAdvancedFunctionality.audioSpatialization": "Χωρικοποίηση Ήχου", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioSpatialization.description": "Πιο τρισδιάστατος και χωρικοποιημένος ήχος (σημείωση: Αυτό δεν είναι Dolby Atmos)", + // Settings - Visual + "settings.header.visual": "Οπτικά", + "settings.header.visual.description": "Προσαρμογή οπτικών ρυθμίσεων για το Cider.", + "settings.option.visual.windowBackgroundStyle": "Στυλ Φόντου Παραθύρου", // Toggle + "settings.header.visual.windowBackgroundStyle.none": "Κανένα", + "settings.header.visual.windowBackgroundStyle.artwork": "Εξώφυλλο", + "settings.option.visual.animatedArtwork": "Κινούμενο Εξώφυλλο", // Dropdown + "settings.header.visual.animatedArtwork.always": "Πάντα", + "settings.header.visual.animatedArtwork.limited": "Περιορισμός σε σελίδες και ειδικές καταχωρήσεις", + "settings.header.visual.animatedArtwork.disable": "Απενεργοποιημένο παντού", + "settings.option.visual.animatedArtworkQuality": "Ποιότητα Κινούμενου Εξωφύλλου", // Dropdown + "settings.header.visual.animatedArtworkQuality.low": "Χαμηλή", + "settings.header.visual.animatedArtworkQuality.medium": "Μέτρια", + "settings.header.visual.animatedArtworkQuality.high": "Υψηλή", + "settings.header.visual.animatedArtworkQuality.veryHigh": "Πολύ Υψηλή", + "settings.header.visual.animatedArtworkQuality.extreme": "Ακραία", + "settings.option.visual.animatedWindowBackground": "Κινούμενο Φόντο Παραθύρου", // Toggle + "settings.option.visual.hardwareAcceleration": "Επιτάχυνση Υλικού", // Dropdown + "settings.option.visual.hardwareAcceleration.description": "Απαιτεί επανεκκίνηση", + "settings.header.visual.hardwareAcceleration.default": "Προεπιλογή", + "settings.header.visual.hardwareAcceleration.webGPU": "WebGPU", + // Refer to term.disabled for the disabled option + "settings.option.visual.showPersonalInfo": "Εμφάνιση Προσωπικών Στοιχείων", // Toggle + // Settings - General (Reserved) + "settings.header.general": "Γενικά", + "settings.header.general.description": "Προσαρμογή γενικών ρυθμίσεων για το Cider.", + + // Settings - Lyrics + "settings.header.lyrics": "Στίχοι", + "settings.header.lyrics.description": "Προσαρμογή ρυθμίσεων στίχων για το Cider.", + "settings.option.lyrics.enableMusixmatch": "Ενεργοποίηση Στίχων Musixmatch", // Toggle + "settings.option.lyrics.enableMusixmatchKaraoke": "Ενεργοποίηση Λειτουργίας Καραόκε (Musixmatch μόνο)", // Toggle + "settings.option.lyrics.musixmatchPreferredLanguage": "Προτιμώμενη Γλώσσα Μετάφρασης Musixmatch", // Dropdown + "settings.option.lyrics.enableYoutubeLyrics": "Ενεργοποίηση Στίχων Youtube για Μουσικά Βίντεο", // Toggle + + // Settings - Connectivity + "settings.header.connectivity": "Σύνδεση", + "settings.header.connectivity.description": "Προσαρμογή ρυθμίσεων σύνδεσης για το Cider.", + "settings.option.connectivity.discordRPC": "Discord Rich Presence", // Dropdown + // Refer to term.disabled for the disabled option + "settings.header.connectivity.discordRPC.cider": "Εμφάνιση ως 'Cider'", + "settings.header.connectivity.discordRPC.appleMusic": "Εμφάνιση ως 'Apple Music'", + "settings.option.connectivity.discordRPC.clearOnPause": "Εκκαθάριση του Discord Rich Presence στην Παύση", // Toggle + "settings.option.connectivity.lastfmScrobble": "LastFM Scrobbling", // Option to Connect + "settings.option.connectivity.lastfmScrobble.delay": "Καθυστέρηση LastFM Scrobble (%)", + "settings.option.connectivity.lastfmScrobble.nowPlaying": "Ενεργοποίηση LastFM \"Now Playing\"", + "settings.option.connectivity.lastfmScrobble.removeFeatured": "Αφαίρεση καλλιτεχνών feature από τον τίτλο του τραγουδιού (LastFM)", + "settings.option.connectivity.lastfmScrobble.filterLoop": "Φιλτράρισμα επανειλημμένου τραγουδιού (LastFM)", + // Refer to term.connect for the connect button + + // Settings - Experimental + "settings.header.experimental": "Πειραματικές", + "settings.header.experimental.description": "Προσαρμογή πειραματικών ρυθμίσεων για το Cider.", + "settings.option.experimental.compactUI": "Συμπαγής Διεπαφή", // Toggle + "settings.option.experimental.closeButtonBehaviour": "Συμπεριφορά Κουμπιού Εξόδου", + "settings.option.experimental.closeButtonBehaviour.quit": "Έξοδος του Cider", + "settings.option.experimental.closeButtonBehaviour.minimizeTaskbar": "Ελαχιστοποίηση στη γραμμή εργασιών", + "settings.option.experimental.closeButtonBehaviour.minimizeTray": "Ελαχιστοποίηση στη γωνία γραμμής εργασιών", + // Refer to term.disabled & term.enabled + + // Spatialization Menu + "spatial.spatialProperties" : "Χωρικές Ιδιότητες", + "spatial.width" : "Πλάτος", + "spatial.height" : "Ύψος", + "spatial.depth" : "Βάθος", + "spatial.gain" : "Απολαβή", + "spatial.roomMaterials" : "Υλικά Δωματίου", + "spatial.roomDimensions" : "Διαστάσεις Δωματίου", + "spatial.roomPositions" : "Θέσεις Δωματίου", + "spatial.setDimensions" : "Ορισμός Διαστάσεων", + "spatial.setPositions" : "Ορισμός Θέσεων", + "spatial.up" : "Πάνω", + "spatial.front" : "Πρόσοψη", + "spatial.left" : "Αριστερά", + "spatial.right" : "Δεξιά", + "spatial.back" : "Πίσω Όψη", + "spatial.down" : "Κάτω", + "spatial.listener" : "Ακροατής", + "spatial.audioSource" : "Πηγή Ήχου", + + // Settings - Unfinished + "settings.header.unfinished": "Ημιτελής", + + // Web Remote + "remote.web.title": "Cider Remote", + "remote.web.description": "Σαρώστε τον κωδικό QR για σύζευξη του Cider με το κινητό σας", + + //About + "about.thanks": "Μεγάλα ευχαριστώ στην Ομάδα Cider Collective και σε όλους τους συνεισφέροντές μας." +} \ No newline at end of file diff --git a/src/i18n/en_GB.jsonc b/src/i18n/en_GB.jsonc new file mode 100644 index 00000000..ac64c887 --- /dev/null +++ b/src/i18n/en_GB.jsonc @@ -0,0 +1,10 @@ +{ + // i18n Info + "i18n.languageName": "English (UK)", // name of language in native language + "i18n.languageNameEnglish": "English (UK)", // name of language in English + "i18n.category": "main", // main = real language, fun = fun community languages + "i18n.authors": "", // Authors, if you contribute to this file feel free to add your name seperated with a space + "date.format": "${d} ${m}, ${y}", + + "home.friendsListeningTo": "Bruv's Listening To" +} \ No newline at end of file diff --git a/src/i18n/en_HODOR.jsonc b/src/i18n/en_HODOR.jsonc new file mode 100644 index 00000000..27498d2c --- /dev/null +++ b/src/i18n/en_HODOR.jsonc @@ -0,0 +1,291 @@ +{ // HODOR + + // i18n Info + "i18n.languageName": "HODOR", // name of language in native language + "i18n.languageNameEnglish": "HODOR", // name of language in English + "i18n.category": "fun", // main = real language, fun = fun community languages + "i18n.authors": "HODOR", // Authors, if you contribute to this file feel free to add your name seperated with a space + + // App info + "app.name": "HODOR", + + "date.format": "${m} ${d}, ${y}", + + // Dialogs + "dialog.cancel": "HODOR", + "dialog.ok": "HODOR", + + // Notification + "notification.updatingLibrarySongs": "HODOR HODOR HODOR...", + "notification.updatingLibraryAlbums": "HODOR HODOR HODOR...", + "notification.updatingLibraryArtists": "HODOR HODOR HODOR...", + // Terms + "term.appleInc": "HODOR Inc.", + "term.appleMusic": "HODOR HODOR", + "term.applePodcasts": "HODOR HODOR", + "term.itunes": "HODOR", + "term.github": "HODOR", + "term.discord": "HODOR", + "term.learnMore": "HODOR HODOR", + "term.accountSettings": "HODOR HODOR", + "term.logout": "HODOR", + "term.login": "HODOR", + "term.about": "HODOR", + "term.privateSession": "HODOR HODOR", + "term.queue": "HODOR", + "term.search": "HODOR", + "term.library": "HODOR", + "term.listenNow": "HODOR HODOR", + "term.browse": "HODOR", + "term.radio": "HODOR", + "term.recentlyAdded": "HODOR HODOR", + "term.songs": "HODOR", + "term.albums": "HODOR", + "term.artists": "HODOR", + "term.podcasts": "HODOR", + "term.playlists": "HODOR", + "term.playlist": "HODOR", + "term.play": "HODOR", + "term.pause": "HODOR", + "term.previous": "HODOR", + "term.next": "HODOR", + "term.shuffle": "HODOR", + "term.repeat": "HODOR", + "term.volume": "HODOR", + "term.mute": "HODOR", + "term.unmute": "HODOR", + "term.share": "HODOR", + "term.settings": "HODOR", + "term.seeAll": "HODOR HODOR", + "term.sortBy": "HODOR HODOR", + "term.sortBy.album": "HODOR", + "term.sortBy.artist": "HODOR", + "term.sortBy.name": "HODOR", + "term.sortBy.genre": "HODOR", + "term.sortBy.releaseDate": "HODOR HODOR", + "term.sortBy.duration": "HODOR", + "term.sortOrder": "HODOR-HODOR", + "term.sortOrder.ascending": "HODOR", + "term.sortOrder.descending": "HODOR", + "term.viewAs": "HODOR HODOR", + "term.viewAs.coverArt": "HODOR HODOR", + "term.viewAs.list": "HODOR", + "term.size": "HODOR", + "term.size.normal": "HODOR", + "term.size.compact": "HODOR", + "term.enable": "HODOR", + "term.disable": "HODOR", + "term.enabled": "HODOR", + "term.disabled": "HODOR", + "term.connect": "HODOR", + "term.connecting": "HODOR", + "term.disconnect": "HODOR", + "term.authed": "HODOR", + "term.confirm": "HODOR ?", + "term.more": "HODOR", + "term.less": "HODOR", + "term.showMore": "HODOR HODOR", + "term.showLess": "HODOR HODOR", + "term.topSongs" : "HODOR HODOR", + "term.latestReleases": "HODOR HODOR", + "term.time.added": "HODOR", + "term.time.released": "HODOR", + "term.time.updated": "HODOR", + "term.fullscreenView": "HODOR HODOR", + "term.defaultView": "HODOR HODOR", + "term.spacializedAudioSetting": "HODOR HODOR HODOR", + "term.clearAll": "HODOR HODOR", + "term.recentStations": "HODOR HODOR", + "term.language": "HODOR", + "term.noLyrics": "HODOR... / HODOR HODOR HODOR./ HODOR.", + "term.copyright": "HODOR", + "term.rightsReserved": "HODOR HODOR HODOR.", + "term.sponsor": "HODOR HODOR HODOR", + "term.ciderTeam": "HODOR HODOR", + "term.developer": "HODOR", + "term.socialTeam": "HODOR HODOR", + "term.contributors": "HODOR", + "term.equalizer": "HODOR", + "term.reset": "HODOR", + "term.tracks": "HODOR", // Assume x amount of tracks. e.g. 50 tracks + "term.time.hours": "HODOR", + "term.time.hour": "HODOR", + "term.time.minutes": "HODOR", + "term.time.minute": "HODOR", + "term.time.seconds": "HODOR", + "term.time.second": "HODOR", + "term.funLanguages": "HODOR", + + // Home + "home.title": "HODOR", + "home.recentlyPlayed": "HODOR HODOR", + "home.recentlyAdded": "HODOR HODOR", + "home.artistsFeed": "HODOR HODOR HODOR", + "home.artistsFeed.noArtist": "HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR", + "home.madeForYou": "HODOR HODOR HODOR", + "home.friendsListeningTo": "HODOR HODOR HODOR", + "home.followedArtists": "HODOR HODOR", + // Errors + "error.appleMusicSubRequired": "HODOR HODOR HODOR HODOR HODOR.", + "error.connectionError": "HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR.", + "error.noResults": "HODOR HODOR.", + "error.noResults.description": "HODOR HODOR HODOR HODOR.", + + //Podcasts + "podcast.followOnCider": "HODOR HODOR HODOR", + "podcast.followedOnCider": "HODOR HODOR HODOR", + "podcast.subscribeOnItunes": "HODOR HODOR HODOR", + "podcast.subscribedOnItunes": "HODOR HODOR HODOR", + "podcast.itunesStore": "HODOR HODOR", + "podcast.episodes": "HODOR", + "podcast.playEpisode": "HODOR HODOR", + "podcast.website": "HODOR HODOR", + + // Actions + "action.addToLibrary": "HODOR HODOR HODOR", + "action.addToLibrary.success": "HODOR HODOR HODOR", + "action.addToLibrary.error": "HODOR HODOR HODOR LiHODORbrary", + "action.removeFromLibrary": "HODOR HODOR HODOR", + "action.removeFromLibrary.success": "HODOR HODOR HODOR", + "action.addToQueue": "HODOR HODOR HODOR", + "action.addToQueue.success": "HODOR HODOR HODOR", + "action.addToQueue.error": "HODOR HODOR HODOR HODOR", + "action.removeFromQueue": "HODOR HODOR HODOR", + "action.removeFromQueue.success": "HODOR HODOR HODOR", + "action.removeFromQueue.error": "HODOR HODOR HODOR HODOR", + "action.addToPlaylist": "HODOR HODOR HODOR", + "action.removeFromPlaylist": "HODOR HODOR HODOR", + "action.addToFavorites": "HODOR HODOR HODOR", + "action.follow": "HODOR", + "action.follow.success": "HODOR", + "action.follow.error": "HODOR HODOR", + "action.unfollow": "HODOR", + "action.unfollow.success": "HODOR", + "action.unfollow.error": "HODOR HODOR", + "action.playNext": "HODOR HODOR", + "action.playLater": "HODOR HODOR", + "action.startRadio": "HODOR HODOR", + "action.goToArtist": "HODOR HODOR HODOR", + "action.goToAlbum": "HODOR HODOR HODOR", + "action.moveToTop": "HODOR HODOR HODOR", + "action.share": "HODOR", + "action.rename": "HODOR", + "action.love": "HODOR", + "action.unlove": "HODOR", + "action.dislike": "HODOR", + "action.undoDislike": "HODOR HODOR", + "action.showWebRemoteQR": "HODOR HODOR HODOR HODOR", + "action.playTracksNext": "HODOR ${app.selectedMediaItems.length} HODOR HODOR", + "action.playTracksLater": "HODOR ${app.selectedMediaItems.length} HODOR HODOR", + "action.removeTracks": "HODOR ${self.selectedItems.length} HODOR HODOR HODOR", + + // Settings - General (Reserved) + "settings.header.general": "HODOR HODOR", + "settings.header.general.description": "HODOR HODOR HODOR HODOR.", + "settings.option.general.language": "HODOR", + + // Language optgroups + "settings.option.general.language.main": "HODOR", + "settings.option.general.language.fun": "HODOR HODOR", + "settings.option.general.language.unsorted": "HODOR", + + // Settings - Audio + "settings.header.audio": "HODOR", + "settings.header.audio.description": "HODOR HODOR HODOR HODOR HODOR HODOR.", + "settings.option.audio.quality": "HODOR HODOR", // Dropdown + "settings.header.audio.quality.high": "HODOR.", + "settings.header.audio.quality.low": "HODOR!", + "settings.header.audio.quality.auto": "HODOR", + "settings.option.audio.seamlessTransition": "HODOR HODOR HODOR", // Toggle + "settings.option.audio.enableAdvancedFunctionality": "HODOR HODOR HODOR", // Toggle + "settings.option.audio.enableAdvancedFunctionality.description": "HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR , HODOR HODOR HODOR, HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR.", + "settings.option.audio.enableAdvancedFunctionality.audioNormalization": "HODOR HODOR", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioNormalization.description": "HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR.", + "settings.option.audio.enableAdvancedFunctionality.audioSpatialization": "HODOR HODOR", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioSpatialization.description": "HODOR HODOR HODOR HODOR HODOR HODOR 3-HODOR (HODOR: HODOR HODOR HODOR HODOR HODOR)", + // Settings - Visual + "settings.header.visual": "HODOR", + "settings.header.visual.description": "HODOR HODOR HODOR HODOR HODOR HODOR.", + "settings.option.visual.windowBackgroundStyle": "HODOR HODOR HODOR", // Toggle + "settings.header.visual.windowBackgroundStyle.none": "HODOR", + "settings.header.visual.windowBackgroundStyle.artwork": "HODOR", + "settings.option.visual.animatedArtwork": "HODOR HODOR", // Dropdown + "settings.header.visual.animatedArtwork.always": "HODOR", + "settings.header.visual.animatedArtwork.limited": "HODOR HODOR HODOR HODOR HODOR HODOR", + "settings.header.visual.animatedArtwork.disable": "HODOR HODOR", + "settings.option.visual.animatedArtworkQuality": "HODOR HODOR HODOR", // Dropdown + "settings.header.visual.animatedArtworkQuality.low": "HODOR..", + "settings.header.visual.animatedArtworkQuality.medium": "HODOR.", + "settings.header.visual.animatedArtworkQuality.high": "HODOR!", + "settings.header.visual.animatedArtworkQuality.veryHigh": "HODOR HODOR!", + "settings.header.visual.animatedArtworkQuality.extreme": "HODOOOR!!", + "settings.option.visual.animatedWindowBackground": "HODOR HODOR HODOR", // Toggle + "settings.option.visual.hardwareAcceleration": "HODOR HODOR", // Dropdown + "settings.option.visual.hardwareAcceleration.description": "HODOR HODOR", + "settings.header.visual.hardwareAcceleration.default": "HODOR.", + "settings.header.visual.hardwareAcceleration.webGPU": "HODOR!!", + // Refer to term.disabled for the disabled option + "settings.option.visual.showPersonalInfo": "HODOR HODOR HODOR?", // Toggle + + // Settings - Lyrics + "settings.header.lyrics": "HODOR", + "settings.header.lyrics.description": "HODOR HODOR HODOR HODOR HODOR HODOR.", + "settings.option.lyrics.enableMusixmatch": "HODOR HODOR HODOR", // Toggle + "settings.option.lyrics.enableMusixmatchKaraoke": "HODOR HODOR HODOR (HODOR HODOR)", // Toggle + "settings.option.lyrics.musixmatchPreferredLanguage": "HODOR HODOR HODOR HODOR", // Dropdown + "settings.option.lyrics.enableYoutubeLyrics": "HODOR HODOR HODOR HODOR HODOR HODOR", // Toggle + + // Settings - Connectivity + "settings.header.connectivity": "HODOR", + "settings.header.connectivity.description": "HODOR HODOR HODOR HODOR HODOR HODOR.", + "settings.option.connectivity.discordRPC": "HODOR HODOR HODOR", // Dropdown + // Refer to term.disabled for the disabled option + "settings.header.connectivity.discordRPC.cider": "HODOR HODOR 'HODOR'", + "settings.header.connectivity.discordRPC.appleMusic": "HODOR HODOR 'HODOR HODOR'", + "settings.option.connectivity.discordRPC.clearOnPause": "HODOR HODOR HODOR HODOR HODOR HODOR", // Toggle + "settings.option.connectivity.lastfmScrobble": "HODOR HODOR", // Option to Connect + "settings.option.connectivity.lastfmScrobble.delay": "HODOR HODOR HODOR (%)", + "settings.option.connectivity.lastfmScrobble.nowPlaying": "HODOR HODOR HODOR HODOR", + "settings.option.connectivity.lastfmScrobble.removeFeatured": "HODOR HODOR HODOR HODOR HODOR HODOR (HODOR)", + "settings.option.connectivity.lastfmScrobble.filterLoop": "HODOR HODOR HODOR (HODOR)", + // Refer to term.connect for the connect button + + // Settings - Experimental + "settings.header.experimental": "HODOR", + "settings.header.experimental.description": "HODOR HODOR HODOR HODOR HODOR HODOR.", + "settings.option.experimental.compactUI": "HODOR UI", // Toggle + "settings.option.experimental.closeButtonBehaviour": "HODOR HODOR HODOR", + "settings.option.experimental.closeButtonBehaviour.quit": "HODOR HODOR", + "settings.option.experimental.closeButtonBehaviour.minimizeTaskbar": "HODOR HODOR HODOR", + "settings.option.experimental.closeButtonBehaviour.minimizeTray": "HODOR HODOR HODOR HODOR", + // Refer to term.disabled & term.enabled + + // Spatialization Menu + "spatial.spatialProperties" : "HODOR HODOR", + "spatial.width" : "HODOR", + "spatial.height" : "HODOR", + "spatial.depth" : "HODOR", + "spatial.roomMaterials" : "HODOR HODOR", + "spatial.roomDimensions" : "HODOR HODOR", + "spatial.roomPositions" : "HODOR HODOR", + "spatial.setDimensions" : "HODOR HODOR", + "spatial.setPositions" : "HODOR HODOR", + "spatial.up" : "HODOR", + "spatial.front" : "HODOR", + "spatial.left" : "HODOR", + "spatial.right" : "HODOR", + "spatial.back" : "HODOR", + "spatial.down" : "HODOR", + "spatial.listener" : "HODOR", + "spatial.audioSource" : "HODOR HODOR", + + // Settings - Unfinished + "settings.header.unfinished": "HODOR", + + // Web Remote + "remote.web.title": "HODOR HODOR", + "remote.web.description": "HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR", + + //About + "about.thanks": "HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR HODOR." +} \ No newline at end of file diff --git a/src/i18n/en_SGA.jsonc b/src/i18n/en_SGA.jsonc new file mode 100644 index 00000000..c5468e07 --- /dev/null +++ b/src/i18n/en_SGA.jsonc @@ -0,0 +1,279 @@ +{ + + // i18n Info + "i18n.languageName": "┤ᖋ|:ᖋᔮᒣ╎ᔮ ᖋ|:i!⍑ᖋᕊᒷᒣ", // name of language in native language + "i18n.languageNameEnglish": "Galactic Alphabet", // name of language in English + "i18n.category": "fun", // main = real language, fun = fun community languages + "i18n.authors": "@kyw504100", // Authors, if you contribute to this file feel free to add your name seperated with a space + + // App info + "app.name": "ᔮ╎↸ᒷ∷", + + "date.format": "${m} ${d}, ${y}", + + // Dialogs + "dialog.cancel": "ᔮᖋリᔮᒷ|:", + "dialog.ok": "ᒍ·ǀ·", + + // Notification + "notification.updatingLibrarySongs": "⚍i!↸ᖋᒣ╎リ┤ |:╎ᕊ∷ᖋ∷॥ ϟᒍリ┤ϟ...", + "notification.updatingLibraryAlbums": "⚍i!↸ᖋᒣ╎リ┤ |:╎ᕊ∷ᖋ∷॥ ᖋ|:ᕊ⚍ᒲϟ...", + "notification.updatingLibraryArtists": "⚍i!↸ᖋᒣ╎リ┤ |:╎ᕊ∷ᖋ∷॥ ᖋ∷ᒣ╎ϟᒣϟ...", + // Terms + "term.appleInc": "ᖋi!i!|:ᒷ ╎リᔮ.", + "term.appleMusic": "ᖋi!i!|:ᒷ ᒲ⚍ϟ╎ᔮ", + "term.applePodcasts": "ᖋi!i!|:ᒷ i!ᒍ↸ᔮᖋϟᒣϟ", + "term.itunes": "╎ᒣ⚍リᒷϟ", + "term.github": "┤╎ᒣ⍑⚍ᕊ", + "term.discord": "↸╎ϟᔮᒍ∷↸", + "term.learnMore": "|:ᒷᖋ∷リ ᒲᒍ∷ᒷ", + "term.accountSettings": "ᖋᔮᔮᒍ⚍リᒣ ϟᒷᒣᒣ╎リ┤ϟ", + "term.logout": "|:ᒍ┤ᒍ⚍ᒣ", + "term.login": "|:ᒍ┤╎リ", + "term.about": "ᖋᕊᒍ⚍ᒣ", + "term.privateSession": "I!∷╎⍊ᖋᒣᒷ ϟᒷϟϟ╎ᒍリ", + "term.queue": "ᑑ⚍ᒷ⚍ᒷ", + "term.search": "ϟᒷᖋ∷ᔮ⍑", + "term.library": "|:╎ᕊ∷ᖋ∷॥", + "term.listenNow": "|:╎ϟᒣᒷリ", + "term.browse": "リᒍ∴", + "term.radio": "∷ᖋ↸╎ᒍ", + "term.recentlyAdded": "∷ᒷᔮᒷリᒣ|:॥ ᖋ↸↸ᒷ↸", + "term.songs": "ϟᒍリ┤ϟ", + "term.albums": "ᖋ|:ᕊ⚍ᒲϟ", + "term.artists": "ᖋ∷ᒣ╎ϟᒣϟ", + "term.podcasts": "I!ᒍ↸ᔮᖋϟᒣϟ", + "term.playlists": "i!|:ᖋ॥|:╎ϟᒣϟ", + "term.playlist": "i!|:ᖋ॥|:╎ϟᒣ", + "term.play": "i!|:ᖋ॥", + "term.pause": "I!ᖋ⚍ϟᒷ", + "term.previous": "I!∷ᒷ⍊╎ᒍ⚍ϟ", + "term.next": "リᒷ/ᒣ", + "term.shuffle": "ϟ⍑⚍⎓⎓|:ᒷ", + "term.repeat": "∷ᒷi!ᒷᖋᒣ", + "term.volume": "⍊ᒍ|:⚍ᒲᒷ", + "term.mute": "ᒲ⚍ᒣᒷ", + "term.unmute": "⚍リᒲ⚍ᒣᒷ", + "term.share": "ϟ⍑ᖋ∷ᒷ", + "term.settings": "ϟᒷᒣᒣ╎リ┤ϟ", + "term.seeAll": "ϟᒷᒷ ᖋ|:|:", + "term.sortBy": "ϟᒍ∷ᒣ ᕊ॥", + "term.sortBy.album": "ᖋ|:ᕊ⚍ᒲ", + "term.sortBy.artist": "ᖋ∷ᒣ╎ϟᒣ", + "term.sortBy.name": "リᖋᒲᒷ", + "term.sortBy.genre": "┤ᒷリ∷ᒷ", + "term.sortBy.releaseDate": "∷ᒷ|:ᒷᖋϟᒷ ↸ᖋᒣᒷ", + "term.sortBy.duration": "↸⚍∷ᖋᒣ╎ᒍリ", + "term.sortOrder": "ᖋ-∩", + "term.sortOrder.ascending": "ᖋϟᔮᒷリ↸╎リ┤", + "term.sortOrder.descending": "↸ᒷϟᔮᒷリ↸╎リ┤", + "term.viewAs": "⍊╎ᒷ∴ ᖋϟ", + "term.viewAs.coverArt": "ᔮᒍ⍊ᒷ∷ ᖋ∷ᒣ", + "term.viewAs.list": "|:╎ϟᒣ", + "term.size": "ϟ╎∩ᒷ", + "term.size.normal": "リᒍ∷ᒲᖋ|:", + "term.size.compact": "ᔮᒍᒲi!ᖋᔮᒣ", + "term.enable": "ᒷリᖋᕊ|:ᒷ", + "term.disable": "↸╎ϟᖋᕊ|:ᒷ", + "term.enabled": "ᒷリᖋᕊ|:ᒷ↸", + "term.disabled": "↸╎ϟᖋᕊ|:ᒷ↸", + "term.connect": "ᔮᒍリリᒷᔮᒣ", + "term.connecting": "ᔮᒍリリᒷᔮᒣ╎リ┤", + "term.disconnect": "↸╎ϟᔮᒍリリᒷᔮᒣ", + "term.authed": "ᖋ⚍ᒣ⍑ᒷ↸", + "term.confirm": "ᔮᒍリ⎓╎∷ᒲ ?", + "term.more": "ᒲᒍ∷ᒷ", + "term.less": "|:ᒷϟϟ", + "term.showMore": "ϟ⍑ᒍ∴ ᒲᒍ∷ᒷ", + "term.showLess": "ϟ⍑ᒍ∴ |:ᒷϟϟ", + "term.topSongs" : "ᒣᒍi! ϟᒍリ┤ϟ", + "term.latestReleases": "|:ᖋᒣᒷϟᒣ ∷ᒷ|:ᒷᖋϟᒷϟ", + "term.time.added": "ᖋ↸↸ᒷ↸", + "term.time.released": "∷ᒷ|:ᒷᖋϟᒷ↸", + "term.time.updated": "⚍i!↸ᖋᒣᒷ↸", + "term.fullscreenView": "⎓⚍|:|:ϟᔮ∷ᒷᒷリ ⍊╎ᒷ∴", + "term.defaultView": "↸ᒷ⎓ᖋ⚍|:ᒣ ⍊╎ᒷ∴", + "term.spacializedAudioSetting": "ϟi!ᖋᔮ╎ᖋ|:╎∩ᒷ↸ ᖋ⚍↸╎ᒍ ϟᒷᒣᒣ╎リ┤", + "term.clearAll": "ᔮ|:ᒷᖋ∷ ᖋ|:|:", + "term.recentStations": "∷ᒷᔮᒷリᒣ ϟᒣᖋᒣ╎ᒍリϟ", + "term.language": "|:ᖋリ┤⚍ᖋ┤ᒷ", + "term.noLyrics": "|:ᒍᖋ↸╎リ┤... / |:॥∷╎ᔮϟ リᒍᒣ ⎓ᒍ⚍リ↸./ ╎リϟᒣ∷⚍ᒲᒷリᒣᖋ|:.", + "term.copyright": "ᔮᒍi!॥∷╎┤⍑ᒣ", + "term.rightsReserved": "ᖋ|:|: ∷╎┤⍑ᒣϟ ∷ᒷϟᒷ∷⍊ᒷ↸.", + "term.sponsor": "ϟi!ᒍリϟᒍ∷ ᒣ⍑╎ϟ i!∷ᒍ⋮ᒷᔮᒣ", + "term.ciderTeam": "ᔮ╎↸ᒷ∷ ᒣᒷᖋᒲ", + "term.developer": "↸ᒷ⍊ᒷ|:ᒍi!ᒷ∷", + "term.socialTeam": "ϟᒍᔮ╎ᖋ|: ᒣᒷᖋᒲ", + "term.contributors": "ᔮᒍリᒣ∷╎ᕊ⚍ᒣᒍ∷ϟ", + "term.equalizer": "ᒷᑑ⚍ᖋ|:╎∩ᒷ∷", + "term.reset": "∷ᒷϟᒷᒣ", + "term.tracks": "ᒣ∷ᖋᔮ·ǀ·ϟ", // Assume x amount of tracks. e.g. 50 tracks + + + // Home + "home.title": "⍑ᒍᒲᒷ", + "home.recentlyPlayed": "∷ᒷᔮᒷリᒣ|:॥ i!|:ᖋ॥ᒷ↸", + "home.recentlyAdded": "∷ᒷᔮᒷリᒣ|:॥ ᖋ↸↸ᒷ↸", + "home.artistsFeed": "॥ᒍ⚍∷ ᖋ∷ᒣ╎ϟᒣϟ ⎓ᒷᒷ↸", + "home.artistsFeed.noArtist": "⎓ᒍ|:|:ᒍ∴ ϟᒍᒲᒷ ᖋ∷ᒣ╎ϟᒣϟ ⎓╎∷ϟᒣ ᖋリ↸ ᒣ⍑ᒷ╎∷ |:ᖋᒣᒷϟᒣ ∷ᒷ|:ᒷᖋϟᒷϟ ∴╎|:|: ᕊᒷ ⍑ᒷ∷ᒷ", + "home.madeForYou": "ᒲᖋ↸ᒷ ⎓ᒍ∷ ॥ᒍ⚍", + "home.friendsListeningTo": "⎓∷╎ᒷリ↸ϟ |:╎ϟᒣᒷリ╎リ┤ ᒣᒍ", + "home.followedArtists": "⎓ᒍ|:|:ᒍ∴ᒷ↸ ᖋ∷ᒣ╎ϟᒣϟ", + // Errors + "error.appleMusicSubRequired": "ᖋi!i!|:ᒷ ᒲ⚍ϟ╎ᔮ ∷ᒷᑑ⚍╎∷ᒷϟ ᖋ ϟ⚍ᕊϟᔮ∷╎i!ᒣ╎ᒍリ.", + "error.connectionError": "ᒣ⍑ᒷ∷ᒷ ∴ᖋϟ ᖋ i!∷ᒍᕊ|:ᒷᒲ ᔮᒍリリᒷᔮᒣ╎リ┤ ᒣᒍ ᖋi!i!|:ᒷ ᒲ⚍ϟ╎ᔮ.", + "error.noResults": "リᒍ ∷ᒷϟ⚍|:ᒣϟ.", + "error.noResults.description": "ᒣ∷॥ ᖋ リᒷ∴ ϟᒷᖋ∷ᔮ⍑.", + + //Podcasts + "podcast.followOnCider": "⎓ᒍ|:|:ᒍ∴ ᒍリ ᔮ╎↸ᒷ∷", + "podcast.followedOnCider": "⎓ᒍ|:|:ᒍ∴╎リ┤ ᒍリ ᔮ╎↸ᒷ∷", + "podcast.subscribeOnItunes": "ϟ⚍ᕊϟᔮ∷╎ᕊᒷ ᒍリ ╎ᒣ⚍リᒷϟ", + "podcast.subscribedOnItunes": "ϟ⚍ᕊϟᔮ∷╎ᕊᒷ↸ ᒍリ ╎ᒣ⚍リᒷϟ", + "podcast.itunesStore": "╎ᒣ⚍リᒷϟ ϟᒣᒍ∷ᒷ", + "podcast.episodes": "ᒷi!╎ϟᒍ↸ᒷϟ", + "podcast.playEpisode": "i!|:ᖋ॥ ᒷi!╎ϟᒍ↸ᒷ", + "podcast.website": "I!ᒍ↸ᔮᖋϟᒣ ∴ᒷᕊϟ╎ᒣᒷ", + + // Actions + "action.addToLibrary": "ᖋ↸↸ ᒣᒍ |:╎ᕊ∷ᖋ∷॥", + "action.addToLibrary.success": "ᖋ↸↸ᒷ↸ ᒣᒍ |:╎ᕊ∷ᖋ∷॥", + "action.addToLibrary.error": "ᒷ∷∷ᒍ∷ ᖋ↸↸╎リ┤ ᒣᒍ |:╎ᕊ∷ᖋ∷॥", + "action.removeFromLibrary": "∷ᒷᒲᒍ⍊ᒷ ⎓∷ᒍᒲ |:╎ᕊ∷ᖋ∷॥", + "action.removeFromLibrary.success": "∷ᒷᒲᒍ⍊ᒷ↸ ⎓∷ᒍᒲ |:╎ᕊ∷ᖋ∷॥", + "action.addToQueue": "ᖋ↸↸ ᒣᒍ ᑑ⚍ᒷ⚍ᒷ", + "action.addToQueue.success": "ᖋ↸↸ᒷ↸ ᒣᒍ ᑑ⚍ᒷ⚍ᒷ", + "action.addToQueue.error": "ᒷ∷∷ᒍ∷ ᖋ↸↸╎リ┤ ᒣᒍ ᑑ⚍ᒷ⚍ᒷ", + "action.removeFromQueue": "∷ᒷᒲᒍ⍊ᒷ ⎓∷ᒍᒲ ᑑ⚍ᒷ⚍ᒷ", + "action.removeFromQueue.success": "∷ᒷᒲᒍ⍊ᒷ↸ ⎓∷ᒍᒲ ᑑ⚍ᒷ⚍ᒷ", + "action.removeFromQueue.error": "ᒷ∷∷ᒍ∷ ∷ᒷᒲᒍ⍊╎リ┤ ⎓∷ᒍᒲ ᑑ⚍ᒷ⚍ᒷ", + "action.addToPlaylist": "ᖋ↸↸ ᒣᒍ i!|:ᖋ॥|:╎ϟᒣ", + "action.removeFromPlaylist": "∷ᒷᒲᒍ⍊ᒷ ⎓∷ᒍᒲ i!|:ᖋ॥|:╎ϟᒣ", + "action.addToFavorites": "ᖋ↸↸ ᒣᒍ ⎓ᖋ⍊ᒍ∷╎ᒣᒷϟ", + "action.follow": "⎓ᒍ|:|:ᒍ∴", + "action.follow.success": "⎓ᒍ|:|:ᒍ∴ᒷ↸", + "action.follow.error": "ᒷ∷∷ᒍ∷ ⎓ᒍ|:|:ᒍ∴╎リ┤", + "action.unfollow": "⚍リ⎓ᒍ|:|:ᒍ∴", + "action.unfollow.success": "⚍リ⎓ᒍ|:|:ᒍ∴ᒷ↸", + "action.unfollow.error": "ᒷ∷∷ᒍ∷ ⚍リ⎓ᒍ|:|:ᒍ∴╎リ┤", + "action.playNext": "i!|:ᖋ॥ リᒷ/ᒣ", + "action.playLater": "i!|:ᖋ॥ |:ᖋᒣᒷ∷", + "action.startRadio": "ϟᒣᖋ∷ᒣ ∷ᖋ↸╎ᒍ", + "action.goToArtist": "┤ᒍ ᒣᒍ ᖋ∷ᒣ╎ϟᒣ", + "action.goToAlbum": "┤ᒍ ᒣᒍ ᖋ|:ᕊ⚍ᒲ", + "action.moveToTop": "ᒲᒍ⍊ᒷ ᒣᒍ ᒣᒍi!", + "action.share": "ϟ⍑ᖋ∷ᒷ", + "action.rename": "∷ᒷリᖋᒲᒷ", + "action.love": "|:ᒍ⍊ᒷ", + "action.unlove": "⚍リ|:ᒍ⍊ᒷ", + "action.dislike": "↸╎ϟ|:╎·ǀ·ᒷ", + "action.undoDislike": "⚍リ↸ᒍ ↸╎ϟ|:╎·ǀ·ᒷ", + "action.showWebRemoteQR": "ϟ⍑ᒍ∴ ∴ᒷᕊ ∷ᒷᒲᒍᒣᒷ ᑑ∷", + "action.playTracksNext": "i!|:ᖋ॥ ${app.selectedMediaItems.length} ᒣ∷ᖋᔮ·ǀ·ϟ リᒷ/ᒣ", + "action.playTracksLater": "i!|:ᖋ॥ ${app.selectedMediaItems.length} ᒣ∷ᖋᔮ·ǀ·ϟ |:ᖋᒣᒷ∷", + "action.removeTracks": "∷ᒷᒲᒍ⍊ᒷ ${self.selectedItems.length} ᒣ∷ᖋᔮ·ǀ·ϟ ⎓∷ᒍᒲ ᑑ⚍ᒷ⚍ᒷ", + + // Settings - Audio + "settings.header.audio": "ᖋ⚍↸╎ᒍ", + "settings.header.audio.description": "ᖋ↸⋮⚍ϟᒣ ᒣ⍑ᒷ ᖋ⚍↸╎ᒍ ϟᒷᒣᒣ╎リ┤ϟ ⎓ᒍ∷ ᔮ╎↸ᒷ∷.", + "settings.option.audio.quality": "ᖋ⚍↸╎ᒍ ᑑ⚍ᖋ|:╎ᒣ॥", // Dropdown + "settings.header.audio.quality.high": "⍑╎┤⍑", + "settings.header.audio.quality.low": "|:ᒍ∴", + "settings.header.audio.quality.auto": "ᖋ⚍ᒣᒍ", + "settings.option.audio.seamlessTransition": "ϟᒷᖋᒲ|:ᒷϟϟ ᖋ⚍↸╎ᒍ ᒣ∷ᖋリϟ╎ᒣ╎ᒍリ", // Toggle + "settings.option.audio.enableAdvancedFunctionality": "ᒷリᖋᕊ|:ᒷ ᖋ↸⍊ᖋリᔮᒷ↸ ⎓⚍リᔮᒣ╎ᒍリᖋ|:╎ᒣ॥", // Toggle + "settings.option.audio.enableAdvancedFunctionality.description": "ᒷリᖋᕊ|:╎リ┤ ᖋ⚍↸╎ᒍᔮᒍリᒣᒷ/ᒣ ⎓⚍リᔮᒣ╎ᒍリᖋ|:╎ᒣ॥ ∴╎|:|: ᖋ|:|:ᒍ∴ ⎓ᒍ∷ ᒷ/ᒣᒷリ↸ᒷ↸ ᖋ⚍↸╎ᒍ ⎓ᒷᖋᒣ⚍∷ᒷϟ |:╎·ǀ·ᒷ ᖋ⚍↸╎ᒍ リᒍ∷ᒲᖋ|:╎∩ᖋᒣ╎ᒍリ , ᒷᑑ⚍ᖋ|:╎∩ᒷ∷ϟ ᖋリ↸ ⍊╎ϟ⚍ᖋ|:╎∩ᒷ∷ϟ, ⍑ᒍ∴ᒷ⍊ᒷ∷ ᒍリ ϟᒍᒲᒷ ϟ॥ϟᒣᒷᒲϟ ᒣ⍑╎ϟ ᒲᖋ॥ ᔮᖋ⚍ϟᒷ ϟᒣ⚍ᒣᒣᒷ∷╎リ┤ ╎リ ᖋ⚍↸╎ᒍ ᒣ∷ᖋᔮ·ǀ·ϟ.", + "settings.option.audio.enableAdvancedFunctionality.audioNormalization": "ᖋ⚍↸╎ᒍ リᒍ∷ᒲᖋ|:╎∩ᖋᒣ╎ᒍリ", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioNormalization.description": "リᒍ∷ᒲᖋ|:╎∩ᒷϟ i!ᒷᖋ·ǀ· ⍊ᒍ|:⚍ᒲᒷ ⎓ᒍ∷ ╎リ↸╎⍊╎↸⚍ᖋ|: ᒣ∷ᖋᔮ·ǀ·ϟ ᒣᒍ ᔮ∷ᒷᖋᒣᒷ ᖋ ᒲᒍ∷ᒷ ⚍リ╎⎓ᒍ∷ᒲ |:╎ϟᒣᒷリ╎リ┤ ᒷ/i!ᒷ∷╎ᒷリᔮᒷ.", + "settings.option.audio.enableAdvancedFunctionality.audioSpatialization": "ᖋ⚍↸╎ᒍ ϟi!ᖋᒣ╎ᖋ|:╎∩ᖋᒣ╎ᒍリ", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioSpatialization.description": "ϟi!ᖋᒣ╎ᖋ|:╎∩ᒷ ᖋ⚍↸╎ᒍ ᖋリ↸ ᒲᖋ·ǀ·ᒷ ᖋ⚍↸╎ᒍ ᒲᒍ∷ᒷ 3-↸╎ᒲᒷリϟ╎ᒍリᖋ|: (リᒍᒣᒷ: ᒣ⍑╎ϟ ╎ϟ リᒍᒣ ↸ᒍ|:ᕊ॥ ᖋᒣᒲᒍϟ)", + // Settings - Visual + "settings.header.visual": "⍊╎ϟ⚍ᖋ|:", + "settings.header.visual.description": "ᖋ↸⋮⚍ϟᒣ ᒣ⍑ᒷ ⍊╎ϟ⚍ᖋ| ϟᒷᒣᒣ╎リ┤ϟ ⎓ᒍ∷ ᔮ╎↸ᒷ∷.", + "settings.option.visual.windowBackgroundStyle": "∴╎リ↸ᒍ∴ ᕊᖋᔮ·ǀ·┤∷ᒍ⚍リ↸ ϟᒣ॥|:ᒷ", // Toggle + "settings.header.visual.windowBackgroundStyle.none": "リᒍリᒷ", + "settings.header.visual.windowBackgroundStyle.artwork": "ᖋ∷ᒣ∴ᒍ∷·ǀ·", + "settings.option.visual.animatedArtwork": "ᖋリ╎ᒲᖋᒣᒷ↸ ᖋ∷ᒣ∴ᒍ∷·ǀ·", // Dropdown + "settings.header.visual.animatedArtwork.always": "ᖋ|:∴ᖋ॥ϟ", + "settings.header.visual.animatedArtwork.limited": "|:╎ᒲ╎ᒣᒷ↸ ᒣᒍ i!ᖋ┤ᒷϟ ᖋリ↸ ϟi!ᒷᔮ╎ᖋ|: ᒷリᒣ∷╎ᒷϟ", + "settings.header.visual.animatedArtwork.disable": "↸╎ϟᖋᕊ|:ᒷ ᒷ⍊ᒷ∷॥∴⍑ᒷ∷ᒷ", + "settings.option.visual.animatedArtworkQuality": "ᖋリ╎ᒲᖋᒣᒷ↸ ᖋ∷ᒣ∴ᒍ∷·ǀ· ᑑ⚍ᖋ|:╎ᒣ॥", // Dropdown + "settings.header.visual.animatedArtworkQuality.low": "|:ᒍ∴", + "settings.header.visual.animatedArtworkQuality.medium": "ᒲᒷ↸╎⚍ᒲ", + "settings.header.visual.animatedArtworkQuality.high": "⍑╎┤⍑", + "settings.header.visual.animatedArtworkQuality.veryHigh": "⍊ᒷ∷॥ ⍑╎┤⍑", + "settings.header.visual.animatedArtworkQuality.extreme": "ᒷ/ᒣ∷ᒷᒲᒷ", + "settings.option.visual.animatedWindowBackground": "ᖋリ╎ᒲᖋᒣᒷ↸ ∴╎リ↸ᒍ∴ ᕊᖋᔮ·ǀ·┤∷ᒍ⚍リ↸", // Toggle + "settings.option.visual.hardwareAcceleration": "⍑ᖋ∷↸∴ᖋ∷ᒷ ᖋᔮᔮᒷ|:ᒷ∷ᖋᒣ╎ᒍリ", // Dropdown + "settings.option.visual.hardwareAcceleration.description": "∷ᒷᑑ⚍╎∷ᒷϟ ∷ᒷ|:ᖋ⚍リᔮ⍑", + "settings.header.visual.hardwareAcceleration.default": "↸ᒷ⎓ᖋ⚍|:ᒣ", + "settings.header.visual.hardwareAcceleration.webGPU": "∴ᒷᕊ┤i!⚍", + // Refer to term.disabled for the disabled option + "settings.option.visual.showPersonalInfo": "ϟ⍑ᒍ∴ i!ᒷ∷ϟᒍリᖋ|: ╎リ⎓ᒍ", // Toggle + // Settings - General (Reserved) + "settings.header.general": "┤ᒷリᒷ∷ᖋ|:", + "settings.header.general.description": "ᖋ↸⋮⚍ϟᒣ ᒣ⍑ᒷ ┤ᒷリᒷ∷ᖋ|: ϟᒷᒣᒣ╎リ┤ϟ ⎓ᒍ∷ ᔮ╎↸ᒷ∷.", + + // Settings - Lyrics + "settings.header.lyrics": "|:॥∷╎ᔮϟ", + "settings.header.lyrics.description": "ᖋ↸⋮⚍ϟᒣ ᒣ⍑ᒷ |:॥∷╎ᔮϟ ϟᒷᒣᒣ╎リ┤ϟ ⎓ᒍ∷ ᔮ╎↸ᒷ∷.", + "settings.option.lyrics.enableMusixmatch": "ᒷリᖋᕊ|:ᒷ ᒲ⚍ϟ╎̇/ᒲᖋᒣᔮ⍑ |:॥∷╎ᔮϟ", // Toggle + "settings.option.lyrics.enableMusixmatchKaraoke": "ᒷリᖋᕊ|:ᒷ ·ǀ·ᖋ∷ᖋᒍ·ǀ·ᒷ ᒲᒍ↸ᒷ (ᒲ⚍ϟ╎̇/ᒲᖋᒣᔮ⍑ ᒍリ|:॥)", // Toggle + "settings.option.lyrics.musixmatchPreferredLanguage": "ᒲ⚍ϟ╎̇/ᒲᖋᒣᔮ⍑ ᒣ∷ᖋリϟ|:ᖋᒣ╎ᒍリ i!∷ᒷ⎓ᒷ∷∷ᒷ↸ |:ᖋリ┤⚍ᖋ┤ᒷ", // Dropdown + "settings.option.lyrics.enableYoutubeLyrics": "ᒷリᖋᕊ|:ᒷ ॥ᒍ⚍ᒣ⚍ᕊᒷ |:॥∷╎ᔮϟ ⎓ᒍ∷ ᒲ⚍ϟ╎ᔮ ⍊╎↸ᒷᒍϟ", // Toggle + + // Settings - Connectivity + "settings.header.connectivity": "ᔮᒍリリᒷᔮᒣ╎⍊╎ᒣ॥", + "settings.header.connectivity.description": "ᖋ↸⋮⚍ϟᒣ ᒣ⍑ᒷ ᔮᒍリリᒷᔮᒣ╎⍊╎ᒣ॥ ϟᒷᒣᒣ╎リ┤ϟ ⎓ᒍ∷ ᔮ╎↸ᒷ∷.", + "settings.option.connectivity.discordRPC": "↸╎ϟᔮᒍ∷↸ ∷╎ᔮ⍑ i!∷ᒷϟᒷリᔮᒷ", // Dropdown + // Refer to term.disabled for the disabled option + "settings.header.connectivity.discordRPC.cider": "↸╎ϟi!|:ᖋ॥ ᖋϟ 'ᔮ╎↸ᒷ∷'", + "settings.header.connectivity.discordRPC.appleMusic": "↸╎ϟi!|:ᖋ॥ ᖋϟ 'ᖋi!i!|:ᒷ ᒲ⚍ϟ╎ᔮ'", + "settings.option.connectivity.discordRPC.clearOnPause": "ᔮ|:ᒷᖋ∷ ↸╎ϟᔮᒍ∷↸ ∷╎ᔮ⍑ i!∷ᒷϟᒷリᔮᒷ ᒍリ i!ᖋ⚍ϟᒷ", // Toggle + "settings.option.connectivity.lastfmScrobble": "|:ᖋϟᒣ⎓ᒲ ϟᔮ∷ᒍᕊᕊ|:╎リ┤", // Option to Connect + "settings.option.connectivity.lastfmScrobble.delay": "|:ᖋϟᒣ⎓ᒲ ϟᔮ∷ᒍᕊᕊ|:ᒷ ↸ᒷ|:ᖋ॥ (%)", + "settings.option.connectivity.lastfmScrobble.nowPlaying": "ᒷリᖋᕊ|:ᒷ |:ᖋϟᒣ⎓ᒲ リᒍ∴ i!|:ᖋ॥╎リ┤", + "settings.option.connectivity.lastfmScrobble.removeFeatured": "∷ᒷᒲᒍ⍊ᒷ ⎓ᒷᖋᒣ⚍∷╎リ┤ ᖋ∷ᒣ╎ϟᒣϟ ⎓∷ᒍᒲ ϟᒍリ┤ ᒣ╎ᒣ|:ᒷ (|:ᖋϟᒣ⎓ᒲ)", + "settings.option.connectivity.lastfmScrobble.filterLoop": "⎓╎|:ᒣᒷ∷ |:ᒍᒍi!ᒷ↸ ᒣ∷ᖋᔮ·ǀ· (|:ᖋϟᒣ⎓ᒲ)", + // Refer to term.connect for the connect button + + // Settings - Experimental + "settings.header.experimental": "ᒷ/i!ᒷ∷╎ᒲᒷリᒣᖋ|:", + "settings.header.experimental.description": "ᖋ↸⋮⚍ϟᒣ ᒣ⍑ᒷ ᒷ/i!ᒷ∷╎ᒲᒷリᒣᖋ|: ϟᒷᒣᒣ╎リ┤ϟ ⎓ᒍ∷ ᔮ╎↸ᒷ∷.", + "settings.option.experimental.compactUI": "ᔮᒍᒲi!ᖋᔮᒣ ⚍╎", // Toggle + "settings.option.experimental.closeButtonBehaviour": "ᔮ|:ᒍϟᒷ ᕊ⚍ᒣᒣᒍリ ᕊᒷ⍑ᖋ⍊╎ᒍ⚍∷", + "settings.option.experimental.closeButtonBehaviour.quit": "ᑑ⚍╎ᒣ ᔮ╎↸ᒷ∷", + "settings.option.experimental.closeButtonBehaviour.minimizeTaskbar": "ᒲ╎リ╎ᒲ╎∩ᒷ ᒣᒍ ᒣᖋϟ·ǀ·ᕊᖋ∷", + "settings.option.experimental.closeButtonBehaviour.minimizeTray": "ᒲ╎リ╎ᒲ╎∩ᒷ ᒣᒍ ϟ॥ϟᒣᒷᒲ ᒣ∷ᖋ॥", + // Refer to term.disabled & term.enabled + + // Spatialization Menu + "spatial.spatialProperties" : "ϟi!ᖋᒣ╎ᖋ|: i!∷ᒍi!ᒷ∷ᒣ╎ᒷϟ", + "spatial.width" : "∴╎↸ᒣ⍑", + "spatial.height" : "⍑ᒷ╎┤⍑ᒣ", + "spatial.depth" : "↸ᒷi!ᒣ⍑", + "spatial.gain" : "┤ᖋ╎リ", + "spatial.roomMaterials" : "∷ᒍᒍᒲ ᒲᖋᒣᒷ∷╎ᖋ|:ϟ", + "spatial.roomDimensions" : "∷ᒍᒍᒲ ↸╎ᒲᒷリϟ╎ᒍリϟ", + "spatial.roomPositions" : "∷ᒍᒍᒲ i!ᒍϟ╎ᒣ╎ᒍリϟ", + "spatial.setDimensions" : "ϟᒷᒣ ↸╎ᒲᒷリϟ╎ᒍリϟ", + "spatial.setPositions" : "ϟᒷᒣ i!ᒍϟ╎ᒣ╎ᒍリϟ", + "spatial.up" : "⚍i!", + "spatial.front" : "⎓∷ᒍリᒣ", + "spatial.left" : "|:ᒷ⎓ᒣ", + "spatial.right" : "∷╎┤⍑ᒣ", + "spatial.back" : "ᕊᖋᔮ·ǀ·", + "spatial.down" : "↸ᒍ∴リ", + "spatial.listener" : "|:╎ϟᒣᒷリᒷ∷", + "spatial.audioSource" : "ᖋ⚍↸╎ᒍ ϟᒍ⚍∷ᔮᒷ", + + // Settings - Unfinished + "settings.header.unfinished": "⚍リ⎓╎リ╎ϟ⍑ᒷ↸", + + // Web Remote + "remote.web.title": "ᔮ╎↸ᒷ∷ ∷ᒷᒲᒍᒣᒷ", + "remote.web.description": "ϟᔮᖋリ ᒣ⍑ᒷ ᑑ∷ ᔮᒍ↸ᒷ ᒣᒍ i!ᖋ╎∷ ॥ᒍ⚍∷ i!⍑ᒍリᒷ ⚍i! ∴╎ᒣ⍑ ᒣ⍑╎ϟ ᔮ╎↸ᒷ∷ ╎リϟᒣᖋリᔮᒷ", + + // About + "about.thanks": "ᒲᖋ⋮ᒍ∷ ᒣ⍑ᖋリ·ǀ·ϟ ᒣᒍ ᒣ⍑ᒷ ᔮ╎↸ᒷ∷ ᔮᒍ|:|:ᒷᔮᒣ╎⍊ᒷ ᒣᒷᖋᒲ ᖋリ↸ ᖋ|:|: ᒍ⎓ ᒍ⚍∷ ᔮᒍリᒣ∷╎ᕊ⚍ᒣᒍ∷ϟ." +} \ No newline at end of file diff --git a/src/i18n/en_US.jsonc b/src/i18n/en_US.jsonc new file mode 100644 index 00000000..9fea2dfa --- /dev/null +++ b/src/i18n/en_US.jsonc @@ -0,0 +1,295 @@ +{ // Base File + + // i18n Info + "i18n.languageName": "English (US)", // name of language in native language + "i18n.languageNameEnglish": "English (US)", // name of language in English + "i18n.category": "main", // main = real language, fun = fun community languages + "i18n.authors": "@maikirakiwi", // Authors, if you contribute to this file feel free to add your name seperated with a space + + // App info + "app.name": "Cider", + + "date.format": "${m} ${d}, ${y}", + + // Dialogs + "dialog.cancel": "Cancel", + "dialog.ok": "OK", + + // Notification + "notification.updatingLibrarySongs": "Updating library songs...", + "notification.updatingLibraryAlbums": "Updating library albums...", + "notification.updatingLibraryArtists": "Updating library artists...", + // Terms + "term.appleInc": "Apple Inc.", + "term.appleMusic": "Apple Music", + "term.applePodcasts": "Apple Podcasts", + "term.itunes": "iTunes", + "term.github": "GitHub", + "term.discord": "Discord", + "term.learnMore": "Learn more", + "term.accountSettings": "Account Settings", + "term.logout": "Logout", + "term.login": "Login", + "term.about": "About", + "term.privateSession": "Private Session", + "term.queue": "Queue", + "term.search": "Search", + "term.library": "Library", + "term.listenNow": "Listen Now", + "term.browse": "Browse", + "term.radio": "Radio", + "term.recentlyAdded": "Recently Added", + "term.songs": "Songs", + "term.albums": "Albums", + "term.artists": "Artists", + "term.podcasts": "Podcasts", + "term.playlists": "Playlists", + "term.playlist": "Playlist", + "term.play": "Play", + "term.pause": "Pause", + "term.previous": "Previous", + "term.next": "Next", + "term.shuffle": "Shuffle", + "term.repeat": "Repeat", + "term.volume": "Volume", + "term.mute": "Mute", + "term.unmute": "Unmute", + "term.share": "Share", + "term.settings": "Settings", + "term.seeAll": "See All", + "term.sortBy": "Sort By", + "term.sortBy.album": "Album", + "term.sortBy.artist": "Artist", + "term.sortBy.name": "Name", + "term.sortBy.genre": "Genre", + "term.sortBy.releaseDate": "Release Date", + "term.sortBy.duration": "Duration", + "term.sortOrder": "A-Z", + "term.sortOrder.ascending": "Ascending", + "term.sortOrder.descending": "Descending", + "term.viewAs": "View As", + "term.viewAs.coverArt": "Cover Art", + "term.viewAs.list": "List", + "term.size": "Size", + "term.size.normal": "Normal", + "term.size.compact": "Compact", + "term.enable": "Enable", + "term.disable": "Disable", + "term.enabled": "Enabled", + "term.disabled": "Disabled", + "term.connect": "Connect", + "term.connecting": "Connecting", + "term.disconnect": "Disconnect", + "term.authed": "Authed", + "term.confirm": "Confirm ?", + "term.more": "More", + "term.less": "Less", + "term.showMore": "Show more", + "term.showLess": "Show less", + "term.topSongs" : "Top Songs", + "term.latestReleases": "Latest Releases", + "term.time.added": "Added", + "term.time.released": "Released", + "term.time.updated": "Updated", + "term.time.hours": "hours", + "term.time.hour": "hour", + "term.time.minutes": "minutes", + "term.time.minute": "minute", + "term.time.seconds": "seconds", + "term.time.second": "second", + "term.fullscreenView": "Fullscreen View", + "term.defaultView": "Default View", + "term.spacializedAudioSetting": "Spacialized Audio Setting", + "term.clearAll": "Clear All", + "term.recentStations": "Recent Stations", + "term.language": "Language", + "term.funLanguages": "Fun", + "term.noLyrics": "Loading... / Lyrics not found./ Instrumental.", + "term.copyright": "Copyright", + "term.rightsReserved": "All Rights Reserved.", + "term.sponsor": "Sponsor this project", + "term.ciderTeam": "Cider Team", + "term.developer": "Developer", + "term.socialTeam": "Social Team", + "term.contributors": "Contributors", + "term.equalizer": "Equalizer", + "term.reset": "Reset", + "term.tracks": "tracks", // Assume x amount of tracks. e.g. 50 tracks + "term.videos": "Videos", + + + // Home + "home.title": "Home", + "home.recentlyPlayed": "Recently Played", + "home.recentlyAdded": "Recently Added", + "home.artistsFeed": "Your Artists Feed", + "home.artistsFeed.noArtist": "Follow some artists first and their latest releases will be here", + "home.madeForYou": "Made For You", + "home.friendsListeningTo": "Friends Listening To", + "home.followedArtists": "Followed Artists", + // Errors + "error.appleMusicSubRequired": "Apple Music requires a subscription.", + "error.connectionError": "There was a problem connecting to Apple Music.", + "error.noResults": "No Results.", + "error.noResults.description": "Try a new search.", + + //Podcasts + "podcast.followOnCider": "Follow On Cider", + "podcast.followedOnCider": "Following On Cider", + "podcast.subscribeOnItunes": "Subscribe On iTunes", + "podcast.subscribedOnItunes": "Subscribed On iTunes", + "podcast.itunesStore": "iTunes Store", + "podcast.episodes": "Episodes", + "podcast.playEpisode": "Play Episode", + "podcast.website": "Podcast Website", + + // Actions + "action.addToLibrary": "Add to Library", + "action.addToLibrary.success": "Added to Library", + "action.addToLibrary.error": "Error Adding to Library", + "action.removeFromLibrary": "Remove from Library", + "action.removeFromLibrary.success": "Removed from Library", + "action.addToQueue": "Add to Queue", + "action.addToQueue.success": "Added to Queue", + "action.addToQueue.error": "Error Adding to Queue", + "action.removeFromQueue": "Remove from Queue", + "action.removeFromQueue.success": "Removed from Queue", + "action.removeFromQueue.error": "Error Removing from Queue", + "action.addToPlaylist": "Add to Playlist", + "action.removeFromPlaylist": "Remove from Playlist", + "action.addToFavorites": "Add to Favorites", + "action.follow": "Follow", + "action.follow.success": "Followed", + "action.follow.error": "Error Following", + "action.unfollow": "Unfollow", + "action.unfollow.success": "Unfollowed", + "action.unfollow.error": "Error Unfollowing", + "action.playNext": "Play Next", + "action.playLater": "Play Later", + "action.startRadio": "Start Radio", + "action.goToArtist": "Go to Artist", + "action.goToAlbum": "Go to Album", + "action.moveToTop": "Move to top", + "action.share": "Share", + "action.rename": "Rename", + "action.love": "Love", + "action.unlove": "Unlove", + "action.dislike": "Dislike", + "action.undoDislike": "Undo dislike", + "action.showWebRemoteQR": "Show Web Remote QR", + "action.playTracksNext": "Play ${app.selectedMediaItems.length} tracks next", + "action.playTracksLater": "Play ${app.selectedMediaItems.length} tracks later", + "action.removeTracks": "Remove ${self.selectedItems.length} tracks from queue", + + // Settings - General + "settings.header.general": "General", + "settings.header.general.description": "Adjust the general settings for Cider.", + "settings.option.general.language": "Language", + + // Language optgroups + "settings.option.general.language.main": "Languages", + "settings.option.general.language.fun": "Fun Languages", + "settings.option.general.language.unsorted": "Unsorted", + + // Settings - Audio + "settings.header.audio": "Audio", + "settings.header.audio.description": "Adjust the audio settings for Cider.", + "settings.option.audio.quality": "Audio Quality", // Dropdown + "settings.header.audio.quality.high": "High", + "settings.header.audio.quality.low": "Low", + "settings.header.audio.quality.auto": "Auto", + "settings.option.audio.seamlessTransition": "Seamless Audio Transition", // Toggle + "settings.option.audio.enableAdvancedFunctionality": "Enable Advanced Functionality", // Toggle + "settings.option.audio.enableAdvancedFunctionality.description": "Enabling AudioContext functionality will allow for extended audio features like Audio Normalization , Equalizers and Visualizers, however on some systems this may cause stuttering in audio tracks.", + "settings.option.audio.enableAdvancedFunctionality.audioNormalization": "Audio Normalization", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioNormalization.description": "Normalizes peak volume for individual tracks to create a more uniform listening experience.", + "settings.option.audio.enableAdvancedFunctionality.audioSpatialization": "Audio Spatialization", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioSpatialization.description": "Spatialize audio and make audio more 3-dimensional (note: This is not Dolby Atmos)", + // Settings - Visual + "settings.header.visual": "Visual", + "settings.header.visual.description": "Adjust the visual settings for Cider.", + "settings.option.visual.windowBackgroundStyle": "Window Background Style", // Toggle + "settings.header.visual.windowBackgroundStyle.none": "None", + "settings.header.visual.windowBackgroundStyle.artwork": "Artwork", + "settings.header.visual.windowBackgroundStyle.image": "Image", + "settings.option.visual.animatedArtwork": "Animated Artwork", // Dropdown + "settings.header.visual.animatedArtwork.always": "Always", + "settings.header.visual.animatedArtwork.limited": "Limited to pages and special entries", + "settings.header.visual.animatedArtwork.disable": "Disable everywhere", + "settings.option.visual.animatedArtworkQuality": "Animated Artwork Quality", // Dropdown + "settings.header.visual.animatedArtworkQuality.low": "Low", + "settings.header.visual.animatedArtworkQuality.medium": "Medium", + "settings.header.visual.animatedArtworkQuality.high": "High", + "settings.header.visual.animatedArtworkQuality.veryHigh": "Very High", + "settings.header.visual.animatedArtworkQuality.extreme": "Extreme", + "settings.option.visual.animatedWindowBackground": "Animated Window Background", // Toggle + "settings.option.visual.hardwareAcceleration": "Hardware Acceleration", // Dropdown + "settings.option.visual.hardwareAcceleration.description": "Requires relaunch", + "settings.header.visual.hardwareAcceleration.default": "Default", + "settings.header.visual.hardwareAcceleration.webGPU": "WebGPU", + // Refer to term.disabled for the disabled option + "settings.option.visual.showPersonalInfo": "Show Personal Info", // Toggle + + // Settings - Lyrics + "settings.header.lyrics": "Lyrics", + "settings.header.lyrics.description": "Adjust the lyrics settings for Cider.", + "settings.option.lyrics.enableMusixmatch": "Enable Musixmatch Lyrics", // Toggle + "settings.option.lyrics.enableMusixmatchKaraoke": "Enable Karaoke Mode (Musixmatch only)", // Toggle + "settings.option.lyrics.musixmatchPreferredLanguage": "Musixmatch Translation Preferred Language", // Dropdown + "settings.option.lyrics.enableYoutubeLyrics": "Enable Youtube Lyrics for Music Videos", // Toggle + + // Settings - Connectivity + "settings.header.connectivity": "Connectivity", + "settings.header.connectivity.description": "Adjust the connectivity settings for Cider.", + "settings.option.connectivity.discordRPC": "Discord Rich Presence", // Dropdown + // Refer to term.disabled for the disabled option + "settings.header.connectivity.discordRPC.cider": "Display as 'Cider'", + "settings.header.connectivity.discordRPC.appleMusic": "Display as 'Apple Music'", + "settings.option.connectivity.discordRPC.clearOnPause": "Clear Discord Rich Presence on Pause", // Toggle + "settings.option.connectivity.lastfmScrobble": "LastFM Scrobbling", // Option to Connect + "settings.option.connectivity.lastfmScrobble.delay": "LastFM Scrobble Delay (%)", + "settings.option.connectivity.lastfmScrobble.nowPlaying": "Enable LastFM Now Playing", + "settings.option.connectivity.lastfmScrobble.removeFeatured": "Remove featuring artists from song title (LastFM)", + "settings.option.connectivity.lastfmScrobble.filterLoop": "Filter looped track (LastFM)", + // Refer to term.connect for the connect button + + // Settings - Experimental + "settings.header.experimental": "Experimental", + "settings.header.experimental.description": "Adjust the experimental settings for Cider.", + "settings.option.experimental.compactUI": "Compact UI", // Toggle + "settings.option.experimental.closeButtonBehaviour": "Close Button Behavior", + "settings.option.experimental.closeButtonBehaviour.quit": "Quit Cider", + "settings.option.experimental.closeButtonBehaviour.minimizeTaskbar": "Minimize to taskbar", + "settings.option.experimental.closeButtonBehaviour.minimizeTray": "Minimize to system tray", + // Refer to term.disabled & term.enabled + + // Spatialization Menu + "spatial.spatialProperties" : "Spatial Properties", + "spatial.width" : "Width", + "spatial.height" : "Height", + "spatial.depth" : "Depth", + "spatial.gain" : "Gain", + "spatial.roomMaterials" : "Room Materials", + "spatial.roomDimensions" : "Room Dimensions", + "spatial.roomPositions" : "Room Positions", + "spatial.setDimensions" : "Set Dimensions", + "spatial.setPositions" : "Set Positions", + "spatial.up" : "Up", + "spatial.front" : "Front", + "spatial.left" : "Left", + "spatial.right" : "Right", + "spatial.back" : "Back", + "spatial.down" : "Down", + "spatial.listener" : "Listener", + "spatial.audioSource" : "Audio Source", + + // Settings - Unfinished + "settings.header.unfinished": "Unfinished", + + // Web Remote + "remote.web.title": "Cider Remote", + "remote.web.description": "Scan the QR code to pair your phone up with this Cider instance", + + // About + "about.thanks": "Major thanks to the Cider Collective Team and all of our contributors." +} \ No newline at end of file diff --git a/src/i18n/hu_HU.jsonc b/src/i18n/hu_HU.jsonc new file mode 100644 index 00000000..d8e1f867 --- /dev/null +++ b/src/i18n/hu_HU.jsonc @@ -0,0 +1,197 @@ +{ // Base File + // App info + "app.name": "Cider", + // Dialogs + "dialog.cancel": "Mégsem", + "dialog.ok": "OK", + + // Notification + "notification.updatingLibrarySongs": "Zenekönyvtár frissítése...", + "notification.updatingLibraryAlbums": "Albumok frissítése...", + "notification.updatingLibraryArtists": "Előadók frissítése...", + + // Terms + "term.appleMusic": "Apple Music", + "term.applePodcasts": "Apple Podcastok", + "term.itunes": "iTunes", + "term.github": "GitHub", + "term.discord": "Discord", + "term.learnMore": "Tudj meg többet", + "term.accountSettings": "Fiókbeállítások", + "term.logout": "Kijelentkezés", + "term.login": "Bejelentkezés", + "term.about": "About", + "term.privateSession": "Privát hallgatás", + "term.queue": "Várólista", + "term.search": "Keresés", + "term.library": "Könyvtár", + "term.listenNow": "Hallgatás most", + "term.browse": "Böngészés", + "term.radio": "Rádió", + "term.recentlyAdded": "Nemrég hozzáadott", + "term.songs": "Dalok", + "term.albums": "Albumok", + "term.artists": "Előadók", + "term.podcasts": "Podcastok", + "term.playlists": "Lejátszási listák", + "term.playlist": "Lejátszási lista", + "term.play": "Lejátszás", + "term.pause": "Megállítás", + "term.previous": "Előző", + "term.next": "Következő", + "term.shuffle": "Keverés", + "term.repeat": "Ismétlés", + "term.volume": "Hangerő", + "term.mute": "Némítás", + "term.unmute": "Némítás feloldása", + "term.share": "Megosztás", + "term.settings": "Beállítások", + "term.seeAll": "Összes", + "term.sortBy": "Rendezés", + "term.sortBy.album": "Album", + "term.sortBy.artist": "Előadó", + "term.sortBy.name": "Név", + "term.sortBy.genre": "Műfaj", + "term.sortBy.releaseDate": "Kiadás dátuma", + "term.sortBy.duration": "Időtartam", + "term.sortOrder": "A-Z", + "term.sortOrder.ascending": "Növekvő", + "term.sortOrder.descending": "Csökkenő", + "term.viewAs": "Megjelenítés", + "term.viewAs.coverArt": "Borító", + "term.viewAs.list": "Lista", + "term.size": "Méret", + "term.size.normal": "Normál", + "term.size.compact": "Kompakt", + "term.enable": "Be", + "term.disable": "Ki", + "term.enabled": "Be", + "term.disabled": "Ki", + "term.connect": "Csatlakoztatás", + "term.confirm": "Jóváhagyás ?", + "term.more": "Több", + "term.less": "Kevesebb", + "term.showMore": "Mutass többet", + "term.showLess": "Mutass kevesebbet", + "term.topSongs" : "A legjobb dalok", + "term.latestReleases": "Új megjelenések", + "term.time.added": "Hozzáadva", + "term.time.released": "Kiadva", + "term.time.updated": "Frissítve", + "term.fullscreenView": "Teljes képernyős mód", + "term.defaultView": "Alapértelmezett nézet", + + + // Home + "home.title": "Kezdőlap", + "home.recentlyPlayed": "Nemrég játszott", + "home.recentlyAdded": "Nemrég hozzáadott", + "home.artistsFeed": "Az előadóid feedje", + "home.madeForYou": "Személyre szabva", + "home.friendsListeningTo": "A barátok épp ezt hallgatják", + "home.followedArtists": "Követett előadók", + // Errors + "error.appleMusicSubRequired": "Apple Music előfizetés szükséges.", + + // Actions + "action.addToLibrary": "Hozzáadás a Könyvtárhoz", + "action.addToLibrary.success": "Hozzáadva a Könyvtárhoz", + "action.addToLibrary.error": "Hiba történt! Sikertelen hozzáadás.", + "action.removeFromLibrary": "Törlés a Könytárból", + "action.removeFromLibrary.success": "Törölve a Könyvtárból", + "action.addToQueue": "Hozzáadás a várólistához", + "action.addToQueue.success": "Hozzáadva a várólistához", + "action.addToQueue.error": "Sikertelen hozzáadás a várólistához", + "action.removeFromQueue": "Törlés a várólistáról", + "action.removeFromQueue.success": "Törölve a várólistáról", + "action.removeFromQueue.error": "Sikertelen törlés a várólistáról", + "action.addToPlaylist": "Lejátszási listához adás", + "action.removeFromPlaylist": "Törlés a lejátszási listáról", + "action.addToFavorites": "Hozzáadás a kedvencekhez", + "action.follow": "Követés", + "action.follow.success": "Követve", + "action.follow.error": "Sikertelen követés", + "action.unfollow": "Követés visszavonása", + "action.unfollow.success": "Követés visszavonva", + "action.unfollow.error": "Sikertelen visszavonás", + "action.playNext": "Lejátszás következőként", + "action.playLater": "Lejátszás utolsóként", + "action.startRadio": "Állomás létrehozása", + "action.goToArtist": "Előadó megjelenítése", + "action.goToAlbum": "Album megjelenítése", + "action.moveToTop": "Mozgatás legfelülre", + "action.share": "Megosztás", + "action.rename": "Átnevezés", + "action.love": "Szeretem", + "action.unlove": "Mégsem szeretem", + "action.dislike": "Kevesebb ilyen javasolása", + "action.undoDislike": "A Kevesebb ilyen javasolása visszavonása", + "action.showWebRemoteQR": "Távirányító QR kód megjelenítése", + // Settings - Audio + "settings.header.audio": "Hang", + "settings.header.audio.description": "A Cider hangbeállításainak módosítása.", + "settings.option.audio.quality": "Hangminőség", // Dropdown + "settings.header.audio.quality.high": "Magas", + "settings.header.audio.quality.low": "Alacsony", + "settings.header.audio.quality.auto": "Auto", + "settings.option.audio.seamlessTransition": "Szünetmentes lejátszás", // Toggle + "settings.option.audio.enableAdvancedFunctionality": "Haladó funkcionalitás engedélyezése", // Toggle + "settings.option.audio.enableAdvancedFunctionality.description": "Az AudioContext funkció engedélyezése lehetővé teszi a fejlettebb hangfunkciókat, például a Normalizásást, az Equalizereket és a Visualizer funkciókat, azonban egyes számítógépeken ez akadozást okozhat a hangsávokban.", + "settings.option.audio.enableAdvancedFunctionality.audioNormalization": "Normalizálás", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioNormalization.description": "Normalizálja az egyes zeneszámok hangosabb részeit, hogy egységesebb hallgatási élményt hozzon létre.", + "settings.option.audio.enableAdvancedFunctionality.audioSpatialization": "Térbeli hang", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioSpatialization.description": "Térbeli hang és a hang háromdimenziósabbá tétele (Ez nem összekeverendő a Dolby Atmos-szal!)", + // Settings - Visual + "settings.header.visual": "Vizuális", + "settings.header.visual.description": "A Cider vizuális beállításainak módosítása.", + "settings.option.visual.windowBackgroundStyle": "Ablak háttér stílusa", // Toggle + "settings.header.visual.windowBackgroundStyle.none": "None", + "settings.header.visual.windowBackgroundStyle.artwork": "Borító", + "settings.option.visual.animatedArtwork": "Animált borító", // Dropdown + "settings.header.visual.animatedArtwork.always": "Mindig", + "settings.header.visual.animatedArtwork.limited": "Oldalakra és speciális bejegyzésekre korlátozva.", + "settings.header.visual.animatedArtwork.disable": "Kikapcsolás mindenhol", + "settings.option.visual.animatedArtworkQuality": "Animált borító minősége", // Dropdown + "settings.header.visual.animatedArtworkQuality.low": "Alacsony", + "settings.header.visual.animatedArtworkQuality.medium": "Közepes", + "settings.header.visual.animatedArtworkQuality.high": "Magas", + "settings.header.visual.animatedArtworkQuality.extreme": "Extrém", + "settings.option.visual.animatedWindowBackground": "Animált ablakháttér", // Toggle + "settings.option.visual.hardwareAcceleration": "Hardveres gyorsítás", // Dropdown + "settings.header.visual.hardwareAcceleration.default": "Alap", + "settings.header.visual.hardwareAcceleration.webGPU": "WebGPU", + // Refer to term.disabled for the disabled option + "settings.option.visual.showPersonalInfo": "Személyes adatok mutatása", // Toggle + // Settings - General (Reserved) + "settings.header.general": "Általános", + "settings.header.general.description": "A Cider általános beállításainak módosítása.", + + // Settings - Lyrics + "settings.header.lyrics": "Dalszöveg", + "settings.header.lyrics.description": "A Cider dalszöveg beállításainak módosítása.", + "settings.option.lyrics.enableMusixmatch": "MusixMatch dalszövegek engedélyezése", // Toggle + "settings.option.lyrics.enableMusixmatchKaraoke": "Karaoke mód bekapcsolása (Csak MusixMatch)", // Toggle + "settings.option.lyrics.musixmatchPreferredLanguage": "MusixMatch fordítás nyelve", // Dropdown + "settings.option.lyrics.enableYoutubeLyrics": "YouTube dalszövegek engedélyezése a zenei videóknál", // Toggle + + // Settings - Connectivity + "settings.header.connectivity": "Csatlakozások", + "settings.header.connectivity.description": "A Cider csatlakozás beállításainak módosítása.", + "settings.option.connectivity.discordRPC": "Discord Rich Presence", // Dropdown + // Refer to term.disabled for the disabled option + "settings.header.connectivity.discordRPC.cider": "Megjelenítés 'Cider'-ként", + "settings.header.connectivity.discordRPC.appleMusic": "Megjelenítés 'Apple Music'-ként", + "settings.option.connectivity.discordRPC.clearOnPause": "Discord Rich Presence törlése megállításnál", // Toggle + "settings.option.connectivity.lastfmScrobble": "LastFM Scrobbling", // Option to Connect + // Refer to term.connect for the connect button + + // Settings - Experimental + "settings.header.experimental": "Kísérleti", + "settings.header.experimental.description": "A Cider kísérleti beállításainak módosítása.", + "settings.option.experimental.compactUI": "Kompakt UI", // Toggle + // Refer to term.disabled & term.enabled + + // Web Remote + "remote.web.title": "Cider Remote", + "remote.web.description": "Olvasd be ezt a QR-kódot a telefonoddal, hogy tudd vezérelni a lejátszót." +} diff --git a/src/i18n/ja_JP.jsonc b/src/i18n/ja_JP.jsonc new file mode 100644 index 00000000..5a8bbd55 --- /dev/null +++ b/src/i18n/ja_JP.jsonc @@ -0,0 +1,283 @@ +{ + // App info + "app.name": "Cider", + + "date.format": "${y}年${m}月${d}日", + + // i18n Info + "i18n.languageName": "日本語", // name of language in native language + "i18n.languageNameEnglish": "Japanese", // name of language in English + "i18n.category": "main", // main = real language, fun = fun community languages + "i18n.authors": "@maikirakiwi", // Authors, if you contribute to this file feel free to add your name seperated with a space + + // Dialogs + "dialog.cancel": "キャンセル", + "dialog.ok": "OK", + + // Notification + "notification.updatingLibrarySongs": "ライブラリの更新中...", + "notification.updatingLibraryAlbums": "ライブラリの更新中...", + "notification.updatingLibraryArtists": "ライブラリの更新中...", + + // Terms + "term.appleMusic": "Apple Music", // Follows brand term + "term.applePodcasts": "Apple Podcasts", // Follows brand term + "term.itunes": "iTunes", // Follows brand term + "term.github": "GitHub", // Follows brand term + "term.discord": "Discord", // Follows brand term + "term.learnMore": "詳しい情報", + "term.accountSettings": "アカウント設定", + "term.logout": "サインアウト", + "term.login": "サインイン", + "term.about": "Ciderについて", + "term.privateSession": "プライベートセッション", + "term.queue": "次はこちら", + "term.search": "検索", + "term.library": "ライブラリ", + "term.listenNow": "今すぐ聴く", + "term.browse": "見つける", + "term.radio": "ラジオ", + "term.recentlyAdded": "最近追加した項目", + "term.songs": "曲", + "term.albums": "アルバム", + "term.artists": "アーティスト", + "term.podcasts": "Podcast", + "term.playlists": "プレイリスト", + "term.playlist": "プレイリスト", + "term.play": "再生", + "term.pause": "停止", + "term.previous": "戻る", + "term.next": "次へ", + "term.shuffle": "シャッフル", + "term.repeat": "リピート", + "term.volume": "音量", + "term.mute": "ミュート", + "term.unmute": "ミュート解除", + "term.share": "共有", + "term.settings": "設定", + "term.seeAll": "すべて見る", + "term.sortBy": "並べ替え", + "term.sortBy.album": "アルバム", + "term.sortBy.artist": "アーティスト", + "term.sortBy.name": "曲名", + "term.sortBy.genre": "ジャンル", + "term.sortBy.releaseDate": "配信開始日", + "term.sortBy.duration": "時間", + "term.sortOrder": "並べ替え", + "term.sortOrder.ascending": "昇順", + "term.sortOrder.descending": "降順", + "term.viewAs": "表示", + "term.viewAs.coverArt": "カバーアート", + "term.viewAs.list": "リスト", + "term.size": "サイズ", + "term.size.normal": "普通", + "term.size.compact": "コンパクト", + "term.enable": "ON", + "term.disable": "OFF", + "term.enabled": "ON", + "term.disabled": "OFF", + "term.connect": "接続", + "term.connecting": "接続中", + "term.disconnect": "切断", + "term.authed": "認証済み", + "term.confirm": "よろしいでしょうか?", + "term.more": "もっと", + "term.less": "減らす", + "term.showMore": "もっと見る", + "term.showLess": "表示数を少なくする", + "term.topSongs" : "トップソング", + "term.latestReleases": "ニューリリース", + "term.time.added": "追加日", + "term.time.released": "配信開始日", + "term.time.updated": "最終更新日", + "term.time.hours": "時間", + "term.time.hour": "時間", + "term.time.minutes": "分", + "term.time.minute": "分", + "term.time.seconds": "秒", + "term.time.second": "秒", + "term.fullscreenView": "全画面表示", + "term.defaultView": "ウィンドウ表示", + "term.spacializedAudioSetting": "オーディオ空間化設定", + "term.clearAll": "消去", + "term.recentStations": "最近の再生", + "term.language": "言語", + "term.noLyrics": "ローディング。。 / 歌詞が見つからない / 器楽曲.", + "term.copyright": "著作権", + "term.rightsReserved": "All Rights Reserved.", // Translation does not exist in Japanese + "term.sponsor": "スポンサーになりましょう", + "term.ciderTeam": "Cider チーム", + "term.developer": "開発者", + "term.socialTeam": "ソーシャル チーム", + "term.contributors": "貢献者", + "term.equalizer": "イコライザー", + "term.reset": "リセット", + "term.tracks": "曲", // Assume x amount of tracks. e.g. 50 tracks + + // Home + "home.title": "ホーム", + "home.recentlyPlayed": "最近の再生", + "home.recentlyAdded": "最近追加した項目", + "home.artistsFeed": "アーティストのフィード", + "home.artistsFeed.noArtist": "自分の好きなアーティストをフォローしましょう・", + "home.madeForYou": "あなたにおすすめ", + "home.friendsListeningTo": "友達が聴いている", + "home.followedArtists": "フォローしているアーティスト", + // Errors + "error.appleMusicSubRequired": "Apple Musicのサブスクリプションが必要です。", + "error.connectionError": "Apple Musicに接続できません。", + "error.noResults": "見つかりませんでした", + "error.noResults.description": "もう一度お試しください。", + + //Podcasts + "podcast.followOnCider": "Ciderでフォロー", + "podcast.followedOnCider": "フォロー中", + "podcast.subscribeOnItunes": "iTunesで購読", + "podcast.subscribedOnItunes": "購読中", + "podcast.itunesStore": "iTunes Store", + "podcast.episodes": "番組", + "podcast.playEpisode": "再生", + "podcast.website": "Podcast ウェブ", + + // Actions + "action.addToLibrary": "ライブラリに追加", + "action.addToLibrary.success": "ライブラリに追加されました", + "action.addToLibrary.error": "ライブラリへの追加に失敗しました", + "action.removeFromLibrary": "ライブラリから削除", + "action.removeFromLibrary.success": "ライブラリから削除されました", + "action.addToQueue": "「次はこちら」に項目を追加", + "action.addToQueue.success": "「次はこちら」に項目を追加されました", + "action.addToQueue.error": "操作を完了できませんでした", + "action.addToPlaylist": "プレイリストに追加", + "action.removeFromPlaylist": "プレイリストから削除", + "action.addToFavorites": "ラブに追加", + "action.removeFromQueue": "「次はこちら」から項目を削除", + "action.removeFromQueue.success": "「次はこちら」から項目を削除されました", + "action.removeFromQueue.error": "操作を完了できませんでした", + "action.follow": "フォロー", + "action.follow.success": "フォロー中", + "action.follow.error": "操作を完了できませんでした", + "action.unfollow": "フォロー解除", + "action.unfollow.success": "フォローをやめました", + "action.unfollow.error": "操作を完了できませんでした", + "action.playNext": "次に再生", + "action.playLater": "最後に再生", + "action.startRadio": "ステーションを作成", + "action.goToArtist": "アーティストへ移動", + "action.goToAlbum": "アルバムへ移動", + "action.moveToTop": "上に戻る", + "action.share": "曲を共有", + "action.rename": "名前の変更", + "action.love": "ラブ", + "action.unlove": "ラブを解除", + "action.dislike": "これに似たものをすすめない", + "action.undoDislike": "「これと似た曲のおすすめを減らす」を取り消す", + "action.showWebRemoteQR": "WEBリモコンQRコードを表示", + "action.playTracksNext": "${app.selectedMediaItems.length}曲が次に再生", + "action.playTracksLater": "${app.selectedMediaItems.length}曲が最後に再生", + "action.removeTracks": "${self.selectedItems.length}曲が「次はこちら」から削除", + + // Settings - Audio + "settings.header.audio": "オーディオ", + "settings.header.audio.description": "Ciderのオーディオ設定", + "settings.option.audio.quality": "音質", // Dropdown + "settings.header.audio.quality.high": "高品質", + "settings.header.audio.quality.low": "高効率", + "settings.header.audio.quality.auto": "自動", + "settings.option.audio.seamlessTransition": "曲間なしで再生", // Toggle + "settings.option.audio.enableAdvancedFunctionality": "先進的な機能", // Toggle + "settings.option.audio.enableAdvancedFunctionality.description": "AudioContext 機能を有効にすると、オーディオノーマライズ、空間オーディオ、イコライザーなどの機能を使用できますが、音が途切れるかもしれません。", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioNormalization": "オーディオノーマライズ", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioNormalization.description": "さまざまな曲の音量を均一にし、より整った音を楽しめるようにする機能です。", + "settings.option.audio.enableAdvancedFunctionality.audioSpatialization": "オーディオ空間化", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioSpatialization.description": "オーディオを空間に分散させる機能です。 (ドルビーアトモスではありません)", + // Settings - Visual + "settings.header.visual": "ビジュアル", + "settings.header.visual.description": "Ciderのビジュアル設定", + "settings.option.visual.windowBackgroundStyle": "アプリウインドウの背景スタイル", // Toggle + "settings.header.visual.windowBackgroundStyle.none": "なし", + "settings.header.visual.windowBackgroundStyle.artwork": "アートワーク", + "settings.option.visual.animatedArtwork": "アニメーションアートワーク", // Dropdown + "settings.header.visual.animatedArtwork.always": "常に表示", + "settings.header.visual.animatedArtwork.limited": "アーティストページのみ表示", + "settings.header.visual.animatedArtwork.disable": "オフ", + "settings.option.visual.animatedArtworkQuality": "アニメーションアートワークの品質", // Dropdown + "settings.header.visual.animatedArtworkQuality.low": "低", + "settings.header.visual.animatedArtworkQuality.medium": "中", + "settings.header.visual.animatedArtworkQuality.high": "高", + "settings.header.visual.animatedArtworkQuality.veryHigh": "超高", + "settings.header.visual.animatedArtworkQuality.extreme": "最高", + "settings.option.visual.animatedWindowBackground": "アプリウィンドウの背景をアニメーション化", // Toggle + "settings.option.visual.hardwareAcceleration": "ハードウェア アクセラレーション", // Dropdown + "settings.option.visual.hardwareAcceleration.description": "アプリを再起動する必要があります", + "settings.header.visual.hardwareAcceleration.default": "既定", + "settings.header.visual.hardwareAcceleration.webGPU": "WebGPU", + // Refer to term.disabled for the disabled option + "settings.option.visual.showPersonalInfo": "プロフィールを表示", // Toggle + // Settings - General (Reserved) + "settings.header.general": "一般", + "settings.header.general.description": "Ciderの一般設定", + + // Settings - Lyrics + "settings.header.lyrics": "歌詞", + "settings.header.lyrics.description": "歌詞の表示設定", + "settings.option.lyrics.enableMusixmatch": "Musixmatchで歌詞を表示する", // Toggle + "settings.option.lyrics.enableMusixmatchKaraoke": "カラオケモードを有効にする (Musixmatchのみ)", // Toggle + "settings.option.lyrics.musixmatchPreferredLanguage": "歌詞の優先言語", // Dropdown + "settings.option.lyrics.enableYoutubeLyrics": "YouTubeの歌詞をミュージックビデオに使用する", // Toggle + + // Settings - Connectivity + "settings.header.connectivity": "アプリと連携", + "settings.header.connectivity.description": "Ciderの連携設定", + "settings.option.connectivity.discordRPC": "Discord Rich Presence", // Dropdown + // Refer to term.disabled for the disabled option + "settings.header.connectivity.discordRPC.cider": "'Cider' を表示する", + "settings.header.connectivity.discordRPC.appleMusic": "'Apple Music' を表示する", + "settings.option.connectivity.discordRPC.clearOnPause": "一時停止時にDiscord Rich Presenceをクリアする", // Toggle + "settings.option.connectivity.lastfmScrobble": "LastFM Scrobbling", // Option to Connect + "settings.option.connectivity.lastfmScrobble.delay": "LastFM Scrobble Delay (%)", + "settings.option.connectivity.lastfmScrobble.nowPlaying": "Enable LastFM Now Playing", + "settings.option.connectivity.lastfmScrobble.removeFeatured": "Remove featuring artists from song title (LastFM)", + // Refer to term.connect for the connect button + + // Settings - Experimental + "settings.header.experimental": "試験的な機能", + "settings.header.experimental.description": "開発中の実験的な機能は不完全で不安定である可能性があります", + "settings.option.experimental.compactUI": "コンパクトインターフェース", // Toggle + "settings.option.experimental.closeButtonBehaviour": "「閉じる」ボタンの動作", // Dropdown + "settings.option.experimental.closeButtonBehaviour.quit": "アプリを終了する", + "settings.option.experimental.closeButtonBehaviour.minimizeTaskbar": "タスクバーに最小化する", + "settings.option.experimental.closeButtonBehaviour.minimizeTray": "トレイに最小化する", + // Refer to term.disabled & term.enabled + + // Spatialization Menu + "spatial.spatialProperties" : "空間化のプロパティ", + "spatial.width" : "幅", + "spatial.height" : "高さ", + "spatial.depth" : "奥行", + "spatial.gain" : "ゲイン", + "spatial.roomMaterials" : "部屋のマテリアル", + "spatial.roomDimensions" : "部屋の大きさ", + "spatial.roomPositions" : "部屋の位置", + "spatial.setDimensions" : "大きさを設定", + "spatial.setPositions" : "位置を設定", + "spatial.up" : "上", + "spatial.front" : "前", + "spatial.left" : "左", + "spatial.right" : "右", + "spatial.back" : "後", + "spatial.down" : "下", + "spatial.listener" : "リスナー", + "spatial.audioSource" : "音源", + + // Settings - Unfinished + "settings.header.unfinished": "未完成", + + // Web Remote + "remote.web.title": "Cider リモート", + "remote.web.description": "QRコードを使用して、Ciderとスマートフォンをペアリングする", + + //About + "about.thanks": "Cider Collective とご協力いただいた貢献者様に感謝申し上げます。" + +} diff --git a/src/i18n/pt_BR.jsonc b/src/i18n/pt_BR.jsonc new file mode 100644 index 00000000..76ff31c5 --- /dev/null +++ b/src/i18n/pt_BR.jsonc @@ -0,0 +1,238 @@ +{ // Base File + // App info + "app.name": "Cider", + + "date.format": "${d} ${m}, ${y}", + + // Dialogs + "dialog.cancel": "Cancelar", + "dialog.ok": "OK", + + // Notification + "notification.updatingLibrarySongs": "Atualizando músicas na biblioteca...", + "notification.updatingLibraryAlbums": "Atualizando albuns na biblioteca...", + "notification.updatingLibraryArtists": "Atualizando artistas na biblioteca...", + "notification.connectionError": "Houve um problema a se conectar no Apple Music", + + // Terms + "term.appleMusic": "Apple Music", + "term.applePodcasts": "Apple Podcasts", + "term.itunes": "iTunes", + "term.github": "GitHub", + "term.discord": "Discord", + "term.learnMore": "Saiba Mais", + "term.accountSettings": "Definições da Conta", + "term.logout": "Sair", + "term.login": "Entrar", + "term.about": "Sobre", + "term.privateSession": "Sessão Privada", + "term.queue": "Fila", + "term.search": "Procurar", + "term.library": "Biblioteca", + "term.listenNow": "Ouça Agora", + "term.browse": "Explorar", + "term.radio": "Radio", + "term.recentlyAdded": "Adicionado Recentemente", + "term.songs": "Músicas", + "term.albums": "Albuns", + "term.artists": "Artistas", + "term.podcasts": "Podcasts", + "term.playlists": "Listas de Reprodução", + "term.playlist": "Lista de Reprodução", + "term.play": "Tocar", + "term.pause": "Pausar", + "term.previous": "Anterior", + "term.next": "Próximo", + "term.shuffle": "Aleatório", + "term.repeat": "Repetir", + "term.volume": "Volume", + "term.mute": "Mudo", + "term.unmute": "Tirar o Mudo", + "term.share": "Partilhar", + "term.settings": "Definições", + "term.seeAll": "Ver Tudo", + "term.sortBy": "Organizar Por", + "term.sortBy.album": "Album", + "term.sortBy.artist": "Artista", + "term.sortBy.name": "Nome", + "term.sortBy.genre": "Genero", + "term.sortBy.releaseDate": "Data de Lançamento", + "term.sortBy.duration": "Duração", + "term.sortOrder": "A-Z", + "term.sortOrder.ascending": "Ascendente", + "term.sortOrder.descending": "Descendente", + "term.viewAs": "Ver Como", + "term.viewAs.coverArt": "Capa", + "term.viewAs.list": "Lista", + "term.size": "Tamanho", + "term.size.normal": "Normal", + "term.size.compact": "Compacto", + "term.enable": "Ativar", + "term.disable": "Desativar", + "term.enabled": "Ativado", + "term.disabled": "Desativado", + "term.connect": "Conectar", + "term.disconnect": "Desconectar", + "term.connecting": "Conectando", + "term.confirm": "Confirmar ?", + "term.more": "Mais", + "term.less": "Menos", + "term.showMore": "Mostrar Mais", + "term.showLess": "Mostrar Menos", + "term.topSongs" : "Top de Músicas", + "term.latestReleases": "Ultimos Lançamentos", + "term.time.added": "Adicionado", + "term.time.released": "Lançado", + "term.time.updated": "Atualizado", + "term.fullscreenView": "Visualização em Tela Cheia", + "term.defaultView": "Visualização Normal", + "term.spacializedAudioSetting": "Definições de Audio Espacial", + "term.clearAll": "Limpar Tudo", + "term.language": "Idioma", + "term.recentStations": "Estações Recentes", + + // Home + "home.title": "Inicio", + "home.recentlyPlayed": "Tocado Recentemente", + "home.recentlyAdded": "Adicionado Recentemente", + "home.artistsFeed": "Novidades dos seus Artistas", + "home.madeForYou": "Feito para Você", + "home.friendsListeningTo": "Amigos Ouvindo", + "home.followedArtists": "Artistas Seguidos", + // Errors + "error.appleMusicSubRequired": "Necessário uma assinatura Apple Music.", + + // Actions + "action.addToLibrary": "Adicionar à Biblioteca", + "action.addToLibrary.success": "Adicionado à Biblioteca", + "action.addToLibrary.error": "Erro ao Adicionar na Biblioteca", + "action.removeFromLibrary": "Remover da Biblioteca", + "action.removeFromLibrary.success": "Removido da Biblioteca", + "action.addToQueue": "Adicionar à Fila", + "action.addToQueue.success": "Adicionado à Fila", + "action.addToQueue.error": "Erro ao Adicionar à Fila", + "action.removeFromQueue": "Remover da Fila", + "action.removeFromQueue.success": "Removido da Fila", + "action.removeFromQueue.error": "Erro ao Remover da Fila", + "action.addToPlaylist": "Adicionar à Lista de Reprodução", + "action.removeFromPlaylist": "Remover da Lista de Reprodução", + "action.addToFavorites": "Adicionar aos Favoritos", + "action.follow": "Seguir", + "action.follow.success": "A Seguir", + "action.follow.error": "Erro ao Seguir", + "action.unfollow": "Deixar de Seguir", + "action.unfollow.success": "Deixou de Seguir", + "action.unfollow.error": "Erro ao Deixar de Seguir", + "action.playNext": "Tocar Proximo", + "action.playLater": "Tocar por Ultimo", + "action.startRadio": "Começar Radio", + "action.goToArtist": "Ir para o Artista", + "action.goToAlbum": "Ir para o Album", + "action.moveToTop": "Mover para cima", + "action.share": "Partilhar", + "action.rename": "Mudar o Nome", + "action.love": "Gostar", + "action.unlove": "Deixar de Gostar", + "action.dislike": "Não Gostar", + "action.undoDislike": "Deixar de Não Gostar", + "action.showWebRemoteQR": "Mostrar o QR para Página Remota", + // Settings - Audio + "settings.header.audio": "Audio", + "settings.header.audio.description": "Ajustar as definições de audio no Cider.", + "settings.option.audio.quality": "Qualidade do Audio", // Dropdown + "settings.header.audio.quality.high": "Alta", + "settings.header.audio.quality.low": "Baixa", + "settings.header.audio.quality.auto": "Automatico", + "settings.option.audio.seamlessTransition": "Transição de Áudio Perfeita", // Toggle + "settings.option.audio.enableAdvancedFunctionality": "Ativar Funcionabilidades Avançadas", // Toggle + "settings.option.audio.enableAdvancedFunctionality.description": "Habilitar a funcionalidade AudioContext permitirá recursos de áudio estendidos, como Normalização de Áudio , Equalizadores e Visualizadores. No entanto, em alguns sistemas, isso pode causar travamentos nas faixas de áudio.", + "settings.option.audio.enableAdvancedFunctionality.audioNormalization": "Normalização de Audio", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioNormalization.description": "Normaliza o volume alto para faixas individuais para criar uma experiência de audição mais uniforme.", + "settings.option.audio.enableAdvancedFunctionality.audioSpatialization": "Audio Espacial", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioSpatialization.description": "Espacialize o áudio e torne o áudio mais tridimensional (nota: isto não é Dolby Atmos)", + // Settings - Visual + "settings.header.visual": "Visual", + "settings.header.visual.description": "Ajustar as Definições de Visual do Cider.", + "settings.option.visual.windowBackgroundStyle": "Estilo do Fundo da Janela", // Toggle + "settings.header.visual.windowBackgroundStyle.none": "Nenhum", + "settings.header.visual.windowBackgroundStyle.artwork": "Capa", + "settings.option.visual.animatedArtwork": "Capa Animada", // Dropdown + "settings.header.visual.animatedArtwork.always": "Sempre", + "settings.header.visual.animatedArtwork.limited": "Limitado a páginas e entradas especiais", + "settings.header.visual.animatedArtwork.disable": "Desativar em Tudo", + "settings.option.visual.animatedArtworkQuality": "Qualidade da Capa Animada", // Dropdown + "settings.header.visual.animatedArtworkQuality.low": "Baixa", + "settings.header.visual.animatedArtworkQuality.medium": "Media", + "settings.header.visual.animatedArtworkQuality.high": "Alta", + "settings.header.visual.animatedArtworkQuality.veryHigh": "Muito Alta", + "settings.header.visual.animatedArtworkQuality.extreme": "Extrema", + "settings.option.visual.animatedWindowBackground": "Fundo de Janela Animado", // Toggle + "settings.option.visual.hardwareAcceleration": "Acelaração no Hardware", // Dropdown + "settings.option.visual.hardwareAcceleration.description": "Necessário reiniciar a aplicação", + "settings.header.visual.hardwareAcceleration.default": "Normal", + "settings.header.visual.hardwareAcceleration.webGPU": "WebGPU", + + + // Refer to term.disabled for the disabled option + "settings.option.visual.showPersonalInfo": "Mostrar Informaçoes Pessoais", // Toggle + // Settings - General (Reserved) + "settings.header.general": "Principal", + "settings.header.general.description": "Ajustar as definiçoes principais no Cider.", + + // Settings - Lyrics + "settings.header.lyrics": "Letras", + "settings.header.lyrics.description": "Ajustar as definições das letras no Cider.", + "settings.option.lyrics.enableMusixmatch": "Ativar Letras do Musixmatch", // Toggle + "settings.option.lyrics.enableMusixmatchKaraoke": "Ativar Modo Karaoke (só para Musixmatch)", // Toggle + "settings.option.lyrics.musixmatchPreferredLanguage": "Idioma Preferido para Tradução Musixmatch", // Dropdown + "settings.option.lyrics.enableYoutubeLyrics": "Ativar letras do Youtube para videos musicais", // Toggle + + // Settings - Connectivity + "settings.header.connectivity": "Conectividade", + "settings.header.connectivity.description": "Ajustar as definições de conectividade no Cider.", + "settings.option.connectivity.discordRPC": "Discord Rich Presence", // Dropdown + // Refer to term.disabled for the disabled option + "settings.header.connectivity.discordRPC.cider": "Mostrar como 'Cider'", + "settings.header.connectivity.discordRPC.appleMusic": "Mostrar como 'Apple Music'", + "settings.option.connectivity.discordRPC.clearOnPause": "Apagar Discord Rich Presence quando estiver pausado", // Toggle + "settings.option.connectivity.lastfmScrobble": "LastFM Scrobbling", // Option to Connect + "settings.option.connectivity.lastfmScrobble.delay": "Atraso dos Scrobbles do LastFM (%)", + "settings.option.connectivity.lastfmScrobble.nowPlaying": "Ativar LastFM Now Playing", + "settings.option.connectivity.lastfmScrobble.removeFeatured": "Remover artistas de colaboração do nome da música (LastFM)", + // Refer to term.connect for the connect button + + // Settings - Experimental + "settings.header.experimental": "Experimental", + "settings.header.experimental.description": "Ajustar as definições experimental no Cider.", + "settings.option.experimental.compactUI": "UI Compacto", // Toggle + // Refer to term.disabled & term.enabled + + // Spatialization Menu + "spatial.spatialProperties" : "Propriedades do Espacial", + "spatial.width" : "Largura", + "spatial.height" : "Altura", + "spatial.depth" : "Profundidade", + "spatial.roomMaterials" : "Materiais da Sala", + "spatial.roomDimensions" : "Dimensões da Sala", + "spatial.roomPositions" : "Posições da Sala", + "spatial.setDimensions" : "Definir Dimensões", + "spatial.setPositions" : "Definir Posições", + "spatial.up" : "Acima", + "spatial.front" : "Frente", + "spatial.left" : "Esquerda", + "spatial.right" : "Direita", + "spatial.back" : "Atrás", + "spatial.down" : "Abaixo", + "spatial.listener" : "Ouvinte", + "spatial.audioSource" : "Fonte de Audio", + + + + + // Settings - Unfinished + "settings.header.unfinished": "Inacabado", + + // Web Remote + "remote.web.title": "Cider Remoto", + "remote.web.description": "Digitalize o código QR para emparelhar seu telefone com esta instância Cider" +} \ No newline at end of file diff --git a/src/i18n/tr_TR.jsonc b/src/i18n/tr_TR.jsonc new file mode 100644 index 00000000..6f15ae17 --- /dev/null +++ b/src/i18n/tr_TR.jsonc @@ -0,0 +1,294 @@ +{ + // i18n Info + "i18n.languageName": "Türkçe", // name of language in native language + "i18n.languageNameEnglish": "Turkish", // name of language in English + "i18n.category": "main", // main = real language, fun = fun community languages + "i18n.authors": "@gms10ur", // Authors, if you contribute to this file feel free to add your name seperated with a space + + // App info + "app.name": "Cider", + + "date.format": "${d} ${m}, ${y}", + + // Dialogs + "dialog.cancel": "İptal", + "dialog.ok": "Tamam", + + // Notification + "notification.updatingLibrarySongs": "Arşiv'deki şarkılar alınıyor...", + "notification.updatingLibraryAlbums": "Arşiv'deki albümler alınıyor...", + "notification.updatingLibraryArtists": "Arşiv'deki sanatçılar alınıyor...", + + // Terms + "term.appleInc": "Apple", + "term.appleMusic": "Apple Müzik", + "term.applePodcasts": "Apple Podcastler", + "term.itunes": "iTunes", + "term.github": "GitHub", + "term.discord": "Discord", + "term.learnMore": "Daha fazla bilgi edin", + "term.accountSettings": "Hesap Ayarları", + "term.logout": "Çıkış Yap", + "term.login": "Giriş yap", + "term.about": "Hakkında", + "term.privateSession": "Özel Oturum", + "term.queue": "Çalma Sırası", + "term.search": "Arama", + "term.library": "Arşiv", + "term.recentlyAdded": "Son Eklenenler", + "term.songs": "Şarkılar", + "term.albums": "Albümler", + "term.artists": "Sanatçılar", + "term.podcasts": "Podcastler", + "term.playlists": "Listeler", + "term.playlist": "Liste", + "term.play": "Oynat", + "term.pause": "Duraklat", + "term.previous": "Önceki", + "term.next": "Sonraki", + "term.shuffle": "Karıştır", + "term.repeat": "Tekrarla", + "term.volume": "Ses", + "term.mute": "Sustur", + "term.unmute": "Sesi Aç", + "term.share": "Paylaş", + "term.settings": "Ayarlar", + "term.seeAll": "Tümünü Gör", + "term.listenNow": "Şimdi Dinle", + "term.browse": "Göz At", + "term.radio": "Radyo", + "term.sortBy": "Sırala", + "term.sortBy.album": "Albüm", + "term.sortBy.artist": "Sanatçı", + "term.sortBy.name": "Şarkı İsmi", + "term.sortBy.genre": "Tür", + "term.sortBy.releaseDate": "Yayınlanma Tarihi", + "term.sortBy.duration": "Süre", + "term.sortOrder": "A-Z", + "term.sortOrder.ascending": "Çoğalan", + "term.sortOrder.descending": "Azalan", + "term.viewAs": "Şöyle Göster", + "term.viewAs.coverArt": "Albüm Kapağı", + "term.viewAs.list": "Liste", + "term.size": "Boyut", + "term.size.normal": "Normal", + "term.size.compact": "Daha Sıkı", + "term.enable": "Aç", + "term.disable": "Kapa", + "term.enabled": "Açık", + "term.disabled": "Kapalı", + "term.connect": "Bağlan", + "term.connecting": "Bağlanıyor", + "term.authed": "Bağlantı sağlandı", + "term.disconnect": "Bağlantıyı Kes", + "term.confirm": "Onayla?", + "term.more": "Daha Fazla", + "term.less": "Daha Az", + "term.showMore": "Daha Fazla Göster", + "term.showLess": "Daha Az Göster", + "term.topSongs" : "Popüler Parçalar", + "term.latestReleases": "Yeni Çıkan", + "term.time.added": "Şu tarihte eklendi: ", + "term.time.released": "Şu tarihte yayınalndı: ", + "term.time.updated": "Şu tarihte güncellendi: ", + "term.fullscreenView": "Tam Ekrana Geç", + "term.defaultView": "Normal Görünüme Dön", + "term.spacializedAudioSetting": "Uzamsal Ses Ayarları", + "term.clearAll": "Tümünü Temizle", + "term.recentStations": "Son İstasyonlar", + "term.language": "Dil", + "term.funLanguages": "Mizahi Diller", + "term.noLyrics": "Yükleniyor... / Şarkı Sözü Bulunamadı./ Enstrumantal.", + "term.copyright": "Copyright", + "term.rightsReserved": "Tüm Haklası Saklıdır.", + "term.sponsor": "Bu Projeye Destek Olun", + "term.ciderTeam": "Cider Ekibi", + "term.developer": "Geliştirici", + "term.socialTeam": "Sosyal Ekip", + "term.contributors": "Katkıda Bulunanlar", + "term.equalizer": "Ekolayzer", + "term.reset": "Sıfırla", + "term.tracks": "adet şarkı", // Assume x amount of tracks. e.g. 50 tracks + "term.time.hours": "saat", + "term.time.hour": "saat", + "term.time.minutes": "dakika", + "term.time.minute": "dakika", + "term.time.seconds": "saniye", + "term.time.second": "saniye", + + // Home + "home.title": "Ana Sayfa", + "home.recentlyPlayed": "Son Oynatılanlar", + "home.recentlyAdded": "Son Eklenenler", + "home.artistsFeed": "Son Çıkanlar", + "home.artistsFeed.noArtist": "Birkaç sanatçı takip ettiğinizde, sanatçılarınızın son çıkan yayınları burada gözükür.", + "home.madeForYou": "Sadece Size Özel", + "home.friendsListeningTo": "Arkadaşlarınızın Dinledikleri", + "home.followedArtists": "Takip Edilen Sanatçılar", + + // Errors + "error.appleMusicSubRequired": "Apple Müzik, aktif bir abonelik gerektirir.", + "error.connectionError": "Apple Müzik ile bağlantı kurulamadı.", + "error.noResults": "Hiç sonuç yok", + "error.noResults.description": "Tekrar deneyin.", + + //Podcasts + "podcast.followOnCider": "Cider'de Takip Et", + "podcast.followedOnCider": "Cider'de Takip Ediliyor", + "podcast.subscribeOnItunes": "itunes'de Abone Ol", + "podcast.subscribedOnItunes": "iTunes'de Abone Olundu", + "podcast.itunesStore": "iTunes Mağazası", + "podcast.episodes": "Bölümler", + "podcast.playEpisode": "Bölümü Oynat", + "podcast.website": "Web Sayfası", + + // Actions + "action.addToLibrary": "Arşiv'e Ekle", + "action.addToLibrary.success": "Arşiv'e Eklendi", + "action.addToLibrary.error": "Arşiv'e Eklenemedi", + "action.removeFromLibrary": "Arşiv'den Sil", + "action.removeFromLibrary.success": "Arşiv'den Silindi", + "action.addToPlaylist": "Liste'ye Ekle", + "action.removeFromPlaylist": "Liste'den Sil", + "action.addToFavorites": "Favorilere Ekle", + "action.moveToTop": "En Başa Taşı", + "action.rename": "Yeniden Adlandır", + "action.addToQueue": "Sıraya Ekle", + "action.addToQueue.success": "Sıraya Eklendi", + "action.addToQueue.error": "Sıraya Eklenemedi", + "action.removeFromQueue": "Sıradan Kaldır", + "action.removeFromQueue.success": "Sıradan Kaldırıldı", + "action.removeFromQueue.error": "Sıradan Kaldırılamadı", + "action.follow": "Takip Et", + "action.follow.success": "Takip Ediliyor", + "action.follow.error": "Takip Edilemedi", + "action.unfollow": "Takibi Bırak", + "action.unfollow.success": "Takipten Çıkıldı", + "action.unfollow.error": "Takipten Çıkılamadı", + "action.playNext": "Sıradaki Yap", + "action.playLater": "En Son Çal", + "action.startRadio": "İstasyon Yarat", + "action.goToArtist": "Sanatçıya Git", + "action.goToAlbum": "Albüme Git", + "action.share": "Paylaş", + "action.love": "Beğen", + "action.unlove": "Beğeniyi Kaldır", + "action.dislike": "Bunun Gibileri Daha Az Öner", + "action.undoDislike": "Bunun Gibileri Daha Az Önermeyi Geri Al", + "action.showWebRemoteQR": "Uzaktan Kumanda Bağla", + "action.playTracksNext": "Sonrasında ${app.selectedMediaItems.length} şarkıları çal", + "action.playTracksLater": "En Son ${app.selectedMediaItems.length} şarkıları çal", + "action.removeTracks": "Sıradan ${self.selectedItems.length} şarkıları kaldır", + + // Settings - General (Reserved) + "settings.header.general": "Genel Ayarlar", + "settings.header.general.description": "Genel ayarları buradan düzenleyin.", + "settings.option.general.language": "Dil Seçeneği", + + // Language optgroups + "settings.option.general.language.main": "Gerçek Diller", + "settings.option.general.language.fun": "Mizahi Diller", + "settings.option.general.language.unsorted": "Henüz Tamamlanmamış Diller", + + // Settings - Audio + "settings.header.audio": "Çalma / Oynatma", + "settings.header.audio.description": "Cider'in sizin için en iyi dinleme deneyimini sağlayabilmesi için ses ayarlarınızı yapın.", + "settings.option.audio.quality": "Ses Kalitesi", // Dropdown + "settings.header.audio.quality.high": "Yüksek Kalite", + "settings.header.audio.quality.low": "Yüksek Verimlilik", + "settings.header.audio.quality.auto": "Otomatik", + "settings.option.audio.seamlessTransition": "Kesintisiz Ses Geçişi", // Toggle + "settings.option.audio.enableAdvancedFunctionality": "Gelişmiş Ses Deneyimi", // Toggle + "settings.option.audio.enableAdvancedFunctionality.description": "Gelişmiş ses deneyiminin etkinleştirilmesi, Ses Normalleştirme, Ekolayzer ve Görselleştirici gibi genişletilmiş ses özelliklerine izin verir, ancak bu durum bazı sistemlerde seste bozulmalara neden olabilir.", + "settings.option.audio.enableAdvancedFunctionality.audioNormalization": "Ses Normalleştirme", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioNormalization.description": "Ses normalleştirme alçak ve yüksek sesli şarkıları dengeler ve daha düzgün bir dinleme deneyimi sağlar.", + "settings.option.audio.enableAdvancedFunctionality.audioSpatialization": "Uzamsal Ses", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioSpatialization.description": "Sesi uzamsallaştırın ve sesi daha 3 boyutlu hale getirin (not: Bu Dolby Atmos değildir)", + + // Settings - Visual + "settings.header.visual": "Görünüm", + "settings.header.visual.description": "Cider'in nasıl gözükmesini istediğinizi ayarlayın", + "settings.option.visual.windowBackgroundStyle": "Uygulama Arka Plan Stili", // Toggle + "settings.header.visual.windowBackgroundStyle.none": "Karanlık", + "settings.header.visual.windowBackgroundStyle.artwork": "Albüm Kapağı", + "settings.option.visual.animatedArtwork": "Hareketli Albüm Kapakları", // Dropdown + "settings.header.visual.animatedArtwork.always": "Her Zaman Açık", + "settings.header.visual.animatedArtwork.limited": "Bazı Sayfalara ve Özel Bölgelerle Sınırlı", + "settings.header.visual.animatedArtwork.disable": "Her Zaman Kapalı", + "settings.option.visual.animatedArtworkQuality": "Hareketli Albüm Kapağı Kalitesi", // Dropdown + "settings.header.visual.animatedArtworkQuality.low": "Düşük", + "settings.header.visual.animatedArtworkQuality.medium": "Orta", + "settings.header.visual.animatedArtworkQuality.high": "Yüksek", + "settings.header.visual.animatedArtworkQuality.veryHigh": "Daha Yüksek", + "settings.header.visual.animatedArtworkQuality.extreme": "Ekstrem", + "settings.option.visual.animatedWindowBackground": "Hareketli Uygulama Arka Planı", // Toggle + "settings.option.visual.hardwareAcceleration": "Donanım Hızlandırması", // Dropdown + "settings.option.visual.hardwareAcceleration.description": "Etki etmesi için uygulamayı yeniden başlatmak gerekir.", + "settings.header.visual.hardwareAcceleration.default": "Varsayılan", + "settings.header.visual.hardwareAcceleration.webGPU": "Gelişmiş", + // Refer to term.disabled for the disabled option + "settings.option.visual.showPersonalInfo": "Kullanıcı Adımı Göster", // Toggle + + // Settings - Lyrics + "settings.header.lyrics": "Şarkı Sözleri", + "settings.header.lyrics.description": "Cider'in şarkı sözlerini nasıl görüntülemesini istediğini buradan ayarlayın.", + "settings.option.lyrics.enableMusixmatch": "Musixmatch Kullan", // Toggle + "settings.option.lyrics.enableMusixmatchKaraoke": "Karaoke Modunu Etkinleştir (Sadece Musixmatch)", // Toggle + "settings.option.lyrics.musixmatchPreferredLanguage": "Musixmatch için Otomatik Çeviri Dili", // Dropdown + "settings.option.lyrics.enableYoutubeLyrics": "Müzik Videoları için Şarkı Sözünü YouTube'dan Al", // Toggle + + // Settings - Connectivity + "settings.header.connectivity": "Diğer Servisler", + "settings.header.connectivity.description": "Cider'i diğer servislere bağlayarak deneyiminizi zenginleştirin.", + "settings.option.connectivity.discordRPC": "Ne Dinlediğimi Discord'da Göster", // Dropdown + // Refer to term.disabled for the disabled option + "settings.header.connectivity.discordRPC.cider": "'Cider' Olarak", + "settings.header.connectivity.discordRPC.appleMusic": "'Apple Music' Olarak", + "settings.option.connectivity.discordRPC.clearOnPause": "Duraklatıldığında Discord'da Gösterme", // Toggle + "settings.option.connectivity.lastfmScrobble": "LastFM Bağlantısı", // Option to Connect + "settings.option.connectivity.lastfmScrobble.delay": "LastFM Scrobblalma Yüzdesi (%)", + "settings.option.connectivity.lastfmScrobble.nowPlaying": "Şimdi çalan şarkıyı LastFM'de göster", + "settings.option.connectivity.lastfmScrobble.removeFeatured": "Albüm sanatçısını Scrobbledan kaldır(LastFM)", + "settings.option.connectivity.lastfmScrobble.filterLoop": "Tekrar edilen şarkıyı filtrele (LastFM)", + // Refer to term.connect for the connect button + + // Settings - Experimental + "settings.header.experimental": "Deneysel", + "settings.header.experimental.description": "Cider'deki deneysel özelliklere erişim sağlayın. (Not: Bazı özellikler düzgün çalışmayabilir.)", + "settings.option.experimental.compactUI": "Kompakt Arayüz", // Toggle + "settings.option.experimental.closeButtonBehaviour": "Kapat düğmesi davranışı", + "settings.option.experimental.closeButtonBehaviour.quit": "Cider'den çık", + "settings.option.experimental.closeButtonBehaviour.minimizeTaskbar": "Simge durumuna küçült", + "settings.option.experimental.closeButtonBehaviour.minimizeTray": "Görev çubuğuna küçült", + // Refer to term.disabled & term.enabled + + // Spatialization Menu + "spatial.spatialProperties" : "Uzamsal Özellikler", + "spatial.width" : "Genişlik", + "spatial.height" : "Yükseklik", + "spatial.depth" : "Derinlik", + "spatial.roomMaterials" : "Oda Materyalleri", + "spatial.roomDimensions" : "Oda Ölçüleri", + "spatial.roomPositions" : "Oda Pozisyonu", + "spatial.setDimensions" : "Ölçüleri Ayarla", + "spatial.setPositions" : "Pozisyonu Ayarla", + "spatial.up" : "Üst", + "spatial.front" : "Ön", + "spatial.left" : "Sol", + "spatial.right" : "Sağ", + "spatial.back" : "Arka", + "spatial.down" : "Aşağı", + "spatial.listener" : "Dinleyici", + "spatial.audioSource" : "Ses Kaynağı", + + // Settings - Unfinished + "settings.header.unfinished": "Geliştirme Aşamasında", + + // Web Remote + "remote.web.title": "Cider'e Bağlan", + "remote.web.description": "Telefonunuzu Bu Cider Oturumuyla Eşleştirmek için QR Kodunu Tarayın", + + //About + "about.thanks": "Cider Collective Ekibine ve tüm katkıda bulunanlara çok teşekkür ederiz." + +} \ No newline at end of file diff --git a/src/i18n/zh_CN.jsonc b/src/i18n/zh_CN.jsonc new file mode 100644 index 00000000..3452cfc8 --- /dev/null +++ b/src/i18n/zh_CN.jsonc @@ -0,0 +1,284 @@ +{ + // App info + "app.name": "Cider", + + "date.format": "${y}年${m}月${d}日", + + // i18n Info + "i18n.languageName": "简体中文(中国)", // name of language in native language + "i18n.languageNameEnglish": "Simp. Chinese (China)", // name of language in English + "i18n.category": "main", // main = real language, fun = fun community languages + "i18n.authors": "@maikirakiwi", // Authors, if you contribute to this file feel free to add your name seperated with a space + + // Dialogs + "dialog.cancel": "取消", + "dialog.ok": "确定", + + // Notification + "notification.updatingLibrarySongs": "正在更新资料库的歌曲信息...", + "notification.updatingLibraryAlbums": "正在更新资料库的专辑信息...", + "notification.updatingLibraryArtists": "正在更新资料库的艺人信息...", + + // Terms + "term.appleMusic": "Apple Music", // Follows brand term + "term.applePodcasts": "Apple Podcasts", // Follows brand term + "term.itunes": "iTunes", // Follows brand term + "term.github": "GitHub", // Follows brand term + "term.discord": "Discord", // Follows brand term + "term.learnMore": "更多信息", + "term.accountSettings": "账户设置", + "term.logout": "登出", + "term.login": "登录", + "term.about": "关于", + "term.privateSession": "私人聆听", + "term.queue": "队列", + "term.search": "搜索", + "term.library": "资料库", + "term.listenNow": "现在就听", + "term.browse": "浏览", + "term.radio": "广播", + "term.recentlyAdded": "最近添加", + "term.songs": "歌曲", + "term.albums": "专辑", + "term.artists": "艺人", + "term.podcasts": "播客", + "term.playlists": "播放列表", + "term.playlist": "播放列表", + "term.play": "播放", + "term.pause": "暂停", + "term.previous": "上一首", + "term.next": "下一首", + "term.shuffle": "随机播放", + "term.repeat": "重复播放", + "term.volume": "音量", + "term.mute": "静音", + "term.unmute": "解除静音", + "term.share": "分享", + "term.settings": "设置", + "term.seeAll": "查看全部", + "term.sortBy": "排序", + "term.sortBy.album": "专辑", + "term.sortBy.artist": "艺人", + "term.sortBy.name": "歌名", + "term.sortBy.genre": "类型", + "term.sortBy.releaseDate": "发行日期", + "term.sortBy.duration": "时长", + "term.sortOrder": "字母排序", + "term.sortOrder.ascending": "升序", + "term.sortOrder.descending": "倒序", + "term.viewAs": "显示模式", + "term.viewAs.coverArt": "专辑封面", + "term.viewAs.list": "列表", + "term.size": "大小", + "term.size.normal": "正常", + "term.size.compact": "紧凑", + "term.enable": "启用", + "term.disable": "禁用", + "term.enabled": "已启用", + "term.disabled": "已禁用", + "term.connect": "连接", + "term.connecting": "连接中", + "term.disconnect": "断开", + "term.authed": "已认证", + "term.confirm": "确认?", + "term.more": "更多", + "term.less": "较少", + "term.showMore": "显示更多", + "term.showLess": "显示更少", + "term.topSongs" : "热门歌曲", + "term.latestReleases": "最新发行", + "term.time.added": "添加于", + "term.time.released": "发行于", + "term.time.updated": "更新于", + "term.time.hours": "小时", + "term.time.hour": "小时", + "term.time.minutes": "分钟", + "term.time.minute": "分钟", + "term.time.seconds": "秒", + "term.time.second": "秒", + "term.fullscreenView": "全屏", + "term.defaultView": "默认", + "term.spacializedAudioSetting": "音频空间化设置", + "term.clearAll": "清空", + "term.recentStations": "最近播放的频道", + "term.language": "语言", + "term.noLyrics": "加载中。。/ 搜索无结果 / 纯音乐", + "term.copyright": "版权所有", + "term.rightsReserved": "保留所有权利。", + "term.sponsor": "赞助", + "term.ciderTeam": "Cider 团队", + "term.developer": "开发者", + "term.socialTeam": "媒体团队", + "term.contributors": "贡献者", + "term.equalizer": "均衡器", + "term.reset": "重置", + "term.tracks": "首歌曲", // Assume x amount of tracks. e.g. 50 tracks + + // Home + "home.title": "主页", + "home.recentlyPlayed": "最近播放", + "home.recentlyAdded": "最近添加", + "home.artistsFeed": "艺人推荐", + "home.artistsFeed.noArtist": "追踪您喜爱的艺人后便可查看他们的最新发行。", + "home.madeForYou": "专属推荐", + "home.friendsListeningTo": "朋友正在听", + "home.followedArtists": "关注的艺人", + + // Errors + "error.appleMusicSubRequired": "需要订阅 Apple Music 以使用 Cider", + "error.connectionError": "无法连接到 Apple Music。", + "error.noResults": "没有结果", + "error.noResults.description": "尝试更改搜索条件。", + + //Podcasts + "podcast.followOnCider": "在 Cider 中追踪", + "podcast.followedOnCider": "已追踪", + "podcast.subscribeOnItunes": "在 iTunes 上订阅", + "podcast.subscribedOnItunes": "已订阅", + "podcast.itunesStore": "iTunes Store", // Follow brand term + "podcast.episodes": "单集", + "podcast.playEpisode": "播放单集", + "podcast.website": "Podcast 网站", + + + // Actions + "action.addToLibrary": "加入资料库", + "action.addToLibrary.success": "成功加入资料库", + "action.addToLibrary.error": "加入资料库的过程发生了错误", + "action.removeFromLibrary": "从资料库中移除", + "action.removeFromLibrary.success": "已从资料库中移除", + "action.addToQueue": "加入队列", + "action.addToQueue.success": "成功加入队列", + "action.addToQueue.error": "加入队列的过程发生了错误", + "action.removeFromQueue": "从队列中移除", + "action.removeFromQueue.success": "已从队列中移除", + "action.removeFromQueue.error": "从队列中移除的过程发生了错误", + "action.addToPlaylist": "加入播放列表", + "action.removeFromPlaylist": "从播放列表中移除", + "action.addToFavorites": "加至收藏", + "action.follow": "关注", + "action.follow.success": "已关注", + "action.follow.error": "尝试关注的过程发生了错误", + "action.unfollow": "取消关注", + "action.unfollow.success": "已取消关注", + "action.unfollow.error": "尝试取消关注的过程发生了错误", + "action.playNext": "下一首播放", + "action.playLater": "最后播放", + "action.startRadio": "开始电台", + "action.goToArtist": "前往艺人", + "action.goToAlbum": "前往专辑", + "action.moveToTop": "移到顶部", + "action.share": "分享歌曲", + "action.rename": "重命名", + "action.love": "喜欢", + "action.unlove": "踩", + "action.dislike": "减少此类建议", + "action.undoDislike": "增加此类建议", + "action.showWebRemoteQR": "显示远程控制的二维码", + "action.playTracksNext": "插播 ${app.selectedMediaItems.length} 首歌曲", + "action.playTracksLater": "最后播放 ${app.selectedMediaItems.length} 首歌曲", + "action.removeTracks": "从队列中移除 ${self.selectedItems.length} 首歌曲", + + // Settings - Audio + "settings.header.audio": "音频", + "settings.header.audio.description": "调整 Cider 的音频设置", + "settings.option.audio.quality": "音质", // Dropdown + "settings.header.audio.quality.high": "高音质", + "settings.header.audio.quality.low": "高效率", + "settings.header.audio.quality.auto": "自动", + "settings.option.audio.seamlessTransition": "无缝播放", // Toggle + "settings.option.audio.enableAdvancedFunctionality": "进阶功能", // Toggle + "settings.option.audio.enableAdvancedFunctionality.description": "启用 AudioContext 将解锁例如音量标准化和音频空间化的功能,但可能会在小部分设备上出现音频上的卡顿。", + "settings.option.audio.enableAdvancedFunctionality.audioNormalization": "音量标准化", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioNormalization.description": "使所感知到的音频响度统一", + "settings.option.audio.enableAdvancedFunctionality.audioSpatialization": "音频空间化", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioSpatialization.description": "使所感知到的音频更有立体感 (注: 这不是杜比全景声)", + // Settings - Visual + "settings.header.visual": "外观", + "settings.header.visual.description": "调整 Cider 的外观", + "settings.option.visual.windowBackgroundStyle": "窗口背景样式", // Toggle + "settings.header.visual.windowBackgroundStyle.none": "无", + "settings.header.visual.windowBackgroundStyle.artwork": "专辑封面", + "settings.option.visual.animatedArtwork": "动态专辑封面", // Dropdown + "settings.header.visual.animatedArtwork.always": "总是显示", + "settings.header.visual.animatedArtwork.limited": "只在艺人页面和专辑封面显示", + "settings.header.visual.animatedArtwork.disable": "关闭", + "settings.option.visual.animatedArtworkQuality": "动态专辑封面画质", // Dropdown + "settings.header.visual.animatedArtworkQuality.low": "低", + "settings.header.visual.animatedArtworkQuality.medium": "中", + "settings.header.visual.animatedArtworkQuality.high": "高", + "settings.header.visual.animatedArtworkQuality.veryHigh": "非常高", + "settings.header.visual.animatedArtworkQuality.extreme": "极高", + "settings.option.visual.animatedWindowBackground": "动态窗口背景", // Toggle + "settings.option.visual.hardwareAcceleration": "硬件加速", // Dropdown + "settings.option.visual.hardwareAcceleration.description": "需要重启 Cider 才会生效", + "settings.header.visual.hardwareAcceleration.default": "默认", + "settings.header.visual.hardwareAcceleration.webGPU": "WebGPU", + // Refer to term.disabled for the disabled option + "settings.option.visual.showPersonalInfo": "显示个人资料", // Toggle + // Settings - General (Reserved) + "settings.header.general": "通用", + "settings.header.general.description": "调整 Cider 的通用设置", + + // Settings - Lyrics + "settings.header.lyrics": "歌词", + "settings.header.lyrics.description": "调整 Cider 的歌词设置", + "settings.option.lyrics.enableMusixmatch": "启用 Musixmatch 歌词", // Toggle + "settings.option.lyrics.enableMusixmatchKaraoke": "启用卡拉 OK 模式(仅 Musixmatch)", // Toggle + "settings.option.lyrics.musixmatchPreferredLanguage": "Musixmatch 歌词语言偏好", // Dropdown + "settings.option.lyrics.enableYoutubeLyrics": "播放 MV 时使用 YouTube 歌词", // Toggle + + // Settings - Connectivity + "settings.header.connectivity": "外部连接", + "settings.header.connectivity.description": "调整Cider与外部应用的交互设置", + "settings.option.connectivity.discordRPC": "Discord 动态", // Dropdown + // Refer to term.disabled for the disabled option + "settings.header.connectivity.discordRPC.cider": "显示正在玩 'Cider'", + "settings.header.connectivity.discordRPC.appleMusic": "显示正在玩 'Apple Music'", + "settings.option.connectivity.discordRPC.clearOnPause": "暂停时清除Discord 动态", // Toggle + "settings.option.connectivity.lastfmScrobble": "LastFM Scrobbling 记录", // Option to Connect + "settings.option.connectivity.lastfmScrobble.delay": "LastFM Scrobble 延迟 (%)", + "settings.option.connectivity.lastfmScrobble.nowPlaying": "启用 LastFM 正在播放", + "settings.option.connectivity.lastfmScrobble.removeFeatured": "从歌名里去除艺人推荐 (LastFM)", + // Refer to term.connect for the connect button + + // Settings - Experimental + "settings.header.experimental": "实验性功能", + "settings.header.experimental.description": "调整Cider的实验性功能", + "settings.option.experimental.compactUI": "紧凑型 UI", // Toggle + "settings.option.experimental.closeButtonBehaviour": "点击关闭按钮时", + "settings.option.experimental.closeButtonBehaviour.quit": "退出 Cider", + "settings.option.experimental.closeButtonBehaviour.minimizeTaskbar": "最小化到任务栏", + "settings.option.experimental.closeButtonBehaviour.minimizeTray": "最小化到系统托盘", + // Refer to term.disabled & term.enabled + + // Spatialization Menu + "spatial.spatialProperties" : "空间属性", + "spatial.width" : "宽度", + "spatial.height" : "高度", + "spatial.depth" : "深度", + "spatial.gain": "增益", + "spatial.roomMaterials" : "空间材质", + "spatial.roomDimensions" : "空间尺寸", + "spatial.roomPositions" : "空间位置", + "spatial.setDimensions" : "设置尺寸", + "spatial.setPositions" : "设置位置", + "spatial.up" : "上", + "spatial.front" : "前", + "spatial.left" : "左", + "spatial.right" : "右", + "spatial.back" : "后", + "spatial.down" : "下", + "spatial.listener" : "您", + "spatial.audioSource" : "音源", + + // Settings - Unfinished + "settings.header.unfinished": "未完成", + + // Web Remote + "remote.web.title": "Cider 远程控制", + "remote.web.description": "扫描以下的二维码以控制 Cider", + + //About + "about.thanks": "郑重感谢 Cider Collective 以及为这个项目提供支持的贡献者。" +} diff --git a/src/i18n/zh_HK.jsonc b/src/i18n/zh_HK.jsonc new file mode 100644 index 00000000..9c101b9c --- /dev/null +++ b/src/i18n/zh_HK.jsonc @@ -0,0 +1,274 @@ +{ + // i18n Info + "i18n.languageName": "繁體中文(香港)", // name of language in native language + "i18n.languageNameEnglish": "Trad. Chinese (Hong Kong)", // name of language in English + "i18n.category": "main", // main = real language, fun = fun community languages + "i18n.authors": "@kyw504100 @maikirakiwi", // Authors, if you contribute to this file feel free to add your name seperated with a space + + // App info + "app.name": "Cider", + + "date.format": "${y}年${m}月${d}日", + + // Dialogs + "dialog.cancel": "取消", + "dialog.ok": "確認", + + // Notification + "notification.updatingLibrarySongs": "正在更新資料庫的歌曲...", + "notification.updatingLibraryAlbums": "正在更新資料庫的專輯...", + "notification.updatingLibraryArtists": "正在更新資料庫的藝人...", + // Terms + "term.appleInc": "Apple Inc.", + "term.appleMusic": "Apple Music", // Follows brand term + "term.applePodcasts": "Apple Podcasts", // Follows brand term + "term.itunes": "iTunes", // Follows brand term + "term.github": "GitHub", // Follows brand term + "term.discord": "Discord", // Follows brand term + "term.learnMore": "了解更多", + "term.accountSettings": "帳號設定", + "term.logout": "登出", + "term.login": "登入", + "term.about": "關於", + "term.privateSession": "私人模式", + "term.queue": "待播清單", + "term.search": "搜尋", + "term.library": "資料庫", + "term.listenNow": "立即聆聽", + "term.browse": "瀏覽", + "term.radio": "廣播", + "term.recentlyAdded": "最近加入", + "term.songs": "歌曲", + "term.albums": "專輯", + "term.artists": "藝人", + "term.podcasts": "Podcasts", + "term.playlists": "播放列表", + "term.playlist": "播放列表", + "term.play": "播放", + "term.pause": "暫停", + "term.previous": "上一首", + "term.next": "下一首", + "term.shuffle": "隨機播放", + "term.repeat": "重複播放", + "term.volume": "音量", + "term.mute": "靜音", + "term.unmute": "取消靜音", + "term.share": "分享", + "term.settings": "設定", + "term.seeAll": "顯示全部", + "term.sortBy": "排序", + "term.sortBy.album": "專輯", + "term.sortBy.artist": "藝人", + "term.sortBy.name": "歌名", + "term.sortBy.genre": "音樂風格", + "term.sortBy.releaseDate": "發行日期", + "term.sortBy.duration": "時長", + "term.sortOrder": "字母排序", + "term.sortOrder.ascending": "順序", + "term.sortOrder.descending": "倒序", + "term.viewAs": "顯示模式", + "term.viewAs.coverArt": "專輯封面", + "term.viewAs.list": "列表", + "term.size": "大小", + "term.size.normal": "正常", + "term.size.compact": "緊凑", + "term.enable": "啟用", + "term.disable": "停用", + "term.enabled": "已啟用", + "term.disabled": "已停用", + "term.connect": "連結", + "term.connecting": "連結中", + "term.disconnect": "取消連結", + "term.authed": "已授權", + "term.confirm": "確認?", + "term.more": "更多", + "term.less": "較少", + "term.showMore": "顯示更多", + "term.showLess": "顯示較少", + "term.topSongs" : "熱門歌曲", + "term.latestReleases": "最新發行", + "term.time.added": "加入於", + "term.time.released": "發行於", + "term.time.updated": "更新於", + "term.fullscreenView": "全螢幕檢視", + "term.defaultView": "一般檢視", + "term.spacializedAudioSetting": "空間音訊設定", + "term.clearAll": "清空", + "term.recentStations": "最近播放的頻道", + "term.language": "語言", + "term.noLyrics": "加載中... / 找不到歌詞。/ 純音樂。", + "term.copyright": "Copyright", + "term.rightsReserved": "保留一切權利。", + "term.sponsor": "贊助這個項目", + "term.ciderTeam": "Cider 團隊", + "term.developer": "開發者", + "term.socialTeam": "社交團隊", + "term.contributors": "貢獻者", + "term.equalizer": "等化器", + "term.reset": "重設", + "term.tracks": "首歌曲", // Assume x amount of tracks. e.g. 50 tracks + + // Home + "home.title": "主頁", + "home.recentlyPlayed": "最近播放", + "home.recentlyAdded": "最近加入", + "home.artistsFeed": "藝人動態", + "home.artistsFeed.noArtist": "追蹤一些藝人來獲得他們的最新歌曲資訊。", + "home.madeForYou": "為您推薦", + "home.friendsListeningTo": "朋友正在聆聽", + "home.followedArtists": "追蹤的藝人", + // Errors + "error.appleMusicSubRequired": "需要訂閱Apple Music以使用Cider", + "error.connectionError": "無法連接到 Apple Music。", + "error.noResults": "沒有結果", + "error.noResults.description": "請嘗試新的搜尋內容。", + + //Podcasts + "podcast.followOnCider": "在Cider上追蹤", + "podcast.followedOnCider": "已在Cider上追蹤", + "podcast.subscribeOnItunes": "在iTunes上訂閱", + "podcast.subscribedOnItunes": "已在iTunes上訂閱", + "podcast.itunesStore": "iTunes Store", + "podcast.episodes": "單集", + "podcast.playEpisode": "播放單集", + "podcast.website": "Podcast 網頁", + + // Actions + "action.addToLibrary": "加入資料庫", + "action.addToLibrary.success": "成功加入資料庫", + "action.addToLibrary.error": "加入資料庫的過程發生錯誤", + "action.removeFromLibrary": "從資料庫刪除", + "action.removeFromLibrary.success": "已從資料庫刪除", + "action.addToQueue": "加入待播清單", + "action.addToQueue.success": "成功加入待播清單", + "action.addToQueue.error": "加入待播清單的過程發生錯誤", + "action.removeFromQueue": "從待播清單刪除", + "action.removeFromQueue.success": "已從待播清單刪除", + "action.removeFromQueue.error": "從待播清單刪除的過程中發生錯誤", + "action.addToPlaylist": "加至播放列表", + "action.removeFromPlaylist": "從播放列表刪除", + "action.addToFavorites": "加至收藏", + "action.follow": "追蹤", + "action.follow.success": "追蹤中", + "action.follow.error": "追蹤的過程發生錯誤", + "action.unfollow": "取消追蹤", + "action.unfollow.success": "已取消追蹤", + "action.unfollow.error": "取消追蹤的過程發生錯誤", + "action.playNext": "插播", + "action.playLater": "稍後播放", + "action.startRadio": "建立電台", + "action.goToArtist": "前往藝人", + "action.goToAlbum": "前往專輯", + "action.moveToTop": "移動到頂部", + "action.share": "分享歌曲", + "action.rename": "重新命名", + "action.love": "喜愛", + "action.unlove": "取消喜愛", + "action.dislike": "減少此類建議", + "action.undoDislike": "還原減小此類建議", + "action.showWebRemoteQR": "顯示遙距控制二維碼", + + // Settings - Audio + "settings.header.audio": "音訊", + "settings.header.audio.description": "調整Cider的音訊設定", + "settings.option.audio.quality": "音訊音質", // Dropdown + "settings.header.audio.quality.high": "高素質", + "settings.header.audio.quality.low": "高效率", + "settings.header.audio.quality.auto": "自動", + "settings.option.audio.seamlessTransition": "無縫播放", // Toggle + "settings.option.audio.enableAdvancedFunctionality": "進階功能", // Toggle + "settings.option.audio.enableAdvancedFunctionality.description": "啟用 AudioContext 將解鎖類似音量平衡和等化器的進階功能。但是會在一些電腦造成音樂卡頓。", + "settings.option.audio.enableAdvancedFunctionality.audioNormalization": "音量平衡", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioNormalization.description": "將平衡輕柔和響亮的歌曲,建立更統一的聆聽體驗。", + "settings.option.audio.enableAdvancedFunctionality.audioSpatialization": "空間音訊", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioSpatialization.description": "將音訊進行空間化處理來製造一個更立體的聆聽體驗(注:此功能不是官方的杜比全景聲)", + // Settings - Visual + "settings.header.visual": "外觀", + "settings.header.visual.description": "調整Cider的外觀", + "settings.option.visual.windowBackgroundStyle": "窗口背景樣式", // Toggle + "settings.header.visual.windowBackgroundStyle.none": "空白", + "settings.header.visual.windowBackgroundStyle.artwork": "專輯封面", + "settings.option.visual.animatedArtwork": "動態專輯封面", // Dropdown + "settings.header.visual.animatedArtwork.always": "總是顯示", + "settings.header.visual.animatedArtwork.limited": "只在藝人頁面和專輯封面顯示", + "settings.header.visual.animatedArtwork.disable": "關閉", + "settings.option.visual.animatedArtworkQuality": "動態專輯封面品質", // Dropdown + "settings.header.visual.animatedArtworkQuality.low": "低", + "settings.header.visual.animatedArtworkQuality.medium": "中", + "settings.header.visual.animatedArtworkQuality.high": "高", + "settings.header.visual.animatedArtworkQuality.veryHigh": "非常高", + "settings.header.visual.animatedArtworkQuality.extreme": "極高", + "settings.option.visual.animatedWindowBackground": "動態窗口背景", // Toggle + "settings.option.visual.hardwareAcceleration": "硬體加速", // Dropdown + "settings.option.visual.hardwareAcceleration.description": "需要重啓 Cider 才能生效", + "settings.header.visual.hardwareAcceleration.default": "默認", + "settings.header.visual.hardwareAcceleration.webGPU": "WebGPU", + // Refer to term.disabled for the disabled option + "settings.option.visual.showPersonalInfo": "顯示個人檔案", // Toggle + // Settings - General (Reserved) + "settings.header.general": "一般", + "settings.header.general.description": "調整Cider的一般設定", + + // Settings - Lyrics + "settings.header.lyrics": "歌詞", + "settings.header.lyrics.description": "調整Cider的歌詞設定", + "settings.option.lyrics.enableMusixmatch": "啟用 Musixmatch 歌詞", // Toggle + "settings.option.lyrics.enableMusixmatchKaraoke": "啟用卡拉OK模式(僅限Musixmatch)", // Toggle + "settings.option.lyrics.musixmatchPreferredLanguage": "Musixmatch 歌詞語言偏好", // Dropdown + "settings.option.lyrics.enableYoutubeLyrics": "播放 MV 時使用 YouTube 歌詞", // Toggle + + // Settings - Connectivity + "settings.header.connectivity": "外部連結", + "settings.header.connectivity.description": "調整Cider與外部的連結", + "settings.option.connectivity.discordRPC": "Discord 狀態", // Dropdown + // Refer to term.disabled for the disabled option + "settings.header.connectivity.discordRPC.cider": "顯示為'Cider'", + "settings.header.connectivity.discordRPC.appleMusic": "顯示為'Apple Music'", + "settings.option.connectivity.discordRPC.clearOnPause": "暫停時清除 Discord 狀態", // Toggle + "settings.option.connectivity.lastfmScrobble": "LastFM Scrobbling 記錄", // Option to Connect + "settings.option.connectivity.lastfmScrobble.delay": "LastFM Scrobble 延遲 (%)", + "settings.option.connectivity.lastfmScrobble.nowPlaying": "啟用 LastFM 正在播放", + "settings.option.connectivity.lastfmScrobble.removeFeatured": "從歌名中移除藝人推薦 (LastFM)", + "settings.option.connectivity.lastfmScrobble.filterLoop": "Filter looped track (LastFM)", + // Refer to term.connect for the connect button + + // Settings - Experimental + "settings.header.experimental": "實驗性功能", + "settings.header.experimental.description": "調整Cider的實驗性功能", + "settings.option.experimental.compactUI": "緊凑型 UI", // Toggle + "settings.option.experimental.closeButtonBehaviour": "關閉按鈕行為", + "settings.option.experimental.closeButtonBehaviour.quit": "結束 Cider", + "settings.option.experimental.closeButtonBehaviour.minimizeTaskbar": "縮小至工作列", + "settings.option.experimental.closeButtonBehaviour.minimizeTray": "縮小至系統托盤", + // Refer to term.disabled & term.enabled + + // Spatialization Menu + "spatial.spatialProperties" : "空間音訊屬性", + "spatial.width" : "闊度", + "spatial.height" : "高度", + "spatial.depth" : "深度", + "spatial.gain" : "增益", + "spatial.roomMaterials" : "空間材質", + "spatial.roomDimensions" : "空間大小", + "spatial.roomPositions" : "空間位置", + "spatial.setDimensions" : "大小設定", + "spatial.setPositions" : "位置設定", + "spatial.up" : "上方", + "spatial.front" : "前方", + "spatial.left" : "左方", + "spatial.right" : "右方", + "spatial.back" : "後方", + "spatial.down" : "下方", + "spatial.listener" : "觀眾", + "spatial.audioSource" : "音源", + + // Settings - Unfinished + "settings.header.unfinished": "未完成", + + // Web Remote + "remote.web.title": "遙距控制 Cider", + "remote.web.description": "掃描以下的二維碼以控制 Cider", + + //About + "about.thanks": "感謝 Cider Collective 以及所有貢獻者所作出的貢獻。" +} \ No newline at end of file diff --git a/src/i18n/zh_TW.jsonc b/src/i18n/zh_TW.jsonc new file mode 100644 index 00000000..b5b23e5a --- /dev/null +++ b/src/i18n/zh_TW.jsonc @@ -0,0 +1,283 @@ +{ + // App info + "app.name": "Cider", + + "date.format": "${y}年${m}月${d}日", + + // i18n Info + "i18n.languageName": "繁體中文(台灣)", // name of language in native language + "i18n.languageNameEnglish": "Trad. Chinese (Taiwan)", // name of language in English + "i18n.category": "main", // main = real language, fun = fun community languages + "i18n.authors": "@maikirakiwi", // Authors, if you contribute to this file feel free to add your name seperated with a space + + + // Dialogs + "dialog.cancel": "取消", + "dialog.ok": "OK", + + // Notification + "notification.updatingLibrarySongs": "正在更新資料庫的歌曲...", + "notification.updatingLibraryAlbums": "正在更新資料庫的專輯...", + "notification.updatingLibraryArtists": "正在更新資料庫的藝人...", + // Terms + "term.appleMusic": "Apple Music", // Follows brand term + "term.applePodcasts": "Apple Podcasts", // Follows brand term + "term.itunes": "iTunes", // Follows brand term + "term.github": "GitHub", // Follows brand term + "term.discord": "Discord", // Follows brand term + "term.learnMore": "更多内容", + "term.accountSettings": "賬戶設定", + "term.logout": "登出", + "term.login": "登入", + "term.about": "關於", + "term.privateSession": "私人時段", + "term.queue": "待播清單", + "term.search": "搜尋", + "term.library": "資料庫", + "term.listenNow": "立即聆聽", + "term.browse": "瀏覽", + "term.radio": "廣播", + "term.recentlyAdded": "最近加入", + "term.songs": "歌曲", + "term.albums": "專輯", + "term.artists": "藝人", + "term.podcasts": "Podcasts", + "term.playlists": "播放列表", + "term.playlist": "播放列表", + "term.play": "播放", + "term.pause": "暫停", + "term.previous": "上一首", + "term.next": "下一首", + "term.shuffle": "隨機播放", + "term.repeat": "重複播放", + "term.volume": "音量", + "term.mute": "靜音", + "term.unmute": "取消靜音", + "term.share": "分享", + "term.settings": "設定", + "term.seeAll": "顯示全部", + "term.sortBy": "排序", + "term.sortBy.album": "專輯", + "term.sortBy.artist": "藝人", + "term.sortBy.name": "歌名", + "term.sortBy.genre": "音樂風格", + "term.sortBy.releaseDate": "發行日期", + "term.sortBy.duration": "時長", + "term.sortOrder": "字母排序", + "term.sortOrder.ascending": "升序", + "term.sortOrder.descending": "降序", + "term.viewAs": "顯示模式", + "term.viewAs.coverArt": "專輯封面", + "term.viewAs.list": "列表", + "term.size": "大小", + "term.size.normal": "正常", + "term.size.compact": "緊凑", + "term.enable": "啟用", + "term.disable": "停用", + "term.enabled": "已啟用", + "term.disabled": "已停用", + "term.connect": "連接", + "term.connecting": "連接中", + "term.disconnect": "斷開", + "term.authed": "已授權", + "term.confirm": "確定?", + "term.more": "更多", + "term.less": "更少", + "term.showMore": "顯示更多", + "term.showLess": "顯示更少", + "term.topSongs" : "熱門歌曲", + "term.latestReleases": "最新發行", + "term.time.added": "加入于", + "term.time.released": "發行于", + "term.time.updated": "更改于", + "term.time.hours": "小時", + "term.time.hour": "小時", + "term.time.minutes": "分鐘", + "term.time.minute": "分鐘", + "term.time.seconds": "秒", + "term.time.second": "秒", + "term.fullscreenView": "全螢幕顯示", + "term.defaultView": "預設顯示", + "term.spacializedAudioSetting": "音頻空間化設置", + "term.clearAll": "清空", + "term.recentStations": "最近收聽的廣播", + "term.language": "語言", + "term.noLyrics": "正在載入。。/ 無歌詞結果 / 純音樂", + "term.copyright": "版權聲明", + "term.rightsReserved": "保留所有權利。", + "term.sponsor": "贊助", + "term.ciderTeam": "Cider 團隊", + "term.developer": "開發者", + "term.socialTeam": "公關團隊", + "term.contributors": "貢獻者", + "term.equalizer": "等化器", + "term.reset": "重置", + "term.tracks": "首歌曲", // Assume x amount of tracks. e.g. 50 tracks + + + // Home + "home.title": "主頁", + "home.recentlyPlayed": "最近播放", + "home.recentlyAdded": "最近加入", + "home.artistsFeed": "藝人追蹤", + "home.artistsFeed.noArtist": "追蹤一些藝人來獲得他們的最新歌曲。", + "home.madeForYou": "為您推薦", + "home.friendsListeningTo": "朋友正在聆聽", + "home.followedArtists": "追蹤的藝人", + // Errors + "error.appleMusicSubRequired": "需要訂閱Apple Music以使用Cider", + "error.connectionError": "無法連接到 Apple Music。", + "error.noResults": "沒有結果", + "error.noResults.description": "嘗試新的搜尋項目。", + + //Podcasts + "podcast.followOnCider": "在 Cider 上追蹤", + "podcast.followedOnCider": "已追蹤", + "podcast.subscribeOnItunes": "在 iTunes 上訂閱", + "podcast.subscribedOnItunes": "已訂閱", + "podcast.itunesStore": "iTunes Store", // Follows brand term + "podcast.episodes": "單集", + "podcast.playEpisode": "播放單集", + "podcast.website": "Podcast 網站", + + // Actions + "action.addToLibrary": "加入資料庫", + "action.addToLibrary.success": "成功加入資料庫", + "action.addToLibrary.error": "加入資料庫的過程發生錯誤", + "action.removeFromLibrary": "從資料庫刪除", + "action.removeFromLibrary.success": "已從資料庫刪除", + "action.addToQueue": "加入待播清單", + "action.addToQueue.success": "成功加入待播清單", + "action.addToQueue.error": "加入待播清單的過程發生錯誤", + "action.removeFromQueue": "從待播清單刪除", + "action.removeFromQueue.success": "已從待播清單刪除", + "action.removeFromQueue.error": "從待播清單刪除的過程發生錯誤", + "action.addToPlaylist": "加入播放列表", + "action.removeFromPlaylist": "從播放列表刪除", + "action.addToFavorites": "加入我的最愛", + "action.follow": "追蹤", + "action.follow.success": "追蹤中", + "action.follow.error": "追蹤的過程發生錯誤", + "action.unfollow": "取消追蹤", + "action.unfollow.success": "已取消追蹤", + "action.unfollow.error": "取消追蹤的過程發生錯誤", + "action.playNext": "插播", + "action.playLater": "最後播放", + "action.startRadio": "建立電台", + "action.goToArtist": "前往藝人", + "action.goToAlbum": "前往專輯", + "action.moveToTop": "移至頂端", + "action.share": "分享歌曲", + "action.rename": "重新命名", + "action.love": "喜愛", + "action.unlove": "取消喜愛", + "action.dislike": "減少此類建議", + "action.undoDislike": "還原減小此類建議", + "action.showWebRemoteQR": "顯示遠程遙控行動條碼", + "action.playTracksNext": "插播 ${app.selectedMediaItems.length} 首歌曲", + "action.playTracksLater": "最後播放 ${app.selectedMediaItems.length} 首歌曲", + "action.removeTracks": "從待播清單刪除 ${self.selectedItems.length} 首歌曲", + + // Settings - Audio + "settings.header.audio": "音訊", + "settings.header.audio.description": "調整Cider的音訊設定", + "settings.option.audio.quality": "音訊音質", // Dropdown + "settings.header.audio.quality.high": "高品質", + "settings.header.audio.quality.low": "高效率", + "settings.header.audio.quality.auto": "自動", + "settings.option.audio.seamlessTransition": "無間斷播放", // Toggle + "settings.option.audio.enableAdvancedFunctionality": "進階機能", // Toggle + "settings.option.audio.enableAdvancedFunctionality.description": "啟用 AudioContext 將解鎖類似音訊標準化和等化器的進階機能。但是會在一些電腦造成音樂卡頓。", + "settings.option.audio.enableAdvancedFunctionality.audioNormalization": "音訊標準化", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioNormalization.description": "將平衡輕柔和響亮的歌曲,建立更統一的聆聽體驗。", + "settings.option.audio.enableAdvancedFunctionality.audioSpatialization": "音訊空間化", // Toggle + "settings.option.audio.enableAdvancedFunctionality.audioSpatialization.description": "將音訊進行空間化處理來製造一個更立體的聆聽體驗(注:此功能不是官方的杜比全景聲)", + // Settings - Visual + "settings.header.visual": "外觀", + "settings.header.visual.description": "調整Cider的外觀", + "settings.option.visual.windowBackgroundStyle": "窗口背景樣式", // Toggle + "settings.header.visual.windowBackgroundStyle.none": "空白", + "settings.header.visual.windowBackgroundStyle.artwork": "專輯封面", + "settings.option.visual.animatedArtwork": "動態專輯封面", // Dropdown + "settings.header.visual.animatedArtwork.always": "總是顯示", + "settings.header.visual.animatedArtwork.limited": "只在藝人頁面和專輯封面顯示", + "settings.header.visual.animatedArtwork.disable": "關閉", + "settings.option.visual.animatedArtworkQuality": "動態專輯封面品質", // Dropdown + "settings.header.visual.animatedArtworkQuality.low": "低", + "settings.header.visual.animatedArtworkQuality.medium": "中", + "settings.header.visual.animatedArtworkQuality.high": "高", + "settings.header.visual.animatedArtworkQuality.veryHigh": "非常高", + "settings.header.visual.animatedArtworkQuality.extreme": "極高", + "settings.option.visual.animatedWindowBackground": "動態窗口背景", // Toggle + "settings.option.visual.hardwareAcceleration": "硬體加速", // Dropdown + "settings.option.visual.hardwareAcceleration.description": "需要重新啟動 Cider 才會生效", + "settings.header.visual.hardwareAcceleration.default": "默認", + "settings.header.visual.hardwareAcceleration.webGPU": "WebGPU", + // Refer to term.disabled for the disabled option + "settings.option.visual.showPersonalInfo": "顯示個人檔案", // Toggle + // Settings - General (Reserved) + "settings.header.general": "一般", + "settings.header.general.description": "調整Cider的一般設定", + + // Settings - Lyrics + "settings.header.lyrics": "歌詞", + "settings.header.lyrics.description": "調整 Cider 的歌詞設定", + "settings.option.lyrics.enableMusixmatch": "啟用 Musixmatch 歌詞", // Toggle + "settings.option.lyrics.enableMusixmatchKaraoke": "啟用K歌模式(僅限Musixmatch)", // Toggle + "settings.option.lyrics.musixmatchPreferredLanguage": "Musixmatch 歌詞語言偏好", // Dropdown + "settings.option.lyrics.enableYoutubeLyrics": "播放 MV 時使用 YouTube 歌詞", // Toggle + + // Settings - Connectivity + "settings.header.connectivity": "外部連接", + "settings.header.connectivity.description": "調整Cider與外部的連接", + "settings.option.connectivity.discordRPC": "Discord 動態", // Dropdown + // Refer to term.disabled for the disabled option + "settings.header.connectivity.discordRPC.cider": "顯示正在玩 'Cider'", + "settings.header.connectivity.discordRPC.appleMusic": "顯示正在玩 'Apple Music'", + "settings.option.connectivity.discordRPC.clearOnPause": "暫停時清除 Discord 動態", // Toggle + "settings.option.connectivity.lastfmScrobble": "LastFM Scrobbling 記錄", // Option to Connect + "settings.option.connectivity.lastfmScrobble.delay": "LastFM Scrobble 延遲 (%)", + "settings.option.connectivity.lastfmScrobble.nowPlaying": "啟用 LastFM 目前聆聽", // Toggle + "settings.option.connectivity.lastfmScrobble.removeFeatured": "從歌名中移除藝人推薦 (LastFM)", + // Refer to term.connect for the connect button + + // Settings - Experimental + "settings.header.experimental": "實驗性功能", + "settings.header.experimental.description": "調整 Cider 的實驗性功能", + "settings.option.experimental.compactUI": "緊凑型 UI", // Toggle + "settings.option.experimental.closeButtonBehaviour": "關閉按鈕行為", // Dropdown + "settings.option.experimental.closeButtonBehaviour.quit": "退出 Cider", + "settings.option.experimental.closeButtonBehaviour.minimizeTaskbar": "最小化到工作列", + "settings.option.experimental.closeButtonBehaviour.minimizeTray": "最小化到系統匣", + // Refer to term.disabled & term.enabled + + // Spatialization Menu + "spatial.spatialProperties" : "空間化屬性", + "spatial.width" : "寬度", + "spatial.height" : "高度", + "spatial.depth" : "深度", + "spatial.gain" : "增益", + "spatial.roomMaterials" : "空間材質", + "spatial.roomDimensions" : "空間尺寸", + "spatial.roomPositions" : "空間位置", + "spatial.setDimensions" : "設定尺寸", + "spatial.setPositions" : "設定位置", + "spatial.up" : "上", + "spatial.front" : "前", + "spatial.left" : "左", + "spatial.right" : "右", + "spatial.back" : "後", + "spatial.down" : "下", + "spatial.listener" : "聽衆", + "spatial.audioSource" : "音訊來源", + + // Settings - Unfinished + "settings.header.unfinished": "未完成", + + // Web Remote + "remote.web.title": "Cider 遠控", + "remote.web.description": "掃描以下的行動條碼以控制 Cider", + + //About + "about.thanks": "著重感謝 Cider Collective 的成員以及所有為項目付出的貢獻者。" +} \ No newline at end of file diff --git a/src/main/base/app.ts b/src/main/base/app.ts new file mode 100644 index 00000000..85f7627b --- /dev/null +++ b/src/main/base/app.ts @@ -0,0 +1,197 @@ +import * as electron from 'electron'; +import * as path from 'path'; + +export class AppEvents { + private static protocols: any = [ + "ame", + "cider", + "itms", + "itmss", + "musics", + "music" + ] + private static plugin: any = null; + private static store: any = null; + private static win: any = null; + + constructor(store: any) { + AppEvents.store = store + AppEvents.start(store); + } + + /** + * Handles all actions that occur for the app on start (Mainly commandline arguments) + * @returns {void} + */ + private static start(store: any): void { + console.info('[AppEvents] App started'); + + /********************************************************************************************************************** + * Startup arguments handling + **********************************************************************************************************************/ + if (electron.app.commandLine.hasSwitch('version') || electron.app.commandLine.hasSwitch('v')) { + console.log(electron.app.getVersion()) + electron.app.exit() + } + + // Verbose Check + if (electron.app.commandLine.hasSwitch('verbose')) { + console.log("[Cider] User has launched the application with --verbose"); + } + + // Log File Location + if (electron.app.commandLine.hasSwitch('log') || electron.app.commandLine.hasSwitch('l')) { + console.log(path.join(electron.app.getPath('userData'), 'logs')) + electron.app.exit() + } + + // Expose GC + electron.app.commandLine.appendSwitch('js-flags','--expose_gc') + + /*********************************************************************************************************************** + * Commandline arguments + **********************************************************************************************************************/ + switch (store.visual.hw_acceleration) { + default: + case "default": + electron.app.commandLine.appendSwitch('enable-accelerated-mjpeg-decode') + electron.app.commandLine.appendSwitch('enable-accelerated-video') + electron.app.commandLine.appendSwitch('disable-gpu-driver-bug-workarounds') + electron.app.commandLine.appendSwitch('ignore-gpu-blacklist') + electron.app.commandLine.appendSwitch('enable-native-gpu-memory-buffers') + electron.app.commandLine.appendSwitch('enable-accelerated-video-decode'); + electron.app.commandLine.appendSwitch('enable-gpu-rasterization'); + electron.app.commandLine.appendSwitch('enable-native-gpu-memory-buffers'); + electron.app.commandLine.appendSwitch('enable-oop-rasterization'); + break; + + case "webgpu": + console.info("WebGPU is enabled."); + electron.app.commandLine.appendSwitch('enable-unsafe-webgpu') + break; + + case "disabled": + console.info("Hardware acceleration is disabled."); + electron.app.commandLine.appendSwitch('disable-gpu') + break; + } + + if (process.platform === "linux") { + electron.app.commandLine.appendSwitch('disable-features', 'MediaSessionService'); + } + + /*********************************************************************************************************************** + * Protocols + **********************************************************************************************************************/ + if (process.defaultApp) { + if (process.argv.length >= 2) { + this.protocols.forEach((protocol: string) => { + electron.app.setAsDefaultProtocolClient(protocol, process.execPath, [path.resolve(process.argv[1])]) + }) + } + } else { + this.protocols.forEach((protocol: string) => { + electron.app.setAsDefaultProtocolClient(protocol) + }) + } + + + } + + public quit() { + console.log('App stopped'); + } + + public ready(plug: any) { + AppEvents.plugin = plug + console.log('[AppEvents] App ready'); + } + + public bwCreated(win: Electron.BrowserWindow) { + AppEvents.win = win + + electron.app.on('open-url', (event, url) => { + event.preventDefault() + if (AppEvents.protocols.some((protocol: string) => url.includes(protocol))) { + AppEvents.LinkHandler(url) + console.log(url) + } + }) + + AppEvents.InstanceHandler() + } + + /*********************************************************************************************************************** + * Private methods + **********************************************************************************************************************/ + + private static LinkHandler(arg: string) { + if (!arg) return; + + // LastFM Auth URL + if (arg.includes('auth')) { + let authURI = arg.split('/auth/')[1] + if (authURI.startsWith('lastfm')) { // If we wanted more auth options + const authKey = authURI.split('lastfm?token=')[1]; + AppEvents.store.set('lastfm.enabled', true); + AppEvents.store.set('lastfm.auth_token', authKey); + AppEvents.win.webContents.send('LastfmAuthenticated', authKey); + AppEvents.plugin.callPlugin('lastfm', 'authenticate', authKey); + } + } + // Play + else if (arg.includes('/play/')) { //Steer away from protocol:// specific conditionals + const playParam = arg.split('/play/')[1] + + const mediaType = { + "s/": "song", + "a/": "album", + "p/": "playlist" + } + + for (const [key, value] of Object.entries(mediaType)) { + if (playParam.includes(key)) { + const id = playParam.split(key)[1] + AppEvents.win.webContents.send('play', value, id) + console.debug(`[LinkHandler] Attempting to load ${value} by id: ${id}`) + } + } + + } else if (arg.includes('music.apple.com')) { // URL (used with itms/itmss/music/musics uris) + console.log(arg) + let url = arg.split('//')[1] + console.warn(`[LinkHandler] Attempting to load url: ${url}`); + AppEvents.win.webContents.send('play', 'url', url) + } + } + + private static InstanceHandler() { + + // Detects of an existing instance is running (So if the lock has been achieved, no existing instance has been found) + const gotTheLock = electron.app.requestSingleInstanceLock() + + if (!gotTheLock) { // Runs on the new instance if another instance has been found + console.log('[Cider] Another instance has been found, quitting.') + electron.app.quit() + } else { // Runs on the first instance if no other instance has been found + electron.app.on('second-instance', (_event, startArgs) => { + console.log("[InstanceHandler] (second-instance) Instance started with " + startArgs.toString()) + + startArgs.forEach(arg => { + console.log(arg) + if (arg.includes("cider://")) { + console.debug('[InstanceHandler] (second-instance) Link detected with ' + arg) + AppEvents.LinkHandler(arg) + } else if (arg.includes("--force-quit")) { + console.warn('[InstanceHandler] (second-instance) Force Quit found. Quitting App.'); + electron.app.quit() + } else if (AppEvents.win) { + if (AppEvents.win.isMinimized()) AppEvents.win.restore() + AppEvents.win.focus() + } + }) + }) + } + + } +} \ No newline at end of file diff --git a/src/main/base/plugins.ts b/src/main/base/plugins.ts new file mode 100644 index 00000000..0262d447 --- /dev/null +++ b/src/main/base/plugins.ts @@ -0,0 +1,65 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as electron from 'electron' + +export default class PluginHandler { + private basePluginsPath = path.join(__dirname, '../plugins'); + private userPluginsPath = path.join(electron.app.getPath('userData'), 'plugins'); + private readonly pluginsList: any = {}; + private readonly _store: any; + + constructor(config: any) { + this._store = config; + this.pluginsList = this.getPlugins(); + } + + public getPlugins(): any { + let plugins: any = {}; + + + if (fs.existsSync(this.basePluginsPath)) { + fs.readdirSync(this.basePluginsPath).forEach(file => { + if (file.endsWith('.ts') || file.endsWith('.js')) { + const plugin = require(path.join(this.basePluginsPath, file)).default; + if (plugins[file] || plugin.name in plugins) { + console.log(`[${plugin.name}] Plugin already loaded / Duplicate Class Name`); + } else { + plugins[file] = new plugin(electron.app, this._store); + } + } + }); + } + + + if (fs.existsSync(this.userPluginsPath)) { + fs.readdirSync(this.userPluginsPath).forEach(file => { + if (file.endsWith('.ts') || file.endsWith('.js')) { + const plugin = require(path.join(this.userPluginsPath, file)).default; + file = file.replace('.ts', '').replace('.js', ''); + if (plugins[file] || plugin in plugins) { + console.log(`[${plugin.name}] Plugin already loaded / Duplicate Class Name`); + } else { + plugins[file] = new plugin(electron.app, this._store); + } + } + }); + } + console.log('[PluginHandler] Loaded plugins:', Object.keys(plugins)); + return plugins; + } + + public callPlugins(event: string, ...args: any[]) { + for (const plugin in this.pluginsList) { + if (this.pluginsList[plugin][event]) { + this.pluginsList[plugin][event](...args); + } + } + } + + public callPlugin(plugin: string, event: string, ...args: any[]) { + if (this.pluginsList[plugin][event]) { + this.pluginsList[plugin][event](...args); + } + } + +} diff --git a/src/main/base/store.ts b/src/main/base/store.ts new file mode 100644 index 00000000..185aceb7 --- /dev/null +++ b/src/main/base/store.ts @@ -0,0 +1,166 @@ +import * as Store from 'electron-store'; +import * as electron from "electron"; + +export class ConfigStore { + private _store: Store; + + private defaults: any = { + "general": { + "close_behavior": 0, // 0 = close, 1 = minimize, 2 = minimize to tray + "startup_behavior": 0, // 0 = nothing, 1 = open on startup + "discord_rpc": 1, // 0 = disabled, 1 = enabled as Cider, 2 = enabled as Apple Music + "discordClearActivityOnPause": 1, // 0 = disabled, 1 = enabled + "language" : "en_US" + }, + "home": { + "followedArtists": [], + "favoriteItems": [] + }, + "libraryPrefs": { + "songs": { + "sort": "name", + "sortOrder": "asc", + "size": "normal" + } + }, + "audio": { + "volume": 1, + "lastVolume": 1, + "muted": false, + "quality": "256", + "seamless_audio": true, + "normalization": false, + "spatial": false, + "maxVolume": 1, + "volumePrecision": 0.1, + "volumeRoundMax": 0.9, + "volumeRoundMin": 0.1, + "spatial_properties": { + "presets": [], + "gain": 0.8, + "listener_position": [0, 0, 0], + "audio_position": [0, 0, 0], + "room_dimensions": { + "width": 32, + "height": 12, + "depth": 32 + }, + "room_materials": { + "left": 'metal', + "right": 'metal', + "front": 'brick-bare', + "back": 'brick-bare', + "down": 'acoustic-ceiling-tiles', + "up": 'acoustic-ceiling-tiles', + } + }, + "equalizer": { + 'preset': "default", + 'frequencies': [32, 63, 125, 250, 500, 1000, 2000, 4000, 8000, 16000], + 'gain': [0,0,0,0,0,0,0,0,0,0], + 'Q' : [1,1,1,1,1,1,1,1,1,1], + 'preamp' : 0, + 'mix' : 1, + 'presets': [], + 'userGenerated': false + } + }, + "visual": { + "theme": "", + "scrollbars": 0, // 0 = show on hover, 2 = always hide, 3 = always show + "refresh_rate": 0, + "window_background_style": "artwork", // "none", "artwork", "color" + "animated_artwork": "limited", // 0 = always, 1 = limited, 2 = never + "animated_artwork_qualityLevel": 1, + "bg_artwork_rotation": false, + "hw_acceleration": "default", // default, webgpu, disabled + "showuserinfo": true, + "miniplayer_top_toggle": true + }, + "lyrics": { + "enable_mxm": false, + "mxm_karaoke": false, + "mxm_language": "en", + "enable_yt": false, + }, + "lastfm": { + "enabled": false, + "scrobble_after": 30, + "auth_token": "", + "enabledRemoveFeaturingArtists": true, + "filterLoop": true, + "NowPlaying": "true" + }, + "advanced": { + "AudioContext": false, + "experiments": [] + } + } + private migrations: any = {} + + constructor() { + this._store = new Store({ + name: 'cider-config', + defaults: this.defaults, + migrations: this.migrations, + }); + + this._store.set(this.mergeStore(this.defaults, this._store.store)) + this.ipcHandler(this._store); + } + + get store() { + return this._store.store; + } + + get(key: string) { + return this._store.get(key); + } + + set(key: string, value: any) { + this._store.set(key, value); + } + + /** + * Merge Configurations + * @param target The target configuration + * @param source The source configuration + */ + private mergeStore = (target: { [x: string]: any; }, source: { [x: string]: any; }) => { + // Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties + for (const key of Object.keys(source)) { + if (key.includes('migrations')) { + continue; + } + if(source[key] instanceof Array) { + continue + } + if (source[key] instanceof Object) Object.assign(source[key], this.mergeStore(target[key], source[key])) + } + // Join `target` and modified `source` + Object.assign(target || {}, source) + return target + } + + /** + * IPC Handler + */ + private ipcHandler(cfg: Store | any): void { + electron.ipcMain.handle('getStoreValue', (event, key, defaultValue) => { + return (defaultValue ? cfg.get(key, true) : cfg.get(key)); + }); + + electron.ipcMain.handle('setStoreValue', (event, key, value) => { + cfg.set(key, value); + }); + + electron.ipcMain.on('getStore', (event) => { + event.returnValue = cfg.store + }) + + electron.ipcMain.on('setStore', (event, store) => { + cfg.store = store + }) + } + +} \ No newline at end of file diff --git a/src/main/base/win.ts b/src/main/base/win.ts new file mode 100644 index 00000000..23fb9126 --- /dev/null +++ b/src/main/base/win.ts @@ -0,0 +1,612 @@ +// @ts-nocheck +import * as path from "path"; +import * as electron from "electron"; +import * as windowStateKeeper from "electron-window-state"; +import * as express from "express"; +import * as getPort from "get-port"; +import * as yt from "youtube-search-without-api-key"; +import * as fs from "fs"; +import { Stream } from "stream"; +import * as qrcode from "qrcode-terminal"; +import * as os from "os"; +import * as mm from 'music-metadata'; +import fetch from 'electron-fetch' +import {wsapi} from "./wsapi"; +import * as jsonc from "jsonc"; + +export class Win { + private win: any | undefined = null; + private app: any | undefined = null; + private store: any | undefined = null; + private devMode: boolean = !electron.app.isPackaged; + + constructor(app: electron.App, store: any) { + this.app = app; + this.store = store; + } + + private paths: any = { + srcPath: path.join(__dirname, "../../src"), + resourcePath: path.join(__dirname, "../../resources"), + ciderCache: path.resolve(electron.app.getPath("userData"), "CiderCache"), + themes: path.resolve(electron.app.getPath("userData"), "Themes"), + plugins: path.resolve(electron.app.getPath("userData"), "Plugins"), + }; + private audioStream: any = new Stream.PassThrough(); + private clientPort: number = 0; + private remotePort: number = 6942; + private EnvironmentVariables: object = { + env: { + platform: process.platform, + dev: electron.app.isPackaged, + }, + }; + private options: any = { + icon: path.join( + this.paths.resourcePath, + `icons/icon.` + (process.platform === "win32" ? "ico" : "png") + ), + width: 1024, + height: 600, + x: undefined, + y: undefined, + minWidth: 900, + minHeight: 390, + frame: false, + title: "Cider", + vibrancy: "dark", + transparent: process.platform === "darwin", + hasShadow: false, + show: false, + backgroundColor: "#1E1E1E", + webPreferences: { + nodeIntegration: true, + sandbox: true, + allowRunningInsecureContent: true, + contextIsolation: false, + webviewTag: true, + plugins: true, + nodeIntegrationInWorker: false, + webSecurity: false, + preload: path.join(this.paths.srcPath, "./preload/cider-preload.js"), + }, + }; + + /** + * Creates the browser window + */ + async createWindow(): Promise { + this.clientPort = await getPort({ port: 9000 }); + this.verifyFiles(); + + // Load the previous state with fallback to defaults + const windowState = windowStateKeeper({ + defaultWidth: 1024, + defaultHeight: 600, + }); + this.options.width = windowState.width; + this.options.height = windowState.height; + + // Start the webserver for the browser window to load + + this.startWebServer(); + + this.win = new electron.BrowserWindow(this.options); + const ws = new wsapi(this.win) + ws.InitWebSockets() + // and load the renderer. + this.startSession(); + this.startHandlers(); + + // Register listeners on Window to track size and position of the Window. + windowState.manage(this.win); + + return this.win; + } + + /** + * Verifies the files for the renderer to use (Cache, library info, etc.) + */ + private verifyFiles(): void { + const expectedDirectories = ["CiderCache"]; + const expectedFiles = [ + "library-songs.json", + "library-artists.json", + "library-albums.json", + "library-playlists.json", + "library-recentlyAdded.json", + ]; + for (let i = 0; i < expectedDirectories.length; i++) { + if ( + !fs.existsSync( + path.join(electron.app.getPath("userData"), expectedDirectories[i]) + ) + ) { + fs.mkdirSync( + path.join(electron.app.getPath("userData"), expectedDirectories[i]) + ); + } + } + for (let i = 0; i < expectedFiles.length; i++) { + const file = path.join(this.paths.ciderCache, expectedFiles[i]); + if (!fs.existsSync(file)) { + fs.writeFileSync(file, JSON.stringify([])); + } + } + } + + /** + * Starts the webserver for the renderer process. + */ + private startWebServer(): void { + const app = express(); + + app.use(express.static(path.join(this.paths.srcPath, "./renderer/"))); + app.set("views", path.join(this.paths.srcPath, "./renderer/views")); + app.set("view engine", "ejs"); + let firstRequest = true; + app.use((req, res, next) => { + // @ts-ignore + if ( + req.url.includes("audio.webm") || + (req.headers.host.includes("localhost") && + (this.devMode || req.headers["user-agent"].includes("Electron"))) + ) { + next(); + } else { + res.redirect("https://discord.gg/applemusic"); + } + }); + + app.get("/", (req, res) => { + res.render("main", this.EnvironmentVariables); + }); + + app.get("/audio.webm", (req, res) => { + try { + req.socket.setTimeout(Number.MAX_SAFE_INTEGER); + // CiderBase.requests.push({req: req, res: res}); + // var pos = CiderBase.requests.length - 1; + // req.on("close", () => { + // console.info("CLOSED", CiderBase.requests.length); + // requests.splice(pos, 1); + // console.info("CLOSED", CiderBase.requests.length); + // }); + this.audioStream.on("data", (data: any) => { + try { + res.write(data); + } catch (ex) { + console.log(ex); + } + }); + } catch (ex) { + console.log(ex); + } + }); + //app.use(express.static()) + + app.listen(this.clientPort, () => { + console.log(`Cider client port: ${this.clientPort}`); + }); + + /* + * Remote Client (I had no idea how to add it to our existing express server, so I just made another one) -@quacksire + * TODO: Broadcast the remote so that /web-remote/ can connect + * https://github.com/ciderapp/Apple-Music-Electron/blob/818ed18940ff600d76eb59d22016723a75885cd5/resources/functions/handler.js#L1173 + */ + const remote = express(); + remote.use(express.static(path.join(this.paths.srcPath, "./web-remote/"))) + remote.set("views", path.join(this.paths.srcPath, "./web-remote/views")); + remote.set("view engine", "ejs"); + getPort({port: 6942}).then((port) => { + this.remotePort = port; + // Start Remote Discovery + this.broadcastRemote() + remote.listen(this.remotePort, () => { + console.log(`Cider remote port: ${this.remotePort}`); + if (firstRequest) { + console.log("---- Ignore Me ;) ---"); + qrcode.generate(`http://${os.hostname}:${this.remotePort}`); + console.log("---- Ignore Me ;) ---"); + /* + * + * USING https://www.npmjs.com/package/qrcode-terminal for terminal + * WE SHOULD USE https://www.npmjs.com/package/qrcode for the remote (or others) for showing to user via an in-app dialog + * -@quacksire + */ + } + firstRequest = false; + }) + remote.get("/", (req, res) => { + res.render("index", this.EnvironmentVariables); + }); + }) + } + + /** + * Starts the session for the renderer process. + */ + private startSession(): void { + // intercept "https://js-cdn.music.apple.com/hls.js/2.141.0/hls.js/hls.js" and redirect to local file "./apple-hls.js" instead + this.win.webContents.session.webRequest.onBeforeRequest( + { + urls: ["https://*/*.js"], + }, + ( + details: { url: string | string[] }, + callback: (arg0: { redirectURL?: string; cancel?: boolean }) => void + ) => { + if (details.url.includes("hls.js")) { + callback({ + redirectURL: `http://localhost:${this.clientPort}/apple-hls.js`, + }); + } else { + callback({ + cancel: false, + }); + } + } + ); + + this.win.webContents.session.webRequest.onBeforeSendHeaders( + async ( + details: { url: string; requestHeaders: { [x: string]: string } }, + callback: (arg0: { requestHeaders: any }) => void + ) => { + if (details.url === "https://buy.itunes.apple.com/account/web/info") { + details.requestHeaders["sec-fetch-site"] = "same-site"; + details.requestHeaders["DNT"] = "1"; + let itspod = await this.win.webContents.executeJavaScript( + `window.localStorage.getItem("music.ampwebplay.itspod")` + ); + if (itspod != null) + details.requestHeaders["Cookie"] = `itspod=${itspod}`; + } + callback({ requestHeaders: details.requestHeaders }); + } + ); + + let location = `http://localhost:${this.clientPort}/`; + + if (electron.app.isPackaged) { + this.win.loadURL(location); + } else { + this.win.loadURL(location, { + userAgent: "Cider Development Environment", + }); + } + } + + /** + * Initializes the window handlers + */ + private startHandlers(): void { + /********************************************************************************************************************** + * ipcMain Events + ****************************************************************************************************************** */ + electron.ipcMain.on("cider-platform", (event) => { + event.returnValue = process.platform; + }); + + electron.ipcMain.on("get-i18n", (event, key) => { + let i18nBase = fs.readFileSync(path.join(__dirname, "../../src/i18n/en_US.jsonc"), "utf8"); + i18nBase = jsonc.parse(i18nBase) + try { + let i18n = fs.readFileSync(path.join(__dirname, `../../src/i18n/${key}.jsonc`), "utf8"); + i18n = jsonc.parse(i18n) + Object.assign(i18nBase, i18n) + }catch(e) { + console.error(e); + event.returnValue = e; + } + + event.returnValue = i18nBase; + + }); + + electron.ipcMain.on("get-i18n-listing", event => { + let i18nFiles = fs.readdirSync(path.join(__dirname, "../../src/i18n")).filter(file => file.endsWith(".jsonc")); + // read all the files and parse them + let i18nListing = [] + for (let i = 0; i < i18nFiles.length; i++) { + let i18n = fs.readFileSync(path.join(__dirname, `../../src/i18n/${i18nFiles[i]}`), "utf8"); + i18n = jsonc.parse(i18n) + i18nListing.push({ + "code": i18nFiles[i].replace(".jsonc", ""), + "nameNative": i18n["i18n.languageName"] ?? i18nFiles[i].replace(".jsonc", ""), + "nameEnglish": i18n["i18n.languageNameEnglish"] ?? i18nFiles[i].replace(".jsonc", ""), + "category": i18n["i18n.category"] ?? "", + "authors": i18n["i18n.authors"] ?? "" + }) + } + event.returnValue = i18nListing; + }) + + electron.ipcMain.on("get-gpu-mode", (event) => { + event.returnValue = process.platform; + }); + + electron.ipcMain.on("is-dev", (event) => { + event.returnValue = this.devMode; + }); + + electron.ipcMain.on("close", () => { + // listen for close event + this.win.close(); + }); + + electron.ipcMain.on("put-library-songs", (event, arg) => { + fs.writeFileSync( + path.join(this.paths.ciderCache, "library-songs.json"), + JSON.stringify(arg) + ); + }); + + electron.ipcMain.on("put-library-artists", (event, arg) => { + fs.writeFileSync( + path.join(this.paths.ciderCache, "library-artists.json"), + JSON.stringify(arg) + ); + }); + + electron.ipcMain.on("put-library-albums", (event, arg) => { + fs.writeFileSync( + path.join(this.paths.ciderCache, "library-albums.json"), + JSON.stringify(arg) + ); + }); + + electron.ipcMain.on("put-library-playlists", (event, arg) => { + fs.writeFileSync( + path.join(this.paths.ciderCache, "library-playlists.json"), + JSON.stringify(arg) + ); + }); + + electron.ipcMain.on("put-library-recentlyAdded", (event, arg) => { + fs.writeFileSync( + path.join(this.paths.ciderCache, "library-recentlyAdded.json"), + JSON.stringify(arg) + ); + }); + + electron.ipcMain.on("get-library-songs", (event) => { + let librarySongs = fs.readFileSync( + path.join(this.paths.ciderCache, "library-songs.json"), + "utf8" + ); + event.returnValue = JSON.parse(librarySongs); + }); + + electron.ipcMain.on("get-library-artists", (event) => { + let libraryArtists = fs.readFileSync( + path.join(this.paths.ciderCache, "library-artists.json"), + "utf8" + ); + event.returnValue = JSON.parse(libraryArtists); + }); + + electron.ipcMain.on("get-library-albums", (event) => { + let libraryAlbums = fs.readFileSync( + path.join(this.paths.ciderCache, "library-albums.json"), + "utf8" + ); + event.returnValue = JSON.parse(libraryAlbums); + }); + + electron.ipcMain.on("get-library-playlists", (event) => { + let libraryPlaylists = fs.readFileSync( + path.join(this.paths.ciderCache, "library-playlists.json"), + "utf8" + ); + event.returnValue = JSON.parse(libraryPlaylists); + }); + + electron.ipcMain.on("get-library-recentlyAdded", (event) => { + let libraryRecentlyAdded = fs.readFileSync( + path.join(this.paths.ciderCache, "library-recentlyAdded.json"), + "utf8" + ); + event.returnValue = JSON.parse(libraryRecentlyAdded); + }); + + electron.ipcMain.handle("getYTLyrics", async (event, track, artist) => { + const u = track + " " + artist + " official video"; + return await yt.search(u); + }); + + electron.ipcMain.handle("setVibrancy", (event, key, value) => { + this.win.setVibrancy(value); + }); + + electron.ipcMain.on("maximize", () => { + // listen for maximize event + if (this.win.isMaximized()) { + this.win.unmaximize(); + } else { + this.win.maximize(); + } + }); + electron.ipcMain.on("unmaximize", () => { + // listen for maximize event + this.win.unmaximize(); + }); + + electron.ipcMain.on("minimize", () => { + // listen for minimize event + this.win.minimize(); + }); + + // Set scale + electron.ipcMain.on("setScreenScale", (event, scale) => { + this.win.webContents.setZoomFactor(parseFloat(scale)); + }); + + electron.ipcMain.on("windowmin", (event, width, height) => { + this.win.setMinimumSize(width,height); + }) + + electron.ipcMain.on("windowontop", (event, ontop) => { + this.win.setAlwaysOnTop(ontop); + }); + + // Set scale + electron.ipcMain.on("windowresize", (event, width, height, lock = false) => { + this.win.setContentSize(width, height); + this.win.setResizable(!lock); + }); + + //Fullscreen + electron.ipcMain.on('setFullScreen', (event, flag) => { + this.win.setFullScreen(flag) + }) + //Fullscreen + electron.ipcMain.on('detachDT', (event, _) => { + this.win.webContents.openDevTools({ mode: 'detach' }); + }) + + + electron.ipcMain.on('play', (event, type, id) => { + this.win.webContents.executeJavaScript(` + MusicKit.getInstance().setQueue({ ${type}: '${id}'}).then(function(queue) { + MusicKit.getInstance().play(); + }); + `) + }) + + function getIp() { + let ip = false; + let alias = 0; + let ifaces = os.networkInterfaces(); + for (var dev in ifaces) { + ifaces[dev].forEach(details => { + if (details.family === 'IPv4') { + if (!/(loopback|vmware|internal|hamachi|vboxnet|virtualbox)/gi.test(dev + (alias ? ':' + alias : ''))) { + if (details.address.substring(0, 8) === '192.168.' || + details.address.substring(0, 7) === '172.16.' || + details.address.substring(0, 3) === '10.' + ) { + ip = details.address; + ++alias; + } + } + } + }); + } + return ip; + } + + //QR Code + electron.ipcMain.handle('showQR', async (event , _) => { + let url = `http://${getIp()}:${this.remotePort}`; + electron.shell.openExternal(`https://cider.sh/pair-remote?url=${btoa(encodeURI(url))}`); + /* + * Doing this because we can give them the link and let them send it via Pocket or another in-browser tool -q + */ + }) + + // Get previews for normalization + electron.ipcMain.on("getPreviewURL", (_event, url) => { + 'get url' + fetch(url) + .then(res => res.buffer()) + .then(async(buffer) => { + try { + const metadata = await mm.parseBuffer(buffer, 'audio/x-m4a'); + let SoundCheckTag = metadata.native.iTunes[1].value + console.log('sc',SoundCheckTag) + this.win.webContents.send('SoundCheckTag', SoundCheckTag) + } catch (error) { + console.error(error.message); + } + }) + }); + + /* ********************************************************************************************* + * Window Events + * **********************************************************************************************/ + if (process.platform === "win32") { + let WND_STATE = { + MINIMIZED: 0, + NORMAL: 1, + MAXIMIZED: 2, + FULL_SCREEN: 3, + }; + let wndState = WND_STATE.NORMAL; + + this.win.on("resize", (_: any) => { + const isMaximized = this.win.isMaximized(); + const isMinimized = this.win.isMinimized(); + const isFullScreen = this.win.isFullScreen(); + const state = wndState; + if (isMinimized && state !== WND_STATE.MINIMIZED) { + wndState = WND_STATE.MINIMIZED; + } else if (isFullScreen && state !== WND_STATE.FULL_SCREEN) { + wndState = WND_STATE.FULL_SCREEN; + } else if (isMaximized && state !== WND_STATE.MAXIMIZED) { + wndState = WND_STATE.MAXIMIZED; + this.win.webContents.executeJavaScript(`app.chrome.maximized = true`); + } else if (state !== WND_STATE.NORMAL) { + wndState = WND_STATE.NORMAL; + this.win.webContents.executeJavaScript( + `app.chrome.maximized = false` + ); + } + }); + } + + this.win.on("closed", () => { + this.win = null; + }); + + // Set window Handler + this.win.webContents.setWindowOpenHandler((x: any) => { + if (x.url.includes("apple") || x.url.includes("localhost")) { + return { action: "allow" }; + } + electron.shell.openExternal(x.url).catch(console.error); + return { action: "deny" }; + }); + } + private async broadcastRemote() { + function getIp() { + let ip :any = false; + let alias = 0; + const ifaces: any = os.networkInterfaces() ; + for (var dev in ifaces) { + ifaces[dev].forEach( (details: any) => { + if (details.family === 'IPv4') { + if (!/(loopback|vmware|internal|hamachi|vboxnet|virtualbox)/gi.test(dev + (alias ? ':' + alias : ''))) { + if (details.address.substring(0, 8) === '192.168.' || + details.address.substring(0, 7) === '172.16.' || + details.address.substring(0, 3) === '10.' + ) { + ip = details.address; + ++alias; + } + } + } + }) ; + } + return ip; + } + const myString = `http://${getIp()}:${this.remotePort}`; + let mdns = require('mdns-js'); + const encoded = new Buffer(myString).toString('base64'); + var x = mdns.tcp('cider-remote'); + var txt_record = { + "Ver": "131077", + 'DvSv': '3689', + 'DbId': 'D41D8CD98F00B205', + 'DvTy': 'Cider', + 'OSsi': '0x212F0', + 'txtvers': '1', + "CtlN": "Cider", + "iV": "196623" + } + let server2 = mdns.createAdvertisement(x, `${await getPort({port: 3839})}`, { name: encoded, txt: txt_record }); + server2.start(); + console.log('remote broadcasted') + } +} + diff --git a/src/main/base/wsapi.ts b/src/main/base/wsapi.ts new file mode 100644 index 00000000..bb82ec49 --- /dev/null +++ b/src/main/base/wsapi.ts @@ -0,0 +1,293 @@ +import * as ws from "ws"; +import * as http from "http"; +import * as https from "https"; +import * as url from "url"; +import * as fs from "fs"; +import * as path from "path"; +import * as electron from "electron"; +const WebSocket = ws; +const WebSocketServer = ws.Server; + +interface standardResponse { + status?: Number, + message?: String, + data?: any, + type?: string, +} + + +export class wsapi { + static clients: any; + port: any = 26369 + wss: any = null + clients: any = [] + private _win : any; + constructor(win : any) { + this._win = win; + } + + + createId() { + // create random guid + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0, + v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } + public async InitWebSockets () { + electron.ipcMain.on('wsapi-updatePlaybackState', (event :any, arg :any) => { + this.updatePlaybackState(arg); + }) + + electron.ipcMain.on('wsapi-returnQueue', (event :any, arg :any) => { + this.returnQueue(JSON.parse(arg)); + }); + + electron.ipcMain.on('wsapi-returnSearch', (event :any, arg :any) => { + console.log("SEARCH") + this.returnSearch(JSON.parse(arg)); + }); + + electron.ipcMain.on('wsapi-returnSearchLibrary', (event :any, arg :any) => { + this.returnSearchLibrary(JSON.parse(arg)); + }); + + electron.ipcMain.on('wsapi-returnDynamic', (event :any, arg :any, type :any) => { + this.returnDynamic(JSON.parse(arg), type); + }); + + electron.ipcMain.on('wsapi-returnMusicKitApi', (event :any, arg :any, method :any) => { + this.returnMusicKitApi(JSON.parse(arg), method); + }); + + electron.ipcMain.on('wsapi-returnLyrics', (event :any, arg :any) => { + this.returnLyrics(JSON.parse(arg)); + }); + this.wss = new WebSocketServer({ + port: this.port, + perMessageDeflate: { + zlibDeflateOptions: { + // See zlib defaults. + chunkSize: 1024, + memLevel: 7, + level: 3 + }, + zlibInflateOptions: { + chunkSize: 10 * 1024 + }, + // Other options settable: + clientNoContextTakeover: true, // Defaults to negotiated value. + serverNoContextTakeover: true, // Defaults to negotiated value. + serverMaxWindowBits: 10, // Defaults to negotiated value. + // Below options specified as default values. + concurrencyLimit: 10, // Limits zlib concurrency for perf. + threshold: 1024 // Size (in bytes) below which messages + // should not be compressed if context takeover is disabled. + } + }) + console.log(`WebSocketServer started on port: ${this.port}`); + + const defaultResponse :standardResponse = {status :0, data:{}, message:"OK", type:"generic"}; + + + this.wss.on('connection', (ws : any) => { + ws.id = this.createId(); + console.log(`Client ${ws.id} connected`) + this.clients.push(ws); + ws.on('message', function incoming(message : any) { + + }); + // ws on message + ws.on('message', (message : any) => { + let data = JSON.parse(message); + let response :standardResponse = {status :0, data:{}, message:"OK", type:"generic"}; + if (data.action) { + data.action.toLowerCase(); + } + switch (data.action) { + default: + response.message = "Action not found"; + break; + case "identify": + response.message = "Thanks for identifying!" + response.data = { + id: ws.id + } + ws.identity = { + name: data.name, + author: data.author, + description: data.description, + version: data.version + } + break; + case "play-next": + this._win.webContents.executeJavaScript(`wsapi.playNext(\`${data.type}\`,\`${data.id}\`)`); + response.message = "Play Next"; + break; + case "play-later": + this._win.webContents.executeJavaScript(`wsapi.playLater(\`${data.type}\`,\`${data.id}\`)`); + response.message = "Play Later"; + break; + case "quick-play": + this._win.webContents.executeJavaScript(`wsapi.quickPlay(\`${data.term}\`)`); + response.message = "Quick Play"; + break; + case "get-lyrics": + this._win.webContents.executeJavaScript(`wsapi.getLyrics()`); + break; + case "shuffle": + this._win.webContents.executeJavaScript(`wsapi.toggleShuffle()`); + break; + case "set-shuffle": + if(data.shuffle == true) { + this._win.webContents.executeJavaScript(`MusicKit.getInstance().shuffleMode = 1`); + }else{ + this._win.webContents.executeJavaScript(`MusicKit.getInstance().shuffleMode = 0`); + } + break; + case "repeat": + this._win.webContents.executeJavaScript(`wsapi.toggleRepeat()`); + break; + case "seek": + this._win.webContents.executeJavaScript(`MusicKit.getInstance().seekToTime(${parseFloat(data.time)})`); + response.message = "Seek"; + break; + case "pause": + this._win.webContents.executeJavaScript(`MusicKit.getInstance().pause()`); + response.message = "Paused"; + break; + case "play": + this._win.webContents.executeJavaScript(`MusicKit.getInstance().play()`); + response.message = "Playing"; + break; + case "stop": + this._win.webContents.executeJavaScript(`MusicKit.getInstance().stop()`); + response.message = "Stopped"; + break; + case "volume": + this._win.webContents.executeJavaScript(`MusicKit.getInstance().volume = ${parseFloat(data.volume)}`); + response.message = "Volume"; + break; + case "mute": + this._win.webContents.executeJavaScript(`MusicKit.getInstance().mute()`); + response.message = "Muted"; + break; + case "unmute": + this._win.webContents.executeJavaScript(`MusicKit.getInstance().unmute()`); + response.message = "Unmuted"; + break; + case "next": + this._win.webContents.executeJavaScript(`MusicKit.getInstance().skipToNextItem()`); + response.message = "Next"; + break; + case "previous": + this._win.webContents.executeJavaScript(`MusicKit.getInstance().skipToPreviousItem()`); + response.message = "Previous"; + break; + case "musickit-api": + this._win.webContents.executeJavaScript(`wsapi.musickitApi(\`${data.method}\`, \`${data.id}\`, ${JSON.stringify(data.params)} , ${data.library})`); + break; + case "musickit-library-api": + break; + case "set-autoplay": + this._win.webContents.executeJavaScript(`wsapi.setAutoplay(${data.autoplay})`); + break; + case "queue-move": + this._win.webContents.executeJavaScript(`wsapi.moveQueueItem(${data.from},${data.to})`); + break; + case "get-queue": + this._win.webContents.executeJavaScript(`wsapi.getQueue()`); + break; + case "search": + if (!data.limit) { + data.limit = 10; + } + this._win.webContents.executeJavaScript(`wsapi.search(\`${data.term}\`, \`${data.limit}\`)`); + break; + case "library-search": + if (!data.limit) { + data.limit = 10; + } + this._win.webContents.executeJavaScript(`wsapi.searchLibrary(\`${data.term}\`, \`${data.limit}\`)`); + break; + case "show-window": + this._win.show() + break; + case "hide-window": + this._win.hide() + break; + case "play-mediaitem": + this._win.webContents.executeJavaScript(`wsapi.playTrackById("${data.id}", \`${data.kind}\`)`); + response.message = "Playing track"; + break; + case "get-status": + response.data = { + isAuthorized: true + }; + response.message = "Status"; + break; + case "get-currentmediaitem": + this._win.webContents.executeJavaScript(`wsapi.getPlaybackState()`); + break; + case "quit": + electron.app.quit(); + break; + } + ws.send(JSON.stringify(response)); + }); + + ws.on('close', () => { + // remove client from list + this.clients.splice(wsapi.clients.indexOf(ws), 1); + console.log(`Client ${ws.id} disconnected`); + }); + ws.send(JSON.stringify(defaultResponse)); + }); + } + sendToClient(id : any) { + // replace the clients.forEach with a filter to find the client that requested + } + updatePlaybackState(attr : any) { + const response : standardResponse = {status: 0, data: attr, message: "OK", type:"playbackStateUpdate"}; + this.clients.forEach(function each(client: any) { + client.send(JSON.stringify(response)); + }); + } + returnMusicKitApi(results :any, method :any) { + const response : standardResponse = {status :0, data: results, message:"OK", type:`musickitapi.${method}`}; + this.clients.forEach(function each(client :any) { + client.send(JSON.stringify(response)); + }); + } + returnDynamic(results :any, type :any) { + const response : standardResponse = {status :0, data: results, message: "OK", type: type}; + this.clients.forEach(function each(client :any) { + client.send(JSON.stringify(response)); + }); + } + returnLyrics(results :any) { + const response : standardResponse = {status :0, data: results, message: "OK", type: "lyrics"}; + this.clients.forEach(function each(client :any) { + client.send(JSON.stringify(response)); + }); + } + returnSearch(results :any) { + const response : standardResponse = {status :0, data: results, message: "OK", type: "searchResults"}; + this.clients.forEach(function each(client :any) { + client.send(JSON.stringify(response)); + }); + } + returnSearchLibrary(results :any) { + const response: standardResponse = {status :0, data :results, message:"OK", type:"searchResultsLibrary"}; + this.clients.forEach(function each(client :any) { + client.send(JSON.stringify(response)); + }); + } + returnQueue(queue :any) { + const response : standardResponse = {status :0,data :queue, message:"OK", type:"queue"}; + this.clients.forEach(function each(client :any) { + client.send(JSON.stringify(response)); + }); + } +} \ No newline at end of file diff --git a/src/main/cider-base.js b/src/main/cider-base.js deleted file mode 100644 index 856e58c3..00000000 --- a/src/main/cider-base.js +++ /dev/null @@ -1,446 +0,0 @@ -const { BrowserWindow, ipcMain, shell, app, screen } = require("electron") -const { join } = require("path") -const getPort = require("get-port"); -const express = require("express"); -const path = require("path"); -const windowStateKeeper = require("electron-window-state"); -const os = require('os'); -const yt = require('youtube-search-without-api-key'); -const discord = require('./discordrpc'); -const lastfm = require('./lastfm'); -const { writeFile, writeFileSync, existsSync, mkdirSync } = require('fs'); -const fs = require('fs'); -const mpris = require('./mpris'); -const mm = require('music-metadata'); -//const mdns = require('mdns') -const qrcode = require('qrcode-terminal') -const fetch = require('electron-fetch').default; -const { Stream } = require('stream'); - -// Analytics for debugging. -const ElectronSentry = require("@sentry/electron"); -ElectronSentry.init({ dsn: "https://68c422bfaaf44dea880b86aad5a820d2@o954055.ingest.sentry.io/6112214" }); - -const CiderBase = { - win: null, - requests: [], - audiostream: new Stream.PassThrough(), - async Start() { - this.clientPort = await getPort({ port: 9000 }); - this.win = this.CreateBrowserWindow() - }, - clientPort: 0, - CreateBrowserWindow() { - this.VerifyFiles() - // Set default window sizes - const mainWindowState = windowStateKeeper({ - defaultWidth: 1024, - defaultHeight: 600 - }); - - let win = null - const options = { - icon: join(__dirname, `../../resources/icons/icon.` + (process.platform === "win32" ? "ico" : "png")), - width: mainWindowState.width, - height: mainWindowState.height, - x: mainWindowState.x, - y: mainWindowState.y, - minWidth: 844, - minHeight: 410, - frame: false, - title: "Cider", - vibrancy: 'dark', - // transparent: true, - hasShadow: false, - webPreferences: { - webviewTag: true, - plugins: true, - nodeIntegration: true, - nodeIntegrationInWorker: false, - webSecurity: false, - allowRunningInsecureContent: true, - enableRemoteModule: true, - sandbox: true, - nativeWindowOpen: true, - contextIsolation: false, - preload: join(__dirname, '../preload/cider-preload.js') - } - } - - CiderBase.InitWebServer() - - // Create the BrowserWindow - win = new BrowserWindow(options) - - // intercept "https://js-cdn.music.apple.com/hls.js/2.141.0/hls.js/hls.js" and redirect to local file "./apple-hls.js" instead - win.webContents.session.webRequest.onBeforeRequest({ - urls: ["https://*/*.js"] - }, - (details, callback) => { - if (details.url.includes("hls.js")) { - callback({ - redirectURL: `http://localhost:${CiderBase.clientPort}/apple-hls.js` - }) - } else { - callback({ - cancel: false - }) - } - } - ) - - win.webContents.session.webRequest.onBeforeSendHeaders(async(details, callback) => { - if (details.url === "https://buy.itunes.apple.com/account/web/info") { - details.requestHeaders['sec-fetch-site'] = 'same-site'; - details.requestHeaders['DNT'] = '1'; - let itspod = await win.webContents.executeJavaScript(`window.localStorage.getItem("music.ampwebplay.itspod")`) - if (itspod != null) - details.requestHeaders['Cookie'] = `itspod=${itspod}` - } - callback({ requestHeaders: details.requestHeaders }) - }) - - win.webContents.session.webRequest.onHeadersReceived((details, callback) => { - if (details.url.match(/^https:\/\/store-\d{3}\.blobstore\.apple\.com/) || details.url.startsWith("https://store-037.blobstore.apple.com")) { - details.responseHeaders['Access-Control-Allow-Origin'] = '*'; - } - callback({ responseHeaders: details.responseHeaders }) - }) - - let location = `http://localhost:${CiderBase.clientPort}/` - win.loadURL(location) - win.on("closed", () => { - win = null - }) - - // Register listeners on Window to track size and position of the Window. - mainWindowState.manage(win); - - // IPC stuff (senders) - ipcMain.on("cider-platform", (event) => { - event.returnValue = process.platform - }) - - ipcMain.on("get-gpu-mode", (event) => { - event.returnValue = process.platform - }) - - ipcMain.on("is-dev", (event) => { - event.returnValue = !app.isPackaged - }) - - // IPC stuff (listeners) - ipcMain.on('close', () => { // listen for close event - win.close(); - }) - - ipcMain.on('put-library-songs', (event, arg) => { - fs.writeFileSync(join(app.paths.ciderCache, "library-songs.json"), JSON.stringify(arg)) - }) - - ipcMain.on('put-library-artists', (event, arg) => { - fs.writeFileSync(join(app.paths.ciderCache, "library-artists.json"), JSON.stringify(arg)) - }) - - ipcMain.on('put-library-albums', (event, arg) => { - fs.writeFileSync(join(app.paths.ciderCache, "library-albums.json"), JSON.stringify(arg)) - }) - - ipcMain.on('put-library-playlists', (event, arg) => { - fs.writeFileSync(join(app.paths.ciderCache, "library-playlists.json"), JSON.stringify(arg)) - }) - - ipcMain.on('put-library-recentlyAdded', (event, arg) => { - fs.writeFileSync(join(app.paths.ciderCache, "library-recentlyAdded.json"), JSON.stringify(arg)) - }) - - ipcMain.on('get-library-songs', (event) => { - let librarySongs = fs.readFileSync(join(app.paths.ciderCache, "library-songs.json"), "utf8") - event.returnValue = JSON.parse(librarySongs) - }) - - ipcMain.on('get-library-artists', (event) => { - let libraryArtists = fs.readFileSync(join(app.paths.ciderCache, "library-artists.json"), "utf8") - event.returnValue = JSON.parse(libraryArtists) - }) - - ipcMain.on('get-library-albums', (event) => { - let libraryAlbums = fs.readFileSync(join(app.paths.ciderCache, "library-albums.json"), "utf8") - event.returnValue = JSON.parse(libraryAlbums) - }) - - ipcMain.on('get-library-playlists', (event) => { - let libraryPlaylists = fs.readFileSync(join(app.paths.ciderCache, "library-playlists.json"), "utf8") - event.returnValue = JSON.parse(libraryPlaylists) - }) - - ipcMain.on('get-library-recentlyAdded', (event) => { - let libraryRecentlyAdded = fs.readFileSync(join(app.paths.ciderCache, "library-recentlyAdded.json"), "utf8") - event.returnValue = JSON.parse(libraryRecentlyAdded) - }) - - ipcMain.handle('getYTLyrics', async(event, track, artist) => { - var u = track + " " + artist + " official video"; - const videos = await yt.search(u); - return videos - }) - - ipcMain.handle('getStoreValue', (event, key, defaultValue) => { - return (defaultValue ? app.cfg.get(key, true) : app.cfg.get(key)); - }); - - ipcMain.handle('setStoreValue', (event, key, value) => { - app.cfg.set(key, value); - }); - - ipcMain.on('getStore', (event) => { - event.returnValue = app.cfg.store - }) - - ipcMain.on('setStore', (event, store) => { - app.cfg.store = store - }) - - ipcMain.handle('setVibrancy', (event, key, value) => { - win.setVibrancy(value) - }); - - ipcMain.on('maximize', () => { // listen for maximize event - if (win.isMaximized()) { - win.unmaximize() - } else { - win.maximize() - } - }) - - ipcMain.on('minimize', () => { // listen for minimize event - win.minimize(); - }) - - ipcMain.on('setFullScreen', (event, flag) => { - win.setFullScreen(flag) - }) - - if (process.platform === "win32") { - let WND_STATE = { - MINIMIZED: 0, - NORMAL: 1, - MAXIMIZED: 2, - FULL_SCREEN: 3 - } - let wndState = WND_STATE.NORMAL - - win.on("resize", (_event) => { - const isMaximized = win.isMaximized() - const isMinimized = win.isMinimized() - const isFullScreen = win.isFullScreen() - const state = wndState; - if (isMinimized && state !== WND_STATE.MINIMIZED) { - wndState = WND_STATE.MINIMIZED - } else if (isFullScreen && state !== WND_STATE.FULL_SCREEN) { - wndState = WND_STATE.FULL_SCREEN - } else if (isMaximized && state !== WND_STATE.MAXIMIZED) { - wndState = WND_STATE.MAXIMIZED - win.webContents.executeJavaScript(`app.chrome.maximized = true`) - } else if (state !== WND_STATE.NORMAL) { - wndState = WND_STATE.NORMAL - win.webContents.executeJavaScript(`app.chrome.maximized = false`) - } - }) - } - - // Set window Handler - win.webContents.setWindowOpenHandler(({ url }) => { - if (url.includes("apple") || url.includes("localhost")) { - return { action: "allow" } - } - shell.openExternal(url).catch(() => {}) - return { - action: 'deny' - } - }) - - // Set scale - ipcMain.on('setScreenScale', (event, scale) => { - win.webContents.setZoomFactor(parseFloat(scale)) - }) - - win.webContents.setZoomFactor(screen.getPrimaryDisplay().scaleFactor) - - mpris.connect(win) - mpris.SetButtons(win, false) - - lastfm.authenticate() - // Discord - discord.connect((app.cfg.get("general.discord_rpc") == 1) ? '911790844204437504' : '886578863147192350'); - ipcMain.on('playbackStateDidChange', (_event, a) => { - app.media = a; - discord.updateActivity(a) - mpris.SetButtons(win, a) - mpris.updateState(a) - lastfm.scrobbleSong(a) - lastfm.updateNowPlayingSong(a) - }); - - ipcMain.on('nowPlayingItemDidChange', (_event, a) => { - app.media = a; - discord.updateActivity(a) - mpris.SetButtons(win, a) - mpris.updateAttributes(a) - lastfm.scrobbleSong(a) - lastfm.updateNowPlayingSong(a) - }); - - ipcMain.on("getPreviewURL", (_event, url) => { - fetch(url) - .then(res => res.buffer()) - .then(async(buffer) => { - try { - const metadata = await mm.parseBuffer(buffer, 'audio/x-m4a'); - SoundCheckTag = metadata.native.iTunes[1].value - win.webContents.send('SoundCheckTag', SoundCheckTag) - } catch (error) { - console.error(error.message); - } - }) - }); - - ipcMain.on('writeAudio', function(event, buffer) { - CiderBase.audiostream.write(Buffer.from(buffer)); - }) - - return win - }, - VerifyFiles() { - const expectedDirectories = [ - "CiderCache" - ] - const expectedFiles = [ - "library-songs.json", - "library-artists.json", - "library-albums.json", - "library-playlists.json", - "library-recentlyAdded.json", - ] - for (let i = 0; i < expectedDirectories.length; i++) { - if (!existsSync(path.join(app.getPath("userData"), expectedDirectories[i]))) { - mkdirSync(path.join(app.getPath("userData"), expectedDirectories[i])) - } - } - for (let i = 0; i < expectedFiles.length; i++) { - const file = path.join(app.paths.ciderCache, expectedFiles[i]) - if (!existsSync(file)) { - writeFileSync(file, JSON.stringify([])) - } - } - }, - EnvironmentVariables: { - "env": { - platform: os.platform(), - dev: app.isPackaged - } - }, - LinkHandler: (startArgs) => { - if (!startArgs) return; - - if (String(startArgs).includes('auth')) { - let authURI = String(startArgs).split('/auth/')[1] - if (authURI.startsWith('lastfm')) { // If we wanted more auth options - console.log("lfmtoken", String(startArgs)) - const authKey = authURI.split('lastfm?token=')[1]; - app.cfg.set('lastfm.enabled', true); - app.cfg.set('lastfm.auth_token', authKey); - CiderBase.win.webContents.send('LastfmAuthenticated', authKey); - lastfm.authenticate() - } - } else { - if (String(startArgs).includes('/play/')) { //Steer away from protocal:// specific conditionals - const playParam = String(startArgs).split('/play/')[1] - if (playParam.includes('s/')) { // setQueue can be done with album, song, url, playlist id - console.log(playParam) - let song = playParam.split('s/')[1] - console.warn(`[LinkHandler] Attempting to load song by id: ${song}`); - this.win.webContents.executeJavaScript(` - MusicKit.getInstance().setQueue({ song: '${song}'}).then(function(queue) { - MusicKit.getInstance().play(); - }); - `).catch((err) => console.error(err)); - } - - - - } - - - - - } - - }, - - async InitWebServer() { - const webapp = express(); - const webRemotePath = path.join(__dirname, '../renderer/'); - webapp.set("views", path.join(webRemotePath, "views")); - webapp.set("view engine", "ejs"); - let firstRequest = true - //const webRemoteMDNS = mdns.createAdvertisement(mdns.tcp('https'), 9000, { name: "cider", domain: 'local' }) - //webRemoteMDNS.start() - //* Prep for remote -quack - webapp.use(function(req, res, next) { - // if not localhost - if (req.url.includes("audio.webm") || (req.headers.host.includes("localhost") && req.headers["user-agent"].includes("Cider"))) { - next(); - - } else { - console.log(req.get('host')) - res.redirect("https://discord.gg/applemusic") - } - }); - webapp.use(express.static(webRemotePath)); - webapp.get('/', function(req, res) { - //if (!req.headers["user-agent"].includes("Cider")) - //res.sendFile(path.join(webRemotePath, 'index_old.html')); - if (firstRequest) { - console.log("---- Ignore Me ;) ---") - qrcode.generate(`http://${os.hostname}:9000`) //Prep for remote - console.log("---- Ignore Me ;) ---") - /* - * - * USING https://www.npmjs.com/package/qrcode-terminal for terminal - * WE SHOULD USE https://www.npmjs.com/package/qrcode for the remote (or others) - * -quack - */ - } - firstRequest = false - - res.render("main", CiderBase.EnvironmentVariables) - }); - webapp.get('/audio.webm', function(req, res) { - console.log('hi') - try { - req.connection.setTimeout(Number.MAX_SAFE_INTEGER); - // CiderBase.requests.push({req: req, res: res}); - // var pos = CiderBase.requests.length - 1; - // req.on("close", () => { - // console.info("CLOSED", CiderBase.requests.length); - // requests.splice(pos, 1); - // console.info("CLOSED", CiderBase.requests.length); - // }); - CiderBase.audiostream.on('data', (data) => { - try { - res.write(data); - } catch (ex) { - console.log(ex) - } - }) - } catch (ex) { console.log(ex) } - }); - webapp.listen(CiderBase.clientPort, function() { - console.log(`Cider hosted on: ${CiderBase.clientPort}`); - }); - }, - -} - -module.exports = CiderBase; \ No newline at end of file diff --git a/src/main/discordrpc.js b/src/main/discordrpc.js deleted file mode 100644 index 6e3b414c..00000000 --- a/src/main/discordrpc.js +++ /dev/null @@ -1,142 +0,0 @@ -const { app } = require('electron'), - DiscordRPC = require('discord-rpc') - -module.exports = { - - /** - * Connects to Discord RPC - * @param {string} clientId - */ - connect: function(clientId) { - app.discord = { isConnected: false }; - if (app.cfg.get('general.discord_rpc') == 0 || app.discord.isConnected) return; - - DiscordRPC.register(clientId) // Apparently needed for ask to join, join, spectate etc. - const client = new DiscordRPC.Client({ transport: "ipc" }); - app.discord = Object.assign(client, { error: false, activityCache: null, isConnected: false }); - - // Login to Discord - app.discord.login({ clientId }) - .then(() => { - app.discord.isConnected = true; - }) - .catch((e) => console.error(`[DiscordRPC][connect] ${e}`)); - - app.discord.on('ready', () => { - console.log(`[DiscordRPC][connect] Successfully Connected to Discord. Authed for user: ${client.user.username} (${client.user.id})`); - }) - - // Handles Errors - app.discord.on('error', err => { - console.error(`[DiscordRPC] ${err}`); - this.disconnect() - app.discord.isConnected = false; - }); - }, - - /** - * Disconnects from Discord RPC - */ - disconnect: function() { - if (app.cfg.get('general.discord_rpc') == 0 || !app.discord.isConnected) return; - - try { - app.discord.destroy().then(() => { - app.discord.isConnected = false; - console.log('[DiscordRPC][disconnect] Disconnected from discord.') - }).catch((e) => console.error(`[DiscordRPC][disconnect] ${e}`)); - } catch (err) { - console.error(err) - } - }, - - /** - * Sets the activity of the client - * @param {object} attributes - */ - updateActivity: function(attributes) { - if (app.cfg.get('general.discord_rpc') == 0) return; - - if (!app.discord.isConnected) { - app.discord.clearActivity().catch((e) => console.error(`[DiscordRPC][updateActivity] ${e}`)); - return; - } - - // console.log('[DiscordRPC][updateActivity] Updating Discord Activity.') - - const listenURL = `https://cider.sh/p?s&id=${attributes.playParams.id}` // cider://play/s/[id] (for song) - //console.log(attributes) - let ActivityObject = { - details: attributes.name, - state: `by ${attributes.artistName}`, - startTimestamp: attributes.startTime, - endTimestamp: attributes.endTime, - largeImageKey: (attributes.artwork.url.replace('{w}', '1024').replace('{h}', '1024')) ?? 'cider', - largeImageText: attributes.albumName, - smallImageKey: (attributes.status ? 'play' : 'pause'), - smallImageText: (attributes.status ? 'Playing' : 'Paused'), - instance: true, - buttons: [ - { label: "Listen on Cider", url: listenURL }, - ] - }; - if (ActivityObject.largeImageKey == "" || ActivityObject.largeImageKey == null) { - ActivityObject.largeImageKey = (app.cfg.get("general.discord_rpc") == 1) ? "cider" : "logo" - } - - // Remove the pause/play icon and test for clear activity on pause - if (app.cfg.get('general.discordClearActivityOnPause') == 1) { - delete ActivityObject.smallImageKey - delete ActivityObject.smallImageText - } - - // Deletes the timestamp if its not greater than 0 - if (!((new Date(attributes.endTime)).getTime() > 0)) { - delete ActivityObject.startTimestamp - delete ActivityObject.endTimestamp - } - - // Artist check - if (!attributes.artistName) { - delete ActivityObject.state - } - - // Album text check - if (!ActivityObject.largeImageText || ActivityObject.largeImageText.length < 2) { - delete ActivityObject.largeImageText - } - - // Checks if the name is greater than 128 because some songs can be that long - if (ActivityObject.details.length > 128) { - ActivityObject.details = ActivityObject.details.substring(0, 125) + '...' - } - - - - - // Check if its pausing (false) or playing (true) - if (!attributes.status) { - if (app.cfg.get('general.discordClearActivityOnPause') == 1) { - app.discord.clearActivity().catch((e) => console.error(`[DiscordRPC][clearActivity] ${e}`)); - ActivityObject = null - } else { - delete ActivityObject.startTimestamp - delete ActivityObject.endTimestamp - ActivityObject.smallImageKey = 'pause' - ActivityObject.smallImageText = 'Paused' - } - } - - - if (ActivityObject && ActivityObject !== app.discord.activityCache && ActivityObject.details && ActivityObject.state) { - try { - // console.log(`[DiscordRPC][setActivity] Setting activity to ${JSON.stringify(ActivityObject)}`); - app.discord.setActivity(ActivityObject) - app.discord.activityCache = ActivityObject - } catch (err) { - console.error(`[DiscordRPC][setActivity] ${err}`) - } - - } - }, -} diff --git a/src/main/index.ts b/src/main/index.ts new file mode 100644 index 00000000..59196a9f --- /dev/null +++ b/src/main/index.ts @@ -0,0 +1,84 @@ +require('v8-compile-cache'); + +// Analytics for debugging fun yeah. +import * as sentry from '@sentry/electron'; +import * as electron from 'electron'; +import {Win} from "./base/win"; +import {ConfigStore} from "./base/store"; +import {AppEvents} from "./base/app"; +import PluginHandler from "./base/plugins"; + +sentry.init({dsn: "https://68c422bfaaf44dea880b86aad5a820d2@o954055.ingest.sentry.io/6112214"}); + +const config = new ConfigStore(); +const App = new AppEvents(config.store); +const Cider = new Win(electron.app, config.store) +const plug = new PluginHandler(config.store); + +let win: Electron.BrowserWindow; + +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * App Event Handlers + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ + +electron.app.on('ready', () => { + App.ready(plug); + + console.log('[Cider] Application is Ready. Creating Window.') + if (!electron.app.isPackaged) { + console.info('[Cider] Running in development mode.') + require('vue-devtools').install() + } + + electron.components.whenReady().then(async () => { + win = await Cider.createWindow() + App.bwCreated(win); + /// please dont change this for plugins to get proper and fully initialized Win objects + plug.callPlugins('onReady', win); + win.on("ready-to-show", () => { + win.show(); + }); + }); + +}); + +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Renderer Event Handlers + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ + +electron.ipcMain.on('playbackStateDidChange', (event, attributes) => { + plug.callPlugins('onPlaybackStateDidChange', attributes); +}); + +electron.ipcMain.on('nowPlayingItemDidChange', (event, attributes) => { + plug.callPlugins('onNowPlayingItemDidChange', attributes); +}); + +electron.app.on('before-quit', () => { + plug.callPlugins('onBeforeQuit'); + console.warn(`${electron.app.getName()} exited.`); +}); + +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Widevine Event Handlers + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ + +// @ts-ignore +electron.app.on('widevine-ready', (version, lastVersion) => { + if (null !== lastVersion) { + console.log('[Cider][Widevine] Widevine ' + version + ', upgraded from ' + lastVersion + ', is ready to be used!') + } else { + console.log('[Cider][Widevine] Widevine ' + version + ' is ready to be used!') + } +}) + +// @ts-ignore +electron.app.on('widevine-update-pending', (currentVersion, pendingVersion) => { + console.log('[Cider][Widevine] Widevine ' + currentVersion + ' is ready to be upgraded to ' + pendingVersion + '!') +}) + +// @ts-ignore +electron.app.on('widevine-error', (error) => { + console.log('[Cider][Widevine] Widevine installation encountered an error: ' + error) + electron.app.exit() +}) diff --git a/src/main/lastfm.js b/src/main/lastfm.js deleted file mode 100644 index b97bcb1b..00000000 --- a/src/main/lastfm.js +++ /dev/null @@ -1,153 +0,0 @@ -const {app, Notification} = require('electron'), - fs = require('fs'), - {resolve} = require('path'), - sessionPath = resolve(app.getPath('userData'), 'session.json'), - apiCredentials = require('../../resources/lfmApiCredentials.json'), - LastfmAPI = require('lastfmapi'); - -const lfm = { - authenticateFromFile: function () { - let sessionData = require(sessionPath) - console.log("[LastFM][authenticateFromFile] Logging in with Session Info.") - app.lastfm.setSessionCredentials(sessionData.name, sessionData.key) - console.log("[LastFM][authenticateFromFile] Logged in.") - }, - - authenticate: function () { - if (app.cfg.get('lastfm.auth_token')) { - app.cfg.set('lastfm.enabled', true); - } - - if (!app.cfg.get('lastfm.enabled') || !app.cfg.get('lastfm.auth_token')) { - app.cfg.set('lastfm.enabled', false); - return - } - - const lfmAPI = new LastfmAPI({ - 'api_key': apiCredentials.key, - 'secret': apiCredentials.secret - }); - - app.lastfm = Object.assign(lfmAPI, {cachedAttributes: false, cachedNowPlayingAttributes: false}); - - fs.stat(sessionPath, function (err) { - if (err) { - console.error("[LastFM][Session] Session file couldn't be opened or doesn't exist,", err) - console.log("[LastFM][Auth] Beginning authentication from configuration") - app.lastfm.authenticate(app.cfg.get('lastfm.auth_token'), function (err, session) { - if (err) { - throw err; - } - console.log("[LastFM] Successfully obtained LastFM session info,", session); // {"name": "LASTFM_USERNAME", "key": "THE_USER_SESSION_KEY"} - console.log("[LastFM] Saving session info to disk.") - let tempData = JSON.stringify(session) - fs.writeFile(sessionPath, tempData, (err) => { - if (err) - console.log("[LastFM][fs]", err) - else { - console.log("[LastFM][fs] File was written successfully.") - lfm.authenticateFromFile() - new Notification({ - title: app.getName(), - body: "Successfully logged into LastFM using Authentication Key." - }).show() - } - }) - }); - } else { - lfm.authenticateFromFile() - } - }) - }, - - scrobbleSong: async function (attributes) { - await new Promise(resolve => setTimeout(resolve, Math.round(attributes.durationInMillis * (app.cfg.get('lastfm.scrobble_after') / 100)))); - const currentAttributes = app.media; - - if (!app.lastfm || app.lastfm.cachedAttributes === attributes ) { - return - } - - if (app.lastfm.cachedAttributes) { - if (app.lastfm.cachedAttributes.playParams.id === attributes.playParams.id) return; - } - - if (currentAttributes.status && currentAttributes === attributes) { - if (fs.existsSync(sessionPath)) { - // Scrobble playing song. - if (attributes.status === true) { - app.lastfm.track.scrobble({ - 'artist': lfm.filterArtistName(attributes.artistName), - 'track': attributes.name, - 'album': attributes.albumName, - 'albumArtist': this.filterArtistName(attributes.artistName), - 'timestamp': new Date().getTime() / 1000 - }, function (err, scrobbled) { - if (err) { - return console.error('[LastFM] An error occurred while scrobbling', err); - } - - console.log('[LastFM] Successfully scrobbled: ', scrobbled); - }); - app.lastfm.cachedAttributes = attributes - } - } else { - this.authenticate(); - } - } else { - return console.log('[LastFM] Did not add ', attributes.name , '—' , lfm.filterArtistName(attributes.artistName), 'because now playing a other song.'); - } - }, - - filterArtistName: function (artist) { - if (!app.cfg.get('lastfm.enabledRemoveFeaturingArtists')) return artist; - - artist = artist.split(' '); - if (artist.includes('&')) { - artist.length = artist.indexOf('&'); - } - if (artist.includes('and')) { - artist.length = artist.indexOf('and'); - } - artist = artist.join(' '); - if (artist.includes(',')) { - artist = artist.split(',') - artist = artist[0] - } - return artist.charAt(0).toUpperCase() + artist.slice(1); - }, - - updateNowPlayingSong: function (attributes) { - if (!app.lastfm ||app.lastfm.cachedNowPlayingAttributes === attributes | !app.cfg.get('lastfm.NowPlaying')) { - return - } - - if (app.lastfm.cachedNowPlayingAttributes) { - if (app.lastfm.cachedNowPlayingAttributes.playParams.id === attributes.playParams.id) return; - } - - if (fs.existsSync(sessionPath)) { - // update Now Playing - if (attributes.status === true) { - app.lastfm.track.updateNowPlaying({ - 'artist': lfm.filterArtistName(attributes.artistName), - 'track': attributes.name, - 'album': attributes.albumName, - 'albumArtist': this.filterArtistName(attributes.artistName) - }, function (err, nowPlaying) { - if (err) { - return console.error('[LastFM] An error occurred while updating nowPlayingSong', err); - } - - console.log('[LastFM] Successfully updated nowPlayingSong', nowPlaying); - }); - app.lastfm.cachedNowPlayingAttributes = attributes - } - - } else { - this.authenticate() - } - } -} - -module.exports = lfm; \ No newline at end of file diff --git a/src/main/mpris.js b/src/main/mpris.js deleted file mode 100644 index 086945da..00000000 --- a/src/main/mpris.js +++ /dev/null @@ -1,149 +0,0 @@ -const { nativeImage } = require("electron"); -const path = require('path') - -let mediaPlayer = null; - -module.exports = { - - /** - * Connects to the MPRIS interface. - * @param {Object} win - The BrowserWindow. - */ - connect: (win) => { - if (process.platform !== "linux") return; - - const Player = require('mpris-service'); - - mediaPlayer = Player({ - name: 'Cider', - identity: 'Cider', - supportedUriSchemes: [], - supportedMimeTypes: [], - supportedInterfaces: ['player'] - }); - mediaPlayer = Object.assign(mediaPlayer, { canQuit: true, canControl: true, canPause: true, canPlay: true, canGoNext: true }) - - - let pos_atr = {durationInMillis: 0}; - mediaPlayer.getPosition = function () { - const durationInMicro = pos_atr.durationInMillis * 1000; - const percentage = parseFloat("0") || 0; - return durationInMicro * percentage; - } - - mediaPlayer.active = true - - mediaPlayer.on('playpause', async () => { - win.webContents.executeJavaScript('MusicKitInterop.pausePlay()').catch(err => console.error(err)) - }); - - mediaPlayer.on('play', async () => { - win.webContents.executeJavaScript('MusicKitInterop.pausePlay()').catch(err => console.error(err)) - }); - - mediaPlayer.on('pause', async () => { - win.webContents.executeJavaScript('MusicKitInterop.pausePlay()').catch(err => console.error(err)) - }); - - mediaPlayer.on('next', async () => { - win.webContents.executeJavaScript('MusicKitInterop.nextTrack()').catch(err => console.error(err)) - }); - - mediaPlayer.on('previous', async () => { - win.webContents.executeJavaScript('MusicKitInterop.previousTrack()').catch(err => console.error(err)) - }); - }, - - /** - * Updates the MPRIS interface. - * @param {Object} attributes - The attributes of the track. - */ - updateAttributes: (attributes) => { - if (process.platform !== "linux") return; - - const MetaData = { - 'mpris:trackid': mediaPlayer.objectPath(`track/${attributes.playParams.id.replace(/[.]+/g, "")}`), - 'mpris:length': attributes.durationInMillis * 1000, // In microseconds - 'mpris:artUrl': (attributes.artwork.url.replace('/{w}x{h}bb', '/512x512bb')).replace('/2000x2000bb', '/35x35bb'), - 'xesam:title': `${attributes.name}`, - 'xesam:album': `${attributes.albumName}`, - 'xesam:artist': [`${attributes.artistName}`,], - 'xesam:genre': attributes.genreNames - } - - if (mediaPlayer.metadata["mpris:trackid"] === MetaData["mpris:trackid"]) { - return - } - - mediaPlayer.metadata = MetaData - }, - - /** - * Updates the playback state of the MPRIS interface. - * @param {Object} attributes - The attributes of the track. - */ - updateState: (attributes) => { - if (process.platform !== "linux") return; - - function setPlaybackIfNeeded(status) { - if (mediaPlayer.playbackStatus === status) { - return - } - mediaPlayer.playbackStatus = status; - } - - switch (attributes.status) { - case true: // Playing - setPlaybackIfNeeded('Playing'); - break; - case false: // Paused - setPlaybackIfNeeded('Paused'); - break; - default: // Stopped - setPlaybackIfNeeded('Stopped'); - break; - } - }, - - SetButtons: (win, attributes) => { - if (process.platform === 'win32') { // Set the Windows Thumbnail Toolbar Buttons - win.setThumbarButtons([ - { - tooltip: 'Previous', - icon: nativeImage.createFromPath(path.join(__dirname, 'thumbaricons/backwardPng.png')), - click() { - console.log("Clicked the bc taskbar button!") - win.webContents.executeJavaScript('MusicKitInterop.previousTrack()').catch(err => console.error(err)) - } - }, - { - tooltip: attributes.status ? 'Pause' : 'Play', - //tooltip: 'Play', - icon: attributes.status ? nativeImage.createFromPath(path.join(__dirname, 'thumbaricons/pausePng.png')) : nativeImage.createFromPath(path.join(__dirname, 'thumbaricons/playPng.png')), - click() { - console.log("Clicked the pl taskbar button!") - win.webContents.executeJavaScript('MusicKitInterop.pausePlay()').catch(err => console.error(err)) - } - }, - { - tooltip: 'Next', - icon: nativeImage.createFromPath(path.join(__dirname, 'thumbaricons/forwardPng.png')), - click() { - console.log("Clicked the fw taskbar button!") - win.webContents.executeJavaScript('MusicKitInterop.nextTrack()').catch(err => console.error(err)) - } - } - ]); - - } - }, - - /** - * Closes the MPRIS interface. - */ - clearActivity: () => { - if (process.platform !== "linux") return; - mediaPlayer.metadata = {'mpris:trackid': '/org/mpris/MediaPlayer2/TrackList/NoTrack'} - mediaPlayer.playbackStatus = 'Stopped'; - }, -} \ No newline at end of file diff --git a/src/main/plugins/Extras/examplePlugin.ts b/src/main/plugins/Extras/examplePlugin.ts new file mode 100644 index 00000000..98b6af4c --- /dev/null +++ b/src/main/plugins/Extras/examplePlugin.ts @@ -0,0 +1,60 @@ +let i = 1, k = 1; +export default class ExamplePlugin { + /** + * Private variables for interaction in plugins + */ + private _win: any; + private _app: any; + private _store: any; + + /** + * Base Plugin Details (Eventually implemented into a GUI in settings) + */ + public name: string = 'examplePlugin'; + public description: string = 'Example plugin'; + public version: string = '1.0.0'; + public author: string = 'Example author'; + + /** + * Runs on plugin load (Currently run on application start) + */ + constructor(app: any, store: any) { + this._app = app; + this._store = store; + console.debug(`[Plugin][${this.name}] Loading Complete.`); + } + + /** + * Runs on app ready + */ + onReady(win: any): void { + this._win = win; + console.debug(`[Plugin][${this.name}] Ready.`); + } + + /** + * Runs on app stop + */ + onBeforeQuit(): void { + console.debug(`[Plugin][${this.name}] Stopped.`); + } + + /** + * Runs on playback State Change + * @param attributes Music Attributes (attributes.state = current state) + */ + onPlaybackStateDidChange(attributes: object): void { + console.log('onPlaybackStateDidChange has been called ' + i + ' times'); + i++ + } + + /** + * Runs on song change + * @param attributes Music Attributes + */ + onNowPlayingItemDidChange(attributes: object): void { + console.log('onNowPlayingDidChange has been called ' + k + ' times'); + k++ + } + +} diff --git a/src/main/plugins/Extras/sendSongToTitlebar.ts b/src/main/plugins/Extras/sendSongToTitlebar.ts new file mode 100644 index 00000000..8b39e47d --- /dev/null +++ b/src/main/plugins/Extras/sendSongToTitlebar.ts @@ -0,0 +1,37 @@ +export default class sendSongToTitlebar { + /** + * Base Plugin Details (Eventually implemented into a GUI in settings) + */ + public name: string = 'sendSongToTitlebar'; + public description: string = 'Sets the app\'s titlebar to the Song title'; + public version: string = '0.0.1'; + public author: string = 'Cider Collective (credit to 8times9 via #147)'; + /** + * Runs on plugin load (Currently run on application start) + */ + private _win: any; + private _app: any; + constructor() {} + /** + * Runs on app ready + */ + onReady(win: any): void { + this._win = win; + } + /** + * Runs on app stop + */ + onBeforeQuit(): void {} + /** + * Runs on playback State Change + * @param attributes Music Attributes (attributes.state = current state) + */ + onPlaybackStateDidChange(attributes: any): void { + this._win.setTitle(`${(attributes != null && attributes.name != null && attributes.name.length > 0) ? (attributes.name + " - ") : ''}Cider`) + } + /** + * Runs on song change + * @param attributes Music Attributes + */ + onNowPlayingItemDidChange(attributes: object): void {} +} \ No newline at end of file diff --git a/src/main/plugins/discordrpc.ts b/src/main/plugins/discordrpc.ts new file mode 100644 index 00000000..f7fb3d26 --- /dev/null +++ b/src/main/plugins/discordrpc.ts @@ -0,0 +1,195 @@ +import * as RPC from 'discord-rpc' + +export default class DiscordRichPresence { + + /** + * Private variables for interaction in plugins + */ + private static _store: any; + private static _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 author: string = 'vapormusic/Core (Cider Collective)'; + + /** + * Plugin Initialization + */ + private _client: any = null; + private _activity: RPC.Presence = { + details: '', + state: '', + largeImageKey: '', + largeImageText: '', + smallImageKey: '', + smallImageText: '', + instance: false + }; + private _activityCache: RPC.Presence = { + details: '', + state: '', + largeImageKey: '', + largeImageText: '', + smallImageKey: '', + smallImageText: '', + instance: false + }; + + /******************************************************************************************* + * Private Methods + * ****************************************************************************************/ + + /** + * Connect to Discord + * @param clientId + * @private + */ + private connect(clientId: any) { + if (DiscordRichPresence._store.general.discord_rpc == 0) { + return + } + + // Apparently needed for ask to join, join, spectate etc. + RPC.register(clientId) + + // Create the client + this._client = new RPC.Client({transport: "ipc"}); + + // Runs on Ready + this._client.on('ready', () => { + console.info(`[DiscordRPC][connect] Successfully Connected to Discord. Authed for user: ${this._client.user.id}.`); + }) + + // Handles Errors + this._client.on('error', (err: any) => { + console.error(`[DiscordRichPresence] ${err}`); + this.disconnect() + }); + + // Login to Discord + this._client.login({clientId}) + .then(() => { + DiscordRichPresence._connection = true; + }) + .catch((e: any) => console.error(`[DiscordRichPresence][connect] ${e}`)); + } + + /** + * Disconnects from Discord RPC + */ + private disconnect() { + if (!this._client) return; + + this._client.destroy().then(() => { + DiscordRichPresence._connection = false; + console.log('[DiscordRPC][disconnect] Disconnected from discord.') + }).catch((e: any) => console.error(`[DiscordRPC][disconnect] ${e}`)); + } + + /** + * Sets the activity of the client + * @param {object} attributes + */ + private updateActivity(attributes: any) { + if (!this._client) return; + + if (!DiscordRichPresence._connection) { + this._client.clearActivity().catch((e: any) => console.error(`[DiscordRichPresence][clearActivity] ${e}`)); + return; + } + + this._activity = { + details: attributes.name, + state: `${attributes.artistName ? `by ${attributes.artistName}` : ''}`, + startTimestamp: ((new Date(attributes.endTime).getTime() < 0) ? null : attributes.startTime), + endTimestamp: ((new Date(attributes.endTime).getTime() < 0) ? null : attributes.endTime), + largeImageKey: (attributes.artwork.url.replace('{w}', '1024').replace('{h}', '1024')) ?? 'cider', + largeImageText: attributes.albumName, + instance: false, // Whether the activity is in a game session + + buttons: [ + {label: "Listen on Cider", url: attributes.url.cider}, + {label: "View on Apple Music", url: attributes.url.appleMusic}, + ] + }; + + + // Checks if the name is greater than 128 because some songs can be that long + if (this._activity.details && this._activity.details.length > 128) { + this._activity.details = this._activity.details.substring(0, 125) + '...' + } + + // Check if its pausing (false) or playing (true) + if (!attributes.status) { + if (DiscordRichPresence._store.general.discordClearActivityOnPause == 1) { + this._client.clearActivity() + .catch((e: any) => console.error(`[DiscordRichPresence][clearActivity] ${e}`)); + } else { + this._activity.smallImageKey = 'pause'; + this._activity.smallImageText = 'Paused'; + delete this._activity.endTimestamp; + delete this._activity.startTimestamp; + this._client.setActivity(this._activity) + .catch((e: any) => console.error(`[DiscordRichPresence][setActivity] ${e}`)); + } + + } else if (this._activity && this._activityCache !== this._activity && this._activity.details) { + if (DiscordRichPresence._store.general.discordClearActivityOnPause != 1) { + this._activity.smallImageKey = 'play'; + this._activity.smallImageText = 'Playing'; + } + + this._client.setActivity(this._activity) + .catch((e: any) => console.error(`[DiscordRichPresence][updateActivity] ${e}`)); + this._activityCache = this._activity; + } + + } + + /******************************************************************************************* + * Public Methods + * ****************************************************************************************/ + + /** + * Runs on plugin load (Currently run on application start) + */ + constructor(_app: any, store: any) { + DiscordRichPresence._store = store + console.debug(`[Plugin][${this.name}] Loading Complete.`); + } + + /** + * Runs on app ready + */ + onReady(_win: any): void { + this.connect((DiscordRichPresence._store.general.discord_rpc == 1) ? '911790844204437504' : '886578863147192350'); + console.debug(`[Plugin][${this.name}] Ready.`); + } + + /** + * Runs on app stop + */ + onBeforeQuit(): void { + console.debug(`[Plugin][${this.name}] Stopped.`); + } + + /** + * Runs on playback State Change + * @param attributes Music Attributes (attributes.state = current state) + */ + onPlaybackStateDidChange(attributes: object): void { + this.updateActivity(attributes) + } + + /** + * Runs on song change + * @param attributes Music Attributes + */ + onNowPlayingItemDidChange(attributes: object): void { + this.updateActivity(attributes) + } +} diff --git a/src/main/plugins/lastfm.ts b/src/main/plugins/lastfm.ts new file mode 100644 index 00000000..51ddcc34 --- /dev/null +++ b/src/main/plugins/lastfm.ts @@ -0,0 +1,253 @@ +import * as electron from 'electron'; +import * as fs from 'fs'; +import {resolve} from 'path'; + +export default class LastFMPlugin { + private sessionPath = resolve(electron.app.getPath('userData'), 'session.json'); + private apiCredentials = { + key: "f9986d12aab5a0fe66193c559435ede3", + secret: "acba3c29bd5973efa38cc2f0b63cc625" + } + /** + * Private variables for interaction in plugins + */ + private _win: any; + private _app: any; + private _lastfm: any; + private _store: any; + + private authenticateFromFile() { + let sessionData = require(this.sessionPath) + console.log("[LastFM][authenticateFromFile] Logging in with Session Info.") + this._lastfm.setSessionCredentials(sessionData.username, sessionData.key) + console.log("[LastFM][authenticateFromFile] Logged in.", sessionData.username, sessionData.key) + } + + + authenticate() { + try { + if (this._store.lastfm.auth_token) { + this._store.lastfm.enabled = true; + } + + if (!this._store.lastfm.enabled || !this._store.lastfm.auth_token) { + this._store.lastfm.enabled = false; + return + } + /// dont move this require to top , app wont load + const LastfmAPI = require('lastfmapi'); + const lfmAPI = new LastfmAPI({ + 'api_key': this.apiCredentials.key, + 'secret': this.apiCredentials.secret + }); + + this._lastfm = Object.assign(lfmAPI, {cachedAttributes: false, cachedNowPlayingAttributes: false}); + + fs.stat(this.sessionPath, (err: any) => { + if (err) { + console.error("[LastFM][Session] Session file couldn't be opened or doesn't exist,", err) + console.log("[LastFM][Auth] Beginning authentication from configuration") + console.log("[LastFM][tk]", this._store.lastfm.auth_token) + this._lastfm.authenticate(this._store.lastfm.auth_token, (err: any, session: any) => { + if (err) { + throw err; + } + console.log("[LastFM] Successfully obtained LastFM session info,", session); // {"name": "LASTFM_USERNAME", "key": "THE_USER_SESSION_KEY"} + console.log("[LastFM] Saving session info to disk.") + let tempData = JSON.stringify(session) + fs.writeFile(this.sessionPath, tempData, (err: any) => { + if (err) + console.log("[LastFM][fs]", err) + else { + console.log("[LastFM][fs] File was written successfully.") + this.authenticateFromFile() + new electron.Notification({ + title: electron.app.getName(), + body: "Successfully logged into LastFM using Authentication Key." + }).show() + } + }) + }); + } else { + this.authenticateFromFile() + } + }) + } catch (err) { + console.log(err) + } + } + + private async scrobbleSong(attributes: any) { + await new Promise(resolve => setTimeout(resolve, Math.round(attributes.durationInMillis * (this._store.lastfm.scrobble_after / 100)))); + const currentAttributes = attributes; + + if (!this._lastfm || this._lastfm.cachedAttributes === attributes) { + return + } + + if (this._lastfm.cachedAttributes) { + if (this._lastfm.cachedAttributes.playParams.id === attributes.playParams.id) return; + } + + if (currentAttributes.status && currentAttributes === attributes) { + if (fs.existsSync(this.sessionPath)) { + // Scrobble playing song. + if (attributes.status === true) { + this._lastfm.track.scrobble({ + 'artist': this.filterArtistName(attributes.artistName), + 'track': attributes.name, + 'album': attributes.albumName, + 'albumArtist': this.filterArtistName(attributes.artistName), + 'timestamp': new Date().getTime() / 1000 + }, function (err: any, scrobbled: any) { + if (err) { + return console.error('[LastFM] An error occurred while scrobbling', err); + } + + console.log('[LastFM] Successfully scrobbled: ', scrobbled); + }); + this._lastfm.cachedAttributes = attributes + } + } else { + this.authenticate(); + } + } else { + return console.log('[LastFM] Did not add ', attributes.name, '—', this.filterArtistName(attributes.artistName), 'because now playing a other song.'); + } + } + + private filterArtistName(artist: any) { + if (!this._store.lastfm.enabledRemoveFeaturingArtists) return artist; + + artist = artist.split(' '); + if (artist.includes('&')) { + artist.length = artist.indexOf('&'); + } + if (artist.includes('and')) { + artist.length = artist.indexOf('and'); + } + artist = artist.join(' '); + if (artist.includes(',')) { + artist = artist.split(',') + artist = artist[0] + } + return artist.charAt(0).toUpperCase() + artist.slice(1); + } + + private updateNowPlayingSong(attributes: any) { + if (!this._lastfm || this._lastfm.cachedNowPlayingAttributes === attributes || !this._store.lastfm.NowPlaying) { + return + } + + if (this._lastfm.cachedNowPlayingAttributes) { + if (this._lastfm.cachedNowPlayingAttributes.playParams.id === attributes.playParams.id) return; + } + + if (fs.existsSync(this.sessionPath)) { + // update Now Playing + if (attributes.status === true) { + this._lastfm.track.updateNowPlaying({ + 'artist': this.filterArtistName(attributes.artistName), + 'track': attributes.name, + 'album': attributes.albumName, + 'albumArtist': this.filterArtistName(attributes.artistName) + }, function (err: any, nowPlaying: any) { + if (err) { + return console.error('[LastFM] An error occurred while updating nowPlayingSong', err); + } + + console.log('[LastFM] Successfully updated nowPlayingSong', nowPlaying); + }); + this._lastfm.cachedNowPlayingAttributes = attributes + } + + } else { + this.authenticate() + } + } + + /** + * Base Plugin Details (Eventually implemented into a GUI in settings) + */ + public name: string = 'LastFMPlugin'; + public description: string = 'LastFM plugin for Cider'; + public version: string = '0.0.1'; + public author: string = 'vapormusic / Cider Collective'; + + /** + * Runs on plugin load (Currently run on application start) + */ + constructor(app: any, store: any) { + this._app = app; + this._store = store + electron.app.on('second-instance', (_e: any, argv: any) => { + // Checks if first instance is authorized and if second instance has protocol args + argv.forEach((value: any) => { + if (value.includes('auth')) { + console.log('[LastFMPlugin ok]') + let authURI = String(argv).split('/auth/')[1]; + if (authURI.startsWith('lastfm')) { // If we wanted more auth options + const authKey = authURI.split('lastfm?token=')[1]; + this._store.lastfm.enabled = true; + this._store.lastfm.auth_token = authKey; + console.log(authKey); + this._win.webContents.send('LastfmAuthenticated', authKey); + this.authenticate(); + } + } + }) + }) + electron.app.on('open-url', (event: any, arg: any) => { + console.log('[LastFMPlugin] yes') + event.preventDefault(); + if (arg.includes('auth')) { + let authURI = String(arg).split('/auth/')[1]; + if (authURI.startsWith('lastfm')) { // If we wanted more auth options + const authKey = authURI.split('lastfm?token=')[1]; + this._store.lastfm.enabled = true; + this._store.lastfm.auth_token = authKey; + this._win.webContents.send('LastfmAuthenticated', authKey); + console.log(authKey); + this.authenticate(); + } + } + }) + } + + /** + * Runs on app ready + */ + onReady(win: any): void { + this._win = win; + this.authenticate(); + } + + /** + * Runs on app stop + */ + onBeforeQuit(): void { + console.log('Example plugin stopped'); + } + + /** + * Runs on playback State Change + * @param attributes Music Attributes (attributes.state = current state) + */ + onPlaybackStateDidChange(attributes: object): void { + this.scrobbleSong(attributes) + this.updateNowPlayingSong(attributes) + } + + /** + * Runs on song change + * @param attributes Music Attributes + */ + onNowPlayingItemDidChange(attributes: object): void { + if (!this._store.lastfm.filterLoop){ + this._lastfm.cachedNowPlayingAttributes = false; + this._lastfm.cachedAttributes = false} + this.scrobbleSong(attributes) + this.updateNowPlayingSong(attributes) + } + +} \ No newline at end of file diff --git a/src/main/plugins/minimizeToTray.ts b/src/main/plugins/minimizeToTray.ts new file mode 100644 index 00000000..98050bc8 --- /dev/null +++ b/src/main/plugins/minimizeToTray.ts @@ -0,0 +1,165 @@ +import * as electron from 'electron'; +import * as path from 'path'; + + +export default class MinimizeToTray { + /** + * Private variables for interaction in plugins + */ + private _win: any; + private _app: any; + private _store: any; + private _tray: any; + private _forceQuit = false; + + /** + * Base Plugin Details (Eventually implemented into a GUI in settings) + */ + public name: string = 'Minimize to tray'; + public description: string = 'Allow Cider to minimize to tray'; + public version: string = '1.0.0'; + public author: string = 'vapormusic'; + + constructor(app: any, store: any) { + this._app = app; + this._store = store; + } + + private SetContextMenu(visibility : any) { + let self = this + if (visibility) { + this._tray.setContextMenu(electron.Menu.buildFromTemplate([ + // { + // label: 'Check for Updates', + // click: function () { + // app.ame.utils.checkForUpdates(true) + // } + // }, + { + label: 'Minimize to Tray', + click: function () { + if (typeof self._win.hide === 'function') { + self._win.hide(); + self.SetContextMenu(false); + } + } + }, + { + label: 'Quit', + click: function () { + self._forceQuit = true; self._app.quit(); + } + } + ])); + } else { + this._tray.setContextMenu(electron.Menu.buildFromTemplate([ + // { + // label: 'Check for Updates', + // click: function () { + // this._app.ame.utils.checkForUpdates(true) + // } + // }, + { + label: `Show ${electron.app.getName()}`, + click: function () { + if (typeof self._win.show === 'function') { + self._win.show(); + self.SetContextMenu(true); + } + } + }, + { + label: 'Quit', + click: function () { + self._forceQuit = true; self._app.quit(); + } + } + ])); + } + return true + + } + + /** + * Runs on app ready + */ + onReady(win: any): void { + this._win = win; + const winTray = electron.nativeImage.createFromPath(path.join(__dirname, `../../resources/icons/icon.ico`)).resize({ + width: 32, + height: 32 + }) + const macTray = electron.nativeImage.createFromPath(path.join(__dirname, `../../resources/icons/icon.png`)).resize({ + width: 20, + height: 20 + }) + const linuxTray = electron.nativeImage.createFromPath(path.join(__dirname, `../../resources/icons/icon.png`)).resize({ + width: 32, + height: 32 + }) + let trayIcon : any ; + if (process.platform === "win32") { + trayIcon = winTray + } else if (process.platform === "linux") { + trayIcon = linuxTray + } else if (process.platform === "darwin") { + trayIcon = macTray + } + + this._tray = new electron.Tray(trayIcon) + this._tray.setToolTip(this._app.getName()); + this.SetContextMenu(true); + + this._tray.on('double-click', () => { + if (typeof this._win.show === 'function') { + if (this._win.isVisible()) { + this._win.focus() + } else { + this._win.show() + } + } + }) + electron.ipcMain.on("minimizeTray", (event, value) => { + // listen for close event + this._win.hide(); + this.SetContextMenu(false); + }); + electron.ipcMain.handle("update-store-mtt", (event, value) => { + this._store.general["close_behavior"] = value; + }) + this._win.on("close", (e :any) => { + if (this._forceQuit || this._store.general["close_behavior"] == '0' ) { + this._app.quit(); + } else if (this._store.general["close_behavior"] == '1') { + e.preventDefault(); + this._win.minimize(); + } else { + e.preventDefault(); + this._win.hide(); + this.SetContextMenu(false); + } + }); + } + + /** + * Runs on app stop + */ + onBeforeQuit(): void { + + } + + /** + * Runs on playback State Change + * @param attributes Music Attributes (attributes.state = current state) + */ + onPlaybackStateDidChange(attributes: object): void { + } + + /** + * Runs on song change + * @param attributes Music Attributes + */ + onNowPlayingItemDidChange(attributes: object): void { + } + +} diff --git a/src/main/plugins/mpris.ts b/src/main/plugins/mpris.ts new file mode 100644 index 00000000..869cbceb --- /dev/null +++ b/src/main/plugins/mpris.ts @@ -0,0 +1,196 @@ +// @ts-ignore +import * as Player from 'mpris-service'; + +export default class MPRIS { + /** + * Private variables for interaction in plugins + */ + private _win: any; + private _app: any; + + /** + * Base Plugin Details (Eventually implemented into a GUI in settings) + */ + public name: string = 'MPRIS Service'; + public description: string = 'Handles MPRIS service calls for Linux systems.'; + public version: string = '1.0.0'; + public author: string = 'Core'; + + /** + * MPRIS Service + */ + private mpris: any; + private mprisEvents: Object = { + "playpause": "pausePlay", + "play": "pausePlay", + "pause": "pausePlay", + "next": "nextTrack", + "previous": "previousTrack", + } + + /******************************************************************************************* + * Private Methods + * ****************************************************************************************/ + + /** + * Runs a media event + * @param type - pausePlay, nextTrack, PreviousTrack + * @private + */ + private runMediaEvent(type: string) { + if (this._win) { + this._win.webContents.executeJavaScript(`MusicKitInterop.${type}()`).catch(console.error) + } + } + + /** + * Blocks non-linux systems from running this plugin + * @private + * @decorator + */ + private static linuxOnly(_target: any, _propertyKey: string, descriptor: PropertyDescriptor) { + if (process.platform !== 'linux') { + descriptor.value = function () { + return + } + } + } + + + /** + * Connects to MPRIS Service + */ + @MPRIS.linuxOnly + private connect() { + this.mpris = Player({ + name: 'Cider', + identity: 'Cider', + supportedUriSchemes: [], + supportedMimeTypes: [], + supportedInterfaces: ['player'] + }); + this.mpris = Object.assign(this.mpris, { + canQuit: true, + canControl: true, + canPause: true, + canPlay: true, + canGoNext: true, + active: true + }) + + + const pos_atr = {durationInMillis: 0}; + this.mpris.getPosition = function () { + const durationInMicro = pos_atr.durationInMillis * 1000; + const percentage = parseFloat("0") || 0; + return durationInMicro * percentage; + } + + for (const [key, value] of Object.entries(this.mprisEvents)) { + this.mpris.on(key, () => { + this.runMediaEvent(value) + }); + } + } + + /** + * Update MPRIS Player Attributes + */ + @MPRIS.linuxOnly + private updatePlayer(attributes: any) { + + const MetaData = { + 'mpris:trackid': this.mpris.objectPath(`track/${attributes.playParams.id.replace(/[.]+/g, "")}`), + 'mpris:length': attributes.durationInMillis * 1000, // In microseconds + 'mpris:artUrl': (attributes.artwork.url.replace('/{w}x{h}bb', '/512x512bb')).replace('/2000x2000bb', '/35x35bb'), + 'xesam:title': `${attributes.name}`, + 'xesam:album': `${attributes.albumName}`, + 'xesam:artist': [`${attributes.artistName}`,], + 'xesam:genre': attributes.genreNames + } + + if (this.mpris.metadata["mpris:trackid"] === MetaData["mpris:trackid"]) { + return + } + + this.mpris.metadata = MetaData + + } + + /** + * Update MPRIS Player State + * @private + * @param attributes + */ + @MPRIS.linuxOnly + private updatePlayerState(attributes: any) { + + let status = 'Stopped'; + if (attributes.status) { + status = 'Playing'; + } else if (attributes.status === false) { + status = 'Paused'; + } + + if (this.mpris.playbackStatus === status) { + return + } + this.mpris.playbackStatus = status; + } + + /** + * Clear state + * @private + */ + private clearState() { + this.mpris.metadata = {'mpris:trackid': '/org/mpris/MediaPlayer2/TrackList/NoTrack'} + this.mpris.playbackStatus = 'Stopped'; + } + + + /******************************************************************************************* + * Public Methods + * ****************************************************************************************/ + + /** + * Runs on plugin load (Currently run on application start) + */ + constructor(app: any, _store: any) { + this._app = app; + console.debug(`[Plugin][${this.name}] Loading Complete.`); + } + + /** + * Runs on app ready + */ + onReady(win: any): void { + this._win = win; + console.debug(`[Plugin][${this.name}] Ready.`); + this.connect() + } + + /** + * Runs on app stop + */ + onBeforeQuit(): void { + console.debug(`[Plugin][${this.name}] Stopped.`); + this.clearState() + } + + /** + * Runs on playback State Change + * @param attributes Music Attributes (attributes.state = current state) + */ + onPlaybackStateDidChange(attributes: object): void { + this.updatePlayerState(attributes) + } + + /** + * Runs on song change + * @param attributes Music Attributes + */ + onNowPlayingItemDidChange(attributes: object): void { + this.updatePlayer(attributes); + } + +} diff --git a/src/preload/cider-preload.js b/src/preload/cider-preload.js index 7d5296a6..d67f0dfd 100644 --- a/src/preload/cider-preload.js +++ b/src/preload/cider-preload.js @@ -1,102 +1,113 @@ -const electron = require('electron') - +global.ipcRenderer = require('electron').ipcRenderer; console.log('Loaded Preload') let cache = {playParams: {id: 0}, status: null, remainingTime: 0}, - playbackCache = {status: null, time: Date.now()}; + playbackCache = {status: null, time: Date.now()}; const MusicKitInterop = { - init: function () { - MusicKit.getInstance().addEventListener(MusicKit.Events.playbackStateDidChange, () => { - if (MusicKitInterop.filterTrack(MusicKitInterop.getAttributes(), true, false)) { - console.log("ayy"); - global.ipcRenderer.send('playbackStateDidChange', MusicKitInterop.getAttributes()) - // if (typeof _plugins != "undefined") { - // _plugins.execute("OnPlaybackStateChanged", {Attributes: MusicKitInterop.getAttributes()}) - // } - } - }); + init: function () { + MusicKit.getInstance().addEventListener(MusicKit.Events.playbackStateDidChange, () => { + if (MusicKitInterop.filterTrack(MusicKitInterop.getAttributes(), true, false)) { + global.ipcRenderer.send('playbackStateDidChange', MusicKitInterop.getAttributes()) + ipcRenderer.send('wsapi-updatePlaybackState', MusicKitInterop.getAttributes()); + // if (typeof _plugins != "undefined") { + // _plugins.execute("OnPlaybackStateChanged", {Attributes: MusicKitInterop.getAttributes()}) + // } + } + }); - MusicKit.getInstance().addEventListener(MusicKit.Events.nowPlayingItemDidChange, () => { - if (MusicKitInterop.filterTrack(MusicKitInterop.getAttributes(), false, true)) { - global.ipcRenderer.send('nowPlayingItemDidChange', MusicKitInterop.getAttributes()); - } - }); + /** wsapi */ + MusicKit.getInstance().addEventListener(MusicKit.Events.playbackProgressDidChange, () => { + ipcRenderer.send('wsapi-updatePlaybackState', MusicKitInterop.getAttributes()); + }); + /** wsapi */ - MusicKit.getInstance().addEventListener(MusicKit.Events.authorizationStatusDidChange, () => { - global.ipcRenderer.send('authorizationStatusDidChange', MusicKit.getInstance().authorizationStatus) - }) + MusicKit.getInstance().addEventListener(MusicKit.Events.nowPlayingItemDidChange, () => { + if (MusicKitInterop.filterTrack(MusicKitInterop.getAttributes(), false, true) || !app.cfg.lastfm.filterLoop) { + global.ipcRenderer.send('nowPlayingItemDidChange', MusicKitInterop.getAttributes()); + } + }); - MusicKit.getInstance().addEventListener(MusicKit.Events.mediaPlaybackError, (e) => { - console.warn(`[mediaPlaybackError] ${e}`); - }) - }, + MusicKit.getInstance().addEventListener(MusicKit.Events.authorizationStatusDidChange, () => { + global.ipcRenderer.send('authorizationStatusDidChange', MusicKit.getInstance().authorizationStatus) + }) - getAttributes: function () { - const nowPlayingItem = MusicKit.getInstance().nowPlayingItem; - const isPlayingExport = MusicKit.getInstance().isPlaying; - const remainingTimeExport = MusicKit.getInstance().currentPlaybackTimeRemaining; - const attributes = (nowPlayingItem != null ? nowPlayingItem.attributes : {}); + MusicKit.getInstance().addEventListener(MusicKit.Events.mediaPlaybackError, (e) => { + console.warn(`[mediaPlaybackError] ${e}`); + }) + }, - attributes.status = isPlayingExport ?? false; - attributes.name = attributes?.name ?? 'No Title Found'; - attributes.artwork = attributes?.artwork ?? { url: '' }; - attributes.artwork.url = attributes?.artwork?.url ?? ''; - attributes.playParams = attributes?.playParams ?? { id: 'no-id-found' }; - attributes.playParams.id = attributes?.playParams?.id ?? 'no-id-found'; - attributes.albumName = attributes?.albumName ?? ''; - attributes.artistName = attributes?.artistName ?? ''; - attributes.genreNames = attributes?.genreNames ?? []; - attributes.remainingTime = remainingTimeExport - ? remainingTimeExport * 1000 - : 0; - attributes.durationInMillis = attributes?.durationInMillis ?? 0; - attributes.startTime = Date.now(); - attributes.endTime = Math.round( - attributes?.playParams?.id === cache.playParams.id - ? Date.now() + attributes?.remainingTime - : attributes?.startTime + attributes?.durationInMillis - ); + getAttributes: function () { + const mk = MusicKit.getInstance() + const nowPlayingItem = mk.nowPlayingItem; + const isPlayingExport = mk.isPlaying; + const remainingTimeExport = mk.currentPlaybackTimeRemaining; + const attributes = (nowPlayingItem != null ? nowPlayingItem.attributes : {}); - return attributes; - }, + attributes.status = isPlayingExport ?? false; + attributes.name = attributes?.name ?? 'No Title Found'; + attributes.artwork = attributes?.artwork ?? {url: ''}; + attributes.artwork.url = (attributes?.artwork?.url ?? '').replace(`{f}`, "png"); + attributes.playParams = attributes?.playParams ?? {id: 'no-id-found'}; + attributes.playParams.id = attributes?.playParams?.id ?? 'no-id-found'; + attributes.url = { + cider: "cider://play/s/" + nowPlayingItem?._songId ?? 'no-id-found', + appleMusic: "https://music.apple.com/song/" + nowPlayingItem?._songId ?? 'no-id-found' + } + if (attributes.playParams.id === 'no-id-found') { + attributes.playParams.id = nowPlayingItem?.id ?? 'no-id-found'; + } + attributes.albumName = attributes?.albumName ?? ''; + attributes.artistName = attributes?.artistName ?? ''; + attributes.genreNames = attributes?.genreNames ?? []; + attributes.remainingTime = remainingTimeExport + ? remainingTimeExport * 1000 + : 0; + attributes.durationInMillis = attributes?.durationInMillis ?? 0; + attributes.startTime = Date.now(); + attributes.endTime = Math.round( + attributes?.playParams?.id === cache.playParams.id + ? Date.now() + attributes?.remainingTime + : attributes?.startTime + attributes?.durationInMillis + ); + return attributes; + }, - filterTrack: function (a, playbackCheck, mediaCheck) { - if (a.title === "No Title Found" || a.playParams.id === "no-id-found") { - return; - } else if (mediaCheck && a.playParams.id === cache.playParams.id) { - return; - } else if (playbackCheck && a.status === playbackCache.status) { - return; - } else if (playbackCheck && !a.status && a.remainingTime === playbackCache.time) { /* Pretty much have to do this to prevent multiple runs when a song starts playing */ - return; - } - cache = a; - if (playbackCheck) playbackCache = {status: a.status, time: a.remainingTime}; - return true; - }, + filterTrack: function (a, playbackCheck, mediaCheck) { + if (a.title === "No Title Found" || a.playParams.id === "no-id-found") { + return; + } else if (mediaCheck && a.playParams.id === cache.playParams.id) { + return; + } else if (playbackCheck && a.status === playbackCache.status) { + return; + } else if (playbackCheck && !a.status && a.remainingTime === playbackCache.time) { /* Pretty much have to do this to prevent multiple runs when a song starts playing */ + return; + } + cache = a; + if (playbackCheck) playbackCache = {status: a.status, time: a.remainingTime}; + return true; + }, - pausePlay: function () { - if (MusicKit.getInstance().isPlaying) { - MusicKit.getInstance().pause(); - } else if (MusicKit.getInstance().nowPlayingItem != null) { - MusicKit.getInstance().play().then(r => console.log(`[MusicKitInterop] Playing ${r}`)); - } - }, + pausePlay: function () { + if (MusicKit.getInstance().isPlaying) { + MusicKit.getInstance().pause(); + } else if (MusicKit.getInstance().nowPlayingItem != null) { + MusicKit.getInstance().play().then(r => console.log(`[MusicKitInterop] Playing ${r}`)); + } + }, - nextTrack: function () { - MusicKit.getInstance().skipToNextItem().then(r => console.log(`[MusicKitInterop] Skipping to Next ${r}`)); - }, + nextTrack: function () { + MusicKit.getInstance().skipToNextItem().then(r => console.log(`[MusicKitInterop] Skipping to Next ${r}`)); + }, - previousTrack: function () { - MusicKit.getInstance().skipToPreviousItem().then(r => console.log(`[MusicKitInterop] Skipping to Previous ${r}`)); - } + previousTrack: function () { + MusicKit.getInstance().skipToPreviousItem().then(r => console.log(`[MusicKitInterop] Skipping to Previous ${r}`)); + } } process.once('loaded', () => { - console.log("Setting ipcRenderer") - global.ipcRenderer = electron.ipcRenderer; - global.MusicKitInterop = MusicKitInterop; -}); \ No newline at end of file + console.log("Setting ipcRenderer") + global.MusicKitInterop = MusicKitInterop; +}); diff --git a/src/renderer/.jsbeautifyrc b/src/renderer/.jsbeautifyrc new file mode 100644 index 00000000..bde13199 --- /dev/null +++ b/src/renderer/.jsbeautifyrc @@ -0,0 +1,5 @@ +{ + "js": { + "beautify.ignore": "src/renderer/index.js" + } +} \ No newline at end of file diff --git a/src/renderer/AppHeader.svg b/src/renderer/AppHeader.svg new file mode 100644 index 00000000..d6a7b723 --- /dev/null +++ b/src/renderer/AppHeader.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/renderer/ameframework.css b/src/renderer/ameframework.css index 7da9b6a2..38c56508 100644 --- a/src/renderer/ameframework.css +++ b/src/renderer/ameframework.css @@ -27,6 +27,7 @@ .md-option-segment.md-option-segment_auto { width: auto; + white-space: nowrap; } .md-option-container .md-option-line:not(:last-child) { diff --git a/src/renderer/assets/feather/mic.svg b/src/renderer/assets/feather/mic.svg new file mode 100644 index 00000000..dc5f780c --- /dev/null +++ b/src/renderer/assets/feather/mic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/assets/feather/plus-circle-white.svg b/src/renderer/assets/feather/plus-circle-white.svg new file mode 100644 index 00000000..bd6a25ec --- /dev/null +++ b/src/renderer/assets/feather/plus-circle-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/assets/feather/video.svg b/src/renderer/assets/feather/video.svg new file mode 100644 index 00000000..8ff156aa --- /dev/null +++ b/src/renderer/assets/feather/video.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/assets/feather/volume-2.svg b/src/renderer/assets/feather/volume-2.svg new file mode 100644 index 00000000..eaf07b56 --- /dev/null +++ b/src/renderer/assets/feather/volume-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/assets/feather/volume.svg b/src/renderer/assets/feather/volume.svg new file mode 100644 index 00000000..a6282072 --- /dev/null +++ b/src/renderer/assets/feather/volume.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/assets/feather/x-circle-white.svg b/src/renderer/assets/feather/x-circle-white.svg new file mode 100644 index 00000000..4ad16a6a --- /dev/null +++ b/src/renderer/assets/feather/x-circle-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/assets/feather/x-circlePng.png b/src/renderer/assets/feather/x-circlePng.png new file mode 100644 index 00000000..9b0f9ca2 Binary files /dev/null and b/src/renderer/assets/feather/x-circlePng.png differ diff --git a/src/renderer/assets/infinity.svg b/src/renderer/assets/infinity.svg new file mode 100644 index 00000000..4d06c7b8 --- /dev/null +++ b/src/renderer/assets/infinity.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/assets/logocut.png b/src/renderer/assets/logocut.png index 3f1e9b8b..29e744c4 100644 Binary files a/src/renderer/assets/logocut.png and b/src/renderer/assets/logocut.png differ diff --git a/src/renderer/assets/pip.svg b/src/renderer/assets/pip.svg new file mode 100644 index 00000000..76253ddc --- /dev/null +++ b/src/renderer/assets/pip.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/src/renderer/assets/repeatOne.svg b/src/renderer/assets/repeatOne.svg new file mode 100644 index 00000000..3b5bba82 --- /dev/null +++ b/src/renderer/assets/repeatOne.svg @@ -0,0 +1,33 @@ + + + + Layer 1 + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + \ No newline at end of file diff --git a/src/renderer/audio/audio.js b/src/renderer/audio/audio.js index 64118b7c..948a4d7a 100644 --- a/src/renderer/audio/audio.js +++ b/src/renderer/audio/audio.js @@ -5,6 +5,8 @@ var CiderAudio = { gainNode : null, spatialNode : null, spatialInput: null, + audioBands : null, + preampNode : null, }, init: function (cb = function () { }) { //AudioOutputs.fInit = true; @@ -21,8 +23,14 @@ var CiderAudio = { }, off: function(){ try{ - CiderAudio.audioNodes.gainNode.disconnect(); - CiderAudio.audioNodes.spatialNode.disconnect(); + try{ + CiderAudio.audioNodes.gainNode.disconnect(); } catch(e){} + try{ CiderAudio.audioNodes.spatialNode.disconnect();} catch(e){} + try{ + CiderAudio.audioNodes.preampNode.disconnect(); + CiderAudio.audioNodes.audioBands[0].disconnect(); + CiderAudio.audioNodes.audioBands[9].disconnect(); + } catch(e){} CiderAudio.source.connect(CiderAudio.context.destination);} catch(e){} }, connectContext: function (mediaElem){ @@ -42,6 +50,7 @@ var CiderAudio = { if (app.cfg.audio.spatial){ CiderAudio.spatialOn() } + CiderAudio.equalizer() }, normalizerOn: function (){}, normalizerOff: function (){ @@ -49,7 +58,7 @@ var CiderAudio = { }, spatialOn: function (){ try{ - CiderAudio.audioNodes.gainNode.connect(CiderAudio.context.destination);} catch(e){} + CiderAudio.audioNodes.gainNode.disconnect(CiderAudio.context.destination);} catch(e){} CiderAudio.audioNodes.spatialNode = new ResonanceAudio(CiderAudio.context); CiderAudio.audioNodes.spatialNode.output.connect(CiderAudio.context.destination); let roomDimensions = { @@ -90,6 +99,42 @@ var CiderAudio = { } ); } + }, + equalizer: function (){ + let BANDS = app.cfg.audio.equalizer.frequencies; + let GAIN = app.cfg.audio.equalizer.gain; + let Q = app.cfg.audio.equalizer.Q; + CiderAudio.audioNodes.audioBands = []; + + for (i = 0; i < BANDS.length; i++) { + CiderAudio.audioNodes.audioBands[i] = CiderAudio.context.createBiquadFilter(); + CiderAudio.audioNodes.audioBands[i].type = 'peaking'; // 'peaking'; + CiderAudio.audioNodes.audioBands[i].frequency.value = BANDS[i]; + CiderAudio.audioNodes.audioBands[i].Q.value = Q[i]; + CiderAudio.audioNodes.audioBands[i].gain.value = GAIN[i] * app.cfg.audio.equalizer.mix; + } + + CiderAudio.audioNodes.preampNode = CiderAudio.context.createBiquadFilter(); + CiderAudio.audioNodes.preampNode.type = 'highshelf'; + CiderAudio.audioNodes.preampNode.frequency.value = 0; // allow all + CiderAudio.audioNodes.preampNode.gain.value = app.cfg.audio.equalizer.preamp; + + if (app.cfg.audio.spatial) { + try{ + CiderAudio.audioNodes.spatialNode.output.disconnect(CiderAudio.context.destination); } catch(e){} + CiderAudio.audioNodes.spatialNode.output.connect(CiderAudio.audioNodes.preampNode); + } else { + try{ + CiderAudio.audioNodes.gainNode.disconnect(CiderAudio.context.destination);} catch(e){} + CiderAudio.audioNodes.gainNode.connect(CiderAudio.audioNodes.preampNode); + } + + CiderAudio.audioNodes.preampNode.connect(CiderAudio.audioNodes.audioBands[0]); + + for (i = 1; i < BANDS.length; i ++) { + CiderAudio.audioNodes.audioBands[i-1].connect(CiderAudio.audioNodes.audioBands[i]); + } + CiderAudio.audioNodes.audioBands[BANDS.length-1].connect(CiderAudio.context.destination); } } diff --git a/src/renderer/js/WSAPI_Interop.js b/src/renderer/js/WSAPI_Interop.js new file mode 100644 index 00000000..e86327be --- /dev/null +++ b/src/renderer/js/WSAPI_Interop.js @@ -0,0 +1,108 @@ +const wsapi = { + cache: {playParams: {id: 0}, status: null, remainingTime: 0}, + playbackCache: {status: null, time: Date.now()}, + search(term, limit) { + MusicKit.getInstance().api.search(term, {limit: limit, types: 'songs,artists,albums,playlists'}).then((results)=>{ + ipcRenderer.send('wsapi-returnSearch', JSON.stringify(results)) + }) + }, + searchLibrary(term, limit) { + MusicKit.getInstance().api.library.search(term, {limit: limit, types: 'library-songs,library-artists,library-albums,library-playlists'}).then((results)=>{ + ipcRenderer.send('wsapi-returnSearchLibrary', JSON.stringify(results)) + }) + }, + getAttributes: function () { + const mk = MusicKit.getInstance(); + const nowPlayingItem = mk.nowPlayingItem; + const isPlayingExport = mk.isPlaying; + const remainingTimeExport = mk.currentPlaybackTimeRemaining; + const attributes = (nowPlayingItem != null ? nowPlayingItem.attributes : {}); + + attributes.status = isPlayingExport ? isPlayingExport : false; + attributes.name = attributes.name ? attributes.name : 'No Title Found'; + attributes.artwork = attributes.artwork ? attributes.artwork : {url: ''}; + attributes.artwork.url = attributes.artwork.url ? attributes.artwork.url : ''; + attributes.playParams = attributes.playParams ? attributes.playParams : {id: 'no-id-found'}; + attributes.playParams.id = attributes.playParams.id ? attributes.playParams.id : 'no-id-found'; + attributes.albumName = attributes.albumName ? attributes.albumName : ''; + attributes.artistName = attributes.artistName ? attributes.artistName : ''; + attributes.genreNames = attributes.genreNames ? attributes.genreNames : []; + attributes.remainingTime = remainingTimeExport ? (remainingTimeExport * 1000) : 0; + attributes.durationInMillis = attributes.durationInMillis ? attributes.durationInMillis : 0; + attributes.startTime = Date.now(); + attributes.endTime = attributes.endTime ? attributes.endTime : Date.now(); + attributes.volume = mk.volume; + attributes.shuffleMode = mk.shuffleMode; + attributes.repeatMode = mk.repeatMode; + attributes.autoplayEnabled = mk.autoplayEnabled; + return attributes + }, + moveQueueItem(oldPosition, newPosition) { + MusicKit.getInstance().queue._queueItems.splice(newPosition,0,MusicKit.getInstance().queue._queueItems.splice(oldPosition,1)[0]) + MusicKit.getInstance().queue._reindex() + }, + setAutoplay(value) { + MusicKit.getInstance().autoplayEnabled = value + }, + returnDynamic(data, type) { + ipcRenderer.send('wsapi-returnDynamic', JSON.stringify(data), type) + }, + musickitApi(method, id, params, library = false) { + if (library) { + MusicKit.getInstance().api.library[method](id, params).then((results)=>{ + ipcRenderer.send('wsapi-returnMusicKitApi', JSON.stringify(results), method) + }) + } else { + MusicKit.getInstance().api[method](id, params).then((results)=>{ + ipcRenderer.send('wsapi-returnMusicKitApi', JSON.stringify(results), method) + }) + } + }, + getPlaybackState () { + ipcRenderer.send('wsapi-updatePlaybackState', MusicKitInterop.getAttributes()); + }, + getLyrics() { + ipcRenderer.send('wsapi-returnLyrics',JSON.stringify(app.lyrics)); + }, + getQueue() { + ipcRenderer.send('wsapi-returnQueue', JSON.stringify(MusicKit.getInstance().queue)) + }, + playNext(type, id) { + var request = {} + request[type] = id + MusicKit.getInstance().playNext(request) + }, + playLater(type, id) { + var request = {} + request[type] = id + MusicKit.getInstance().playLater(request) + }, + love() { + + }, + playTrackById(id, kind = "song") { + MusicKit.getInstance().setQueue({ [kind]: id }).then(function (queue) { + MusicKit.getInstance().play() + }) + }, + quickPlay(term) { + // Quick play by song name + MusicKit.getInstance().api.search(term, { limit: 2, types: 'songs' }).then(function (data) { + MusicKit.getInstance().setQueue({ song: data["songs"][0]["id"] }).then(function (queue) { + MusicKit.getInstance().play() + }) + }) + }, + toggleShuffle() { + MusicKit.getInstance().shuffleMode = MusicKit.getInstance().shuffleMode === 0 ? 1 : 0 + }, + toggleRepeat() { + if(MusicKit.getInstance().repeatMode == 0) { + MusicKit.getInstance().repeatMode = 1 + }else if(MusicKit.getInstance().repeatMode == 1){ + MusicKit.getInstance().repeatMode = 2 + }else{ + MusicKit.getInstance().repeatMode = 0 + } + } +} \ No newline at end of file diff --git a/src/renderer/js/bootbox.min.js b/src/renderer/js/bootbox.min.js new file mode 100644 index 00000000..822080e5 --- /dev/null +++ b/src/renderer/js/bootbox.min.js @@ -0,0 +1 @@ +!function(t,e){"use strict";"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof exports?module.exports=e(require("jquery")):t.bootbox=e(t.jQuery)}(this,function e(c,p){"use strict";var r,n,i,l;Object.keys||(Object.keys=(r=Object.prototype.hasOwnProperty,n=!{toString:null}.propertyIsEnumerable("toString"),l=(i=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"]).length,function(t){if("function"!=typeof t&&("object"!=typeof t||null===t))throw new TypeError("Object.keys called on non-object");var e,o,a=[];for(e in t)r.call(t,e)&&a.push(e);if(n)for(o=0;o',header:'',footer:'',closeButton:'',form:'',button:'',option:"",promptMessage:'',inputs:{text:'',textarea:'',email:'',select:'',checkbox:'',radio:'',date:'',time:'',number:'',password:'',range:''}},b={locale:"en",backdrop:"static",animate:!0,className:null,closeButton:!0,show:!0,container:"body",value:"",inputType:"text",swapButtonOrder:!1,centerVertical:!0,multiple:!1,scrollable:!1,reusable:!1};function f(t,e,o){return c.extend(!0,{},t,function(t,e){var o=t.length,a={};if(o<1||2").attr("label",e.group)),o=i[e.group]);var a=c(d.option);a.attr("value",e.value).text(e.text),o.append(a)}),w(i,function(t,e){r.append(e)}),r.val(n.value);break;case"checkbox":var l=c.isArray(n.value)?n.value:[n.value];if(!(a=n.inputOptions||[]).length)throw new Error('prompt with "inputType" set to "checkbox" requires at least one option');r=c(''),w(a,function(t,o){if(o.value===p||o.text===p)throw new Error('each option needs a "value" property and a "text" property');var a=c(d.inputs[n.inputType]);a.find("input").attr("value",o.value),a.find("label").append("\n"+o.text),w(l,function(t,e){e===o.value&&a.find("input").prop("checked",!0)}),r.append(a)});break;case"radio":if(n.value!==p&&c.isArray(n.value))throw new Error('prompt with "inputType" set to "radio" requires a single, non-array value for "value"');if(!(a=n.inputOptions||[]).length)throw new Error('prompt with "inputType" set to "radio" requires at least one option');r=c('');var s=!0;w(a,function(t,e){if(e.value===p||e.text===p)throw new Error('each option needs a "value" property and a "text" property');var o=c(d.inputs[n.inputType]);o.find("input").attr("value",e.value),o.find("label").append("\n"+e.text),n.value!==p&&e.value===n.value&&(o.find("input").prop("checked",!0),s=!1),r.append(o)}),s&&r.find('input[type="radio"]').first().prop("checked",!0)}return o.append(r),o.on("submit",function(t){t.preventDefault(),t.stopPropagation(),e.find(".bootbox-accept").trigger("click")}),""!==c.trim(n.message)&&(a=c(d.promptMessage).html(n.message),o.prepend(a)),n.message=o,(e=u.dialog(n)).off("shown.bs.modal",g),e.on("shown.bs.modal",function(){r.focus()}),!0===t&&e.modal("show"),e},u}); \ No newline at end of file diff --git a/src/renderer/js/bootstrap.min.js b/src/renderer/js/bootstrap.min.js new file mode 100644 index 00000000..534d5334 --- /dev/null +++ b/src/renderer/js/bootstrap.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v4.0.0 (https://getbootstrap.com) + * Copyright 2011-2018 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e(t.bootstrap={},t.jQuery,t.Popper)}(this,function(t,e,n){"use strict";function i(t,e){for(var n=0;n0?i:null}catch(t){return null}},reflow:function(t){return t.offsetHeight},triggerTransitionEnd:function(n){t(n).trigger(e.end)},supportsTransitionEnd:function(){return Boolean(e)},isElement:function(t){return(t[0]||t).nodeType},typeCheckConfig:function(t,e,n){for(var s in n)if(Object.prototype.hasOwnProperty.call(n,s)){var r=n[s],o=e[s],a=o&&i.isElement(o)?"element":(l=o,{}.toString.call(l).match(/\s([a-zA-Z]+)/)[1].toLowerCase());if(!new RegExp(r).test(a))throw new Error(t.toUpperCase()+': Option "'+s+'" provided type "'+a+'" but expected type "'+r+'".')}var l}};return e=("undefined"==typeof window||!window.QUnit)&&{end:"transitionend"},t.fn.emulateTransitionEnd=n,i.supportsTransitionEnd()&&(t.event.special[i.TRANSITION_END]={bindType:e.end,delegateType:e.end,handle:function(e){if(t(e.target).is(this))return e.handleObj.handler.apply(this,arguments)}}),i}(e),L=(a="alert",h="."+(l="bs.alert"),c=(o=e).fn[a],u={CLOSE:"close"+h,CLOSED:"closed"+h,CLICK_DATA_API:"click"+h+".data-api"},f="alert",d="fade",_="show",g=function(){function t(t){this._element=t}var e=t.prototype;return e.close=function(t){t=t||this._element;var e=this._getRootElement(t);this._triggerCloseEvent(e).isDefaultPrevented()||this._removeElement(e)},e.dispose=function(){o.removeData(this._element,l),this._element=null},e._getRootElement=function(t){var e=P.getSelectorFromElement(t),n=!1;return e&&(n=o(e)[0]),n||(n=o(t).closest("."+f)[0]),n},e._triggerCloseEvent=function(t){var e=o.Event(u.CLOSE);return o(t).trigger(e),e},e._removeElement=function(t){var e=this;o(t).removeClass(_),P.supportsTransitionEnd()&&o(t).hasClass(d)?o(t).one(P.TRANSITION_END,function(n){return e._destroyElement(t,n)}).emulateTransitionEnd(150):this._destroyElement(t)},e._destroyElement=function(t){o(t).detach().trigger(u.CLOSED).remove()},t._jQueryInterface=function(e){return this.each(function(){var n=o(this),i=n.data(l);i||(i=new t(this),n.data(l,i)),"close"===e&&i[e](this)})},t._handleDismiss=function(t){return function(e){e&&e.preventDefault(),t.close(this)}},s(t,null,[{key:"VERSION",get:function(){return"4.0.0"}}]),t}(),o(document).on(u.CLICK_DATA_API,'[data-dismiss="alert"]',g._handleDismiss(new g)),o.fn[a]=g._jQueryInterface,o.fn[a].Constructor=g,o.fn[a].noConflict=function(){return o.fn[a]=c,g._jQueryInterface},g),R=(m="button",E="."+(v="bs.button"),T=".data-api",y=(p=e).fn[m],C="active",I="btn",A="focus",b='[data-toggle^="button"]',D='[data-toggle="buttons"]',S="input",w=".active",N=".btn",O={CLICK_DATA_API:"click"+E+T,FOCUS_BLUR_DATA_API:"focus"+E+T+" blur"+E+T},k=function(){function t(t){this._element=t}var e=t.prototype;return e.toggle=function(){var t=!0,e=!0,n=p(this._element).closest(D)[0];if(n){var i=p(this._element).find(S)[0];if(i){if("radio"===i.type)if(i.checked&&p(this._element).hasClass(C))t=!1;else{var s=p(n).find(w)[0];s&&p(s).removeClass(C)}if(t){if(i.hasAttribute("disabled")||n.hasAttribute("disabled")||i.classList.contains("disabled")||n.classList.contains("disabled"))return;i.checked=!p(this._element).hasClass(C),p(i).trigger("change")}i.focus(),e=!1}}e&&this._element.setAttribute("aria-pressed",!p(this._element).hasClass(C)),t&&p(this._element).toggleClass(C)},e.dispose=function(){p.removeData(this._element,v),this._element=null},t._jQueryInterface=function(e){return this.each(function(){var n=p(this).data(v);n||(n=new t(this),p(this).data(v,n)),"toggle"===e&&n[e]()})},s(t,null,[{key:"VERSION",get:function(){return"4.0.0"}}]),t}(),p(document).on(O.CLICK_DATA_API,b,function(t){t.preventDefault();var e=t.target;p(e).hasClass(I)||(e=p(e).closest(N)),k._jQueryInterface.call(p(e),"toggle")}).on(O.FOCUS_BLUR_DATA_API,b,function(t){var e=p(t.target).closest(N)[0];p(e).toggleClass(A,/^focus(in)?$/.test(t.type))}),p.fn[m]=k._jQueryInterface,p.fn[m].Constructor=k,p.fn[m].noConflict=function(){return p.fn[m]=y,k._jQueryInterface},k),j=function(t){var e="carousel",n="bs.carousel",i="."+n,o=t.fn[e],a={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0},l={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean"},h="next",c="prev",u="left",f="right",d={SLIDE:"slide"+i,SLID:"slid"+i,KEYDOWN:"keydown"+i,MOUSEENTER:"mouseenter"+i,MOUSELEAVE:"mouseleave"+i,TOUCHEND:"touchend"+i,LOAD_DATA_API:"load"+i+".data-api",CLICK_DATA_API:"click"+i+".data-api"},_="carousel",g="active",p="slide",m="carousel-item-right",v="carousel-item-left",E="carousel-item-next",T="carousel-item-prev",y={ACTIVE:".active",ACTIVE_ITEM:".active.carousel-item",ITEM:".carousel-item",NEXT_PREV:".carousel-item-next, .carousel-item-prev",INDICATORS:".carousel-indicators",DATA_SLIDE:"[data-slide], [data-slide-to]",DATA_RIDE:'[data-ride="carousel"]'},C=function(){function o(e,n){this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this._config=this._getConfig(n),this._element=t(e)[0],this._indicatorsElement=t(this._element).find(y.INDICATORS)[0],this._addEventListeners()}var C=o.prototype;return C.next=function(){this._isSliding||this._slide(h)},C.nextWhenVisible=function(){!document.hidden&&t(this._element).is(":visible")&&"hidden"!==t(this._element).css("visibility")&&this.next()},C.prev=function(){this._isSliding||this._slide(c)},C.pause=function(e){e||(this._isPaused=!0),t(this._element).find(y.NEXT_PREV)[0]&&P.supportsTransitionEnd()&&(P.triggerTransitionEnd(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},C.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config.interval&&!this._isPaused&&(this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},C.to=function(e){var n=this;this._activeElement=t(this._element).find(y.ACTIVE_ITEM)[0];var i=this._getItemIndex(this._activeElement);if(!(e>this._items.length-1||e<0))if(this._isSliding)t(this._element).one(d.SLID,function(){return n.to(e)});else{if(i===e)return this.pause(),void this.cycle();var s=e>i?h:c;this._slide(s,this._items[e])}},C.dispose=function(){t(this._element).off(i),t.removeData(this._element,n),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},C._getConfig=function(t){return t=r({},a,t),P.typeCheckConfig(e,t,l),t},C._addEventListeners=function(){var e=this;this._config.keyboard&&t(this._element).on(d.KEYDOWN,function(t){return e._keydown(t)}),"hover"===this._config.pause&&(t(this._element).on(d.MOUSEENTER,function(t){return e.pause(t)}).on(d.MOUSELEAVE,function(t){return e.cycle(t)}),"ontouchstart"in document.documentElement&&t(this._element).on(d.TOUCHEND,function(){e.pause(),e.touchTimeout&&clearTimeout(e.touchTimeout),e.touchTimeout=setTimeout(function(t){return e.cycle(t)},500+e._config.interval)}))},C._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.which){case 37:t.preventDefault(),this.prev();break;case 39:t.preventDefault(),this.next()}},C._getItemIndex=function(e){return this._items=t.makeArray(t(e).parent().find(y.ITEM)),this._items.indexOf(e)},C._getItemByDirection=function(t,e){var n=t===h,i=t===c,s=this._getItemIndex(e),r=this._items.length-1;if((i&&0===s||n&&s===r)&&!this._config.wrap)return e;var o=(s+(t===c?-1:1))%this._items.length;return-1===o?this._items[this._items.length-1]:this._items[o]},C._triggerSlideEvent=function(e,n){var i=this._getItemIndex(e),s=this._getItemIndex(t(this._element).find(y.ACTIVE_ITEM)[0]),r=t.Event(d.SLIDE,{relatedTarget:e,direction:n,from:s,to:i});return t(this._element).trigger(r),r},C._setActiveIndicatorElement=function(e){if(this._indicatorsElement){t(this._indicatorsElement).find(y.ACTIVE).removeClass(g);var n=this._indicatorsElement.children[this._getItemIndex(e)];n&&t(n).addClass(g)}},C._slide=function(e,n){var i,s,r,o=this,a=t(this._element).find(y.ACTIVE_ITEM)[0],l=this._getItemIndex(a),c=n||a&&this._getItemByDirection(e,a),_=this._getItemIndex(c),C=Boolean(this._interval);if(e===h?(i=v,s=E,r=u):(i=m,s=T,r=f),c&&t(c).hasClass(g))this._isSliding=!1;else if(!this._triggerSlideEvent(c,r).isDefaultPrevented()&&a&&c){this._isSliding=!0,C&&this.pause(),this._setActiveIndicatorElement(c);var I=t.Event(d.SLID,{relatedTarget:c,direction:r,from:l,to:_});P.supportsTransitionEnd()&&t(this._element).hasClass(p)?(t(c).addClass(s),P.reflow(c),t(a).addClass(i),t(c).addClass(i),t(a).one(P.TRANSITION_END,function(){t(c).removeClass(i+" "+s).addClass(g),t(a).removeClass(g+" "+s+" "+i),o._isSliding=!1,setTimeout(function(){return t(o._element).trigger(I)},0)}).emulateTransitionEnd(600)):(t(a).removeClass(g),t(c).addClass(g),this._isSliding=!1,t(this._element).trigger(I)),C&&this.cycle()}},o._jQueryInterface=function(e){return this.each(function(){var i=t(this).data(n),s=r({},a,t(this).data());"object"==typeof e&&(s=r({},s,e));var l="string"==typeof e?e:s.slide;if(i||(i=new o(this,s),t(this).data(n,i)),"number"==typeof e)i.to(e);else if("string"==typeof l){if("undefined"==typeof i[l])throw new TypeError('No method named "'+l+'"');i[l]()}else s.interval&&(i.pause(),i.cycle())})},o._dataApiClickHandler=function(e){var i=P.getSelectorFromElement(this);if(i){var s=t(i)[0];if(s&&t(s).hasClass(_)){var a=r({},t(s).data(),t(this).data()),l=this.getAttribute("data-slide-to");l&&(a.interval=!1),o._jQueryInterface.call(t(s),a),l&&t(s).data(n).to(l),e.preventDefault()}}},s(o,null,[{key:"VERSION",get:function(){return"4.0.0"}},{key:"Default",get:function(){return a}}]),o}();return t(document).on(d.CLICK_DATA_API,y.DATA_SLIDE,C._dataApiClickHandler),t(window).on(d.LOAD_DATA_API,function(){t(y.DATA_RIDE).each(function(){var e=t(this);C._jQueryInterface.call(e,e.data())})}),t.fn[e]=C._jQueryInterface,t.fn[e].Constructor=C,t.fn[e].noConflict=function(){return t.fn[e]=o,C._jQueryInterface},C}(e),H=function(t){var e="collapse",n="bs.collapse",i="."+n,o=t.fn[e],a={toggle:!0,parent:""},l={toggle:"boolean",parent:"(string|element)"},h={SHOW:"show"+i,SHOWN:"shown"+i,HIDE:"hide"+i,HIDDEN:"hidden"+i,CLICK_DATA_API:"click"+i+".data-api"},c="show",u="collapse",f="collapsing",d="collapsed",_="width",g="height",p={ACTIVES:".show, .collapsing",DATA_TOGGLE:'[data-toggle="collapse"]'},m=function(){function i(e,n){this._isTransitioning=!1,this._element=e,this._config=this._getConfig(n),this._triggerArray=t.makeArray(t('[data-toggle="collapse"][href="#'+e.id+'"],[data-toggle="collapse"][data-target="#'+e.id+'"]'));for(var i=t(p.DATA_TOGGLE),s=0;s0&&(this._selector=o,this._triggerArray.push(r))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}var o=i.prototype;return o.toggle=function(){t(this._element).hasClass(c)?this.hide():this.show()},o.show=function(){var e,s,r=this;if(!this._isTransitioning&&!t(this._element).hasClass(c)&&(this._parent&&0===(e=t.makeArray(t(this._parent).find(p.ACTIVES).filter('[data-parent="'+this._config.parent+'"]'))).length&&(e=null),!(e&&(s=t(e).not(this._selector).data(n))&&s._isTransitioning))){var o=t.Event(h.SHOW);if(t(this._element).trigger(o),!o.isDefaultPrevented()){e&&(i._jQueryInterface.call(t(e).not(this._selector),"hide"),s||t(e).data(n,null));var a=this._getDimension();t(this._element).removeClass(u).addClass(f),this._element.style[a]=0,this._triggerArray.length>0&&t(this._triggerArray).removeClass(d).attr("aria-expanded",!0),this.setTransitioning(!0);var l=function(){t(r._element).removeClass(f).addClass(u).addClass(c),r._element.style[a]="",r.setTransitioning(!1),t(r._element).trigger(h.SHOWN)};if(P.supportsTransitionEnd()){var _="scroll"+(a[0].toUpperCase()+a.slice(1));t(this._element).one(P.TRANSITION_END,l).emulateTransitionEnd(600),this._element.style[a]=this._element[_]+"px"}else l()}}},o.hide=function(){var e=this;if(!this._isTransitioning&&t(this._element).hasClass(c)){var n=t.Event(h.HIDE);if(t(this._element).trigger(n),!n.isDefaultPrevented()){var i=this._getDimension();if(this._element.style[i]=this._element.getBoundingClientRect()[i]+"px",P.reflow(this._element),t(this._element).addClass(f).removeClass(u).removeClass(c),this._triggerArray.length>0)for(var s=0;s0&&t(n).toggleClass(d,!i).attr("aria-expanded",i)}},i._getTargetFromElement=function(e){var n=P.getSelectorFromElement(e);return n?t(n)[0]:null},i._jQueryInterface=function(e){return this.each(function(){var s=t(this),o=s.data(n),l=r({},a,s.data(),"object"==typeof e&&e);if(!o&&l.toggle&&/show|hide/.test(e)&&(l.toggle=!1),o||(o=new i(this,l),s.data(n,o)),"string"==typeof e){if("undefined"==typeof o[e])throw new TypeError('No method named "'+e+'"');o[e]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.0.0"}},{key:"Default",get:function(){return a}}]),i}();return t(document).on(h.CLICK_DATA_API,p.DATA_TOGGLE,function(e){"A"===e.currentTarget.tagName&&e.preventDefault();var i=t(this),s=P.getSelectorFromElement(this);t(s).each(function(){var e=t(this),s=e.data(n)?"toggle":i.data();m._jQueryInterface.call(e,s)})}),t.fn[e]=m._jQueryInterface,t.fn[e].Constructor=m,t.fn[e].noConflict=function(){return t.fn[e]=o,m._jQueryInterface},m}(e),W=function(t){var e="dropdown",i="bs.dropdown",o="."+i,a=".data-api",l=t.fn[e],h=new RegExp("38|40|27"),c={HIDE:"hide"+o,HIDDEN:"hidden"+o,SHOW:"show"+o,SHOWN:"shown"+o,CLICK:"click"+o,CLICK_DATA_API:"click"+o+a,KEYDOWN_DATA_API:"keydown"+o+a,KEYUP_DATA_API:"keyup"+o+a},u="disabled",f="show",d="dropup",_="dropright",g="dropleft",p="dropdown-menu-right",m="dropdown-menu-left",v="position-static",E='[data-toggle="dropdown"]',T=".dropdown form",y=".dropdown-menu",C=".navbar-nav",I=".dropdown-menu .dropdown-item:not(.disabled)",A="top-start",b="top-end",D="bottom-start",S="bottom-end",w="right-start",N="left-start",O={offset:0,flip:!0,boundary:"scrollParent"},k={offset:"(number|string|function)",flip:"boolean",boundary:"(string|element)"},L=function(){function a(t,e){this._element=t,this._popper=null,this._config=this._getConfig(e),this._menu=this._getMenuElement(),this._inNavbar=this._detectNavbar(),this._addEventListeners()}var l=a.prototype;return l.toggle=function(){if(!this._element.disabled&&!t(this._element).hasClass(u)){var e=a._getParentFromElement(this._element),i=t(this._menu).hasClass(f);if(a._clearMenus(),!i){var s={relatedTarget:this._element},r=t.Event(c.SHOW,s);if(t(e).trigger(r),!r.isDefaultPrevented()){if(!this._inNavbar){if("undefined"==typeof n)throw new TypeError("Bootstrap dropdown require Popper.js (https://popper.js.org)");var o=this._element;t(e).hasClass(d)&&(t(this._menu).hasClass(m)||t(this._menu).hasClass(p))&&(o=e),"scrollParent"!==this._config.boundary&&t(e).addClass(v),this._popper=new n(o,this._menu,this._getPopperConfig())}"ontouchstart"in document.documentElement&&0===t(e).closest(C).length&&t("body").children().on("mouseover",null,t.noop),this._element.focus(),this._element.setAttribute("aria-expanded",!0),t(this._menu).toggleClass(f),t(e).toggleClass(f).trigger(t.Event(c.SHOWN,s))}}}},l.dispose=function(){t.removeData(this._element,i),t(this._element).off(o),this._element=null,this._menu=null,null!==this._popper&&(this._popper.destroy(),this._popper=null)},l.update=function(){this._inNavbar=this._detectNavbar(),null!==this._popper&&this._popper.scheduleUpdate()},l._addEventListeners=function(){var e=this;t(this._element).on(c.CLICK,function(t){t.preventDefault(),t.stopPropagation(),e.toggle()})},l._getConfig=function(n){return n=r({},this.constructor.Default,t(this._element).data(),n),P.typeCheckConfig(e,n,this.constructor.DefaultType),n},l._getMenuElement=function(){if(!this._menu){var e=a._getParentFromElement(this._element);this._menu=t(e).find(y)[0]}return this._menu},l._getPlacement=function(){var e=t(this._element).parent(),n=D;return e.hasClass(d)?(n=A,t(this._menu).hasClass(p)&&(n=b)):e.hasClass(_)?n=w:e.hasClass(g)?n=N:t(this._menu).hasClass(p)&&(n=S),n},l._detectNavbar=function(){return t(this._element).closest(".navbar").length>0},l._getPopperConfig=function(){var t=this,e={};return"function"==typeof this._config.offset?e.fn=function(e){return e.offsets=r({},e.offsets,t._config.offset(e.offsets)||{}),e}:e.offset=this._config.offset,{placement:this._getPlacement(),modifiers:{offset:e,flip:{enabled:this._config.flip},preventOverflow:{boundariesElement:this._config.boundary}}}},a._jQueryInterface=function(e){return this.each(function(){var n=t(this).data(i);if(n||(n=new a(this,"object"==typeof e?e:null),t(this).data(i,n)),"string"==typeof e){if("undefined"==typeof n[e])throw new TypeError('No method named "'+e+'"');n[e]()}})},a._clearMenus=function(e){if(!e||3!==e.which&&("keyup"!==e.type||9===e.which))for(var n=t.makeArray(t(E)),s=0;s0&&r--,40===e.which&&rdocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},p._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},p._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent"},f="show",d="out",_={HIDE:"hide"+o,HIDDEN:"hidden"+o,SHOW:"show"+o,SHOWN:"shown"+o,INSERTED:"inserted"+o,CLICK:"click"+o,FOCUSIN:"focusin"+o,FOCUSOUT:"focusout"+o,MOUSEENTER:"mouseenter"+o,MOUSELEAVE:"mouseleave"+o},g="fade",p="show",m=".tooltip-inner",v=".arrow",E="hover",T="focus",y="click",C="manual",I=function(){function a(t,e){if("undefined"==typeof n)throw new TypeError("Bootstrap tooltips require Popper.js (https://popper.js.org)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var I=a.prototype;return I.enable=function(){this._isEnabled=!0},I.disable=function(){this._isEnabled=!1},I.toggleEnabled=function(){this._isEnabled=!this._isEnabled},I.toggle=function(e){if(this._isEnabled)if(e){var n=this.constructor.DATA_KEY,i=t(e.currentTarget).data(n);i||(i=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(n,i)),i._activeTrigger.click=!i._activeTrigger.click,i._isWithActiveTrigger()?i._enter(null,i):i._leave(null,i)}else{if(t(this.getTipElement()).hasClass(p))return void this._leave(null,this);this._enter(null,this)}},I.dispose=function(){clearTimeout(this._timeout),t.removeData(this.element,this.constructor.DATA_KEY),t(this.element).off(this.constructor.EVENT_KEY),t(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&t(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,null!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},I.show=function(){var e=this;if("none"===t(this.element).css("display"))throw new Error("Please use show on visible elements");var i=t.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){t(this.element).trigger(i);var s=t.contains(this.element.ownerDocument.documentElement,this.element);if(i.isDefaultPrevented()||!s)return;var r=this.getTipElement(),o=P.getUID(this.constructor.NAME);r.setAttribute("id",o),this.element.setAttribute("aria-describedby",o),this.setContent(),this.config.animation&&t(r).addClass(g);var l="function"==typeof this.config.placement?this.config.placement.call(this,r,this.element):this.config.placement,h=this._getAttachment(l);this.addAttachmentClass(h);var c=!1===this.config.container?document.body:t(this.config.container);t(r).data(this.constructor.DATA_KEY,this),t.contains(this.element.ownerDocument.documentElement,this.tip)||t(r).appendTo(c),t(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new n(this.element,r,{placement:h,modifiers:{offset:{offset:this.config.offset},flip:{behavior:this.config.fallbackPlacement},arrow:{element:v},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){e._handlePopperPlacementChange(t)}}),t(r).addClass(p),"ontouchstart"in document.documentElement&&t("body").children().on("mouseover",null,t.noop);var u=function(){e.config.animation&&e._fixTransition();var n=e._hoverState;e._hoverState=null,t(e.element).trigger(e.constructor.Event.SHOWN),n===d&&e._leave(null,e)};P.supportsTransitionEnd()&&t(this.tip).hasClass(g)?t(this.tip).one(P.TRANSITION_END,u).emulateTransitionEnd(a._TRANSITION_DURATION):u()}},I.hide=function(e){var n=this,i=this.getTipElement(),s=t.Event(this.constructor.Event.HIDE),r=function(){n._hoverState!==f&&i.parentNode&&i.parentNode.removeChild(i),n._cleanTipClass(),n.element.removeAttribute("aria-describedby"),t(n.element).trigger(n.constructor.Event.HIDDEN),null!==n._popper&&n._popper.destroy(),e&&e()};t(this.element).trigger(s),s.isDefaultPrevented()||(t(i).removeClass(p),"ontouchstart"in document.documentElement&&t("body").children().off("mouseover",null,t.noop),this._activeTrigger[y]=!1,this._activeTrigger[T]=!1,this._activeTrigger[E]=!1,P.supportsTransitionEnd()&&t(this.tip).hasClass(g)?t(i).one(P.TRANSITION_END,r).emulateTransitionEnd(150):r(),this._hoverState="")},I.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},I.isWithContent=function(){return Boolean(this.getTitle())},I.addAttachmentClass=function(e){t(this.getTipElement()).addClass("bs-tooltip-"+e)},I.getTipElement=function(){return this.tip=this.tip||t(this.config.template)[0],this.tip},I.setContent=function(){var e=t(this.getTipElement());this.setElementContent(e.find(m),this.getTitle()),e.removeClass(g+" "+p)},I.setElementContent=function(e,n){var i=this.config.html;"object"==typeof n&&(n.nodeType||n.jquery)?i?t(n).parent().is(e)||e.empty().append(n):e.text(t(n).text()):e[i?"html":"text"](n)},I.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},I._getAttachment=function(t){return c[t.toUpperCase()]},I._setListeners=function(){var e=this;this.config.trigger.split(" ").forEach(function(n){if("click"===n)t(e.element).on(e.constructor.Event.CLICK,e.config.selector,function(t){return e.toggle(t)});else if(n!==C){var i=n===E?e.constructor.Event.MOUSEENTER:e.constructor.Event.FOCUSIN,s=n===E?e.constructor.Event.MOUSELEAVE:e.constructor.Event.FOCUSOUT;t(e.element).on(i,e.config.selector,function(t){return e._enter(t)}).on(s,e.config.selector,function(t){return e._leave(t)})}t(e.element).closest(".modal").on("hide.bs.modal",function(){return e.hide()})}),this.config.selector?this.config=r({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},I._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},I._enter=function(e,n){var i=this.constructor.DATA_KEY;(n=n||t(e.currentTarget).data(i))||(n=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(i,n)),e&&(n._activeTrigger["focusin"===e.type?T:E]=!0),t(n.getTipElement()).hasClass(p)||n._hoverState===f?n._hoverState=f:(clearTimeout(n._timeout),n._hoverState=f,n.config.delay&&n.config.delay.show?n._timeout=setTimeout(function(){n._hoverState===f&&n.show()},n.config.delay.show):n.show())},I._leave=function(e,n){var i=this.constructor.DATA_KEY;(n=n||t(e.currentTarget).data(i))||(n=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(i,n)),e&&(n._activeTrigger["focusout"===e.type?T:E]=!1),n._isWithActiveTrigger()||(clearTimeout(n._timeout),n._hoverState=d,n.config.delay&&n.config.delay.hide?n._timeout=setTimeout(function(){n._hoverState===d&&n.hide()},n.config.delay.hide):n.hide())},I._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},I._getConfig=function(n){return"number"==typeof(n=r({},this.constructor.Default,t(this.element).data(),n)).delay&&(n.delay={show:n.delay,hide:n.delay}),"number"==typeof n.title&&(n.title=n.title.toString()),"number"==typeof n.content&&(n.content=n.content.toString()),P.typeCheckConfig(e,n,this.constructor.DefaultType),n},I._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},I._cleanTipClass=function(){var e=t(this.getTipElement()),n=e.attr("class").match(l);null!==n&&n.length>0&&e.removeClass(n.join(""))},I._handlePopperPlacementChange=function(t){this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},I._fixTransition=function(){var e=this.getTipElement(),n=this.config.animation;null===e.getAttribute("x-placement")&&(t(e).removeClass(g),this.config.animation=!1,this.hide(),this.show(),this.config.animation=n)},a._jQueryInterface=function(e){return this.each(function(){var n=t(this).data(i),s="object"==typeof e&&e;if((n||!/dispose|hide/.test(e))&&(n||(n=new a(this,s),t(this).data(i,n)),"string"==typeof e)){if("undefined"==typeof n[e])throw new TypeError('No method named "'+e+'"');n[e]()}})},s(a,null,[{key:"VERSION",get:function(){return"4.0.0"}},{key:"Default",get:function(){return u}},{key:"NAME",get:function(){return e}},{key:"DATA_KEY",get:function(){return i}},{key:"Event",get:function(){return _}},{key:"EVENT_KEY",get:function(){return o}},{key:"DefaultType",get:function(){return h}}]),a}();return t.fn[e]=I._jQueryInterface,t.fn[e].Constructor=I,t.fn[e].noConflict=function(){return t.fn[e]=a,I._jQueryInterface},I}(e),x=function(t){var e="popover",n="bs.popover",i="."+n,o=t.fn[e],a=new RegExp("(^|\\s)bs-popover\\S+","g"),l=r({},U.Default,{placement:"right",trigger:"click",content:"",template:''}),h=r({},U.DefaultType,{content:"(string|element|function)"}),c="fade",u="show",f=".popover-header",d=".popover-body",_={HIDE:"hide"+i,HIDDEN:"hidden"+i,SHOW:"show"+i,SHOWN:"shown"+i,INSERTED:"inserted"+i,CLICK:"click"+i,FOCUSIN:"focusin"+i,FOCUSOUT:"focusout"+i,MOUSEENTER:"mouseenter"+i,MOUSELEAVE:"mouseleave"+i},g=function(r){var o,g;function p(){return r.apply(this,arguments)||this}g=r,(o=p).prototype=Object.create(g.prototype),o.prototype.constructor=o,o.__proto__=g;var m=p.prototype;return m.isWithContent=function(){return this.getTitle()||this._getContent()},m.addAttachmentClass=function(e){t(this.getTipElement()).addClass("bs-popover-"+e)},m.getTipElement=function(){return this.tip=this.tip||t(this.config.template)[0],this.tip},m.setContent=function(){var e=t(this.getTipElement());this.setElementContent(e.find(f),this.getTitle());var n=this._getContent();"function"==typeof n&&(n=n.call(this.element)),this.setElementContent(e.find(d),n),e.removeClass(c+" "+u)},m._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},m._cleanTipClass=function(){var e=t(this.getTipElement()),n=e.attr("class").match(a);null!==n&&n.length>0&&e.removeClass(n.join(""))},p._jQueryInterface=function(e){return this.each(function(){var i=t(this).data(n),s="object"==typeof e?e:null;if((i||!/destroy|hide/.test(e))&&(i||(i=new p(this,s),t(this).data(n,i)),"string"==typeof e)){if("undefined"==typeof i[e])throw new TypeError('No method named "'+e+'"');i[e]()}})},s(p,null,[{key:"VERSION",get:function(){return"4.0.0"}},{key:"Default",get:function(){return l}},{key:"NAME",get:function(){return e}},{key:"DATA_KEY",get:function(){return n}},{key:"Event",get:function(){return _}},{key:"EVENT_KEY",get:function(){return i}},{key:"DefaultType",get:function(){return h}}]),p}(U);return t.fn[e]=g._jQueryInterface,t.fn[e].Constructor=g,t.fn[e].noConflict=function(){return t.fn[e]=o,g._jQueryInterface},g}(e),K=function(t){var e="scrollspy",n="bs.scrollspy",i="."+n,o=t.fn[e],a={offset:10,method:"auto",target:""},l={offset:"number",method:"string",target:"(string|element)"},h={ACTIVATE:"activate"+i,SCROLL:"scroll"+i,LOAD_DATA_API:"load"+i+".data-api"},c="dropdown-item",u="active",f={DATA_SPY:'[data-spy="scroll"]',ACTIVE:".active",NAV_LIST_GROUP:".nav, .list-group",NAV_LINKS:".nav-link",NAV_ITEMS:".nav-item",LIST_ITEMS:".list-group-item",DROPDOWN:".dropdown",DROPDOWN_ITEMS:".dropdown-item",DROPDOWN_TOGGLE:".dropdown-toggle"},d="offset",_="position",g=function(){function o(e,n){var i=this;this._element=e,this._scrollElement="BODY"===e.tagName?window:e,this._config=this._getConfig(n),this._selector=this._config.target+" "+f.NAV_LINKS+","+this._config.target+" "+f.LIST_ITEMS+","+this._config.target+" "+f.DROPDOWN_ITEMS,this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,t(this._scrollElement).on(h.SCROLL,function(t){return i._process(t)}),this.refresh(),this._process()}var g=o.prototype;return g.refresh=function(){var e=this,n=this._scrollElement===this._scrollElement.window?d:_,i="auto"===this._config.method?n:this._config.method,s=i===_?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),t.makeArray(t(this._selector)).map(function(e){var n,r=P.getSelectorFromElement(e);if(r&&(n=t(r)[0]),n){var o=n.getBoundingClientRect();if(o.width||o.height)return[t(n)[i]().top+s,r]}return null}).filter(function(t){return t}).sort(function(t,e){return t[0]-e[0]}).forEach(function(t){e._offsets.push(t[0]),e._targets.push(t[1])})},g.dispose=function(){t.removeData(this._element,n),t(this._scrollElement).off(i),this._element=null,this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},g._getConfig=function(n){if("string"!=typeof(n=r({},a,n)).target){var i=t(n.target).attr("id");i||(i=P.getUID(e),t(n.target).attr("id",i)),n.target="#"+i}return P.typeCheckConfig(e,n,l),n},g._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},g._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},g._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},g._process=function(){var t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),n=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=n){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(var s=this._offsets.length;s--;){this._activeTarget!==this._targets[s]&&t>=this._offsets[s]&&("undefined"==typeof this._offsets[s+1]||t li > .active",g='[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',p=".dropdown-toggle",m="> .dropdown-menu .active",v=function(){function n(t){this._element=t}var i=n.prototype;return i.show=function(){var e=this;if(!(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&t(this._element).hasClass(a)||t(this._element).hasClass(l))){var n,i,s=t(this._element).closest(f)[0],o=P.getSelectorFromElement(this._element);if(s){var h="UL"===s.nodeName?_:d;i=(i=t.makeArray(t(s).find(h)))[i.length-1]}var c=t.Event(r.HIDE,{relatedTarget:this._element}),u=t.Event(r.SHOW,{relatedTarget:i});if(i&&t(i).trigger(c),t(this._element).trigger(u),!u.isDefaultPrevented()&&!c.isDefaultPrevented()){o&&(n=t(o)[0]),this._activate(this._element,s);var g=function(){var n=t.Event(r.HIDDEN,{relatedTarget:e._element}),s=t.Event(r.SHOWN,{relatedTarget:i});t(i).trigger(n),t(e._element).trigger(s)};n?this._activate(n,n.parentNode,g):g()}}},i.dispose=function(){t.removeData(this._element,e),this._element=null},i._activate=function(e,n,i){var s=this,r=("UL"===n.nodeName?t(n).find(_):t(n).children(d))[0],o=i&&P.supportsTransitionEnd()&&r&&t(r).hasClass(h),a=function(){return s._transitionComplete(e,r,i)};r&&o?t(r).one(P.TRANSITION_END,a).emulateTransitionEnd(150):a()},i._transitionComplete=function(e,n,i){if(n){t(n).removeClass(c+" "+a);var s=t(n.parentNode).find(m)[0];s&&t(s).removeClass(a),"tab"===n.getAttribute("role")&&n.setAttribute("aria-selected",!1)}if(t(e).addClass(a),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!0),P.reflow(e),t(e).addClass(c),e.parentNode&&t(e.parentNode).hasClass(o)){var r=t(e).closest(u)[0];r&&t(r).find(p).addClass(a),e.setAttribute("aria-expanded",!0)}i&&i()},n._jQueryInterface=function(i){return this.each(function(){var s=t(this),r=s.data(e);if(r||(r=new n(this),s.data(e,r)),"string"==typeof i){if("undefined"==typeof r[i])throw new TypeError('No method named "'+i+'"');r[i]()}})},s(n,null,[{key:"VERSION",get:function(){return"4.0.0"}}]),n}();return t(document).on(r.CLICK_DATA_API,g,function(e){e.preventDefault(),v._jQueryInterface.call(t(this),"show")}),t.fn.tab=v._jQueryInterface,t.fn.tab.Constructor=v,t.fn.tab.noConflict=function(){return t.fn.tab=i,v._jQueryInterface},v}(e);!function(t){if("undefined"==typeof t)throw new TypeError("Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.");var e=t.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1===e[0]&&9===e[1]&&e[2]<1||e[0]>=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}(e),t.Util=P,t.Alert=L,t.Button=R,t.Carousel=j,t.Collapse=H,t.Dropdown=W,t.Modal=M,t.Popover=x,t.Scrollspy=K,t.Tab=V,t.Tooltip=U,Object.defineProperty(t,"__esModule",{value:!0})}); +//# sourceMappingURL=bootstrap.min.js.map \ No newline at end of file diff --git a/src/renderer/js/jquery-3.2.1.slim.min.js b/src/renderer/js/jquery-3.2.1.slim.min.js new file mode 100644 index 00000000..105d00e6 --- /dev/null +++ b/src/renderer/js/jquery-3.2.1.slim.min.js @@ -0,0 +1,4 @@ +/*! jQuery v3.2.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/parseXML,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-event/ajax,-effects,-effects/Tween,-effects/animatedSelector | (c) JS Foundation and other contributors | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/parseXML,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-event/ajax,-effects,-effects/Tween,-effects/animatedSelector",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a); +}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S),a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,"",""],thead:[1,"",""],col:[2,"",""],tr:[2,"",""],td:[3,"",""],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="x",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/ + + \ No newline at end of file diff --git a/src/renderer/views/components/cider-modal.ejs b/src/renderer/views/components/cider-modal.ejs new file mode 100644 index 00000000..e7bebd90 --- /dev/null +++ b/src/renderer/views/components/cider-modal.ejs @@ -0,0 +1,82 @@ + + + \ No newline at end of file diff --git a/src/renderer/views/components/equalizer.ejs b/src/renderer/views/components/equalizer.ejs new file mode 100644 index 00000000..fca497dd --- /dev/null +++ b/src/renderer/views/components/equalizer.ejs @@ -0,0 +1,387 @@ + + + \ No newline at end of file diff --git a/src/renderer/views/components/fullscreen.ejs b/src/renderer/views/components/fullscreen.ejs index 796eb955..e35c760b 100644 --- a/src/renderer/views/components/fullscreen.ejs +++ b/src/renderer/views/components/fullscreen.ejs @@ -26,7 +26,7 @@ {{ app.mk.nowPlayingItem["attributes"]["name"] }} + style="display: inline-block; -webkit-box-orient: horizontal; white-space: nowrap; margin-top: 0.25vh; overflow: hidden;"> {{ app.mk.nowPlayingItem["attributes"]["artistName"] }} @@ -71,17 +71,17 @@ - - - - + + diff --git a/src/renderer/views/components/libraryartist-item.ejs b/src/renderer/views/components/libraryartist-item.ejs index 34ca1b3b..817cf623 100644 --- a/src/renderer/views/components/libraryartist-item.ejs +++ b/src/renderer/views/components/libraryartist-item.ejs @@ -2,28 +2,20 @@ + :class="{'mediaitem-selected': app.select_hasMediaItem(guid)}" + @contextmenu="contextMenu"> - {{ item.attributes.name }} - - - - {{ item.attributes.artistName }} - - - @@ -62,22 +54,19 @@ return minutes + ":" + (seconds < 10 ? '0' : '') + seconds; }, getDataType() { - if (this.item.attributes.playParams.isLibrary) { - return this.item.type - } else { - return this.item.attributes.playParams.kind - } + return this.item.type }, async select(e) { let u = this.item - let u1 = await app.mk.api.library.artistRelationship(u.id,"albums", - {platform: "web", - "include[library-albums]": "artists,tracks", - "include[library-artists]": "catalog", - "fields[artists]": "url", - "includeOnly": "catalog,artists"} - ) - app.showCollection({data : Object.assign({},u1)}, u.attributes.name?? '', ''); + let u1 = await app.mk.api.v3.music(`/v1/me/library/artists/${u.id}/albums`, { + "platform": "web", + "include[library-albums]": "artists,tracks", + "include[library-artists]": "catalog", + "fields[artists]": "url", + "includeOnly": "catalog,artists" + }) + app.showCollection({data : Object.assign({},u1.data.data)}, u.attributes.name?? '', ''); + app.select_selectMediaItem(u.id, this.getDataType(), this.index, this.guid, true) }, getArtwork(){ let u = "" @@ -87,80 +76,38 @@ return u; }, contextMenu(event) { + let self = this let data_type = this.getDataType() - let item_id = this.item.attributes.playParams.id ?? this.item.id - let isLibrary = this.item.attributes.playParams.isLibrary ?? false + + let item = self.item + item.attributes.artistName = item.attributes.name; let useMenu = "normal" if (app.selectedMediaItems.length <= 1) { app.selectedMediaItems = [] - app.select_selectMediaItem(item_id, data_type, this.index, this.guid, isLibrary) + app.select_selectMediaItem(this.item.id, data_type, this.index, this.guid, true) } else { useMenu = "multiple" } + let menus = { multiple: { - items: [ - { - "name": "Add to Playlist...", - "action": function () { - app.promptAddToPlaylist() - } - }, - { - name: `Play ${app.selectedMediaItems.length} tracks next`, - action: () => { - let itemsToPlay = {} - app.selectedMediaItems.forEach(item => { - if (!itemsToPlay[item.kind]) { - itemsToPlay[item.kind] = [] - } - itemsToPlay[item.kind].push(item.id) - }) - // loop through itemsToPlay - for (let kind in itemsToPlay) { - let ids = itemsToPlay[kind] - if (ids.length > 0) { - app.mk.playNext({[kind + "s"]: itemsToPlay[kind]}) - } - } - console.log(itemsToPlay) - app.selectedMediaItems = [] - } - }, - { - name: `Play ${app.selectedMediaItems.length} tracks later`, - action: () => { - let itemsToPlay = {} - app.selectedMediaItems.forEach(item => { - if (!itemsToPlay[item.kind]) { - itemsToPlay[item.kind] = [] - } - itemsToPlay[item.kind].push(item.id) - }) - // loop through itemsToPlay - for (let kind in itemsToPlay) { - let ids = itemsToPlay[kind] - if (ids.length > 0) { - app.mk.playLater({[kind + "s"]: itemsToPlay[kind]}) - } - } - app.selectedMediaItems = [] - } - }, - ] + items: [] // }, normal: { items: [ { - "name": "Add to Playlist...", + "name": app.getLz('action.goToArtist'), + "icon": "./assets/feather/user.svg", "action": function () { - app.promptAddToPlaylist() + app.searchAndNavigate(self.item, 'artist') + console.log(self.item) } }, { - "name": "Start Radio", + "icon": "./assets/feather/radio.svg", + "name": app.getLz('action.startRadio'), "action": function () { app.mk.setStationQueue({song: self.item.attributes.playParams.id ?? self.item.id}).then(() => { app.mk.play() @@ -169,31 +116,15 @@ } }, { - "name": "Play Next", + "icon": "./assets/feather/share.svg", + "name": app.getLz('action.share'), "action": function () { - app.mk.playNext({[self.item.attributes.playParams.kind ?? self.item.type]: self.item.attributes.playParams.id ?? self.item.id}) - app.mk.queue._reindex() - app.selectedMediaItems = [] - } - }, - { - "name": "Play Later", - "action": function () { - app.mk.playLater({[self.item.attributes.playParams.kind ?? self.item.type]: self.item.attributes.playParams.id ?? self.item.id}) - app.mk.queue._reindex() - app.selectedMediaItems = [] - } - }, - { - "name": "Go to Artist", - "action": function () { - app.searchAndNavigate(self.item, 'artist') - } - }, - { - "name": "Go to Album", - "action": function () { - app.searchAndNavigate(self.item, 'album') + if (!self.item.attributes.url && self.item.relationships){ + if (self.item.relationships.catalog){ + app.mkapi(self.item.attributes.playParams.kind, false, self.item.relationships.catalog.data[0].id).then(u => {self.app.copyToClipboard((u.data.data.length && u.data.data.length > 0)? u.data.data[0].attributes.url : u.data.data.attributes.url)}) + } + } else { + self.app.copyToClipboard(self.item.attributes.url)} } }, ] @@ -208,7 +139,9 @@ menus.multiple.items = menus.multiple.items.concat(this.contextExt.multiple) } } - CiderContextMenu.Create(event, menus[useMenu]) + //CiderContextMenu.Create(event, menus[useMenu]); // Depreciated Context Menu + app.showMenuPanel(menus[useMenu], event); + }, visibilityChanged: function (isVisible, entry) { this.isVisible = isVisible diff --git a/src/renderer/views/components/listennow-child.ejs b/src/renderer/views/components/listennow-child.ejs index d6c51187..cf85cae7 100644 --- a/src/renderer/views/components/listennow-child.ejs +++ b/src/renderer/views/components/listennow-child.ejs @@ -6,7 +6,7 @@ {{ recom.attributes.title ? recom.attributes.title.stringForDisplay : ""}} - See All + {{app.getLz('term.seeAll')}} diff --git a/src/renderer/views/components/lyrics-view.ejs b/src/renderer/views/components/lyrics-view.ejs index 9576486d..3047bde6 100644 --- a/src/renderer/views/components/lyrics-view.ejs +++ b/src/renderer/views/components/lyrics-view.ejs @@ -35,7 +35,7 @@ - Loading... / Lyrics not found./ Instrumental. + {{app.getLz('term.noLyrics')}} diff --git a/src/renderer/views/components/mediaitem-artwork.ejs b/src/renderer/views/components/mediaitem-artwork.ejs index 7e169943..e39a621b 100644 --- a/src/renderer/views/components/mediaitem-artwork.ejs +++ b/src/renderer/views/components/mediaitem-artwork.ejs @@ -1,10 +1,10 @@ + + \ No newline at end of file diff --git a/src/renderer/views/components/qrcode-modal.ejs b/src/renderer/views/components/qrcode-modal.ejs new file mode 100644 index 00000000..efd4bd38 --- /dev/null +++ b/src/renderer/views/components/qrcode-modal.ejs @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/src/renderer/views/components/queue-item.ejs b/src/renderer/views/components/queue-item.ejs index f41a01ba..0dd56c64 100644 --- a/src/renderer/views/components/queue-item.ejs +++ b/src/renderer/views/components/queue-item.ejs @@ -48,7 +48,7 @@ let self = this CiderContextMenu.Create(event, { items: [{ - "name": "Remove from queue", + "name": $root.getLz('action.removeFromQueue'), "action": function () { } diff --git a/src/renderer/views/components/queue.ejs b/src/renderer/views/components/queue.ejs index 2b30c3ea..6e8d657c 100644 --- a/src/renderer/views/components/queue.ejs +++ b/src/renderer/views/components/queue.ejs @@ -2,10 +2,10 @@ - Queue + {{app.getLz('term.queue')}} - ∞ + @@ -33,7 +33,7 @@ @@ -76,7 +76,7 @@ let menus = { single: { items: [{ - "name": "Remove from queue", + "name": app.getLz('action.removeFromQueue'), "action": function () { self.queueItems.splice(position, 1) app.mk.queue._queueItems = self.queueItems; @@ -84,7 +84,7 @@ } }, { - "name": "Start Radio", + "name": app.getLz('action.startRadio'), "action": function () { app.mk.setStationQueue({ song: item.attributes.playParams.id ?? item.id @@ -97,7 +97,7 @@ }, multiple: { items: [{ - "name": `Remove ${self.selectedItems.length} tracks from queue`, + "name": app.getLz('action.removeTracks'), "action": function () { // add property to items to be removed self.selectedItems.forEach(function (item) { diff --git a/src/renderer/views/components/sidebar-playlist.ejs b/src/renderer/views/components/sidebar-playlist.ejs index 6756288a..83df7072 100644 --- a/src/renderer/views/components/sidebar-playlist.ejs +++ b/src/renderer/views/components/sidebar-playlist.ejs @@ -7,7 +7,7 @@ @dragover="dragOver" @drop="onDrop" :href="item.href" - @click='item.type != "library-playlist-folders" ? openPlaylist(item) : getPlaylistChildren(item)'> + @click='clickEvent()'> {{ item.attributes.name }} @@ -15,7 +15,7 @@ - + @@ -31,6 +31,10 @@ item: { type: Object, required: true + }, + playlistSelect: { + type: Function, + required: false } }, data: function () { @@ -50,6 +54,17 @@ } }, methods: { + clickEvent() { + if(this.item.type != "library-playlist-folders") { + if(this.playlistSelect) { + this.playlistSelect(this.item) + }else{ + this.openPlaylist(this.item) + } + }else{ + this.getPlaylistChildren(this.item) + } + }, rename() { this.renaming = false @@ -105,7 +120,7 @@ let menu = { items: { "moveToParent": { - name: "Move to top", + name: this.$root.getLz('action.moveToTop'), action: () => { let self = this this.move(this.item, { @@ -116,7 +131,7 @@ } }, "rename": { - name: "Rename", + name: this.$root.getLz('action.rename'), action: () => { this.renaming = true setTimeout(()=>{ @@ -126,13 +141,13 @@ } }, "deleteFromPlaylist": { - name: "Delete from library", + name: this.$root.getLz('action.removeFromLibrary'), action: () => { this.$root.deletePlaylist(playlist_id) } }, "addToFavorites": { - name: "Add to favorites", + name: this.$root.getLz('action.addToFavorites'), disabled: true, hidden: true, action: () => { @@ -181,7 +196,9 @@ this.children = [] this.getChildren() this.toggleFolder() - this.$root.mk.api.library.playlistFolderChildren(item.id).then(children => { + + this.$root.mk.api.v3.music(`v1/me/library/playlist-folders/${item.id}/children`).then(data => { + let children = data.data.data; children.forEach(child => { if(!self.$root.playlists.listing.find(listing => listing.id == child.id)) { child.parent = self.item.id diff --git a/src/renderer/views/components/spatial-properties.ejs b/src/renderer/views/components/spatial-properties.ejs index 4627f8f6..93613e05 100644 --- a/src/renderer/views/components/spatial-properties.ejs +++ b/src/renderer/views/components/spatial-properties.ejs @@ -2,15 +2,15 @@ - Spatial Properties + {{$root.getLz('spatial.spatialProperties')}} - Room Dimensions + {{$root.getLz('spatial.roomDimensions')}} - Set Positions + {{$root.getLz('spatial.setPositions')}} @@ -18,7 +18,7 @@ - Width + {{$root.getLz('spatial.width')}} - Height + {{$root.getLz('spatial.height')}} - Depth + {{$root.getLz('spatial.depth')}} - Gain + {{$root.getLz('spatial.gain')}} @@ -73,9 +73,9 @@ - Room Positions + {{$root.getLz('spatial.roomPositions')}} - Set Dimensions + {{$root.getLz('spatial.setDimensions')}} @@ -83,7 +83,7 @@ - X (Listener) + X ({{$root.getLz('spatial.listener')}}) - Y (Listener) + Y ({{$root.getLz('spatial.listener')}}) - Z (Listener) + Z ({{$root.getLz('spatial.listener')}}) - X (Audio Source) + X ({{$root.getLz('spatial.audioSource')}}) - Y (Audio Source) + Y ({{$root.getLz('spatial.audioSource')}}) - Z (Audio Source) + Z ({{$root.getLz('spatial.audioSource')}}) - Room Materials + {{$root.getLz('spatial.roomMaterials')}} - Up + {{$root.getLz('spatial.up')}} {{ prop }} @@ -197,7 +197,7 @@ - Left + {{$root.getLz('spatial.left')}} {{ prop }} @@ -206,14 +206,14 @@ - Front + {{$root.getLz('spatial.front')}} {{ prop }} - Back + {{$root.getLz('spatial.back')}} {{ prop }} @@ -222,7 +222,7 @@ - Right + {{$root.getLz('spatial.right')}} {{ prop }} @@ -234,7 +234,7 @@ - Down + {{$root.getLz('spatial.down')}} {{ prop }} diff --git a/src/renderer/views/main.ejs b/src/renderer/views/main.ejs index c5f5466b..b153e0d5 100644 --- a/src/renderer/views/main.ejs +++ b/src/renderer/views/main.ejs @@ -16,17 +16,20 @@ Cider - - - - - - - - + + + + + + + + + + + @@ -38,9 +41,10 @@ - + - + @@ -67,9 +71,9 @@ - - @@ -83,29 +87,38 @@ + :class="[isElementOverflowing('#app-main > div.app-chrome > div.app-chrome--center > div > div > div.playback-info > div.song-name') ? 'marquee' : '']" + :style="[mk.nowPlayingItem['attributes']['contentRating'] == 'explicit' ? {'margin-left' : '23px'} : {'margin-left' : '0px'} ]"> {{ mk.nowPlayingItem["attributes"]["name"] }} - + - + - {{ mk.nowPlayingItem["attributes"]["artistName"] }} - + {{"—"}} {{(mk.nowPlayingItem["attributes"]["albumName"]) ? (mk.nowPlayingItem["attributes"]["albumName"]) : "" }} + - {{ convertToMins(getSongProgress()) }} - {{ convertToMins(mk.currentPlaybackDuration) }} + {{ convertToMins(mk.currentPlaybackDuration) }} + - - ❤️ + + + + - 🖤 @@ -129,34 +142,40 @@ - - + + <%- include("svg/cast.svg") %> + + + - - - + + + - + - + @@ -169,11 +188,9 @@ {search.showHints = false}, 300)" - v-on:keyup.enter="searchQuery();search.showHints = false" - @change="showSearch();" @input="getSearchHints()" placeholder="Search..." - v-model="search.term" - ref="searchInput" - class="search-input"> + v-on:keyup.enter="searchQuery();search.showHints = false" @change="showSearch();" + @input="getSearchHints()" :placeholder="$root.getLz('term.search') + '...'" v-model="search.term" + ref="searchInput" class="search-input"> @@ -186,40 +203,53 @@ - Apple Music + Cider - - - - + + - Library + {{$root.getLz('term.appleMusic')}} - + + + + + + {{$root.getLz('term.library')}} + + - - - + + + - Playlists + {{ $root.getLz('term.playlists') }} - + + - + - Show Personal Info + {{$root.getLz('action.showWebRemoteQR')}} + + + + + + + {{$root.getLz('settings.option.visual.showPersonalInfo')}} ✔️ @@ -229,31 +259,35 @@ - Private Session + {{$root.getLz('term.privateSession')}} ✔️ + + {{$root.getLz('term.equalizer')}} + - Spatialized Audio Settings + {{$root.getLz('term.spacializedAudioSetting')}} - Account Settings + {{$root.getLz('term.accountSettings')}} - About + {{$root.getLz('term.about')}} - Discord + {{$root.getLz('term.discord')}} - Settings + {{$root.getLz('term.settings')}} - - Sign Out + + {{$root.getLz('term.logout')}} @@ -283,23 +317,23 @@ - - + - - + + - {chrome.menuOpened = false}, 100)" @click="(chrome.userinfo.id) ? chrome.menuOpened = !chrome.menuOpened : false"> @@ -308,9 +342,11 @@ - {{ chrome.userinfo.attributes.name }} + {{ chrome.userinfo.attributes.name + }} - @{{ chrome.userinfo.attributes.handle + {{ + chrome.userinfo.attributes.handle }} @@ -325,18 +361,36 @@ - - {{ library.downloadNotification.message }} ({{ - library.downloadNotification.progress }} / {{ library.downloadNotification.total }}) + + {{ library.backgroundNotification.message }} ({{ + library.backgroundNotification.progress }} / {{ library.backgroundNotification.total }}) - <%- include('svg/chevron-left.svg') %> - <%- include('svg/chevron-right.svg') %> + + <%- include('svg/chevron-left.svg') %> + + + <%- include('svg/chevron-right.svg') %> + + + + + + + + + + + + + + + @@ -407,53 +461,53 @@ + + Subtext + + + + + {{$root.getLz('term.listenNow')}} + + 990kbps + + Audio Quality Settings + + + Toggle Drawer + Button + Button + --> @@ -467,8 +521,8 @@ - Radio - Recent Stations + {{$root.getLz('term.radio')}} + {{$root.getLz('term.recentStations')}} @@ -522,7 +576,8 @@ - + @@ -533,7 +588,7 @@ :richlyrics="richlyrics"> @@ -543,25 +598,28 @@ - - - + + + + + + + - - - + + @@ -573,6 +631,12 @@ + + + + + + @@ -585,9 +649,11 @@ {{((lyricon) ? ((lyrics.length > 0 && lyrics[currentLyricsLine] && lyrics[currentLyricsLine].line ) ? - lyrics[currentLyricsLine].line.replace('lrcInstrumental','') : "") : '') + ((lyricon) ? ((lyrics.length + lyrics[currentLyricsLine].line.replace('lrcInstrumental','') : "") : '') + ((lyricon) ? + ((lyrics.length > 0 && lyrics[currentLyricsLine] && lyrics[currentLyricsLine].line ) ? - (lyrics[currentLyricsLine].translation ? ('\n\r' + lyrics[currentLyricsLine].translation) : ""): "") : + (lyrics[currentLyricsLine].translation ? ('\n\r' + lyrics[currentLyricsLine].translation) : ""): "") + : '')}} + + +<%- include('pages/podcasts') %> <%- include('pages/apple-account-settings') %> @@ -648,12 +717,17 @@ <%- include('pages/about') %> - - + +<%- include('components/artwork-material') %> <%- include('components/menu-panel') %> -<%- include('components/sidebar-playlist') %> +<%- include('components/sidebar-playlist') +%> -<%- include('components/spatial-properties') %> +<%- include('components/spatial-properties') +%> + +<%- include('components/qrcode-modal') +%> + +<%- include('components/equalizer') +%> -<%- include('components/add-to-playlist') %> +<%- include('components/add-to-playlist') +%> -<%- include('components/queue') %> +<%- include('components/queue') +%> -<%- include('components/queue-item') %> +<%- include('components/queue-item') +%> -<%- include('components/mediaitem-scroller-horizontal') %> +<%- include('components/mediaitem-scroller-horizontal') +%> -<%- include('components/mediaitem-scroller-horizontal-large') %> +<%- include('components/mediaitem-scroller-horizontal-large') +%> -<%- include('components/mediaitem-scroller-horizontal-sp') %> +<%- include('components/mediaitem-scroller-horizontal-sp') +%> -<%- include('components/mediaitem-scroller-horizontal-mvview') %> +<%- include('components/mediaitem-scroller-horizontal-mvview') +%> -<%- include('components/mediaitem-list-item') %> +<%- include('components/mediaitem-list-item') +%> -<%- include('components/mediaitem-hrect') %> +<%- include('components/mediaitem-hrect') +%> -<%- include('components/mediaitem-square') %> +<%- include('components/mediaitem-square') +%> -<%- include('components/mediaitem-square-sp') %> +<%- include('components/mediaitem-square-sp') +%> -<%- include('components/mediaitem-mvview') %> +<%- include('components/mediaitem-mvview') +%> -<%- include('components/libraryartist-item') %> -<%- include('components/listennow-child') %> +<%- include('components/libraryartist-item') +%> +<%- include('components/listennow-child') +%> -<%- include('components/mediaitem-mvview-sp') %> +<%- include('components/mediaitem-mvview-sp') +%> -<%- include('components/animatedartwork-view') %> +<%- include('components/animatedartwork-view') +%> -<%- include('components/lyrics-view') %> +<%- include('components/lyrics-view') +%> -<%- include('components/fullscreen') %> +<%- include('components/fullscreen') +%> - + +<%- include('components/miniplayer') +%> + + - - - + + + + +
{{ convertToMins(getSongProgress()) }}
{{ convertToMins(mk.currentPlaybackDuration) }}
{{ convertToMins(mk.currentPlaybackDuration) }} +