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