diff --git a/.config/example.yml b/.config/example.yml new file mode 100644 index 0000000000..0e167ccb77 --- /dev/null +++ b/.config/example.yml @@ -0,0 +1,57 @@ +# サーバーのメンテナ情報 +maintainer: + # メンテナの名前 + name: + + # メンテナの連絡先(URLかmailto形式のURL) + url: + +# (Misskeyを動かす)URL +url: + +# 待受ポート +port: + +# TLSの設定(利用しない場合は省略可能) +https: + # 証明書のパス... + key: + cert: + +# MongoDBの設定 +mongodb: + host: localhost + port: 27017 + db: misskey + user: + pass: + +# Redisの設定 +redis: + host: localhost + port: 6379 + pass: + +# reCAPTCHAの設定 +recaptcha: + site_key: + secret_key: + +# ServiceWrokerの設定 +sw: + # VAPIDの公開鍵 + public_key: + + # VAPIDの秘密鍵 + private_key: + +# Google Maps API +google_maps_api_key: + +# Twitterインテグレーションの設定(利用しない場合は省略可能) +twitter: + # インテグレーション用アプリのコンシューマーキー + consumer_key: + + # インテグレーション用アプリのコンシューマーシークレット + consumer_secret: diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000000..7a74d6ef9b --- /dev/null +++ b/.eslintrc @@ -0,0 +1,26 @@ +{ + "parserOptions": { + "parser": "typescript-eslint-parser" + }, + "extends": [ + "eslint:recommended", + "plugin:vue/recommended" + ], + "rules": { + "vue/require-v-for-key": false, + "vue/max-attributes-per-line": false, + "vue/html-indent": false, + "vue/html-self-closing": false, + "vue/no-unused-vars": false, + "vue/attributes-order": false, + "vue/require-prop-types": false, + "no-console": 0, + "no-unused-vars": 0, + "no-empty": 0 + }, + "globals": { + "ENV": true, + "VERSION": true, + "API": true + } +} diff --git a/.gitattributes b/.gitattributes index c6c5947baf..952d6cd0e9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,3 @@ *.svg -diff -text *.psd -diff -text *.ai -diff -text - -*.tag linguist-language=HTML diff --git a/.gitignore b/.gitignore index 42b1bde94f..be8689e2ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,13 @@ -/.config +/.config/* +!/.config/example.yml /.vscode /node_modules +/build /built -/uploads +/data npm-debug.log *.pem run.bat api-docs.json package-lock.json +version.json diff --git a/.travis.yml b/.travis.yml index 91e1244432..d2552bb460 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,13 @@ # travis file # https://docs.travis-ci.com/user/customizing-the-build +notifications: + email: false + language: node_js node_js: - - 7.10.0 + - 9.8.0 env: - CXX=g++-4.8 NODE_ENV=production @@ -33,14 +36,8 @@ before_script: # --only=dev オプションを付けてそれらもインストールされるようにする: - npm install --only=dev - # 設定ファイルを設定 - - mkdir ./.config + # 設定ファイルを配置 - cp ./.travis/default.yml ./.config - cp ./.travis/test.yml ./.config - - npm run build - -after_success: - # リリース - - chmod u+x ./.travis/release.sh - - if [ $TRAVIS_BRANCH = "master" ] && [ $TRAVIS_PULL_REQUEST = "false" ]; then ./.travis/release.sh; else echo "Skipping releasing task"; fi + - travis_wait npm run build diff --git a/.travis/.gitignore-release b/.travis/.gitignore-release deleted file mode 100644 index ad1d3724fc..0000000000 --- a/.travis/.gitignore-release +++ /dev/null @@ -1,8 +0,0 @@ -# Realizing whitelist by excluding everything and specifying exceptions. - -/* - -!/built -!/tools -!/elasticsearch -!/package.json diff --git a/.travis/default.yml b/.travis/default.yml index 1875748d68..471a2a7c46 100644 --- a/.travis/default.yml +++ b/.travis/default.yml @@ -22,5 +22,5 @@ elasticsearch: port: 9200 pass: '' recaptcha: - siteKey: hima - secretKey: saku + site_key: hima + secret_key: saku diff --git a/.travis/release.sh b/.travis/release.sh deleted file mode 100644 index 5def2ab03a..0000000000 --- a/.travis/release.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -echo "Starting releasing task" -openssl aes-256-cbc -K $encrypted_ceda82069128_key -iv $encrypted_ceda82069128_iv -in ./.travis/travis_rsa.enc -out travis_rsa -d -cp travis_rsa ~/.ssh/id_rsa -chmod 600 ~/.ssh/id_rsa -echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config -git checkout -b release -cp -f ./.travis/.gitignore-release .gitignore -node ./.travis/shapeup.js -git add --all -git rm --cached `git ls-files --full-name -i --exclude-standard` -git config --global user.email "AyaMorisawa4869@gmail.com" -git config --global user.name "Aya Morisawa" -git commit -m "Release build for $TRAVIS_COMMIT" -git push -f git@github.com:syuilo/misskey release -echo "Finished releasing task" diff --git a/.travis/shapeup.js b/.travis/shapeup.js deleted file mode 100644 index 9a5d85a188..0000000000 --- a/.travis/shapeup.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict' - -const fs = require('fs') -const filename = process.argv[2] || 'package.json' - -fs.readFile(filename, (err, data) => { - if (err) process.exit(2) - const object = JSON.parse(data) - delete object.devDependencies - fs.writeFile(filename, JSON.stringify(object, null, '\t') + '\n', err => { - if (err) process.exit(3) - }) -}) diff --git a/.travis/test.yml b/.travis/test.yml index f311310c7c..6a115d6ab8 100644 --- a/.travis/test.yml +++ b/.travis/test.yml @@ -22,5 +22,5 @@ elasticsearch: port: 9200 pass: '' recaptcha: - siteKey: hima - secretKey: saku + site_key: hima + secret_key: saku diff --git a/.travis/travis_rsa.enc b/.travis/travis_rsa.enc deleted file mode 100644 index ec45f8a6bb..0000000000 Binary files a/.travis/travis_rsa.enc and /dev/null differ diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 72a584ddb0..0000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,23 +0,0 @@ -ChangeLog -========= -主に notable な changes を書いていきます - -2380 ----- -アプリケーションが作れない問題を修正 - -2367 ----- -Statsのユーザー数グラフに「アカウントが作成された**回数**」(その日時点での「アカウント数」**ではなく**)グラフも併記するようにした - -2364 ----- -デザインの微調整 - -2361 ----- -Statsを実装するなど - -2357 ----- -Statusを実装するなど diff --git a/DONORS.md b/DONORS.md new file mode 100644 index 0000000000..6b56b13e0b --- /dev/null +++ b/DONORS.md @@ -0,0 +1,24 @@ +DONORS +====== +The list of people who have sent donation for Misskey. + +(no particular order) + +* らふぁ +* 俺様 +* なぎうり +* スルメ https://surume.tk/ +* 藍 +* 音船 https://otofune.me/ +* aqz https://misskey.xyz/aqz +* kotodu "虚無創作中" + +:heart: Thanks for donating, guys! + +--- + +If your name is missing, please contact us! + +If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link]. + +[syuilo-link]: https://syuilo.com diff --git a/LICENSE b/LICENSE index e3733b3961..dba13ed2dd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,661 @@ -The MIT License (MIT) + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 -Copyright (c) 2014-2017 syuilo + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + Preamble -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +<http://www.gnu.org/licenses/>. diff --git a/README.md b/README.md index 9d2d38149c..46288e0c4a 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,75 @@ - +<img src="https://github.com/syuilo/misskey/blob/b3f42e62af698a67c2250533c437569559f1fdf9/src/himasaku/resources/himasaku.png?raw=true" align="right" width="320px"/> + +[](https://misskey.xyz/) ================================================================ [![][travis-badge]][travis-link] [![][dependencies-badge]][dependencies-link] [![][himawari-badge]][himasaku] [![][sakurako-badge]][himasaku] -[![][mit-badge]][mit] +[](http://makeapullrequest.com) -[Misskey](https://misskey.xyz) is a completely open source, +> Lead Maintainer: [syuilo][syuilo-link] + +**[Misskey](https://misskey.xyz)** is a completely open source, ultimately sophisticated new type of mini-blog based SNS. - - -Key features +:sparkles: Features ---------------------------------------------------------------- * Automatically updated timeline * Private messages -* Free 1GB storage for each all users -* Mobile device support (smartphone, tablet, etc) +* Two-Factor Authentication support +* ServiceWorker support * Web API for third-party applications -* No ads +* ActivityPub compatible and more! You can touch with your own eyes at https://misskey.xyz/. -Setup and Installation +:package: Setup ---------------------------------------------------------------- -Please see [Setup and installation guide](./docs/setup.en.md). +If you want to run your own instance of Misskey, +please see [Setup and installation guide](./docs/setup.en.md). -Contribution +:yen: Donation ---------------------------------------------------------------- -Please see [Contribution guide](./CONTRIBUTING.md). +If you want to donate to Misskey, please see [this](./docs/donate.ja.md). -Sponsors & Backers ----------------------------------------------------------------- -Misskey have no 100+ GitHub stars currently. However, donation are always welcome! -If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link]. +[List of all donors](./DONORS.md) -Collaborators +:mortar_board: Notable contributors ---------------------------------------------------------------- -| ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon] | -|------------------------|-----------------------------------|---------------------------------| -| [syuilo][syuilo-link] | [Aya Morisawa][ayamorisawa-link] | [otofune][otofune-link] | +| ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon] | ![akihikodaki][akihikodaki-icon] | ![rinsuki][rinsuki-icon] | +|:-:|:-:|:-:|:-:|:-:| +| [syuilo][syuilo-link]<br>Owner | [Aya Morisawa][ayamorisawa-link]<br>Collaborator | [otofune][otofune-link]<br>Collaborator | [akihikodaki][akihikodaki-link] | [rinsuki][rinsuki-link] | [List of all contributors](https://github.com/syuilo/misskey/graphs/contributors) -Copyright +:four_leaf_clover: Copyright ---------------------------------------------------------------- -Misskey is an open-source software licensed under [The MIT License](LICENSE). +> Copyright (c) 2014-2018 syuilo -[mit]: http://opensource.org/licenses/MIT -[mit-badge]: https://img.shields.io/badge/license-MIT-444444.svg?style=flat-square +Misskey is an open-source software licensed under [GNU AGPLv3](LICENSE). + +[![][agpl-3.0-badge]][AGPL-3.0] + +[agpl-3.0]: https://www.gnu.org/licenses/agpl-3.0.en.html +[agpl-3.0-badge]: https://img.shields.io/badge/license-AGPL--3.0-444444.svg?style=flat-square [travis-link]: https://travis-ci.org/syuilo/misskey [travis-badge]: http://img.shields.io/travis/syuilo/misskey/master.svg?style=flat-square -[dependencies-link]: https://gemnasium.com/syuilo/misskey -[dependencies-badge]: https://img.shields.io/gemnasium/syuilo/misskey.svg?style=flat-square +[dependencies-link]: https://david-dm.org/syuilo/misskey +[dependencies-badge]: https://img.shields.io/david/syuilo/misskey.svg?style=flat-square [himasaku]: https://himasaku.net [himawari-badge]: https://img.shields.io/badge/%E5%8F%A4%E8%B0%B7-%E5%90%91%E6%97%A5%E8%91%B5-1684c5.svg?style=flat-square [sakurako-badge]: https://img.shields.io/badge/%E5%A4%A7%E5%AE%A4-%E6%AB%BB%E5%AD%90-efb02a.svg?style=flat-square -<!-- Collaborators Info --> +<!-- Contributors Info --> [syuilo-link]: https://syuilo.com [syuilo-icon]: https://avatars2.githubusercontent.com/u/4439005?v=3&s=70 [ayamorisawa-link]: https://github.com/ayamorisawa [ayamorisawa-icon]: https://avatars0.githubusercontent.com/u/10798641?v=3&s=70 [otofune-link]: https://github.com/otofune [otofune-icon]: https://avatars0.githubusercontent.com/u/15062473?v=3&s=70 +[akihikodaki-link]: https://github.com/akihikodaki +[akihikodaki-icon]: https://avatars2.githubusercontent.com/u/17036990?s=70&v=4 +[rinsuki-link]: https://github.com/rinsuki +[rinsuki-icon]: https://avatars0.githubusercontent.com/u/6533808?s=70&v=4 diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index d26cbc27e8..0000000000 --- a/appveyor.yml +++ /dev/null @@ -1,31 +0,0 @@ -# appveyor file -# http://www.appveyor.com/docs/appveyor-yml - -environment: - matrix: - - nodejs_version: 7.10.0 - -build: off - -install: - # Update Node.js - # 標準で入っている Node.js を更新します (2014/11/13 時点では、v0.10.32 が標準) - - ps: Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) - - node --version - - # Update NPM - - npm install -g npm - - npm --version - - # Update node-gyp - # 必須! node-gyp のバージョンを上げないと、ネイティブモジュールのコンパイルに失敗します - - npm install -g node-gyp - - - npm install - -init: - # git clone の際の改行を変換しないようにします - - git config --global core.autocrlf false - -test_script: - - npm run build diff --git a/assets/apple-touch-icon.png b/assets/apple-touch-icon.png index 623cf6bb9a..b3c4be42af 100644 Binary files a/assets/apple-touch-icon.png and b/assets/apple-touch-icon.png differ diff --git a/assets/favicon.ico b/assets/favicon.ico new file mode 100644 index 0000000000..d63c68b016 Binary files /dev/null and b/assets/favicon.ico differ diff --git a/assets/favicon/128.png b/assets/favicon/128.png index 16e8dfb5b4..1ccaaeee1c 100644 Binary files a/assets/favicon/128.png and b/assets/favicon/128.png differ diff --git a/assets/favicon/128.svg b/assets/favicon/128.svg new file mode 100644 index 0000000000..34888557b9 --- /dev/null +++ b/assets/favicon/128.svg @@ -0,0 +1,161 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="896" + height="896" + viewBox="0 0 237.06667 237.06667" + version="1.1" + id="svg8" + inkscape:version="0.92.1 r15371" + sodipodi:docname="128.svg" + inkscape:export-filename="C:\Users\syuilo\projects\misskey\assets\favicon\128.png" + inkscape:export-xdpi="13.714286" + inkscape:export-ydpi="13.714286"> + <defs + id="defs2"> + <inkscape:path-effect + effect="simplify" + id="path-effect5115" + is_visible="true" + steps="1" + threshold="0.000408163" + smooth_angles="360" + helper_size="0" + simplify_individual_paths="false" + simplify_just_coalesce="false" + simplifyindividualpaths="false" + simplifyJustCoalesce="false" /> + <inkscape:path-effect + effect="simplify" + id="path-effect5111" + is_visible="true" + steps="1" + threshold="0.000408163" + smooth_angles="360" + helper_size="0" + simplify_individual_paths="false" + simplify_just_coalesce="false" + simplifyindividualpaths="false" + simplifyJustCoalesce="false" /> + <inkscape:path-effect + effect="simplify" + id="path-effect5104" + is_visible="true" + steps="1" + threshold="0.000408163" + smooth_angles="360" + helper_size="0" + simplify_individual_paths="false" + simplify_just_coalesce="false" + simplifyindividualpaths="false" + simplifyJustCoalesce="false" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="0.7071068" + inkscape:cx="908.16505" + inkscape:cy="468.2779" + inkscape:document-units="px" + inkscape:current-layer="g4502" + showgrid="true" + units="px" + inkscape:snap-bbox="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-center="true" + inkscape:snap-page="true" + inkscape:window-width="1920" + inkscape:window-height="1017" + inkscape:window-x="-8" + inkscape:window-y="1072" + inkscape:window-maximized="1" + inkscape:snap-object-midpoints="true" + inkscape:snap-midpoints="true" + inkscape:object-paths="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + objecttolerance="1" + guidetolerance="1" + inkscape:snap-nodes="true" + inkscape:snap-others="false" + inkscape:bbox-paths="true" + inkscape:snap-bbox-midpoints="true"> + <inkscape:grid + type="xygrid" + id="grid4504" + spacingx="4.2333334" + spacingy="4.2333334" + empcolor="#ff3fff" + empopacity="0.25098039" + empspacing="4" /> + </sodipodi:namedview> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="レイヤー 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-30.809093,-10.18601)"> + <g + id="g4502" + transform="matrix(1.096096,0,0,1.096096,47.839369,-94.823577)" + inkscape:export-xdpi="6" + inkscape:export-ydpi="6"> + <rect + style="opacity:1;fill:#2fa1bb;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.96554804;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.97647059" + id="rect4506" + width="216.28276" + height="216.28278" + x="-15.537212" + y="95.803268" /> + <g + style="fill:#ffffff;fill-opacity:1" + transform="translate(-1.3333333e-6,-1.3439941e-6)" + id="g5125"> + <g + transform="matrix(0.91391326,0,0,0.91391326,7.9719907,17.595761)" + id="text4489" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:141.03404236px;line-height:476.69509888px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.28950602px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + aria-label="Mi"> + <path + sodipodi:nodetypes="zccssscssccscczzzccsccsscscsccz" + inkscape:connector-curvature="0" + id="path5210" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#ffffff;fill-opacity:1;stroke-width:0.28950602px" + d="m 75.196381,231.17126 c -5.855419,0.0202 -10.885068,-3.50766 -13.2572,-7.61584 -1.266603,-1.79454 -3.772419,-2.43291 -3.807919,0 v 11.2332 c 0,4.51309 -1.645397,8.41504 -4.936191,11.70583 -3.196772,3.19677 -7.098714,4.79516 -11.705826,4.79516 -4.513089,0 -8.415031,-1.59839 -11.705825,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -61.7729 c 0,-3.47884 0.987238,-6.6286 2.961715,-9.44928 2.068499,-2.91471 4.701135,-4.9362 7.897906,-6.06447 1.786431,-0.65816 3.666885,-0.98724 5.641362,-0.98724 5.077225,0 9.308247,1.97448 12.693064,5.92343 1.786431,1.97448 2.820681,3.00873 3.102749,3.10275 0,0 13.408119,16.21319 13.78421,16.49526 0.376091,0.28206 1.480789,2.43848 4.127113,2.43848 2.646324,0 3.89218,-2.15642 4.26827,-2.43848 0.376091,-0.28207 13.784088,-16.49526 13.784088,-16.49526 0.09402,0.094 1.081261,-0.94022 2.961715,-3.10275 3.478837,-3.94895 7.756866,-5.92343 12.834096,-5.92343 1.88045,0 3.76091,0.32908 5.64136,0.98724 3.19677,1.12827 5.7824,3.14976 7.75688,6.06447 2.06849,2.82068 3.10274,5.97044 3.10274,9.44928 v 61.7729 c 0,4.51309 -1.6454,8.41504 -4.93619,11.70583 -3.19677,3.19677 -7.09871,4.79516 -11.70582,4.79516 -4.51309,0 -8.41504,-1.59839 -11.705828,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -11.2332 c -0.277898,-3.06563 -2.987588,-1.13379 -3.948953,0 -2.538613,4.70114 -7.401781,7.59567 -13.2572,7.61584 z" /> + <path + inkscape:connector-curvature="0" + id="path5212" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#ffffff;fill-opacity:1;stroke-width:0.28950602px" + d="m 145.83461,185.00361 q -5.92343,0 -10.15445,-4.08999 -4.08999,-4.23102 -4.08999,-10.15445 0,-5.92343 4.08999,-10.01342 4.23102,-4.23102 10.15445,-4.23102 5.92343,0 10.15445,4.23102 4.23102,4.08999 4.23102,10.01342 0,5.92343 -4.23102,10.15445 -4.23102,4.08999 -10.15445,4.08999 z m 0.14103,2.82068 q 5.92343,0 10.01342,4.23102 4.23102,4.23102 4.23102,10.15445 v 34.83541 q 0,5.92343 -4.23102,10.15445 -4.08999,4.08999 -10.01342,4.08999 -5.92343,0 -10.15445,-4.08999 -4.23102,-4.23102 -4.23102,-10.15445 v -34.83541 q 0,-5.92343 4.23102,-10.15445 4.23102,-4.23102 10.15445,-4.23102 z" /> + </g> + </g> + </g> + </g> +</svg> diff --git a/assets/favicon/16.png b/assets/favicon/16.png index 7e36d1cda0..a1d3e1be72 100644 Binary files a/assets/favicon/16.png and b/assets/favicon/16.png differ diff --git a/assets/favicon/16.svg b/assets/favicon/16.svg new file mode 100644 index 0000000000..03aa8bc6bd --- /dev/null +++ b/assets/favicon/16.svg @@ -0,0 +1,149 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="512" + height="512" + viewBox="0 0 135.46667 135.46667" + version="1.1" + id="svg8" + inkscape:version="0.92.1 r15371" + sodipodi:docname="16.svg" + inkscape:export-filename="C:\Users\syuilo\projects\misskey\assets\favicon\16.png" + inkscape:export-xdpi="3" + inkscape:export-ydpi="3"> + <defs + id="defs2"> + <inkscape:path-effect + effect="simplify" + id="path-effect5115" + is_visible="true" + steps="1" + threshold="0.000408163" + smooth_angles="360" + helper_size="0" + simplify_individual_paths="false" + simplify_just_coalesce="false" + simplifyindividualpaths="false" + simplifyJustCoalesce="false" /> + <inkscape:path-effect + effect="simplify" + id="path-effect5111" + is_visible="true" + steps="1" + threshold="0.000408163" + smooth_angles="360" + helper_size="0" + simplify_individual_paths="false" + simplify_just_coalesce="false" + simplifyindividualpaths="false" + simplifyJustCoalesce="false" /> + <inkscape:path-effect + effect="simplify" + id="path-effect5104" + is_visible="true" + steps="1" + threshold="0.000408163" + smooth_angles="360" + helper_size="0" + simplify_individual_paths="false" + simplify_just_coalesce="false" + simplifyindividualpaths="false" + simplifyJustCoalesce="false" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.4142136" + inkscape:cx="110.21885" + inkscape:cy="235.92965" + inkscape:document-units="px" + inkscape:current-layer="g4502" + showgrid="true" + units="px" + inkscape:snap-bbox="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="false" + inkscape:snap-smooth-nodes="true" + inkscape:snap-center="true" + inkscape:snap-page="true" + inkscape:window-width="1920" + inkscape:window-height="1017" + inkscape:window-x="-8" + inkscape:window-y="1072" + inkscape:window-maximized="1" + inkscape:snap-object-midpoints="true" + inkscape:snap-midpoints="true" + inkscape:object-paths="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + objecttolerance="1" + guidetolerance="1" + inkscape:snap-nodes="false" + inkscape:snap-others="false"> + <inkscape:grid + type="xygrid" + id="grid4504" + spacingx="4.2333334" + spacingy="4.2333334" + empcolor="#ff3fff" + empopacity="0.25098039" + empspacing="4" /> + </sodipodi:namedview> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="レイヤー 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-30.809093,-111.78601)"> + <g + id="g4502" + transform="matrix(1.096096,0,0,1.096096,-2.960633,-44.023579)"> + <g + style="fill:#2fa1bb;fill-opacity:1" + id="g5125"> + <g + transform="matrix(0.91391326,0,0,0.91391326,7.9719907,17.595761)" + id="text4489" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:141.03404236px;line-height:476.69509888px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#2fa1bb;fill-opacity:1;stroke:none;stroke-width:0.28950602px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + aria-label="Mi"> + <path + sodipodi:nodetypes="zccssscssccscczzzccsccsscscsccz" + inkscape:connector-curvature="0" + id="path5210" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#2fa1bb;fill-opacity:1;stroke-width:0.28950602px" + d="m 75.196381,231.17126 c -5.855419,0.0202 -10.885068,-3.50766 -13.2572,-7.61584 -1.266603,-1.79454 -3.772419,-2.43291 -3.807919,0 v 11.2332 c 0,4.51309 -1.645397,8.41504 -4.936191,11.70583 -3.196772,3.19677 -7.098714,4.79516 -11.705826,4.79516 -4.513089,0 -8.415031,-1.59839 -11.705825,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -61.7729 c 0,-3.47884 0.987238,-6.6286 2.961715,-9.44928 2.068499,-2.91471 4.701135,-4.9362 7.897906,-6.06447 1.786431,-0.65816 3.666885,-0.98724 5.641362,-0.98724 5.077225,0 9.308247,1.97448 12.693064,5.92343 1.786431,1.97448 2.820681,3.00873 3.102749,3.10275 0,0 13.408119,16.21319 13.78421,16.49526 0.376091,0.28206 1.480789,2.43848 4.127113,2.43848 2.646324,0 3.89218,-2.15642 4.26827,-2.43848 0.376091,-0.28207 13.784088,-16.49526 13.784088,-16.49526 0.09402,0.094 1.081261,-0.94022 2.961715,-3.10275 3.478837,-3.94895 7.756866,-5.92343 12.834096,-5.92343 1.88045,0 3.76091,0.32908 5.64136,0.98724 3.19677,1.12827 5.7824,3.14976 7.75688,6.06447 2.06849,2.82068 3.10274,5.97044 3.10274,9.44928 v 61.7729 c 0,4.51309 -1.6454,8.41504 -4.93619,11.70583 -3.19677,3.19677 -7.09871,4.79516 -11.70582,4.79516 -4.51309,0 -8.41504,-1.59839 -11.705828,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -11.2332 c -0.277898,-3.06563 -2.987588,-1.13379 -3.948953,0 -2.538613,4.70114 -7.401781,7.59567 -13.2572,7.61584 z" /> + <path + inkscape:connector-curvature="0" + id="path5212" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#2fa1bb;fill-opacity:1;stroke-width:0.28950602px" + d="m 145.83461,185.00361 q -5.92343,0 -10.15445,-4.08999 -4.08999,-4.23102 -4.08999,-10.15445 0,-5.92343 4.08999,-10.01342 4.23102,-4.23102 10.15445,-4.23102 5.92343,0 10.15445,4.23102 4.23102,4.08999 4.23102,10.01342 0,5.92343 -4.23102,10.15445 -4.23102,4.08999 -10.15445,4.08999 z m 0.14103,2.82068 q 5.92343,0 10.01342,4.23102 4.23102,4.23102 4.23102,10.15445 v 34.83541 q 0,5.92343 -4.23102,10.15445 -4.08999,4.08999 -10.01342,4.08999 -5.92343,0 -10.15445,-4.08999 -4.23102,-4.23102 -4.23102,-10.15445 v -34.83541 q 0,-5.92343 4.23102,-10.15445 4.23102,-4.23102 10.15445,-4.23102 z" /> + </g> + </g> + </g> + </g> +</svg> diff --git a/assets/favicon/256.png b/assets/favicon/256.png index 623cf6bb9a..b3c4be42af 100644 Binary files a/assets/favicon/256.png and b/assets/favicon/256.png differ diff --git a/assets/favicon/256.svg b/assets/favicon/256.svg new file mode 100644 index 0000000000..5ecee9e0be --- /dev/null +++ b/assets/favicon/256.svg @@ -0,0 +1,161 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="896" + height="896" + viewBox="0 0 237.06667 237.06667" + version="1.1" + id="svg8" + inkscape:version="0.92.1 r15371" + sodipodi:docname="256.svg" + inkscape:export-filename="C:\Users\syuilo\projects\misskey\assets\favicon\256.png" + inkscape:export-xdpi="27.428572" + inkscape:export-ydpi="27.428572"> + <defs + id="defs2"> + <inkscape:path-effect + effect="simplify" + id="path-effect5115" + is_visible="true" + steps="1" + threshold="0.000408163" + smooth_angles="360" + helper_size="0" + simplify_individual_paths="false" + simplify_just_coalesce="false" + simplifyindividualpaths="false" + simplifyJustCoalesce="false" /> + <inkscape:path-effect + effect="simplify" + id="path-effect5111" + is_visible="true" + steps="1" + threshold="0.000408163" + smooth_angles="360" + helper_size="0" + simplify_individual_paths="false" + simplify_just_coalesce="false" + simplifyindividualpaths="false" + simplifyJustCoalesce="false" /> + <inkscape:path-effect + effect="simplify" + id="path-effect5104" + is_visible="true" + steps="1" + threshold="0.000408163" + smooth_angles="360" + helper_size="0" + simplify_individual_paths="false" + simplify_just_coalesce="false" + simplifyindividualpaths="false" + simplifyJustCoalesce="false" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="0.7071068" + inkscape:cx="908.16505" + inkscape:cy="468.2779" + inkscape:document-units="px" + inkscape:current-layer="g4502" + showgrid="true" + units="px" + inkscape:snap-bbox="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-center="true" + inkscape:snap-page="true" + inkscape:window-width="1920" + inkscape:window-height="1017" + inkscape:window-x="-8" + inkscape:window-y="1072" + inkscape:window-maximized="1" + inkscape:snap-object-midpoints="true" + inkscape:snap-midpoints="true" + inkscape:object-paths="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + objecttolerance="1" + guidetolerance="1" + inkscape:snap-nodes="true" + inkscape:snap-others="false" + inkscape:bbox-paths="true" + inkscape:snap-bbox-midpoints="true"> + <inkscape:grid + type="xygrid" + id="grid4504" + spacingx="4.2333334" + spacingy="4.2333334" + empcolor="#ff3fff" + empopacity="0.25098039" + empspacing="4" /> + </sodipodi:namedview> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="レイヤー 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-30.809093,-10.18601)"> + <g + id="g4502" + transform="matrix(1.096096,0,0,1.096096,47.839369,-94.823577)" + inkscape:export-xdpi="6" + inkscape:export-ydpi="6"> + <rect + style="opacity:1;fill:#2fa1bb;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.96554804;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.97647059" + id="rect4506" + width="216.28276" + height="216.28278" + x="-15.537212" + y="95.803268" /> + <g + style="fill:#ffffff;fill-opacity:1" + transform="translate(-1.3333333e-6,-1.3439941e-6)" + id="g5125"> + <g + transform="matrix(0.91391326,0,0,0.91391326,7.9719907,17.595761)" + id="text4489" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:141.03404236px;line-height:476.69509888px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.28950602px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + aria-label="Mi"> + <path + sodipodi:nodetypes="zccssscssccscczzzccsccsscscsccz" + inkscape:connector-curvature="0" + id="path5210" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#ffffff;fill-opacity:1;stroke-width:0.28950602px" + d="m 75.196381,231.17126 c -5.855419,0.0202 -10.885068,-3.50766 -13.2572,-7.61584 -1.266603,-1.79454 -3.772419,-2.43291 -3.807919,0 v 11.2332 c 0,4.51309 -1.645397,8.41504 -4.936191,11.70583 -3.196772,3.19677 -7.098714,4.79516 -11.705826,4.79516 -4.513089,0 -8.415031,-1.59839 -11.705825,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -61.7729 c 0,-3.47884 0.987238,-6.6286 2.961715,-9.44928 2.068499,-2.91471 4.701135,-4.9362 7.897906,-6.06447 1.786431,-0.65816 3.666885,-0.98724 5.641362,-0.98724 5.077225,0 9.308247,1.97448 12.693064,5.92343 1.786431,1.97448 2.820681,3.00873 3.102749,3.10275 0,0 13.408119,16.21319 13.78421,16.49526 0.376091,0.28206 1.480789,2.43848 4.127113,2.43848 2.646324,0 3.89218,-2.15642 4.26827,-2.43848 0.376091,-0.28207 13.784088,-16.49526 13.784088,-16.49526 0.09402,0.094 1.081261,-0.94022 2.961715,-3.10275 3.478837,-3.94895 7.756866,-5.92343 12.834096,-5.92343 1.88045,0 3.76091,0.32908 5.64136,0.98724 3.19677,1.12827 5.7824,3.14976 7.75688,6.06447 2.06849,2.82068 3.10274,5.97044 3.10274,9.44928 v 61.7729 c 0,4.51309 -1.6454,8.41504 -4.93619,11.70583 -3.19677,3.19677 -7.09871,4.79516 -11.70582,4.79516 -4.51309,0 -8.41504,-1.59839 -11.705828,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -11.2332 c -0.277898,-3.06563 -2.987588,-1.13379 -3.948953,0 -2.538613,4.70114 -7.401781,7.59567 -13.2572,7.61584 z" /> + <path + inkscape:connector-curvature="0" + id="path5212" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#ffffff;fill-opacity:1;stroke-width:0.28950602px" + d="m 145.83461,185.00361 q -5.92343,0 -10.15445,-4.08999 -4.08999,-4.23102 -4.08999,-10.15445 0,-5.92343 4.08999,-10.01342 4.23102,-4.23102 10.15445,-4.23102 5.92343,0 10.15445,4.23102 4.23102,4.08999 4.23102,10.01342 0,5.92343 -4.23102,10.15445 -4.23102,4.08999 -10.15445,4.08999 z m 0.14103,2.82068 q 5.92343,0 10.01342,4.23102 4.23102,4.23102 4.23102,10.15445 v 34.83541 q 0,5.92343 -4.23102,10.15445 -4.08999,4.08999 -10.01342,4.08999 -5.92343,0 -10.15445,-4.08999 -4.23102,-4.23102 -4.23102,-10.15445 v -34.83541 q 0,-5.92343 4.23102,-10.15445 4.23102,-4.23102 10.15445,-4.23102 z" /> + </g> + </g> + </g> + </g> +</svg> diff --git a/assets/favicon/32.png b/assets/favicon/32.png index f13ebb1473..f0466cce91 100644 Binary files a/assets/favicon/32.png and b/assets/favicon/32.png differ diff --git a/assets/favicon/32.svg b/assets/favicon/32.svg new file mode 100644 index 0000000000..4dfcc68606 --- /dev/null +++ b/assets/favicon/32.svg @@ -0,0 +1,152 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="640" + height="640" + viewBox="0 0 169.33333 169.33333" + version="1.1" + id="svg8" + inkscape:version="0.92.1 r15371" + sodipodi:docname="32.svg" + inkscape:export-filename="C:\Users\syuilo\projects\misskey\assets\favicon\32.png" + inkscape:export-xdpi="4.8000002" + inkscape:export-ydpi="4.8000002"> + <defs + id="defs2"> + <inkscape:path-effect + effect="simplify" + id="path-effect5115" + is_visible="true" + steps="1" + threshold="0.000408163" + smooth_angles="360" + helper_size="0" + simplify_individual_paths="false" + simplify_just_coalesce="false" + simplifyindividualpaths="false" + simplifyJustCoalesce="false" /> + <inkscape:path-effect + effect="simplify" + id="path-effect5111" + is_visible="true" + steps="1" + threshold="0.000408163" + smooth_angles="360" + helper_size="0" + simplify_individual_paths="false" + simplify_just_coalesce="false" + simplifyindividualpaths="false" + simplifyJustCoalesce="false" /> + <inkscape:path-effect + effect="simplify" + id="path-effect5104" + is_visible="true" + steps="1" + threshold="0.000408163" + smooth_angles="360" + helper_size="0" + simplify_individual_paths="false" + simplify_just_coalesce="false" + simplifyindividualpaths="false" + simplifyJustCoalesce="false" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="0.7071068" + inkscape:cx="16.781901" + inkscape:cy="343.6089" + inkscape:document-units="px" + inkscape:current-layer="g4502" + showgrid="true" + units="px" + inkscape:snap-bbox="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="false" + inkscape:snap-smooth-nodes="true" + inkscape:snap-center="true" + inkscape:snap-page="true" + inkscape:window-width="1920" + inkscape:window-height="1017" + inkscape:window-x="-8" + inkscape:window-y="1072" + inkscape:window-maximized="1" + inkscape:snap-object-midpoints="true" + inkscape:snap-midpoints="true" + inkscape:object-paths="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + objecttolerance="1" + guidetolerance="1" + inkscape:snap-nodes="false" + inkscape:snap-others="false"> + <inkscape:grid + type="xygrid" + id="grid4504" + spacingx="4.2333334" + spacingy="4.2333334" + empcolor="#ff3fff" + empopacity="0.25098039" + empspacing="4" /> + </sodipodi:namedview> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="レイヤー 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-30.809093,-77.919343)"> + <g + id="g4502" + transform="matrix(1.096096,0,0,1.096096,13.972699,-60.956914)" + inkscape:export-xdpi="6" + inkscape:export-ydpi="6"> + <g + style="fill:#2fa1bb;fill-opacity:1" + transform="translate(-1.7127735e-6,-1.5982974e-6)" + id="g5125"> + <g + transform="matrix(0.91391326,0,0,0.91391326,7.9719907,17.595761)" + id="text4489" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:141.03404236px;line-height:476.69509888px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#2fa1bb;fill-opacity:1;stroke:none;stroke-width:0.28950602px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + aria-label="Mi"> + <path + sodipodi:nodetypes="zccssscssccscczzzccsccsscscsccz" + inkscape:connector-curvature="0" + id="path5210" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#2fa1bb;fill-opacity:1;stroke-width:0.28950602px" + d="m 75.196381,231.17126 c -5.855419,0.0202 -10.885068,-3.50766 -13.2572,-7.61584 -1.266603,-1.79454 -3.772419,-2.43291 -3.807919,0 v 11.2332 c 0,4.51309 -1.645397,8.41504 -4.936191,11.70583 -3.196772,3.19677 -7.098714,4.79516 -11.705826,4.79516 -4.513089,0 -8.415031,-1.59839 -11.705825,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -61.7729 c 0,-3.47884 0.987238,-6.6286 2.961715,-9.44928 2.068499,-2.91471 4.701135,-4.9362 7.897906,-6.06447 1.786431,-0.65816 3.666885,-0.98724 5.641362,-0.98724 5.077225,0 9.308247,1.97448 12.693064,5.92343 1.786431,1.97448 2.820681,3.00873 3.102749,3.10275 0,0 13.408119,16.21319 13.78421,16.49526 0.376091,0.28206 1.480789,2.43848 4.127113,2.43848 2.646324,0 3.89218,-2.15642 4.26827,-2.43848 0.376091,-0.28207 13.784088,-16.49526 13.784088,-16.49526 0.09402,0.094 1.081261,-0.94022 2.961715,-3.10275 3.478837,-3.94895 7.756866,-5.92343 12.834096,-5.92343 1.88045,0 3.76091,0.32908 5.64136,0.98724 3.19677,1.12827 5.7824,3.14976 7.75688,6.06447 2.06849,2.82068 3.10274,5.97044 3.10274,9.44928 v 61.7729 c 0,4.51309 -1.6454,8.41504 -4.93619,11.70583 -3.19677,3.19677 -7.09871,4.79516 -11.70582,4.79516 -4.51309,0 -8.41504,-1.59839 -11.705828,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -11.2332 c -0.277898,-3.06563 -2.987588,-1.13379 -3.948953,0 -2.538613,4.70114 -7.401781,7.59567 -13.2572,7.61584 z" /> + <path + inkscape:connector-curvature="0" + id="path5212" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#2fa1bb;fill-opacity:1;stroke-width:0.28950602px" + d="m 145.83461,185.00361 q -5.92343,0 -10.15445,-4.08999 -4.08999,-4.23102 -4.08999,-10.15445 0,-5.92343 4.08999,-10.01342 4.23102,-4.23102 10.15445,-4.23102 5.92343,0 10.15445,4.23102 4.23102,4.08999 4.23102,10.01342 0,5.92343 -4.23102,10.15445 -4.23102,4.08999 -10.15445,4.08999 z m 0.14103,2.82068 q 5.92343,0 10.01342,4.23102 4.23102,4.23102 4.23102,10.15445 v 34.83541 q 0,5.92343 -4.23102,10.15445 -4.08999,4.08999 -10.01342,4.08999 -5.92343,0 -10.15445,-4.08999 -4.23102,-4.23102 -4.23102,-10.15445 v -34.83541 q 0,-5.92343 4.23102,-10.15445 4.23102,-4.23102 10.15445,-4.23102 z" /> + </g> + </g> + </g> + </g> +</svg> diff --git a/assets/favicon/64.png b/assets/favicon/64.png index 72751fe774..9710052ae7 100644 Binary files a/assets/favicon/64.png and b/assets/favicon/64.png differ diff --git a/assets/favicon/64.svg b/assets/favicon/64.svg new file mode 100644 index 0000000000..e2378791a7 --- /dev/null +++ b/assets/favicon/64.svg @@ -0,0 +1,152 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="768" + height="768" + viewBox="0 0 203.2 203.2" + version="1.1" + id="svg8" + inkscape:version="0.92.1 r15371" + sodipodi:docname="64.svg" + inkscape:export-filename="C:\Users\syuilo\projects\misskey\assets\favicon\64.png" + inkscape:export-xdpi="8" + inkscape:export-ydpi="8"> + <defs + id="defs2"> + <inkscape:path-effect + effect="simplify" + id="path-effect5115" + is_visible="true" + steps="1" + threshold="0.000408163" + smooth_angles="360" + helper_size="0" + simplify_individual_paths="false" + simplify_just_coalesce="false" + simplifyindividualpaths="false" + simplifyJustCoalesce="false" /> + <inkscape:path-effect + effect="simplify" + id="path-effect5111" + is_visible="true" + steps="1" + threshold="0.000408163" + smooth_angles="360" + helper_size="0" + simplify_individual_paths="false" + simplify_just_coalesce="false" + simplifyindividualpaths="false" + simplifyJustCoalesce="false" /> + <inkscape:path-effect + effect="simplify" + id="path-effect5104" + is_visible="true" + steps="1" + threshold="0.000408163" + smooth_angles="360" + helper_size="0" + simplify_individual_paths="false" + simplify_just_coalesce="false" + simplifyindividualpaths="false" + simplifyJustCoalesce="false" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="0.7071068" + inkscape:cx="16.781901" + inkscape:cy="343.6089" + inkscape:document-units="px" + inkscape:current-layer="g5125" + showgrid="true" + units="px" + inkscape:snap-bbox="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="false" + inkscape:snap-smooth-nodes="true" + inkscape:snap-center="true" + inkscape:snap-page="true" + inkscape:window-width="1920" + inkscape:window-height="1017" + inkscape:window-x="-8" + inkscape:window-y="1072" + inkscape:window-maximized="1" + inkscape:snap-object-midpoints="true" + inkscape:snap-midpoints="true" + inkscape:object-paths="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + objecttolerance="1" + guidetolerance="1" + inkscape:snap-nodes="false" + inkscape:snap-others="false"> + <inkscape:grid + type="xygrid" + id="grid4504" + spacingx="4.2333334" + spacingy="4.2333334" + empcolor="#ff3fff" + empopacity="0.25098039" + empspacing="4" /> + </sodipodi:namedview> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="レイヤー 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-30.809093,-44.052677)"> + <g + id="g4502" + transform="matrix(1.096096,0,0,1.096096,30.906034,-77.890245)" + inkscape:export-xdpi="6" + inkscape:export-ydpi="6"> + <g + style="fill:#2fa1bb;fill-opacity:0.94117647" + transform="translate(-1.3333333e-6,-1.3439941e-6)" + id="g5125"> + <g + transform="matrix(0.91391326,0,0,0.91391326,7.9719903,17.595761)" + id="text4489" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:141.03404236px;line-height:476.69509888px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#2fa1bb;fill-opacity:1;stroke:none;stroke-width:0.28950602px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + aria-label="Mi"> + <path + sodipodi:nodetypes="zccssscssccscczzzccsccsscscsccz" + inkscape:connector-curvature="0" + id="path5210" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#2fa1bb;fill-opacity:1;stroke-width:0.28950602px" + d="m 75.196381,231.17126 c -5.855419,0.0202 -10.885068,-3.50766 -13.2572,-7.61584 -1.266603,-1.79454 -3.772419,-2.43291 -3.807919,0 v 11.2332 c 0,4.51309 -1.645397,8.41504 -4.936191,11.70583 -3.196772,3.19677 -7.098714,4.79516 -11.705826,4.79516 -4.513089,0 -8.415031,-1.59839 -11.705825,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -61.7729 c 0,-3.47884 0.987238,-6.6286 2.961715,-9.44928 2.068499,-2.91471 4.701135,-4.9362 7.897906,-6.06447 1.786431,-0.65816 3.666885,-0.98724 5.641362,-0.98724 5.077225,0 9.308247,1.97448 12.693064,5.92343 1.786431,1.97448 2.820681,3.00873 3.102749,3.10275 0,0 13.408119,16.21319 13.78421,16.49526 0.376091,0.28206 1.480789,2.43848 4.127113,2.43848 2.646324,0 3.89218,-2.15642 4.26827,-2.43848 0.376091,-0.28207 13.784088,-16.49526 13.784088,-16.49526 0.09402,0.094 1.081261,-0.94022 2.961715,-3.10275 3.478837,-3.94895 7.756866,-5.92343 12.834096,-5.92343 1.88045,0 3.76091,0.32908 5.64136,0.98724 3.19677,1.12827 5.7824,3.14976 7.75688,6.06447 2.06849,2.82068 3.10274,5.97044 3.10274,9.44928 v 61.7729 c 0,4.51309 -1.6454,8.41504 -4.93619,11.70583 -3.19677,3.19677 -7.09871,4.79516 -11.70582,4.79516 -4.51309,0 -8.41504,-1.59839 -11.705828,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -11.2332 c -0.277898,-3.06563 -2.987588,-1.13379 -3.948953,0 -2.538613,4.70114 -7.401781,7.59567 -13.2572,7.61584 z" /> + <path + inkscape:connector-curvature="0" + id="path5212" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#2fa1bb;fill-opacity:1;stroke-width:0.28950602px" + d="m 145.83461,185.00361 q -5.92343,0 -10.15445,-4.08999 -4.08999,-4.23102 -4.08999,-10.15445 0,-5.92343 4.08999,-10.01342 4.23102,-4.23102 10.15445,-4.23102 5.92343,0 10.15445,4.23102 4.23102,4.08999 4.23102,10.01342 0,5.92343 -4.23102,10.15445 -4.23102,4.08999 -10.15445,4.08999 z m 0.14103,2.82068 q 5.92343,0 10.01342,4.23102 4.23102,4.23102 4.23102,10.15445 v 34.83541 q 0,5.92343 -4.23102,10.15445 -4.08999,4.08999 -10.01342,4.08999 -5.92343,0 -10.15445,-4.08999 -4.23102,-4.23102 -4.23102,-10.15445 v -34.83541 q 0,-5.92343 4.23102,-10.15445 4.23102,-4.23102 10.15445,-4.23102 z" /> + </g> + </g> + </g> + </g> +</svg> diff --git a/assets/icon.ai b/assets/icon.ai deleted file mode 100644 index c2d5219c73..0000000000 --- a/assets/icon.ai +++ /dev/null @@ -1,4612 +0,0 @@ -%PDF-1.5 %���� -1 0 obj <</Metadata 2 0 R/OCProperties<</D<</ON[5 0 R]/Order 6 0 R/RBGroups[]>>/OCGs[5 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <</Length 46596/Subtype/XML/Type/Metadata>>stream -<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> -<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 "> - <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> - <rdf:Description rdf:about="" - xmlns:dc="http://purl.org/dc/elements/1.1/"> - <dc:format>application/pdf</dc:format> - <dc:title> - <rdf:Alt> - <rdf:li xml:lang="x-default">icon</rdf:li> - </rdf:Alt> - </dc:title> - </rdf:Description> - <rdf:Description rdf:about="" - xmlns:xmp="http://ns.adobe.com/xap/1.0/" - xmlns:xmpGImg="http://ns.adobe.com/xap/1.0/g/img/"> - <xmp:MetadataDate>2017-03-30T06:16:53+09:00</xmp:MetadataDate> - <xmp:ModifyDate>2017-03-30T06:16:53+09:00</xmp:ModifyDate> - <xmp:CreateDate>2017-03-30T06:16:53+10:00</xmp:CreateDate> - <xmp:CreatorTool>Adobe Illustrator CS6 (Windows)</xmp:CreatorTool> - <xmp:Thumbnails> - <rdf:Alt> - <rdf:li rdf:parseType="Resource"> - <xmpGImg:width>256</xmpGImg:width> - <xmpGImg:height>120</xmpGImg:height> - <xmpGImg:format>JPEG</xmpGImg:format> - <xmpGImg:image>/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA
AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK
DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f
Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAeAEAAwER
AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA
AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB
UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE
1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ
qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy
obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp
0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo
+DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq8
2/Nv88fLn5fQfVeI1HzDKvKDTI2pwB6STsK8F8B1bttvgJSBb5U83/nd+ZXmmZze6xNaWjE8bCxZ
raBVP7JCHk4/12bI22CIYK7u7s7sWdiWZmNSSdySTgSybyz+Z/n/AMsyq+ja7d28adLZpDLAfnDL
zj/4XDaDEPpj8n/+cldN803MGheZo49M12YiO1uUJFrcuei/ESYpG7AkgnoakDJAsDGnuGFi7FXY
q7FXYq7FXYq7FXh/5wf85K6b5WuZ9C8sxx6nrsJMd1cuSbW2cdV+EgyyL3AIAPU1BGAlkI2+Z/M3
5n+f/M0rPrOu3dxG/W2WQxQD5QxcI/8AhcjbMRDGUd0dXRirqQyspoQRuCCMCWdeT/zv/MrytOhs
9Ymu7RSOVhfs1zAVH7IDnkn+wZcNoMQ+q/yk/PHy5+YMH1XiNO8wxLyn0yRq8wOskDGnNfEdV77b
5IFrIp6ThQ7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqxP8ANHz5beRvJd9r0qiS4QCGwgPSS5kqI1PT
4Ruzf5IOJSBb4H1fVtR1jU7nVNSna5v7yRpbidzVmdjU/IeAGwG2VtqDxS7FXYq2CQajYjocVfaX
/OOH5oz+cvKkmnapL6mu6Jwinlb7U1u4Poynxb4Sr/IE/ayYLVIU9dwsXYq7FXYq7FXYq8h/5yQ/
NCfyd5Uj03S5fS13XOcUEqkhoLdQBNMCOjfEFT5kj7OAllEW+LiSTU7k9TkG1rFXYq7FUZpGrajo
+p22qabO1tf2ciy286GjK6mo+Y8QdiNsUPvj8rvPlt558l2OvRKI7hwYb+AdI7mOgkUdfhOzL/kk
ZYGoimWYodirsVdirsVdirsVdirsVdirsVdir5h/5zH1yY3nl3QVakKRzX0ydmZmEUZ/2IV/vyMm
cHzbkWx2KuxV2KuxV6r/AM4za7Lpf5t6bAGIg1SKeynFaAgxmVNu/wC8iXCGEuT7bybW7FXYq7FX
Yq7FXxJ/zkzrsuqfm3qUBYmDS4oLKAVqABGJX27fvJWyBbI8nlWBm7FXYq7FXYq+kv8AnDjXJhee
YtBZqwvHDfQp2VlYxSH/AGQZPuyUWub6eyTB2KuxV2KuxV2KuxV2KuxV2KuxV2Kvkz/nMOwkTzro
moEH07jTfq6ntWCeRzT/AJHjIybIPAsizdirsVdirsVeh/8AOP1rLc/nB5bSMVKTSSsfBYoJHP8A
xHCGMuT7tybU7FXYq7FXYq7FXwl/zkDay235weZEkFC80cqnxWWCNx/xLIFtjyeeYGTsVdirsVdi
r33/AJw7sHfzrreoAHhb6b9XY9qzzxuK+/7g5KLCb6zyTW7FXYq7FXYq7FXYq7FXYq7FXYq7FXjv
/OUHkabzF5BGqWcfqX/l52u6AVY2rqBcAfIKrn2XAQyid3xjkG12KuxV2KuxV9I/84ieRpmvNR86
XUZWCNDp+m8h9p2KtPItf5QFQH3YdslFrmX0/kmDsVdirsVdirsVfMH/ADl35GmW807zpaxloJEG
n6lxH2XUs0EjU/mBZCfZR3yMmcC+bsi2OxV2KuxV2Kvs7/nF/wAjS+XfIB1S7j9O/wDMLrdlSKML
ZFItwfmGZx/rZMBqkd3sWFi7FXYq7FXYq7FXYq7FXYq7FXYq7FXzt/zk/wDnD9RtpPImhzf6ZcoP
05cId44XFRbAj9qQbv8A5O37RpElnEPljItjsVdirsVdir2n/nHH84f8JayPLusz08uapIBHI5+G
1uW+ESV7RybB/DZtt6yBYSD7HyTW7FXYq7FXYq7FXxx/zkd+cP8Ai3WT5d0aevlzS5CJJEPw3Vyv
wmSveOPcJ47tvtSJLZEPFsizdirsVdirsVfU/wDzjB+cP162j8ia5N/plsh/Qdw53khQVNsSf2ox
un+Tt+yKyBa5B9E5Jg7FXYq7FXYq7FXYq7FXYq7FXYq88/Ov81bX8v8Ayu00JWTXr8NFpNs1DRgP
incfyRV+k0HeoBKQLfDF5eXV7dzXl3K091cu0s80hLO7ueTMxPUknINqjil9Gfkl/wA436Vr/ld9
e84xzoupKDpNtE5idIevrttuZP2ARTjvvUUkA1ykyx/+cPvIBY8NX1UL2Be2J+/0Rh4UcZQ3m/8A
5xP8rR+ULpfK0l1J5ig/fW0t3MretxBrAQqxxrz/AGWp9qlTSuNKJPlOaGaCaSCeNopomKSxOCrK
ymjKyncEHqMg2LMUvrX/AJxm/OL9OacnkzXJ66xYR/7jLiQ73Fsg/uyT1khH0lP9UnJgtUg97wsX
Yq7FXYq8E/5yZ/OL9B6c/kzQ56axfx/7k7iM729s4/uwR0kmH0hP9YHASyiHyVkG1fDDNPNHBBG0
s0rBIokBZmZjRVVRuST0GKH1Z5Q/5xP8rSeULVfNMl1H5in/AH1zLaTKvo8gKQAMskbcP2mp9qtD
SmTprMkSn/OH3kAMOer6qV7gPbA/f6Jx4V4yxL87P+cb9L8v+V013yck7rpqk6vbTOZXeHr9YXYU
Mf7YApx32oaghMZPnTItitZ3l1ZXcN5aStBdWzrLBNGSro6HkrKR0IIxQ+5/yU/NW1/MDyus0xWP
XrALFq1stBViPhnQfyS0+g1HapmC1EU9DwodirsVdirsVdirsVdirsVSrzV5n0jyvoF5rurS+lY2
UZdztyZuixoDSru1FUeOKvgj8wvPWr+d/NF1r2pHi0p4WtsCSkECk+nEvyrUnuanvkCW4CmN4EvX
P+cfPygbztr/AOldUiP+GdKkU3IYbXM4+Jbcf5PeT22/arkgGEi+01VUUIgCqoAVQKAAdABkmtvF
XYq+Z/8AnKD8nv73z7oUHh+nraMfQLoAfdJ9DfzHIkM4l8z5FsRWmanf6VqNtqWnztbX1pIs1tOh
oyOhqCMUPu38oPzPsPzA8qx6gnGLVbWkOrWa/wC65qfbUHf05Oqn5jqDkwWoimc4UOxVg35v/mfY
fl/5Vk1B+Muq3VYdJs2/3ZNT7bAb+nH1Y/IdSMBKQLfCWp6nf6rqNzqWoTtc313I01zO5qzu5qSc
g2oXFL6Y/wCcX/ye/uvPuuweP6BtpB9BuiD90f0t/KckA1yL6YyTB2KtMqupRwGVgQykVBB6gjFX
xZ/zkH+UDeSdf/SulxH/AAzqsjG2Cja2nPxNbn/J7x+237NciQ2RLyPIs2Sfl7561fyR5otde008
miPC6tiSEngYj1Im+dKg9jQ9sIKCLfe/lXzPpHmjQLPXdJl9WxvYw6HbkrdGjcCtHRqqw8cm0pri
rsVdirsVdiqV+YvNHl7y3pzajruoQ6fZrt6kzU5GleKL9p2/yVBOKvHtc/5y78iWkrRaVp19qnE7
TEJbRNv+zzLSfegwWy4Cltt/zmR5daUC68uXcUP7TxTxSMPkrCMf8NjxJ4C8q/PT86ZPzC1G3tdN
Wa18t2ID29vNRZJZ2X4pZVRnX4a8UFTQVP7WRJTGNPK8DNkn5e+RdX87+aLXQdNHFpTzurkglIIF
I9SVvlWgHc0HfCAgmn3v5V8saR5X0Cz0LSYvSsbKMIg25M3VpHIpV3arMfHJtKa4q7FXYqsnhhnh
kgnRZYZVKSxuAysrCjKwOxBGKviL8+Pykm8heZPXsUZvLepsz6dJuRC3V7Zz4pX4K9V8SGyBDbE2
8vwMmW/lj+YeqeQ/NVvrVnWS2P7rUbOtFnt2PxJ/rD7SHsfaowgsSLfQeof85i+Uo5eOn6Df3Mfd
53hgP0BTNkuJhwF2n/8AOYvlKSXjqGg39tH2eB4Zz9IYw48S8BfPn5nfmHqnnzzVca1eVjth+606
zrVYLdT8Kf6x+057n2oMiSzApiWBk9Q/If8AKSbz75k9e+Rl8t6YyvqMm4EzdUtkPi9Pjp0XwJXC
AxkafbsEMMEMcECLFDEoSKNAFVVUUVVA2AAybUvxV2KuxVKvNXljSPNGgXmhatF6tjexlHG3JW6r
IhNaOjUZT44q+CPzC8i6v5I80XWg6kOTRHna3IBCTwMT6cq/OlCOxqO2QIbgbY3gS9U/Iv8AOmT8
vdRuLXUlmuvLd8C9xbw0aSKdV+GWJXZF+KnFxUVFD+zhBYSjb1W5/wCcyPLqykWvly7lh/ZeWeKN
j81USD/hslxI4CmWh/8AOXfkS7lWLVdOvtL5HeYBLmJd/wBrgVk+5DjaOAvYfLvmjy95k05dR0LU
IdQs229SFq8TSvF1+0jf5LAHCxTTFWG/mp+Zukfl95abVLxfrF7OTFptgDRppqV3P7KJ1du3zIGA
lIFvh3zh508x+b9Zl1fXrtrm5ckRpuIoUrURwp0RB/aanfI22gUkeBLsVdiqtaWlzeXUNpaxNNc3
EixQQoKs8jkKqqO5JNBih9z/AJKflVa/l/5XWGYLJr1+Fl1a5WhowHwwIf5Iq/SanvQTAaibeh4U
OxV2KuxV2KpH518naP5w8t3mgasnK2ul+CUU5xSj7EsZPRkO/v0OxxUF8D+dPKOq+UfMt75f1RQL
qzegkX7EkbDlHIn+S6kH26HfIFuBtI8CXYq7FXYqnnkvyjqvm7zLZeX9LUG6vHoZG+xHGo5SSP8A
5KKCffoN8IQTT748leTtH8n+W7PQNJTjbWq/HKac5ZT9uWQjqznf26DYZNpJTzFXYq7FXYq7FXnn
51/lVa/mB5XaGELHr1gGl0m5agqxHxQOf5JafQaHtQghINPhi7tLmzuprS6iaG5t5GinhcUZJEJV
lYdiCKHINqjil2KuxVPPJ/nTzH5Q1mLV9Bu2trlCBIm5imStTHMnR0P9ood8NoIt9xflX+ZukfmD
5aXVLNfq97ARFqVgTVoZqV2P7SP1Ru/zBGSBaiKfJv8AzkL53m80fmTqCLITp2jO2nWKfs/uWImf
w+OXlv3Xj4ZEtkRs8zwMnYq7FXYqujkkikSWJzHJGQyOpIZWBqCCOhGKH39+UfnF/OH5e6RrczVv
ZIvRvv8AmIgJjkanbmV5j2OWBqIZhih2KuxV2KuxVDanqFtpunXWo3TcLWyhkuJ38I4lLsfoAxV+
eHm7zNqHmjzLqOv6gxa51CZpStahE6Rxr/kxoAo9hkC3AUk+BLsVdirsVTjyj5m1Dyv5l07X9PYr
c6fMsoWtA6dJI2/yZEJU+xwhBFv0P0zULbUtOtdRtW52t7DHcQP4xyqHU/SDk2lE4q7FXYq7FXYq
w/8ANzzi/k/8vdX1uFqXscXo2P8AzETkRxtTvwLcz7DEpAfAMkkksjyyuZJJCWd2JLMxNSST1Jyt
tW4pdirsVdir0z/nHrzvN5X/ADJ09GkI07WXXTr5P2f3zAQv4fBLx37Ly8cIYyGzzi5uJrm5luZm
5zTO0kjnuzmrH7zgSpYpdirsVdirsVfXf/OIE8r/AJe6pCzExxarJ6YPblbwkge1d8nFqnze64WL
sVdirsVdirBvzyupLX8pfM8kf2mszEf9WZ1jb/hXOJSOb4IytudirsVdirsVdir73/I26kuvyl8s
SSfaWzEQ/wBWF2jX/hUGWBpPNnOKHYq7FXYq7FXhX/OX88qfl7pcKsRHLqsfqAd+NvMQD7V3wSZQ
5vkTINrsVdirsVdiqrbXE1tcxXMLcJoXWSNx2ZDVT94xQnHnny/L5e846zokiFPqN5NFGD3iDkxM
PZoyrDCVHJIsCXYq7FXYq7FX2n/zi35el0n8q4LmZSsmsXU18ARQ+n8MCfQVh5D55MNUju9dwsXY
q7FXYq7FWP8A5haFLr3kbXtHhHKe9sZ4rceMpQmP/hwMVD88GVlYqwIYGhB2IIytuaxS7FXYq7FW
1VmYKoJYmgA3JJxQ/Q/8vdCl0HyNoOjzDjPZWMEVwPCUIDJ/w5OWNJZBirsVdirsVdiryL/nKTy9
Lq35Vz3MKlpNHuob4gCp9P4oH+gLNyPywFlE7vizINrsVdirsVdiqe+RvL8vmHzjo2iRoX+vXkMU
gHaIuDKx9ljDMcIQeT6H/wCcp/youL+NfPWjwmSe2jEWtwRrVmhT7FzQbn0x8L/5ND0U4SGES+W8
i2OxV2KuxVmP5V/lvqnn3zVBpVsrJYRlZdUvQPhhgB336c3pxQdz7A4QGJNPvfTtPs9O0+20+yiE
FnZxJBbQr0SONQqKPkBk2pEYq7FXYq7FXYq7FXxr/wA5I/lRc+V/M83mPT4a+XtZlMhKja3unq0k
TeCuaun0jtvEhsiXjORZuxV2KuxV7N/zjd+VFz5o8zw+Y9Qhp5e0aUSAsNri6SjRxL4qho7/AEDv
tIBhIvsrJNbsVdirsVdirsVQ+o6fZ6jp9zp97EJ7O8ieC5hbo8cilXU/MHFXwR+an5b6p5C81T6V
cqz2EhaXS70j4ZoCdt+nNK8XHY+xGQIbQbYdgZOxV2KuxV9Sf84sflRcWEbeetYhMc9zGYtEgkWj
LC/27mh3HqD4U/yanowyQDXIvowgMCCKg7EHoRkmD55/NL/nFaz1O4m1byRLHYXUhLzaPMStszHc
+g4B9Kv8hHHw4jAQzEngWuflN+ZOhzNFqPly+Xj1lhha4i+iWH1I/wAcjTLiCW23kfzrdSiK20DU
ppT0SO0nY/cExpNh6d5G/wCcWvPmtzxzeYAvl/TKgv6pWS6detEiQkLXp+8Ip4HDwsTN9UeSfI3l
zyZokej6FbehbqeU0rfFLNJQAySvtyY0+Q6AAZJrJT/FXYq7FXYq7FXYq7FUJq2kaZrGnT6bqltH
eWFyvCe3lXkjL13Hsdwe2KvmH8xf+cTdYtZpr7yROL6zYlhpVy4juI678Y5WokgHbkVP+sd8iYsx
N41qX5defdMnaC+8u6jA6mlTaylT/quqlWHuDgpnYdpv5defdTnWCx8u6jO7GlRayhR/rOyhVHuT
jS2Hsv5df84m6xdTQ33necWNmpDHSrZxJcSU34ySrVIwe/Esf9U74RFgZvp7SdI0zR9Og03S7aOz
sLZeEFvEvFFXrsPc7k98kwReKuxV2KuxV2KuxV2KpB528jeXPOeiSaPrtt69ux5Qyr8MsMlCBJE+
/FhX5HoQRioL5X88/wDOLXnzRJ5JvL4XzBplSU9IrHdIvWjxOQGp0/dk18BkeFsE3mNz5H862spi
udA1KGUdUktJ1P3FMFMrCZaH+U35k65MsWneXL5uXSWaFreL6ZZvTj/HGkcQe+/lb/zitZ6ZcQ6t
53ljv7qMh4dHhJa2VhuPXcgerT+QDj48hkgGJk+hgAoAAoBsAOgGFg//2Q==</xmpGImg:image> - </rdf:li> - </rdf:Alt> - </xmp:Thumbnails> - </rdf:Description> - <rdf:Description rdf:about="" - xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" - xmlns:stRef="http://ns.adobe.com/xap/1.0/sType/ResourceRef#" - xmlns:stEvt="http://ns.adobe.com/xap/1.0/sType/ResourceEvent#"> - <xmpMM:InstanceID>uuid:651d8d31-f036-4c44-b437-97d1a0d4a5a3</xmpMM:InstanceID> - <xmpMM:DocumentID>xmp.did:48D0D506C514E71195F4EDFDBE8CC04C</xmpMM:DocumentID> - <xmpMM:OriginalDocumentID>uuid:5D20892493BFDB11914A8590D31508C8</xmpMM:OriginalDocumentID> - <xmpMM:RenditionClass>proof:pdf</xmpMM:RenditionClass> - <xmpMM:DerivedFrom rdf:parseType="Resource"> - <stRef:instanceID>uuid:46a98622-f87f-704a-a86a-73dd28eea131</stRef:instanceID> - <stRef:documentID>xmp.did:A4D5BD7DC365E111835FF2FC000FBA2B</stRef:documentID> - <stRef:originalDocumentID>uuid:5D20892493BFDB11914A8590D31508C8</stRef:originalDocumentID> - <stRef:renditionClass>proof:pdf</stRef:renditionClass> - </xmpMM:DerivedFrom> - <xmpMM:History> - <rdf:Seq> - <rdf:li rdf:parseType="Resource"> - <stEvt:action>saved</stEvt:action> - <stEvt:instanceID>xmp.iid:48D0D506C514E71195F4EDFDBE8CC04C</stEvt:instanceID> - <stEvt:when>2017-03-30T06:16:51+09:00</stEvt:when> - <stEvt:softwareAgent>Adobe Illustrator CS6 (Windows)</stEvt:softwareAgent> - <stEvt:changed>/</stEvt:changed> - </rdf:li> - </rdf:Seq> - </xmpMM:History> - </rdf:Description> - <rdf:Description rdf:about="" - xmlns:illustrator="http://ns.adobe.com/illustrator/1.0/"> - <illustrator:Type>Document</illustrator:Type> - <illustrator:StartupProfile>Print</illustrator:StartupProfile> - </rdf:Description> - <rdf:Description rdf:about="" - xmlns:xmpTPg="http://ns.adobe.com/xap/1.0/t/pg/" - xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#" - xmlns:xmpG="http://ns.adobe.com/xap/1.0/g/"> - <xmpTPg:HasVisibleOverprint>False</xmpTPg:HasVisibleOverprint> - <xmpTPg:HasVisibleTransparency>False</xmpTPg:HasVisibleTransparency> - <xmpTPg:NPages>1</xmpTPg:NPages> - <xmpTPg:MaxPageSize rdf:parseType="Resource"> - <stDim:w>256.000000</stDim:w> - <stDim:h>256.000000</stDim:h> - <stDim:unit>Pixels</stDim:unit> - </xmpTPg:MaxPageSize> - <xmpTPg:PlateNames> - <rdf:Seq> - <rdf:li>Black</rdf:li> - </rdf:Seq> - </xmpTPg:PlateNames> - <xmpTPg:SwatchGroups> - <rdf:Seq> - <rdf:li rdf:parseType="Resource"> - <xmpG:groupName>初期設定のスウォッチグループ</xmpG:groupName> - <xmpG:groupType>0</xmpG:groupType> - <xmpG:Colorants> - <rdf:Seq> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>ホワイト</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>0.000000</xmpG:magenta> - <xmpG:yellow>0.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>ブラック</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>0.000000</xmpG:magenta> - <xmpG:yellow>0.000000</xmpG:yellow> - <xmpG:black>100.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>CMYK レッド</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>100.000000</xmpG:magenta> - <xmpG:yellow>100.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>CMYK イエロー</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>0.000000</xmpG:magenta> - <xmpG:yellow>100.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>CMYK グリーン</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>100.000000</xmpG:cyan> - <xmpG:magenta>0.000000</xmpG:magenta> - <xmpG:yellow>100.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>CMYK シアン</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>100.000000</xmpG:cyan> - <xmpG:magenta>0.000000</xmpG:magenta> - <xmpG:yellow>0.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>CMYK ブルー</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>100.000000</xmpG:cyan> - <xmpG:magenta>100.000000</xmpG:magenta> - <xmpG:yellow>0.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>CMYK マゼンタ</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>100.000000</xmpG:magenta> - <xmpG:yellow>0.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=15 M=100 Y=90 K=10</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>14.999998</xmpG:cyan> - <xmpG:magenta>100.000000</xmpG:magenta> - <xmpG:yellow>90.000000</xmpG:yellow> - <xmpG:black>10.000002</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=0 M=90 Y=85 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>90.000000</xmpG:magenta> - <xmpG:yellow>85.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=0 M=80 Y=95 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>80.000000</xmpG:magenta> - <xmpG:yellow>95.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=0 M=50 Y=100 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>50.000000</xmpG:magenta> - <xmpG:yellow>100.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=0 M=35 Y=85 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>35.000004</xmpG:magenta> - <xmpG:yellow>85.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=5 M=0 Y=90 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>5.000001</xmpG:cyan> - <xmpG:magenta>0.000000</xmpG:magenta> - <xmpG:yellow>90.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=20 M=0 Y=100 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>19.999998</xmpG:cyan> - <xmpG:magenta>0.000000</xmpG:magenta> - <xmpG:yellow>100.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=50 M=0 Y=100 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>50.000000</xmpG:cyan> - <xmpG:magenta>0.000000</xmpG:magenta> - <xmpG:yellow>100.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=75 M=0 Y=100 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>75.000000</xmpG:cyan> - <xmpG:magenta>0.000000</xmpG:magenta> - <xmpG:yellow>100.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=85 M=10 Y=100 K=10</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>85.000000</xmpG:cyan> - <xmpG:magenta>10.000002</xmpG:magenta> - <xmpG:yellow>100.000000</xmpG:yellow> - <xmpG:black>10.000002</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=90 M=30 Y=95 K=30</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>90.000000</xmpG:cyan> - <xmpG:magenta>30.000002</xmpG:magenta> - <xmpG:yellow>95.000000</xmpG:yellow> - <xmpG:black>30.000002</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=75 M=0 Y=75 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>75.000000</xmpG:cyan> - <xmpG:magenta>0.000000</xmpG:magenta> - <xmpG:yellow>75.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=80 M=10 Y=45 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>80.000000</xmpG:cyan> - <xmpG:magenta>10.000002</xmpG:magenta> - <xmpG:yellow>45.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=70 M=15 Y=0 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>70.000000</xmpG:cyan> - <xmpG:magenta>14.999998</xmpG:magenta> - <xmpG:yellow>0.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=85 M=50 Y=0 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>85.000000</xmpG:cyan> - <xmpG:magenta>50.000000</xmpG:magenta> - <xmpG:yellow>0.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=100 M=95 Y=5 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>100.000000</xmpG:cyan> - <xmpG:magenta>95.000000</xmpG:magenta> - <xmpG:yellow>5.000001</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=100 M=100 Y=25 K=25</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>100.000000</xmpG:cyan> - <xmpG:magenta>100.000000</xmpG:magenta> - <xmpG:yellow>25.000000</xmpG:yellow> - <xmpG:black>25.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=75 M=100 Y=0 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>75.000000</xmpG:cyan> - <xmpG:magenta>100.000000</xmpG:magenta> - <xmpG:yellow>0.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=50 M=100 Y=0 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>50.000000</xmpG:cyan> - <xmpG:magenta>100.000000</xmpG:magenta> - <xmpG:yellow>0.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=35 M=100 Y=35 K=10</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>35.000004</xmpG:cyan> - <xmpG:magenta>100.000000</xmpG:magenta> - <xmpG:yellow>35.000004</xmpG:yellow> - <xmpG:black>10.000002</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=10 M=100 Y=50 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>10.000002</xmpG:cyan> - <xmpG:magenta>100.000000</xmpG:magenta> - <xmpG:yellow>50.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=0 M=95 Y=20 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>95.000000</xmpG:magenta> - <xmpG:yellow>19.999998</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=25 M=25 Y=40 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>25.000000</xmpG:cyan> - <xmpG:magenta>25.000000</xmpG:magenta> - <xmpG:yellow>39.999996</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=40 M=45 Y=50 K=5</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>39.999996</xmpG:cyan> - <xmpG:magenta>45.000000</xmpG:magenta> - <xmpG:yellow>50.000000</xmpG:yellow> - <xmpG:black>5.000001</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=50 M=50 Y=60 K=25</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>50.000000</xmpG:cyan> - <xmpG:magenta>50.000000</xmpG:magenta> - <xmpG:yellow>60.000004</xmpG:yellow> - <xmpG:black>25.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=55 M=60 Y=65 K=40</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>55.000000</xmpG:cyan> - <xmpG:magenta>60.000004</xmpG:magenta> - <xmpG:yellow>65.000000</xmpG:yellow> - <xmpG:black>39.999996</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=25 M=40 Y=65 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>25.000000</xmpG:cyan> - <xmpG:magenta>39.999996</xmpG:magenta> - <xmpG:yellow>65.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=30 M=50 Y=75 K=10</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>30.000002</xmpG:cyan> - <xmpG:magenta>50.000000</xmpG:magenta> - <xmpG:yellow>75.000000</xmpG:yellow> - <xmpG:black>10.000002</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=35 M=60 Y=80 K=25</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>35.000004</xmpG:cyan> - <xmpG:magenta>60.000004</xmpG:magenta> - <xmpG:yellow>80.000000</xmpG:yellow> - <xmpG:black>25.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=40 M=65 Y=90 K=35</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>39.999996</xmpG:cyan> - <xmpG:magenta>65.000000</xmpG:magenta> - <xmpG:yellow>90.000000</xmpG:yellow> - <xmpG:black>35.000004</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=40 M=70 Y=100 K=50</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>39.999996</xmpG:cyan> - <xmpG:magenta>70.000000</xmpG:magenta> - <xmpG:yellow>100.000000</xmpG:yellow> - <xmpG:black>50.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=50 M=70 Y=80 K=70</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>50.000000</xmpG:cyan> - <xmpG:magenta>70.000000</xmpG:magenta> - <xmpG:yellow>80.000000</xmpG:yellow> - <xmpG:black>70.000000</xmpG:black> - </rdf:li> - </rdf:Seq> - </xmpG:Colorants> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:groupName>グレー</xmpG:groupName> - <xmpG:groupType>1</xmpG:groupType> - <xmpG:Colorants> - <rdf:Seq> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=0 M=0 Y=0 K=100</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>0.000000</xmpG:magenta> - <xmpG:yellow>0.000000</xmpG:yellow> - <xmpG:black>100.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=0 M=0 Y=0 K=90</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>0.000000</xmpG:magenta> - <xmpG:yellow>0.000000</xmpG:yellow> - <xmpG:black>89.999405</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=0 M=0 Y=0 K=80</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>0.000000</xmpG:magenta> - <xmpG:yellow>0.000000</xmpG:yellow> - <xmpG:black>79.998795</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=0 M=0 Y=0 K=70</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>0.000000</xmpG:magenta> - <xmpG:yellow>0.000000</xmpG:yellow> - <xmpG:black>69.999702</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=0 M=0 Y=0 K=60</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>0.000000</xmpG:magenta> - <xmpG:yellow>0.000000</xmpG:yellow> - <xmpG:black>59.999104</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=0 M=0 Y=0 K=50</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>0.000000</xmpG:magenta> - <xmpG:yellow>0.000000</xmpG:yellow> - <xmpG:black>50.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=0 M=0 Y=0 K=40</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>0.000000</xmpG:magenta> - <xmpG:yellow>0.000000</xmpG:yellow> - <xmpG:black>39.999401</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=0 M=0 Y=0 K=30</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>0.000000</xmpG:magenta> - <xmpG:yellow>0.000000</xmpG:yellow> - <xmpG:black>29.998802</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=0 M=0 Y=0 K=20</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>0.000000</xmpG:magenta> - <xmpG:yellow>0.000000</xmpG:yellow> - <xmpG:black>19.999701</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=0 M=0 Y=0 K=10</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>0.000000</xmpG:magenta> - <xmpG:yellow>0.000000</xmpG:yellow> - <xmpG:black>9.999103</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=0 M=0 Y=0 K=5</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>0.000000</xmpG:magenta> - <xmpG:yellow>0.000000</xmpG:yellow> - <xmpG:black>4.998803</xmpG:black> - </rdf:li> - </rdf:Seq> - </xmpG:Colorants> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:groupName>輝き</xmpG:groupName> - <xmpG:groupType>1</xmpG:groupType> - <xmpG:Colorants> - <rdf:Seq> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=0 M=100 Y=100 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>100.000000</xmpG:magenta> - <xmpG:yellow>100.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=0 M=75 Y=100 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>75.000000</xmpG:magenta> - <xmpG:yellow>100.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=0 M=10 Y=95 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>0.000000</xmpG:cyan> - <xmpG:magenta>10.000002</xmpG:magenta> - <xmpG:yellow>95.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=85 M=10 Y=100 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>85.000000</xmpG:cyan> - <xmpG:magenta>10.000002</xmpG:magenta> - <xmpG:yellow>100.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=100 M=90 Y=0 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>100.000000</xmpG:cyan> - <xmpG:magenta>90.000000</xmpG:magenta> - <xmpG:yellow>0.000000</xmpG:yellow> - <xmpG:black>0.000000</xmpG:black> - </rdf:li> - <rdf:li rdf:parseType="Resource"> - <xmpG:swatchName>C=60 M=90 Y=0 K=0</xmpG:swatchName> - <xmpG:mode>CMYK</xmpG:mode> - <xmpG:type>PROCESS</xmpG:type> - <xmpG:cyan>60.000004</xmpG:cyan> - <xmpG:magenta>90.000000</xmpG:magenta> - <xmpG:yellow>0.003099</xmpG:yellow> - <xmpG:black>0.003099</xmpG:black> - </rdf:li> - </rdf:Seq> - </xmpG:Colorants> - </rdf:li> - </rdf:Seq> - </xmpTPg:SwatchGroups> - </rdf:Description> - <rdf:Description rdf:about="" - xmlns:pdf="http://ns.adobe.com/pdf/1.3/"> - <pdf:Producer>Adobe PDF library 10.01</pdf:Producer> - </rdf:Description> - </rdf:RDF> -</x:xmpmeta> - - - - - - - - - - - - - - - - - - - - - -<?xpacket end="w"?> -endstream endobj 3 0 obj <</Count 1/Kids[7 0 R]/Type/Pages>> endobj 7 0 obj <</ArtBox[8.0 72.0 248.0 184.0]/BleedBox[0.0 0.0 256.0 256.0]/Contents 8 0 R/LastModified(D:20170330061653+10'00')/MediaBox[0.0 0.0 256.0 256.0]/Parent 3 0 R/PieceInfo<</Illustrator 9 0 R>>/Resources<</ColorSpace<</CS0 10 0 R>>/ExtGState<</GS0 11 0 R>>/Properties<</MC0 5 0 R>>>>/Thumb 12 0 R/TrimBox[0.0 0.0 256.0 256.0]/Type/Page>> endobj 8 0 obj <</Filter/FlateDecode/Length 263>>stream -H��SMO�0��W��uR;�W�4`�3��*�� �>NZ�&��!�(v�헼���X�=���G���z���;|��ׄ�GJ� �A��M����G(���a��ʈ��ax�h�13en�u�|ű��Y�ʇ���=Z��uN.���1v��A_8J��9���9V2�J��� V�++�iN�,^N�O������z����]?���Xa��[�b��`��&G}tdC���?H!��i)��y�_��<\����Z?�>��� -endstream endobj 12 0 obj <</BitsPerComponent 8/ColorSpace 13 0 R/Filter[/ASCII85Decode/FlateDecode]/Height 32/Length 121/Width 32>>stream -8;Z\q_%"1&$j/Xd?Le$$YYLHa(6Lpsqr_95O>2DC%NfX$Np=Ie)QSce&CkFM;Vllc -UCjT1mC9>aMqMb?I;lQ*h_iThApH<qH1u@TH[\-ReLqmLf)Q\Y`o6~> -endstream endobj 13 0 obj [/Indexed/DeviceRGB 255 14 0 R] endobj 14 0 obj <</Filter[/ASCII85Decode/FlateDecode]/Length 428>>stream -8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 -b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` -E1r!/,*0[*9.aFIR2&b-C#s<Xl5FH@[<=!#6V)uDBXnIr.F>oRZ7Dl%MLY\.?d>Mn -6%Q2oYfNRF$$+ON<+]RUJmC0I<jlL.oXisZ;SYU[/7#<&37rclQKqeJe#,UF7Rgb1 -VNWFKf>nDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j<etJICj7e7nPMb=O6S7UOH< -PO7r\I.Hu&e0d&E<.')fERr/l+*W,)q^D*ai5<uuLX.7g/>$XKrcYp0n+Xl_nU*O( -l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> -endstream endobj 5 0 obj <</Intent 15 0 R/Name(��0�0�0�0�� �1)/Type/OCG/Usage 16 0 R>> endobj 15 0 obj [/View/Design] endobj 16 0 obj <</CreatorInfo<</Creator(Adobe Illustrator 16.0)/Subtype/Artwork>>>> endobj 11 0 obj <</AIS false/BM/Normal/CA 1.0/OP false/OPM 1/SA true/SMask/None/Type/ExtGState/ca 1.0/op false>> endobj 10 0 obj [/ICCBased 17 0 R] endobj 17 0 obj <</Filter/FlateDecode/Length 381584/N 4>>stream -H��� 4�]��ƾdɖ-È�c�.j��}���[v*�E�F��5�-%Kxl����"J��x��t����>�}��=����n�w��������.���DQ������*�XTh��<����xp���N��wL������<1�xH�D�%()��&�9��\�6��u�PBGaؿ���{r��0����@� V��#I��6�=fo4��o�@Z���:e�ƣ��:��8"�*PM�x��O��"aBH�Q�%�x_"g���8w�*4���Ȁ �=��������(��@�E���P3�����aLi -�S�Q-��3h�i���r���L��XZN�&�؝8tO�s -pQs�s����}r��/�?V� ��<�VK��r@� b�g��g%��H��z&�(S+[)W*_�S�K��)F(+T��>ju �97M-��6,f�&:FH#]ËFz��f��6Fv�&N��f���XK�U��5���yv�����&�]6\�(V����������λT���;�����Ʃ�-~��%����A{!ܡ�+Fa^�1�-Q�۱�q�W��a���&�ޘJ�LfJ����i���(���h�Z6u�`�R����Ȃ��六E#��%��9J%h�Y��V�T�T�V�>\�٫;�H�N�sC`�ͦ��'�Z��q��?7j�j�������d�v����}.������M�_/}���W�0����|��};�8�>��.�p�x��}�Rʇ菄eԊ�'�U������/��Ӿ�6P��[���0�|���{���.{����?��E�!��#��#����+*�4E]@s�V���n���!�ѓI�����$�������8jN�p�qA���Wx�ysN���� -�, -v��F�"b���+�~��g�Ń$\�"%e������d&e��ȧ@��`�peE��2��ʒ�ڀz�F��\�$���|(m{K����E==�����!�j,o"o -5��+[hX"���-l�m����=|�NW��\R]s�JQ��N�a��U̶��%._?��ZXS7G�&Vt���փɡlWDÔ� #P�AQ��e1���q{�8�KƟO����T��<e:u7�+~�*#�vffs�L�A.w���|LA�݂§ES���q�W(5�_�R^]1X����D �U�(�qC�p×&�f�'Z���<��Z�|�m��y��z�m7���ٽU}���˃?_������G�ƒƋ'�'_O-�!�坁��y����]�X�{�d��C������OS�3k�_h����m 6�����w��}��m�ѿ7�?�s����� ���/�m�A*�h��M2�+���~���1�ɛY���e�D-k,�-�;�c�d g��$�g����M>~��X�PpZ��t�0 l)� �!�k8�%J�!).�,�UzD�I��\��7�TA&g��(�+ *7��������j8�3�TӒ>/t�A���������}y�[�U�Ѡְ¨Ըؤд��y�E�e�U�u�M�m�]�}��=�2�j�G.M�O��Q=�A�Q�i�E̚���/����X��&NoM@�a��9Ae��!}��W>��#8"!QJ�z1N����Ws��]���#�=I*��v3+�6������L�,X�I�O�;m�3��<E��%�{)�+K{,�B�jU��ć�5��=u���y`�&M�ͱO�[��?[}N���.ۡ����u�;���}��}�X!/�_Y����y4:0�a�<�?���j�6i�p�an�����"�{�%�F]��+1�2V[�>~a]W���Y�5�}�M���.�G�^�~�ϡ���O���T��� -�2���M��ޤ)���T�Vz��c&� 3-s;K� ��u� -64�����v����\y�.<`�%��S��`|{���M�x��O���""�E����3S���Z�-�,�,}C�YV^$7)_�V��I�A�i�:�e���*��Z��=��s��*Z\Z��G.�!Ҵ�����ݻ8�ס_j�dH0r0F�H�r��2[5�贬�ʷN� ���s�7s�qTq�u���tcFѠ��}��;��� � ��K[>�|�`���C< N�T� 6 q�\!�E�'G�E�E5FwŌ�.�m\%� �����/�@�R�d�w�������őڒ���2�I�a�z�����ɝ���>�^����Ħ��4�\�����֯�.�]������&�\���̻�C�������A��� �Tǜ���2�z����_Ю���QԤ���Lء���Kܞ���A�����.��:���E���L���N���P���V���2�������������P������������>��������� �y_�P�]�Ȁi>����9[�ل%��}ߌ��@}����~���V~P�z��~���w���[���S=�׆p��ۉ/�7|&�L��|T�rő|��˫K|�����}v��v.~��Z�~�=]���������zϦ��{�×{i�@�u{�яe|��t�}5�+Y�~��<�_��F�<�n��yγ��z����z��ҧ�{ -�W�{��As�|���Y}y��<�~Ҙt}�����y�-�5y]���^yͳ���zf���{ ��s {��oX�|���<;~_�{����x��}��x�ū�%yA�{��y��z���r[{���X|���<�~�LЀ��!�Ix$���xg���(x�ɳ��y��<�[zR�{q�{A��W�|Q�l;�}��p�X�5�=w��D��x#�Vx��|��yA�>��z��qA{�WH|��;�}��E��D�ۉ/~�)�=}��f�]~�|��~3�H�~�o���~�T����7�@����v�w�:�P�'� -�m���m�������'���{�ƅ�m�����S����B79�[����E�����R�ք����T�Z�T�����k�ロ��l����`R����T6����I��8�� �s���σў��l�J�W���N�q�ĕ�k��pQ�����6F�ّVg�R���L��������тs�{���$�|�2���j����Q:�]�L5��B�����⽂ϻDʪ�<�H�~�˯˚a���;�o�ZiƁ��cP��ڟz5��Ş`��6�l�L��Ƀ���±n�G�I�m��l�t��� i.���PU�p�|5��`�m=�r�\�Q��ԒȖ�U�u����������e�̀��lh��̳�P�$��5n�� j� �z�c����ׁؖ�耦�o��n�&�@�h��h<����O����59��������� ��||���|���%|�����|�}��e}^eƋ`}�L���~�06�������8���:���Ô�V�|��<��d��\�MK��ۄF0V�S� ��e��ֈ�z���:�ё*�y�T��e�%�p{�+�cM�h��J����;/�}��)�J�����B�B�o�������\���Ί4�yʼnK��bH����J�9�{/�������X���c�[����l�B���b���y��x�����al���Iq���/]��o���.��������B�7����?�y���wLj���`��X�sI��V/4����e��i���8�~���ż�+�~�I���l�w���\`2��}H����j/�����[�������}��k�9�p�#�������v��9�x_�����Hh�F�f/��ʟ���#���ѹA�1Ҷ�ۈ�ˁ�l���"v -����_^�b��H���.ӆ�������ɧ�M{Z�G��{k�ȗ3{����{�r�|s\.�`}D]�I}�)���#��с��|�І8�ǘ�!�u�ńO�Ǔ���q���l[�f�JC��g�o)L�Ʉj_����ŭ���-� ���ޔ��&�X����pz����Z����B͎��I) ��= �e��X�O�Y����(�^���W������oW��Y6���oBA�Ǐ{(��� ��l�{�.�f�r�O��M�'�럠�ّ�nnb�W��X}���fAƍ�(��_�N - ������Dz��E�w���)�Y����u�m��ˡ�W�k��Ah����(��ɚ� -q���&�R���p��ܗ^�岓�;���m�]��Wm����A!���(��L�p -��c�F�T���Ó��V�����f�����?����W��5�Jݛ�JTJ��"�\��"��A��D�A�13�af�w�}�0;�(�::-*�Q�O��N�۹����y;ݳIt���S+�Z��C^�v�<I�gQ���8��|�;��>Ȉ�{�] �v�����Lu; !�Z����L�Y�ʗ&$���G�f��|�a��4���'�,��F����6NoyI�]�لf�$ZZp��~�˔oI���S���U��D� �Л��}K�nj��C(?���l���?[N���v���>E^ =HK���\kA.?�&���;�b�W��F�4D��n���� ���,���#7�~��IR�3�� (�,��e�u��j`F��n���%,3��������N��&��(����J��.O@E\��>'�漉ꂒ��>̏p�&K�.�Ǫ�����uL���$]�B�V͟W�(�圴xBR̺*��a� -���Y��f��y����N*�>!��-j���潥{8��Cp��r� +N�ĘR�v�����n�e< ���Ic��w���ߩ!���.�tpm�S'� ��c�,�FM@��~�������$�iH��f��E�t}�f-����hNd??���{�X �y_�>F���m�q����+c�K{�$�K؇��`&qT ��b�nr�,{�� qU��擄�|ܱB��b��7p�fsi#\��%�Q�/���Ί|�_I��p�}�@����F �B��WD#������&O�"�{\g���Z����(�RSN?�$Q�¿M��G�,��rA����U���p�X ���ʓ9��W�ixuz���>/�T�����Ʒ��;��k�d���'e�1V�ń��b4n����_��DX4�. _/p���0x��'k��f�Ķ��*w�-�Nk�.s -s�7�)iE�6���a�?K�ߧ�K4;rg���)x��`pi�g4N&��͊D��Zɴ�$�7a� - -��}V��I�.�>w:��0˖M@7�o���?�n��nU���|/�OB7M'`�iM���y�v�����d�&�r#�Dp�+��Q��1�fh�2_���$�gG��л��6'����+��ɩb�X&C�a<��H�L���cJW��Qg�W�Љú5X�o�0�ɋ��[�L�:�� ��=8����B����j �䌟Ha���i��c�z���#���^Ө�#��)y��~ݓ�Ŷ��6���u�����{�}�~���$%�Zk���x#*QN��}ա�OY��M�b�Ҋu?�cjԻ���9ڸ�� z�ZP�����b����&!V��:Ê�d^Y��l��W�ñ��r'(��E�[�Z��QRD������n!(�o6���RMZ�X�YyMX,�e ���F�EkA�5Dy���,T��>6�-��SqZWuD�}���`H:)� -_r�H�h���7��zI�M��ߊڍV-�SC5}M;�ŊI�Kp��\���G�O��C��D�������g�#��H:Sί��� ��ߧ��P;^D*ĭ��KC��lDS"GD� *�} T�<�N��ٞ/ :��}�=�>��HƦ�������]�����_}O<���duw��nf�o��-�E�?��xG�}1��j3�GE�ԅ��:[�X�| :k{��{�� "?�@���(�>C2��=Aw�|}e��b���#�ڐz^���Ofafs���0���O���5��&���Z�;M��^9�(����"JeP$.��s�Ù +Z��C��P��������+��6K�O]��S��\Y��[&�-�s2�;6Hp�K-";L��\p�\�8zب��[m_�;�i��B����&}�睾<3��^��`�(�b� ����O���� ��e��^SՈ2 �����ue����3�I����M��9�O�R�3�O�j�G��Wb�SQ�"jP� �8Tv�'��*��v�����_�.` ��{�g5�T���j.dפ�~@�3Pz��F�������{�v�¨s��~�^�ውs��G�l+T��$�0l�uk;Q��1:D@���@��Ŵ0��Z�����ޏ�B�7D��ERE��i�sfΙs���4�i�w�f�I��i�����s��ǃ�q WO<�|��^�z[���5f��ުo�;� �W�M����]�>ƠN��ԫ�)�~��=3ޓ�у����2�_ڴ�����u���k�k2s�Sw@=����<+��`T�"F�.v�D�z���]#Z��%�J�7��Մ�3zz��:�:����7�/���Ԇ�/m/��&���mp��]�m�4���l��:�S8���T�?�_P6�[�f��/��U*y��n����_m�5"�� K�S�:�3�0�����N�y\�:���Ɗ�Eœ�n�����a7��: ��u:գS�G�+2���8�q�s ��U���~.���}n��e�m)7�������Щ��#�����>�[�px͚��?lT����BZ�n�pS7AeGl+z��7�#�G�V�z�(\��#���ˉ�N>Y=G�N~Ë�YPiA4����z*M�g@B�b�����$[���K�xI����|N��:80���4S�Ak�м�����Ð�E#��>)�6�ח5�����ė� ���d;nd�i�9gS�w��ro5��),���eowgb����"���6��R�)"�B $=��M6� s�� ��zQ;����@~��]� 3wezֆ�] �\���Y�Qi$�I�T���pAG(@��o�n�[^�3����ӯ�$~�[��:���\,!���a9g]�JL� V�ņ���O�f/>�W�ɲ�6-|�1M�ml!�VS�lϨP���2�-9�?�E�o�UD���+$����$ž��r�&L�mK�čN�;�` -u�co@�4�O-��ҭ�D��Q��S�$��P��ȣ�>�sԷB��,u�)<�-m��M��D��A���n�0�U^��B:O��+�=wȼjS+j�0�)����#�^���1�%>/�Œ���vrL���x�B�T�]ְ'�m�4z��+�����Ls�r�5c��t����`1��;�n-��Ӟ�`'��n��C��;A���/ -��O5:��_�*Ѕ����Gٹ���B� Ȋa�>��δEgMgY>���=�OE=��ʂކp��|�<;��(������la�hp�`�G%ß��,��~H�-��h5��ʑ����2��q�*iEJ?V�ŭ���?<>�$�=p ?�oV]���@�P|銣�4��˧m���B;Y=In����+6�nS~�$�nѐ�6`I2f��l�OURS�6� *�ݰVD�(/d�S;�]H�4�I;i�5�����l�U�R:e����-�ǥ,��u&RW���R7jF�ߩ!�Q�A�Jq��'�X���B����3`G���Y�-n�d1E�H�VP�w�5攱�<a8ոw%��J�t����r���b�Z�B�+s�q�;5&0 ��T<9�G�+nJ��w�IL%İ�I'p�a�w�=P|���>L���y������6'���\~�] q�1-��x����s(1E���mb̍�2\�<cΏ�8v|��ls.ZڃYp��ch�:��h�Oѓ�E��?r��v�uF7 �JD14�)�u�x��y-a�9��.������. ������q����`-(q�ZQ��e2XF����;��@�}j,<y=.�.���.Z]4f��>A������ZdE�~Q�@�(�\T1�"�]JS�2��4l )p)��z��[�o}��C�=+��D����XɰYë�wP}m�Y=(���?�T���0N�m��ǜ{Y��c���g�@;��[�&G@GeY����kȣ��K2�]*7 D6c��v,���& ,u-C�}�~�R�*�4��}G�#�֩߇������\�L�0�ԉ\f��G��ubQ,A6�\�Bk� �I�/��+>?�ɩ���d=ۉ�6c�ƺ( h˳ʩ���&@��+�f :��/�}6�y2 -Ԭ�/�˴jO}�������\iv v�n'B�^��C:�c���c�K�no^�~�/���6���3�!��"F��]�i�;�T������߄����3\�M$�GWD�V16����t�r�Kq�`��"�,]�$!��I��^IH!-J�)��zXPV�r����z��>���=m�4Ś2ᶍr��tqUJ:f��Y��S{�ץx��[�+����o�eS`n&2���jȟU�zǂ?���t�fF�������Z����G�j��3]�k�w�d-2�����m���KYFz����ޞ^�J'��t"u�~�4����>�ʮ�[�Vל�.U�q���mx���u -�P���X�7TF{r�e�ɔ���7t����2x�\�n�t�N���m�![rt�7��O�2��d�!-���Y��Z�RC����-���=!��9���7@@��v1h$g5+���E����sU�Ū�U��B��G�\x�E}1|�kq��gM��Ŗʀ�l>�y��<?!���xvJ8�?v~���G�����fŷ�۳��]Q�t����P��nR:��ŞU��WM��9/���� �r�g� ��*�7�[�5{e���.�;$��t~�h3�t�����s3i�86����@���c�Q�P��y�˅i� -�=1_:��E�*�y͂-��גhb1ن�G��M~v��L%j7ӊqo�+���g���Ξ�'HR�X��r����k��Sb���i���u�X0��������Qz>��i���4t!.^��if��K�2�k^F:�/��b�[� ���`߱j��q� -���9k�&:p76W���g�K`��~���v�it�*oaY��2����»W(�-�}=��7���AրY�'�ik���pl�Y_Wz�*�Ka_^����xGZi���|mo��2;��S��ҹ��6YD����n����t�����1V~_2���;P�j?�.�רpijyr �pxB��n᭼f�j|�c��z�Nd-@<���,T�+��j�IrUOV@�{x��)m+4��z� )��7Q -y3�V!�Yk�,��U[,T$���Is���D#�p����*��4��*(Q;���Y�<P�-�+����g|%b��C�_X����#��^Y<��i!��Q�@x�v�����O�sE��;2L�E<&�������n|G��W����_�@�AD��;��թʜ:t�WY�����8^u<.UpY� l�ǒ�ya����� -e\#�:տ��V[)W�,)�Ig�/R��ֆ�8{�Y��0!�_��Vΰr"r���F![���]_�V��<-��^1�R��VoL��Ǵ)�0�o{ρe���s�`/� <G���@~)��DΟ��6��&N�K -��p4��#��*L��ëƵ8��D�}���@���Џ��ĥ�����Eٱ�6��� �n����$�R�;��@1��ר��<D��ZO�J -nΈ#�d��Γ�inW<�U� -\�i����]r}�l�r啊���"j�����oȠ=Ϝ�gQI8R� }��VS9����AY�i�8T�Q����� �ޫ��%dRu,����XQo�"�� �;���[i!�lټ�ڈ�1Do�U���d]�~�q�x�:PȊ2)&�ׂle[�vK$ -��_e�P -����]�_l�!@� hcTW�T -�(�#_�M -�\=�ů�Ci��x%�\6ε���uO�24���U!:MT�|\�!��S5��"� w��B�k�]u�x�(�k7d�5-�'�U�M�QK�� u��kM����B�Ø��F�7 �y�_�g�t�_M���/p��i>Zpm6&���6,rEi�E�ŏ ��?�u��Qر��K���m�Xo̳�al����36��Z?��A)�V8+�}�Q���j#�y��j���-O+�z#H��44ۼ�tbjPV��曖H@Q+;q��y����SS������$�>ڥ�n��fT��ݠY�l��_�H7�H���Bދ<�v��=�c����ljHg��XX�Z���-QO���v���~��BTG�<&w�mPwJmr^�8��h{�����SK�Ŭ�SM��*Ue�А���?j�s���Ș*����(�P�7��T�����W����,�qT9HY%P����R��J�P� !�͛7�I�H� 32 �@�! ����^d\� -�V�j�T[�xs���y~���W�'�n7�˶�Mr�u�]����+yYOUyi|aǞ֤���M�ށ���r�3��u[��J��ql�}�.� -�)L��9kHUܥ�OV.);�G��mM��[�d&�_�Ml5z�sru�)� ����[T��dU���-�܂�M�>��*��ӂ*�(�����v\Q.�(T��»NCT-�����o8\ -���|zzQTc��]3�sV�.5��N�y�N�t*L���Ħ�]a`���3����1b|fU0��ٷӇ�k��ѼsFaD�Y��T�To�Ilʨ~~*�|P��|]�����}��w.Wj�����mn����B��uR �rFޔnb<�SJ̃����r��8l'���m�(<���1ƲN�թ� ����[�����#��'���/�㣀�і!��A+܊�zӭ�L�Jө� Cq ��prC�_� -�V-?�J:B��.� -iժM�ܫ�O�3-���*�)�8_�����j5�����UUl��!����N����晌�?�_��c�*�/���xh��Q��c������*�����2,��l~�����4�1WujA���̲#�' ��*GrJ���{��.`���@yFƆ���@щ��i��Ƭ�� �M��m�˓�@{\�L���3�%�lr#pv������W�G�hD�\�{�_7U���Cw��9�]�����"�D6���ۤG�+��+��=E�eG�+��`˖�,�J� >.N@lO��3�0�?����SŨ�pbca��$��g�X` ��W���=C .{�eT�Uhq�EaIv_�1�o�K�w�-BwYu��Bi���m b����ҡ��F=3�v���h�b r�G�HΖ�6�+beU���b�5�,JVi>2M25�2�]͐}��ܵ�^ �A�A��h�I�̉o�;TA�[?��#��Y4�UOm������%�o�_f��( Ϫ�m�:� ����S���+�t�q��4W��;�Z9����n?���2�PB�����p�w |m��=����H� 3����O��&�h�~���L��J�x9g����.���%RjH"2M��{�׀L����+��С� 9�� t��C���4/�<�)���H8��ֈ��ޘ������dКg����j��9�TG�6�U����BX#3hY�ܜ�(V��a�'8Cn��V�˥] W)�����`�0�(Shw��h�� d�W�~Jϋ7�"���j�����r��Q�o�@5�w�����R�X�0@�҅o�T�t9D�o��!��^��QY�jZ#k��hq4�3y�^��x"��S��jϠ�\� G�Vؙ������5�k��6q��E�D�n�Z�m�C�Q� I�MW�y�z�%��:��_x�,��ϳ��%ƶ�� ���>&�����ˤ�nc�����YЅ7��� ��hʬ���X!x<w&�}�JhO���IA�#��dG57��� _`P�ۼ\�,Q��h�6j�s���&�F���r�E,{�2~>�{�5±��."!��1% ;��=J�70�� �`90H�� �ul(e4�ރ/ӯ�$� L,�I>KduA!��IhӠ=��4�,G*�����3��_��@�W� ӊ���abF��b&3]sX&��,�:3}������bT���g�e�5}��5��Q��H:�m��G�e^��;3�'2b��O߿N�y��ѱ��������G��2^�u�!��2@IO*�t4d |3#'V"��� ii��|!��KO"� ��%z� -oW/\;��<pQ�w;��'��)gk����nP�(w�`aX���7���6�I��jX\�����txB����8Ӓ��z��)�������m�5�������/F�����=p�D��>��b��QñT���*�a����D� �Wie}�I�4�*��9�VG��m8S�8"�U ���9���J���u��d �7�Gq�5u�a *�E@!VhR�*��.��ZG-� B�,@HnBr���}�7 ��"�ت0�3U+�ֺ{�"��Vp���>���=�W\�%E(R��iO� ~_cڪ�]�1�B��F�(u��ă2a -�1�;�ljE]� ��_'�³���?����gaذ�~���g;6��d�<���{P��Y3ɷ�G|�_X�2YP7���0Y�\���?��u�����p��o?S��4���?�5�t7x�y=q"�;��>�ޔ����k��'�8���B1���Ǽ���.C�(#�(�ǭ�����x����/��ݖ�/��5��Q�~0�Wd�"�p���C��i��#`���LWZ1���ˋr����t�"�Vީ6�=�$Ԃ7�ćփ����Bΐ�cl��p��U-p��#Z�m$������=�M�ǿ�����:8g]��N~���rN���Y�)���U�\gysn�Wq_�+c��j|���X��C�b��9����b��Z��b�;�o$�R �U�(��=]�������TI#��% v�eI�6�^�]�����ͯ/�2�'�?K����U��J&���Ky��� �Zj�z�,���F �V���JCT:�dIU?b��(��4��g��?<�%1��[/��J73n�\����I}�ÈB�6���A�Z�Vh`탬��T�Z�?�+�$V�V�&�X���7D�s��r�@�����p�Yy��--L���^/��<Í�h⤔{B��'z@,���E -�Y�A�������ՂE2�i�,��k��e�a�J�B�*^F�����[R��_@C�|�u>!�t�Q^H"���������qa�)0%\fȏ>w订q�I�G? 8�7ȳ{;�S҆� -J�����A꺓K���s��EG���-�"�Q ��0t����W���.M�S|Ĕ�/�E�� Է������ ��S����/I�� -:����*XomUnͲ���a��#{J�������;H�"5m5�n��D�P��Β�5����:i�L�L@��Ȕ��&���aZ�$I��IyNSgR�~Ot�>D�(�m!��ߜ<�c -�x�R�f��EԄ���*6�0<J9D/�4=�:�$�,��O���=�W��/�A� -4r��I��Bb����P�{�W�s�5���f��Θ7��1�D�b���z^��*�u�TE��lx��Wp���a�.D]P⣦&�<���Ϯ -�6jF���_���8���-�V�&�&��Q�RD�(ѯ&/g#@�o�_١yj�O� -4f�QP���+e�u��������QW�n4wn���}ܙ�9��Z�T���jS�ԑ0"�Yϯ��v�>�����HcC��p�X5�I/�]ﵝx�I�.ܢ|��)��M���mj~��_����_$��!���-�̩L�1�U�����l\�c_����~K��"<���#����[�(a���/j\����2�]�{��v�f��1g�{)a�������V#��߸y@X��k�a=�w�q�"��H�7ߧ����~�5F<#·kb3��(=��m��m!}�<��(���sC�;�������t��a�>�}� ��<Du(�H� -Y$�Z�[r�����ș�%Y���`�Bc��aX���³��z37QÈ�mIֽ!R4��olRվ���8��b�(��ٮk��`ڗ�2�:7Q7��4ى��usɤ��q"'�tE�H 1��|c�sZ���Y������@1�\��&�h��������8t`�Td{�r[�do���>`/jL��Kʵ��~DM" -��'ܶk;>��������+)2}�+z�%m�-��lS�.��4�uٸ"���\Y~���ʹ|F���Vzg{W�դ��|�X���;X��#y�8��,C�8,�U�h����"����ww*k��[�w��8��M;+ț� ���(/���Q=������=!���O����2�'�� ���:��˂�:&����2V"�.���}��rR�(X�p��CH��2���?����ԡ�q<� Pqh�Le�P��2u�(��"�� -�B�'9��@a� �!����jH]�ZX�}��?����y�7j�m���ܚ��qNk��W�Sax���*��Y5����]y0}���bꘫ�~yuԈ�Jd�ф�J���3�C� �ͼzX��ʪoUV�n|\�)}�p�v5���Lr.��j�9៥F�$�lS W~��R|�-�*DP����Y�h��� ��M�_�/"�[��/���\�kvY�(<�~�J��Ӡ�E�Cw{kO���C�=�N�+婨���G��u��K�2��KN�����T&�Ѧ��0���;X���:IT�fw��?N�$?�R�b�9`%i~�'� - " -��уu�X�ڏ�L�xy�t 5�3�g��AgM���U�[�aB�Kۍ@�ހ��Te&s`���:i��+�k,b��Zbg�B]���ܡۗ��Q�@ ��YY�V&����[co�ܤ��^�m������c����Ӷ���d��B+�|�܅�w#�Ȝ���%�_���cS����! �^���k;o�ç.o"��7�VbX�M�W~�9;X�M��/��~m -��R=W �ލZ�����9 ��j�D����_�2 �mL*��<����VKB�ª��;���;��(-W���h�3��/�]I�[d� se�$u� ))���T�S�o'��ϫ�}�n�Q��,g�TU���#���Mi�eF(�&��'(cMƧ�k����d/I{�����J�s�,D�7o��1RGXV�Шn�6h�b[��Z��A�|�U��.��q^]�d~Ly�� D<��<4o�Ic����"\�d�RC�a Q��3�ִy2�XÃjd����6\6Ҫ~M����;�c:,a�q�z���g��k��9e�Hñ��O�m�j�,Y-���8l�oQ[����:E*��w�����ێ�S����FcAv�̣ś�M&U�w��hW����8A|�.�eBD9��� #�䮲�@�Wj��a�\�~P� ���,��mW{�o̬8�u����[�P�R O�s�>�����PJ���}}���{�������S�Pez� -_n��*�Hq��-�=s��q<�4q��D���/�(��.D���+�� k�&��EH�7��q̚�G�\8<Kn�t��Ȓ��>�����}���Qo�j�+����HG1jCo�nHWq8�`j�`I)�K��ԓ"כ`��i�i!���[y�������Q� ��q =С��DÅ��� -���h�u+�P��?VMQm��%Xc#�ބ����s �Ty�����������WA�h�k7�[��F<��L�8A�+a2�|��22}�Uȴ�Q�>ͼ�kT,�u(��6^�Q��]d* -����vBE���w���~�K�3���c�O�+��l?�w�>�9�%�+�5da�B�+�\Q G1�iG˚O蹥�� �`��sQp��X.�66Oxy���O����ꯐ�� $���S�F��.J���4�p�B�q��sq� ��/g�[���jJ[' �Qd���M�����|��|���`���X_�cs�#h݂����*�o;=kK�����P�o�B9WI?ɡ\��F7��N���3�)�6,����vi��͈�3(����s0DQ�9� ��f�J��t�^�D��O�����dy ���Ţnp�_pC�k��h:0I�6�:N�����w;�Eݫ�y/��d�l�D�A�z� �=��FU���%�}�C��/~��|�/�>�1/�����Qb,6lh�Tș�E��6R~���|�����{ -�пI~ �"@��K��k����c2p�-�i�J��k�_�,p)�:�+7;� ��������E�;�� /��̳�+�+���=�U������V0����C�#��3[�����X7Y��fLf5 �u�����n�ϒ��|�Uux�0S�I�l*��p�����5�@Z�CGyܮe��jKS)��@��M��A�����7�%L�w��>�^��/s�jԖ<P�����lx S1j�!��'�>������A�:�-qf0XJ�QPt�E@�� %) �4x/yI^�K}�Ћ���;�.�v��Z`@q�ˎ�eD��y���9��s~���o���Y�8�����%�o�~K�pؖ��b�.�R�q^����պ)���5��GLS5/z���Y;v�����a�9Ŏky�=�D�Z¹����C�8Օ@�fW|dJ��9sM�y���.a%�f�D�&�����Ʈ� Cz���2Q�9�����^��x��<�ć�')*�$�v$/;X��R�%.��>J��o�wJ��؞/�1�����.�����v��*J���97�w����N�x{��0/qy�/Y�r�Jo��4��hb��Z&z��@�(���4�g����(aw-!�L��v~�XRہ; ���ј!�_R��[|���0T���,dNd8C B�8+��+ ���*Z�����l7�6�A5��D�+r�� [�)}�`��,n�`�,.传Zڎ�ƤmN�B�e����V�<)8`�͛ǟ0�J��i�����uڱ �p !��7����I$��$�'�����|qs)���눤��=�e���Z��Q�ĔB��������(x����tQ�/�猁@�D�Hl�Nk>5�m�;"C\�R�<�/W�փr�.p��^�@I�0'$�ʊi��n$-ptem��}��A��Ox>^������t(R4 ^���mhi�������pUrYFlRT�6� �Z�mԤVR��㏎h/T6�D2����1��q���UJZ���L�1�ŠU��j����V0E�-�K�'�*a�*��cBp\(�W�/�p�V��ss���}�,�x�)�y؞"\�t*G0��'�G)���`�v����R��~� -�s�? ��� -O�AL-p�UJ�����+_Wf��+�r�d8�/I_I�I�1���L E�*a{��z`&�\��&\!0͏���*~����rF���I�b�p��2�Uey����V���8u�|�� -:dM W�V����I�-;w/��Lsc�����ix6J����)��:%:�\-���*��h]~p���x\��A}Y\W���D�%>�טǔ�l����t�����c7�`-v�$���ۣ�b���5j1�J�Y����C�� �EŬV�$u%u��Vґ��ĥRmG6lD����܊�������� +l֊>V��Z�+FM{���C��3W�1Y�k��M|���L�X��d6`���K_�o�r�bN�~H��!����s��?�^��D�6�3푮��5aj>���X�e\,�Wx�/ �=ԅ��w�n>)�[�~ ����G�v��K���m�!�0S�>�Wu�d+�vjgK^�=Q�����N$"�:���]}N��)�`�φf�,����ޣ�M;�ŧ�h���q�J�������6�Zy����gaj6��c�O�:��Ԃ��o͏R�� FK\$r�)b�� -�L�+�k�xj��� Fьv^U�N�Rx����Q�JPݮ����k_�P�h=G�*� {<{��`l���`*]x��n뱘�&Mf�ڔ!�f١Dp-G�-�7���݊~C��)el�ԟ��}���hAic[eqǁo��%Kw/v��J�l�W�>/�ʾ���e=Ν��rl���o�Wۦʯ��̪�Ϟi���`��!��͗l����8��g�bL��Z �S��Yߴz�N��6m`]�.i`�Fcl3l����լg���lԅk*�zc��Xy�5|G �q�?7���M�c�N��6����"�/41�S.ӆn�L%���M�B�`O��l�x%0-d�+�)\���h=�}Ť944ڏ�,L%5�k�Q��E���?��*(a�9�9mܡxIڋ�U�����sn� ���|�W��kM�����=3/Z� �ſ0�Х1*��NJD���6�����t��(���jll�Bu�ot�2��~I|��Ƕ(f����L�T��l-���#��4�]C�Wm3������l���/���r��~�=��{�!��j�]0U �h+�6�r�O�=��j���Χ%CE�i�V�i�u�/��4*�D 8hV.wN�5�t\*�ƥ��lS���H�6\������"�� ���RJ��6]m�Y3eǩۤ5g��=�[�{j������9�s^��(�j� g -���o�x#�b_G�g8l�t��C����ד�C����r����Z���MQ�{1�9~]��V�c5O0�c�{��B��3��O�2��,��ؽ�n�/Ev�~.M��ΚͿ�5����n><���o[�`H�'�S4��%蚖� �;X����ɯ��,��O�g��a�-�b���V�8�o����E�휨[�Lmg�pӯ�Ox%�0�����nͮZ�����^ْ/{撢:%�9�?�*��Qu ��:�N�� -���Iri��m�Ű�����ݑ� s�m��S�9���`kr��O�H�'4���=M�MZ�Ն�}�}�7ޭZeu���Ɛ2W� --a�l�̏��?���tc�!#���P�t]ע�k�����5/T'��T -�V9ՆRRpv������h��N����=��8ҵ/�ί���]Zݛ�S�@����>o#��<�ķC1�J����N��>3���} -��_�=L�u���@�&=+����^#B�h���'7Ѓ!�=�tT��k���_۩���ow�2����/��I�{ ��<1ۇ�A㺇�"lw���:ʹ�ʎ=�@�*w�������� BO��'��^���SW�(7�@�>������h��� a�Sm��p��x����p��e� ��T\�b�GVX�f��H��l��$;���i@#p��r��Q-�uk�d�õӫ!fUM���foi"����44�}����#�&�q(��<J`��<�jZ�������cN����+ ��]-����|�ա�:��-x��ձ&�|�����6Q%��.��� -��7!E8�$.~"K~�m��(���/��H<Z��^H��}/����R#���E`g�U�=x�!i$�R��9�u�ˑ�� -�I d�E�����ǘ�=�P�\�}�#W���[#~��H��a��璜G���~���#y$ү���dC]0T�T��A��0Ed��r.q����0+���E�% W�D1Z�Nx��Ƽ���`P^�HD-����`+�G�u�l|CՊ$Obc�X��ۊC�$�#��~w���]�\�� z�����1� �,�-����0���k_y�QoJ��ٝ�֞�E���4���ԯ��x��l�}lkS��y�En����ڳ�!�=�|c����G�o䫺k�&Ԓ��l8�̴���������/#���=����'džΛ�Ws������7�U�ۅ��|ыn�!)��1��%No6�߀��DxP�H�tXiY^������{�s�>�9 �(.aB�bD8k5Ѱu��T��p`�[�T�*r`�<H|�6G���cd:G\2DiF���Q�e,��%N2��!!],}�-�;�l?y�x�ݨ�b��p���3l0��� -5Fm�KX�sr�;�f�gH"��r�s��쾫�]�oZ� ����KI��Bf@5jɿ?xEu��[�K����oi�����ieGz=����:&~i��RN�����A���-PG+MB�/+r���X&�\I�������%��#j�aUM7+��ת~��, ��FT�ޏ���?R�5�uTz�b�8S&Ru��r���U�qN����Ki�x�.1�1K�9�_��JyLV7. -�Q4 -�O�#х����~͖bE�=i�1R"��a>�L0��E�0��!|��D��1b�VF�h�s_��Hj�����Lcz��Q��r�se�0��}=�9kd m{��Gr�raI}^��}��4��� 9�W��A#�O��)������-�#\H�+=NR�n5�"!�z�Z�.%h� -�< ,J����@=GC&��zP�� �:����\�{��?Lk#x�4� �]1���r+�ɼ�J%�X)��c ��X���l���S�H�^�x�[Ku�I�%Z�H"������"����e�F0ה�OU2�@gY�7|9�e`�5|���Ke�@�FKJ�Ѣ���Ӵ;��#��6Y?|�a��md0m���\�oMj�Y�U��KQ$��˰H�/ �b��#$a��}rN�Ir�' { -UD��Zp��{�T��:k�W\��Z�O�>����}߯���1�.�B!9�6Mr2�)�= F�}�c�^�<y@\ȥ�Ta�YT��\C�Bt>�қ�G�O�_�~W��Kq��C['���9��8֔]�#y�8�r�rQo����n��&mD�����Z.�� �+>k�3�%$t�<�и��T�Dvk-H�A i��`Q�,>�Ӝ��KX& �y�7���7 �`�Ѡ��]I�~c�d��7"y�|��rD��R댏��Vw:¬H��� :R� [Eq�J�D8�;u�MYu5w���t'>I�"�h1��?D�Q`�)��a5�it�L]������Q�%qR�N�9���xu]GvZP��B��?A��x��K��wЎU�t��>��Jx�ex]r"�w`c�;�>r��� �I(���%_�v� �sE��g=��rx.<v���׀���F�%����=.+��8������Qq��.���I]Ŏs�E˃�s˅=�)�M�`A5��$V�~X�ӂ�Ǭ[js�o�9��L1;X��fwȇ����Q�����E��i�̌֕�~�1����y�ɹ<��b��~���g�ϲnh���r�cNⷠ�xv�^�F�b�uS����n2�[w���փ�|V����:�S���=��[PX���5��� @� �6la����*{]vo`&um��b^i�=0�Uo� -�ov�a�}�[#�WΔ��K�+�9��8Pn�r߾^6�w��(�'�e��l�&�5]Y���3V���kL6M��pZ���}o�ԫ.!#&�sb��~ G7P�V�& ��� -��&P��U0LǛ%��ME��yjOz�O�io���`/>M�u-�k��m���@�}��p�V�r�8�?�i]�c\��y��3�?)��|t5���,�oQ$C��%�U -a�h���"M�������/�t��P/�:#��mJ�C����V���o�/� -V�4d˶c�*�Tt5�T:������\���.��!OK,� -x��u��~��̺C۰�����Ղu�?W0�z��wpZ��)h -,�ZE��ۋp�����o�e�w�.�3@�i�?S��qÂhI����Qfm}�q:�'%o@:��0Ӟ%"Z&������U �,&�L�K�3�*�C�de��?�s�p���I���I�ޚ\����Z�s�vQ�I���C:�Q�ѝ'�/۬M�Y�yQ}�66�Ou��V���ֆ����U�4����)��s�ڕ�&r~i�zy*�Qy��#�Y1L��FI݇�S��*]B�-[B� ��'�j����Re{hw>��J"������D�Fb�@��I�)&V�����4an����v���H�\J�mG?�]L��$��Qz+�f��_.?,Oޕ+�/}�} �Ó�8��z���7.�(�� ���UxaB��+�&�����F*T���a�J����:���������EU�T���ߑ/t�b�,زf�~n��FK~7�C�J� ��䭦f)(Ʌ�m��q�e�%��]G���Sq;�0���L�mQ��IP}Qr@C^��A����������W��qj�Yy)��ug�A,˪�1��vU�A�X�_�;�">)Ϊz�6B5�⇖x��܇V?��T\SdM@�:S_m49օ���W�|�)�Q�6�.�KJ�p���n�594�K�5y5�E�7��f�� �-j�&ۿ�XhGm�o-a}VՔ���c1mfΤ� ۘS�|� �]L�.�9�b��C���Ѧj.7��˺M�r���S|u����Bm1���Z3x��j����$�kU��"�)K5�H�e_�w���?h�Jt�_h�ē�5S\��AD�_+ҀP�"�4�վ/[a0i -����d�憲*ꍺDQ�٦�(0�yj�,�Q�T��E�3�K1����{t�ƾ�,M��#�{�a��C�X{�3]u�t� ��*θ�CWڭ?�����֒�줾o��Rm��� &g�M*/d��]�r�jS#�+�����U�H7����%���~yȷ���"�ο�>�8�A"Z���� -DD1�,jY+R�P�������{�$`�H��$de �(�V�G�.�U�EQ8��ʴ�e�E�x�=����y�~G� -���i-a�h�i�S��x�ӯ�"��&+�t��n�!�h]H9~��yJ��i� -��kYڜ�����P�W)2;�q3v������ۙ3~"S'+by��'�H D.���u���{��ה@k��=�"��iP������� �> t�[�F��� D���bK�K�W��`C���bh���3���=!���W]���D�}�ox���m�E�T�{��H+��i�bY�ƭ�=�S�\=J�uA�>)��S��8b1T��l7�U��!j*N�8���mCu�)ny^��#�,��Oj�-J�C0?��H�l��$����5��[����jU��/_x�*�'����a�`�F�� �zN��������6o���5�+<�v��DY#v�[!��7��)CY(���}��ް ����!��X�ꢮxl����Y|�B�T�bQ�10�_/�����q��0A'��Hi���xvlB\ -[��߃@�C��E5��Ns����T����th�i�<}0�V�;cۚ�����l��\�\U��3��V����� -���Gx����@��o��B��c<��o��|˂�����w�uÉ�LQ|�ʱY8��e����5�Q���q�J��p������G]�es�M�����q)5<�iR2�`��z3쉒J� -K���J�I�9[q���p���̖����T��j�prڻ�$���QR���I��ɨ �_tp�/ǟm�Q���������(�x��� ��T"]�]����:�t3�!�F-&��eU]I���u�P��]��VY���u ��R=�����9S�`�=/M=�b���R*UNx{t�H�i�˔����3�ׯyv:j`SKd5Fa/��A�)�_��� C@�}DMiazťӐ�2(��®r�q��� G8���t�neD~'�N�7��1��~�a�@���)���GT�`�6n�洔P,0���]%�����;�D����9�o��l�K@��XN��m�|qO�鬀J�8L�T���.����'��g���z�ϔ����d�I���Yf�D���K5>�>\���Y������Z�y�PJ�q�*���eEv�1�"�-�!��:��u�,���K��m9� ��{�����B{(m -�O�8z^V0L�T�ӍE�+��#0�\���N�j��ͱ���V[�(��/-2�dL�2�����5~G]���N���#�J�B��g����PY/�V�k�?w\C�ƛ'R���_��p�,�{q@�]��t����*=.�J����.�`��Y�ae`�l!�`��Աk�;@��{�k�&9��[۠��VB)'��>!;1M�`J+2�!�=������]��/�\?=g%�蒓�\p�'�����f�$:�)'���B�.�u��s���Ӎ���QL7~�_s��y��v�X��#�X�բ������vX!���B~ ���SM��H =@���"�s�`�h�~��Y����)[Lç�=����׳+d#�4�{�{�2j���������W>��dFi���b���ı�f��0ޮ\�5Il�LO�4)�G�R���}US�.��W%w, -�ٜ�t���F���o�_�I8@����P�y���dTߌ�EN�n�e�˕f*���\��i�T��ro^&�a��,ό8F�w��K�?�=�o�źF�T`@}����~��d<�з�jS�W].E�hU�*�=�jKl�#�2����#��p,i���� ���.Cܗ'b����9I�[�ȂA5J���_��3�#����E``�G"D�/����������#$n�sw�� ��d%b�7Hݳ�&�B{��@�a���t1��R<H})c-��7�ďb%�Fp��-쪣��ML�,w'76�K�����v}��n�>���3eY�E�����Al�~���E�KQk�&vt�0� '���o�0�ɁX�O^Z$���J���L�O��K{O0�H&�0�zɱ��.I��#l�$ -2��jFH����Q`Ry���$WEc�Ԫ��\^M�{�� ��b��(�� *�%CT6�Y� -�@B�o��f��H"+�b�^����(rᪧ��z�ZDpPgmm�����<�������]��h��Ȕ)F�.d��ڀ�|��U�-��o8՚=!i����dd�@� -��[@��R������$�p��o@ �MR��Z��w�&��=��v~\��,n�̿GE:{��)�"f�m7��a6z�������������>5siׁ��m��=��)$�X����si�����0P�h�$`Rz�,��BJ���U�v��W۫�d��v������Gk��Ǥt4Ez7;C��0*'$H*�`�:j���~���F8�! �D'�${��C/J�tZ.�H�dV T�5��*���� ��_3sw5�Y�9VQ�/��d�%�rZ���u.���^��(J�c�W;���N���@8�=(����}��D)k�D�Et70&r�"x#'P���h�9��톲wv���l4�"�'�;��h|����@�w+�V�q�`�A?A<� ��oqNi��\*�0��Ҭ^���\�}F�Jp+5���/c-o�$�X���c�������;v7�xsM0kT�o�n�ʡO�ļ����_�겓�:�3��V�GlG�|'�>w��5�'~%|ҭ�m�ȃ�x[���>b���(Kg��22��:�1����!�9�~��x��>��S��`�U����ګ�R�OھJ}������3���kw���M�{��,㪖ե1������EJ�a�H�"ɷe+��?��B��tS-�jP�UOhs�%�'��B�������i��a!Z�[Y��yfC:x�eʀ�a���P`�5Dɀ�Iv�O��ʆ������x��BR�et)���DJ�#��O8(��������<�y��[�#��S��9�Xb�a� ���Y���餏P� }�V"0��@��qZ����1�7���p�ؖ��9�_�b�'U�+�%*��%�V`�JxJD� ��=��Uu��A ��ڒ����'��+����*�I����O�j�'"��ϫ6:��U��Z|S^�M4,zg�~e/�k��y()Q��J_0�A��z*� {:�x/<��,�G�g�v\���Ԭ��H[�ϴxPyXl���5�r�l��{Of�E�5�n -���߄"�$���3�vI��*akꩍ�1^&,/1\������&�ď�w����琣1(�}��Dw�z~n+y�W�_(E�G��Tj�C]�.S�����o���H���Ȁ��Rf���l����ke\<� -��^������I�펻��}�Q��#F�0�����h�4����t闉�x�HF�J$ĉ����T�$�o��g�*^�>�֩���D���1�g�ߪ*�o(=�¸8�Aq&��p(�0[���ݞr��lҞU�fe�y}��2����L�0��ӡG��� �%�m���L��vx�R�Q� -����T7*JZ6�}�Ԝ� -������-k��-�1a�y�מ?Cx0ۭ(V�^g|�b�v��E�PEkr�i����S�kO�i-��`e��|���nE �/��]����rU������#��u��oƂ�̺�~��ں����l��U@(!ũ¯,�XX;��S���L j����(�i;���k� I��Z�Iܙ<?M_9����JZ�7��5��㹸&$�N���K�Zg9Kkdž[��8?�ML��@�\�Z� k9M.6�,�VȞ�aY~�%o2�9f�c��3� 4ur��]�,�#��K��ߴ����&K=p6����& {p��@4v��6�F�VxZ�?��̓���HӸ"+�bl�%1 %�ֳ@+6I_�3F�c�gp��V5/Mb}w�s�S���7P��I�1p�TW�y���Ei����*9��I���+����C�yØ�.�c�9Kx�o:����L۰��#�����(e̫h�{�����Ϻ�/��&�t�l]�D�����X��a-��T�K�nU^|���؎;�ȻP�C=�U��W]�=�C']u5H��+�fʋ������a�mn+��������"�{�����[����� }Ծ��2�R�����mSę���ï 3�x �Q�+%��ڣ��h�@� 8��2!{/2 $� dI ! �0Æ -��j�^�W��nD�A�UAk��?��y�ϻ�ߣ�r�Kky�)�)� ����i~�g������F}lZI�7_g\���u��ʖ��7K"{��'.�Ӝ�1�<3�q�SM��i���T��@>$�_R�Z��`���� ��L��H��L�>tYC������?�d��{�|���2g��#�S��%�%��^G&7��C�gE_����. -s�қ*g���Pc�� �m�+6&�6\��#W<���\�_�����'�s\��R�svE��f���0�qHӘ�4��9�P�P���y�4xT�Np[�M���U�35.�R����Xè�.���bҡt�n�f�-�-��dS�U����\>y`n�Z9Y`[wy�ZX�j�߈��l|�4j�`��ڻ�<����U.�z�z� -MtLFg��W�qH�,��� �W/���G��3��� -�K�z -�f0����3����=�)~��5�e�u�*�Xg�B-r�c�2d� -[K��o��t�'�H%��Xΐ�"��ޞ�j������:�i^��ڕ�ے�������.w^{��C�;��m���a�aOr�Y�б������:�~7`�3��� -\T{�Kr��f���i�����M���S@�)�"ٮ�Ά��C�;6U���o}P� -�������;��ъ�,6i0���.�eWk?(b'V-�>kG�z�:[?WA��:)���ܩ�����'y6��a���������Ղ�?�-֭��n�T���,�ZS`ws��9Jy -(6Ċ����`��77��Y����iq/�����(4�,Չ�GTG��(�W�b@h~��ҳ�罃�"��T�ცEQ���O�ƈ��ǰ�x�r����T*�����4'�[����h��?b^�,<h�� -�pv*�}ɑ%W��Qݤ��sA��-%UN�<��6�>�`m}r.G��\ͯ�]/d -f -�љe�$P&����l2H�"\�)!�S:��2�N�O[uH�;U��$忕�^ ?���cD�*�ڒ�s�8��|$� J�8�m,��n���9]y%{�柌�� ���S�xr�eT�24�����4+Ԡ�b��ZkV -�q��TF���bt�\C= � �B;�\x��#2 �LΠٚ�@���KZ|�o�B4_����e���J�m{�g�<|3\i��% 1b\q9g�8�R���+�C��VPҁ![���Ѿ��W��n7�:۫C���klE�+���AD:5-������q\�4ں�2�P�Kɛ��!�͌qTW�|Qvl� ��pl�����L��O)�2��ـ��O�O&��ːd8AE���������1Ġ���2�c\�y1OvWTqN7��)���=��ߎ?�<H���&��ر�5�N�F ݑ[��C��.����$��[/-�=��Jo^�&����=�͉��� �(�|���w� -og��}�o��zIZI1�_|?�)S�&�&D�D_ѡ X�ET&��:�cE��ݹ�j��J��ԏ�w�T}8J��gea?V$Ac�xyN�"�u�8XCbT�;�ZvP�^�<{�q[5O`���(��'b�F:i�2U�ZiBn�fyx��&�'b��#�v��z��o[�K����(�*ߡI�"Λ~θ�R64'8b���cq�zop>�\[�H�"j�gY{�Hfr���Y�{G�Xp�56�<h�7b�������ތ��%xԦ;j���ZW�d����9��Ϫ��<E�sQ����h�s\ o#����5�o�!��������_�.�x잢�E5x�$��8��eO�'���˨�a��^t�ץ���W�DM/�*��I��-0����bFS��d݉mfy��������^\<�.ϓ�����!�DN� ���F@�B�Ƨ���l=�h.5�e�=��L�����.,U�S��|B�#** -V��+q�6ʽ����4]�[h*���ݫ�h���Ϧx�^�|.X��(�F���)>�'��}���7Vo�1SȄ!P���y�2uUK�M~���c��j�Q|�w�v��)�%)�x�&4Ɨܴ�=��[=�������:�j����� =�E!*e� �0��)�M\�A�B���$/��#�˞@ �@uQ�:V�=����;ъ⊢T��֡'�����~��;���5|1��+t������U��m��h��M�(��h֓�zW��߰����J���� ���v�i�*Otwb -��/6%��;�%�S��E�+i#-FO��o�+�В��Ȁ��`��X^�m�/��"�V���ION��P��>U,�?)�O�~Q~J�&U1���]RG�X@�.�" �]����g���>c�t��1�=�����9����譖� -t�E�������Wc ��{ſ���s��~䯜��ty����rM�ުN�I�U��' /g\�"R�C�t�/���p����.�36q&X���J5'Vm��Ä/^&����[���4��-��/�J6̘���~^=�� �d�i4��D���dN�zI�$���w�gFZ� -Eq��v=�EX���u�������+Hd�1�M���`�%�(�.~=�p��O����U��3m2��6�ҋ'2��s�Ǫ��J�b�4���`����9��J�k�ڕ$��,w�� �Sz��[��Ջ�w��1���ì�-�ְ�9nB���]�#���TĂ~[��P����")����Ȇʪ5d�x��&�;��(7>)�@x[�_F��ʚ2���ؽ垼�ܿN �E�_���K���Pe�_��1�g����i��c9&O�R���@&e�&��W�z�GZ�˹97TQ��9��V�7�n�H<�h/�lv��,Nz)�� -�9q>���B�\ZM?�a�P-'��Pn�*p!�۔�&�(����k�'�)������wHE&���,���-�T��w"��}�?���'�P^��z�t�Uy�K���j�?�����m�b��^z^xW?�'H�N��R�YnW��&b�Q^���b:�����d�;$.x�=� -&꺽��̬yq�d$ڮ�W91ۯWer�'�>�η��N�P.�͑�����oZ-�%K#k�����xcE�y1��c����T\��3"l�1EWUx<�UV�eۉ�3ۍDK_���-��;X[[t�U����l�<f�.�R���4���S�ϴ[2�d�f�r�zS}��Z�V�u��UӖ�#� ���p��V�)V��]-��퐢Y��ߐҧ�L�a���Ws{����IJ-� z����A<�P�Њ���vE>&�12�P<��@Γx�� /�dF�"�HH�+��ǟ�, ��� 4nW�i٨��.LT��)�u -~�&X�@E�~���ԚPy�g��&�똫��U$���@���-�2o?eR�=(ۦ� -t��h�=KkՏ����]�$#�%��K�r�[�b�|k�#/A�最��m�?<Sd��)�#���Y��8�ƕ�J>6زRZلK���a�Z��4��|�{F�"�=�k̊�_Ǝ�d) -�חVf�o�-sI�h,�ֆ����Ajr&/8�TPi�4�'U`�Vw����T��̡z\�w�K�)�GK�{�@}��ˢ��)�(�\��3S�S�X7H.�����2���6�V��,�� -嘓�F����\0�kL[�W� -��߰l�����XS6�nj"���}�M�ni�*C;� ��X����n\Ļ���!�fW�iC��M^�����>�O�XKֵ��C�u0�[� ���э�S��p�����yA��`:N[ ��u�܇�!g5�a6����V�)��=����s�4!�G�K�#jnܐ��|�T�#}���6�>�QU�M~V��sFn�:�DH���1�kG�F琝�|��a���w����W�XkS.�j���j�).E5h��V�,b�)վZ�k��6�V����]z�eݫK&��8�6���<Xv�^�{�c{����&`l�'�Zf�r<?�.��:�}e#)��7<&�t۵J?���֪Z�E��9Nw�K�[���Y[��1Dn��67�Tb˿(�u���6���8�ٓ�r���z�t�B[7����{�B����n�g�W7������;�?Êf��l��m�sϒǝ��P�6�،g���Ä{���Σ���0�G6�ʮ��.H�BD�D0B����,����$���@�@XD\Z���i]��[G�ZQ:R7�Kl�s���w�{�s�s�Ϫ.BD�&u1,�EW��[��*\M�b��xx^�����T#���ND��;�A����^,�"i���� �47?��-����&�FUa�Ԝ�����)M��o,{�E4U�}�r��)K�Te>2u_$)�N��q�����FHůk_V]�� -3�iϹL������Z� �g�d2�Ĺ��lFW1��5��x�z��:A-�TA~Օ�^~D��̭Vt�'�ݷ�� ��°�� -�hH&�a���:���$�!��n:(��A��"��t���p�E8�p�3�8��&���E��o%m�ZE�N��I�h�P݃_��G�;�_I�Y�������u��g��� ���3������@���o!�-�l�?�%�=�8�y#�� �6��l��0�F���$`��2�rP��9��h���?�$�/��((��j�x�g�x�|C,�88�^P}�x,�f?-Y��.�pOi�'��,e��ϕ�E�j|(����ѐ�����@���Ň�o>���4�~n�T���ִ�� �uȶz�F��Y�@���A-��)]>Sg��"�����hج;<]w_��_T�Ż���h[��M˔����X3�y? G(�Iqy�E�բ�i�1��J�Vg&&�Z�����2{�y473l|�"<d<l,y!����V�:��T�w�+�7l�����G'�R?���*�����l�Z�G�R�}���<��_�<F��z�|�i�����0�I+=W���'�]�N7s�I�s��g� Q.]U�O�)���H�.ID�y�Z�e� �{���rP��ru\��AK�p�'" ٫p�̠R��r -۬H�0���SK���:���*m����s�o3eS4x��b��i)��<{��[��1�܀�t��s�䭊Q[q��Z����ucQ��K55�1���� ��_ZrOU1AW�Ć�k�?���$u�ꚸ���zMJr_����H�E�9��Μ}0�l�-qQ�����Z�r�����N������ҩII�:�oS۲�O3�6۲a�&PbiΟvy�$����C�&��M�H��_��zM=�� ��0\�bȷ�'1�g5��]Bj��y��A��*������V��0ɪr�M<���L�J���P�� -��� �0� �w؟�v��s��Q ^����g+ŘI�牯a� �$w�+ԗRPp}g��P�?�{�����7��y�Q����:� �C���p����1�Z��L2����J]�������Pg8ӔZJ'���9���4M�1�˔��7d�S�'�ާ�M�ٖ���g´�Ҷ��j�qxO���Ҽ���8k���@� ߥ~��N�R�R�(�������l` d��1Dij*��LT����A/ M�^�o��退l�P[�.�a��of���ۺ���Ԛ����N�*��h��զG�4K���"���P�R�+V�.k�,�[�*`��D�$z}�_�1=!Wנ�Q���7U��l����FO�1�]e����@�� ��>w -y���P�N��bwd���B6�L-��}�߷< _��M2X8=��I�"/��ȯRM ����L�P��!�d���j0 Bs*���/���g�����X'w[�)E���� -�d.TS�T�l�<.���TB�M]A����I�6EN{/���E�GQe0����y���]�B-+ ���ԓ���)���!���H�DJeۜh�ҡu��ՔF�#�N�%�܊�P�/��JTIX�zE_��OzL�R��OT��:(dU<0�S���)�6W��;����=q5��#��mҽN�t+�z��*K�qI��z�v��kN����z�Q�ZObr^�U������҇�5;�5�3�낈|����o�|�J�����sz�T�����,,j�^��v�nEC�lk�� ���`V9>�J����Q<gw�x� /@_J8-��]A������(~nOeC�֏�h�x�ۂ��j�y�Eu}�5u�q�O�a%� AeUBʞ�(�TC"CBI�{��y�! -"Zy�u��jU.UoEq]T����EE�JU��zmW�����9�������a��O�Ma$%MХCR"͐��)g�,��,���%��G��V�g����s�����"@���W����D��o0�O\�?�T�]�����B~#���#؍�b ���8?���y�� S|��^�J*�a�ʧ�4�rDq�D�S6� 3��۫��Ӫ�<2���.a �z~���Q�|�Y20��%��-�< -�0��vv������1t��ð,�VX>=����o�u��*��c�X����Lm���t�u8���o�$t)������ۭD���u'�������N�H[� -h���-��ڐ�yt��x(�q� �!>b�������"jtC�F�6��o�����g�kP����� q����ym�|��-+܊wH�;�8WUm�z����(Y�;Q?;Ӯ�&!�`�C��^�7g -�~T���%�G.PAU��=$�ج.n�#cL��2z����_i�v�&6��? �i��Z}�����_d�X>s��c�H%���G���}��q�r�K��4��xC�~���w�6�a�����^G����2���,٤���B��2%ų�1����s�W�eK���+�"@�3eL�6��2ƣ�D���ܥ�U�c_�Ebfst���F]\:�B�����r-�2]��i�J/r+���tk"�$��r��Ů��,h�6_N��}g�E��&̵�&�Ms��+�[:���kG��q�C�}�)�g���4���(W��X+�ߝ��C܊f������vh�|����e�d�KK�gonWD�s������'uV����ȭ[Wc�5+�j�<ي���X�����o�����<|������M�+�7�8ó���[Fz���ƭ��~��O ��b�D�S������<���+�䩱5^��Z��t��ڭ��9���ƄP����ٹ�G�~��^�O�0��b�+�T-�ɒ�j8/#'Um��ŷ���$ T�;���]�v+�UY���j�+,>��'5e��e1���岽ᾪP9�w��]xpZ-C�E�_$���)��e����˦����-�߰%j��5ǧ�R�l��h�\���dguHH`r�C��MHO���ʔ����^�ް�9fTp��.B���g�bE�K���l/�̒ -{4,]�g���bs[�U>��M���0�u_��M�w�h`mC�꜂%ȟ�Q3Uug[/��|� W9�Oo��f4ջ�n_TI[X="�ɺD�ѳ�u�������5��oV֞-w4���|��e�����_�D�5�6Ά#�x -�s�y'յ�ιRy��K+����(��Uֲ�w)Y�tڞ�{�u���{0c���qͅ��'�D�x&��d�'W�sb���Ǹ��n�W�lx�Y���f���Ձ��j�H7*����5",�W}�8:9OM|3�g(����t �k'���]��[a���Jϒ5��J%��-W�I:�u��#���Ul8E7���2�g�+SQ�kJ �N=����$��"�5s��ZsN/����/}�|��ŇTH�֜��AR�:�RqSm������2��K4If*L���*�+3[�9�9�o'TE��#6��������d�z-żj�*}�9�"$��p��4^wK�?Rۍ]�=�𦕸�s�nN�x��B��]��j�[���M����:Y80 �q���!����NdϹ�B������r�m��!�w�+ۇ^��kI���]i@f�Y�*C�Rپ9�TZҲ�Er��{� Kv9��s��;��g:s?��G���0����u: ��� �&�o��VKd�>��A�)�Qn����06$�A�}q��w -�A����*����Q���U�l���Ƣm����֧ra}!��h�U<�������2w�S��h���&;��e���f �LB�[uY�( -7N�v�c��)I��腦H��C&�6�l� Zgj1<������6��`���K����`�-#$�k��.��=�'p�9S�8w%����w�Y�,���Vo��X\u���hM����都�5�Q�JU�Z���@A#�Ȑ��ޛ�_&ل,!cd�Z�Vi���핫��QGETZ�zԪxպ�^ �?�������<��y�+���� �����d���/������qV"��á/� �4HIW<�n��n -��r���a�8)�l_*�ߺ��_4=���36��� +"�<�F�k7�p��g�䶀@ :�/)��Ug�����}��jm�_q����p6xN���տRl�M ��j_N�q�g�۠N�n� �S��}T�����ôۏj^ҍVO�B�{TJ��( ckGU����|U|�wnW��40?Q�-k�G`��6�h�նA���֎�[J�O���韂@u/M���U����g�}����=V��pw� -���c�����b�}P!���������WZc^�S�w!��Ku��4�κ��]��ZYٟ*o-q�m;O��tUlz��"�y��?�C��ʽ��d_H4�x���aR��b�{b�u��L��?��2��;�����ig��/Jб�# ��;&��R������5/9�k�%^������ �0A$�X�������em�2٦�,R>�3]���̽ 9f2j�Y7bLb'� - ��Oy=�A\��/�L�Yey�Z�aO3��Ǜ3k�5�#�M���م����_�u[�P�zX� k�_^����!{�Ɣi�5���Fvڄe�!3g� 9��8]k�ui?�o5sUn��@Z�!Fz�U4m�f�e��lr@�G�<��s�x����s��sû�� ��1%?8�e���*��´wƭr=������۩'�ᢡL����&/h��%Y�6��aG�����Kn�߅��c\��'��=2��_�,Y�yZ4����V���A�G���XZ��;�Dn�������8�|�:G@4���P�h�A�?��;G�AR�+���g�.v0t�Ʌ�7H���|{[eww�WS~�����>D]ŚŞ�H�ظ���^��ċ�{�2��I�R�U�n�7T��j�+�N≊^�fʈ`f�0�G1�����BK `�p�̷E�qk�-q��p�k�o�1?HkHpB�|��]d�6�z@�8ڟ&UG��ީ�M*c��lAOYU���Х��A�טy�7 -��Zo*�����&K��x���]Ls� ��b��_�fN��[����=f!�Fh��\��6�D��6�G����SI�Y��np�N�6;Fʳ -Ɏ,|$�R�N8�l�>)%#�����G|��)e��.�P��G[�sL��w�lE nW���ͮ��]�dȍا�2p8�l��!%�� iFfw{�V��V9Tx�;R����Ζ\Y��3IĂ<̠�@�3�B�`�i<� -�̶ ����[�`�/�FZ��/*�͂.rH�9����o��0���&����<�Z�E�g��*�幝����B�����!a,?-0B)�T���ۤG,��9mZ��.����=��)�CW��ĉ�z�>�` F�#]2��*�B,XX+�!|+N����J�1i�y��qO���ڤP�]�)���r����V���W%����Ա�^<�*�t���<{��Ds-�%m�zG��٥ER�I��S�P���@C�Y����V�W���^S�@|^�Jl%�� +�{M�R9�L�RJt�5;P���g� �)��Z�V�r[A ��}�J�{���a���u�SJaȽ���w�p����x�ԇx\��9�9�Muf$�pw� �5�UgJ`��t�qJ�]=К̓l�6�\ŋDT��}�iG�^;#��նC�R��{e���)%SW!�se�PL"L��Q�6($���Wʵ�qUۘ����h?�!��ŤR뤡����@�H&���i�k:�g����*�eq�J�г��t�m�QA�/�?IH���|vR�^�y�n�%nz��^����$�BX-�$sD����,���RY-:L-�_b�cЅ�X8뤸n�\��ISpK���QّP�`�\��}��R�+����r�23��QEp��ѩ�d�U�9`���� -`i����t��n(臺��|�v}:�+�)�?��T�aa��� ��)�x�U�٘�/�6�b����ȚBS`�,oPd|��Fޅ��]�QM�Y�D �MPY�BX,�DPv�-����,�=�,dO�$$�T�0EǺQ8���k�)�S=��Q�Td�E���|0������|�{������N�`,��j�!�ے,�#��,s�S� -kv\��g3Y���s�Z�s��S{d[F�6$f�8fY[̶=�c�ٚ�N�`�)����U��MWs���;G���w�ƴ��U����ƃ�TF����������?s��\q.�9kH�+���R�H�j���k`� JL��М��e�`��3���o�.��Ą�Y����CK����������˻��0�ʟ�m^�}��!}�k�:|��&�П��^$)�T]����9/`ir����0l.�0g�g -ʙ��t&ócR��������.d/Pt/W�{��{�Ҩw*n<��M�0�%[�e_IQ��p�r����]�\�;p��������(Ǖ]�������c�jt��-5�S���+mԄA����(ʮ�q�l~j1��!�!%@$���m�t8 @<f���H�+��l�h�^�J��К��n��͔�����FJ��q�zߺ�����M����3�,Ry� ��������v~�_����#�)��ADrit�̛�] ����gȬ�>k��!%�����ؕ��"\���%e���d�ˍ���y=�yu6a�2���* %�T��iP?.��V3@����>+���3���BOЕ�E��̒��=�K��[���i����!-U��Ծ �S�=P�v7<�̱���傑2����|g���9�����2C�%n��gR�@ ���iYѷp/���0?]̃��b���U�FG -މ��S8]��gX�BRmSh�����@�yjZo9��3�����ŕ)��t�J���+ܬ�B���T`Z6�\���F����0�i�0.-jSy�n�/�ot>����ծ2�!ri������w���b̄-w���s,��2(#���Dqgͳ�'�#漥T^5ٱ�?u5UX��\���Id&�m8(�y����'g�v�Hz܊_Je�l��I -~Gd�U�1�6�����o>W�Aq���{�CE��g�z��IH�Q�(��_aG\�i'Q�i�$;}^U�����W���p���6�����2z5��7��Sb+t�4]��n�^�9Z�Ɍ�-�,�I�n�k��}U[��� -뇘%��֠Z�� x���M��k�{7 ���`�[��+�xp� �w�m����z#�/�8)o�ȅy��*�W�ڥ���@���%�zѴ�=z��L�%�d���I��X��[����Ϊܖ�0�5w�*��oR�G� -��$� -M��R���X�c��%-Ew�JZ.5�c���Kh18����B/��.�]{�J#s)�2βZZ0�l�eS�{.ςe����A�˔�{ Ѳ��g��R -��y[�D�s�cV��7͟��R��������W;'4�s�ڪ���8�+���o����W�o}�V� L���Xi�/vt�.i�{�AX��zi �J3��2Il�������?XOQ���5��_�Ӷ�R7���QnN�#����'��YiQ�:c#6�d�b���h -*+�4c�}]��g9Pp»���*1��V�a-�ń=�ax;�����g�~T����\8Nx0'z�� -7�Xt���b,�VU+A��c*�ç4�ʧ�P�^ -�)���b��<� �^�X�6�*M� ��E1�U�� �v���vs)�e\��\F'9��A��9i���_{NS��ֻ<����K��C�aLGS*��U(�䨱� 1ނ�7���ij�)��T>� ��ޝ~���/�K/����\!����6��g�8�t��4��,;���7-�;Î��s�[N{e���G]�jyF���M�%����+�,~�m�O�a��~C�בƼH_�!c�<vuP�uC3�:�|�2~�"�Q�X�y��� -��_.��jm��~� ������:B�;pV7�HeXGX��5��qd�͙��af�t��m�FBzk���^2R� &�2�ɂ�{5J���jq^s{]�N��a�e���W�P�����!��%/� -�?KIn#&�l����������(��H]��ѩ�^��`R�q����&�K�Gm��W$Y�֞ ��]�=���eh_y�W���|�|d �~x}����~���������������������������������mft2���� ����������������������������������������$���w\D. � -��� ��q[E/�������qaRD6 )!"##�$�%�&�'�(�)�*�+�,�-�.�/�0�1�2�3�4}5w6q7m8i9e:c;`<_=]>X?U@QAOBMCKDKEJFKGLHOIRJUKWLYM[N^ObPgQmRtS|T�U�V�W�X�Y�Z�[�\�]�^�`ab1cDdXemf�g�h�i�j�k�mno7pQqkr�s�t�u�v�xy0zK{f|�}�~�Հ��+�H�e��������� -�#�<�U�n�������ҕ���5�N�g�~�������ѡ����'�=�S�j�������ǭ����*�C�\�u�����ø���/�K�f������������)�E�]�tʊˡ̶���������*�:�I�W�d�o�yڂۊܑݗޜߟ������q�`�N�9�#� -�������o�`�P�<�%����������]�4������_����}�#���b�����Z��e��h6��� d ->�� �_3���W0���rL(���` A!$""�#�$�%�&v'](D),**�+�,�-�.�/�0v1_2J364#566�7�8�9�:�;�<�=�>?s@iA`BXCRDMEIFGGDH=I8J3K0L/M/N0O2P6Q;RASHTQUXV[W_XdYjZr[z\�]�^�_�`�a�b�c�d�e�f�hij!k/l>mNn_oqp�q�r�s�t�u�v�w�yz"{5|J}`~v������܃���)�?�V�n�������َ���3�S�s�����ۗ���/�J�e�������ס���1�P�n�����˪� -�'�D�`�|�����д��$�@�\�x�����˾����:�V�qńƗǫȿ��������#�7�L�a�vӋԡշ��������0�J�d�~ߙ���������������������.�A�T�f�w����������������������_�0�����V��i$���^3 �� � -g8 -� ��V*���xQ)���iD ����t R!1""�#�$�%�&y'](A)&**�+�,�-�.�/g0N1623 3�4�5�6�7�8�9|:j;Y<I=9>+?@AA�B�C�D�E�F�G�H�I�J�K�L�M�N�O�P�Q�R�S�T�U�V�W�X�Y�Z�[�\�]�^�_�`�a�b�c�d�e�f�g�ijkl(m2n=oHpTq`rms{t�u�v�w�x�y�z�{�}~#5�G�[�o�������LJވ���*�D�_�{�����Ȓ����2�M�i����������4�Q�o�����Ǧ����9�V�r�����Ű����2�M�g�������л���-�C�Y�oÅĜŲ��������#�:�R�iρИѱ��������6�S�pڋܴ۟��������"�7�K�^�p�������7�_�������"�J�q���������������k�7�����S��L��A��S�q'� � -M�x 3 ��a��W��] ��l2���N��r =!!�"�#m$;%%�&�'u(D))�*�+�,\-/..�/�0�1U2+33�4�5�6a7:88�9�:�;~<Z=7>>�?�@�A�BpCRD3EE�F�G�H�I�JqKYLAM)NN�O�P�Q�R�S�T�U{VkW]XOYCZ7[-\#]^_``�a�b�c�d�e�f�g�h�i�j�k�m�no -pqrs&t/u9vCwMxYyezq{~|�}�~���Áт�������/�@�R�d�w�������őڒ���2�I�a�z�����ɝ���>�^����Ħ��4�\�����֯�.�]������&�\���̻�C�������A��� �Tǜ���2�z����_Ю���QԤ���Lء���Kܞ���A�����.��:���E���L���N���P���V���2����_��q�UKw�T\(���=��jˮr -R�2�<3���ɓI!!D ld��� -"Dq��"rTr�w�?���7�952��L�W���������ּ���Gx}<cI��mT���x�����,�Ag�/�Ap �Ӵ��F���;)h�,Ȁ�{�`�F�*q;��-�'���^�`:i/�7����8��g<N�b���S,w(t�I��JQ-� �%� m>nxN�&Ą} ���^���~ʶ5����&u��\��)j�)P�%�g�B����=�x���ȅ�N�%�-�3��� -[-X��?癲�.�K��Gu��%��$$�{Ma���[!�π�T����� a���J}sqҗ��9ݬ�|w�;$N|���� -~� Ь�4��t0�t��5f���~%��M`R\�Cv+kCٴ8�5V0���~�5˝�v��5� -p�C�G>�^}�(S�5�H&-k[dLj�K���G.V���o�e��h�j�����Q���u�����}C�����& i3-b�� ���U�j�h��=������b��[�[���3�`A�#�X���܅eX3�H����7�7#��'s�������b�P�[5nG~�}�� �����]������(�e�;����4� ���D-�=v�h��&S)����䛪ޠI�a�!*#$I�,n�M�4!�$�v�yvZt?w��J�;�j]w�R�^�O�{T X��>fƥ^��ۍ ���n!-��Wא�B�k ܗù��]i氩���SO�,�^�N�s���cn�`�x��$*xy�n��Q ەm)g@u�G�3Pb�C�b(����_��u~������ -x�Y.4<�����������}�ڕМⵘ�!�}�&c��Jh�p���v�#�z���Y�) �6-H=��20���kr�OƁS�y�}�L�o���<��uL@KzJ���N+Mx���V���(�/�pr#x+3��2�Qn�$��j�3G��,9�I)��p�Ց�*� -ؔ����`�q��<�C�o��g�����,��5\t��wa"�[�&� h��MF!��ш|�J<��Ï!=}Gě�f�4/����7A����K���D� +���<���:>_���o�/�w�Kl�%�\ͽ)�����d]r��(�-��o�N��.B~�@��%��aqw/�4W�p��e>ˉ8��dZr�6�����~цoVx�>⟕��c#�3s�9�;*�,Ϭ�(+��$�rR��)��L}M�&�^3�`�ʅ.�.�d�q�K��S�u�f�Cc8��4Vr����[�����ӏ�:b"�6���y�M$�H��Ri�#k#Z�Y�DW���/,f���4��76����t�.��1�.���YEEߛ_�N�\�eA�ZU�"<�օ�ɇJ�������� ��������#��^���N��I�'[dأ<�nr�8[ۥU��p3��q$��\�éw��V����4�-ߴ��:�N�?&B-(YB]̔� [�,D����Q�#��y#e���#� ����I�J{�n���s��B�|/���]��MdN����ç�poƄ����M20V�Xu%5�$H�^����(�I�O� Jv�\�W�㇆�sTc�|M��-�B<��yeO��m��2So˶��p��\�c�T�,�-L���$�d�0��4�DZ�*w�z�Fޛ�B�X�1n�%,�)]�3 ��ީՇ�dsF 1"N���-�AQ�<��G�V���w�"�eN}�Q�-cg� -ӥ�C��D���u'NQ�FE�U��BnZH�UDa<Q����~��UA���1�.Ґ���$;Y�Ǐ�&M# -����dۿIY(�7XaKD���X���(-�gj���-#cҘ��T����������~�C!�m�y�3�'���+�<ߧz��В<���*�����_�-�t���[�R2��.��t�!�ԾDL��)=�0jX9��I^o�����cX>�:��k�,�qzU�]�%jP0@=���ð�X������'y�!P�2ZDQ���.,�x=����ᵐ��p���n���5u�������������X��%�3fUV�8�Q�� -��^?,�ā�9*�l��[>P��6AzS`P�{n�� ZJ|�x�cV�$8���|x����9��ݮT}�)���(U;�d���b�yIݱ�P�P������������l�xE�Մ7�Gꥼw�V�'|,ثm;�������F�e H^�( ^�M�}��º��5�f !��o^�"�����#P��R1K�U%Wy$|ŋ�v�8/�AY�k�yg���w�ޠ���(�X��x��W6�N����D��|�?��q��� -��y������҇�f�B�Jy��t��V���b�N�뾉��T��T�ɲ ���.+��kV�����l>���2~��|��Ǎ�_Q���cM�� ��]��v���g1�L�(eQ�f�!/Ї�UddU�ai,�$��� -I+�C2Ö��Io���ȸ���6�-�װ�SN�cZɺS�d2��y��4���ΌY�|��`�Ǩ�~C�Cj^=������S<M��U���煩d�"m�l��3(�fq`�&F��A�F����uF7'e�!�A�f�X���em��F�U9)N}.�,�q�e@,I�����B���fM�aN��\�v�z[n�2A� -�Ii�Y�!��N���_����д��I��i?%�PS�e��4nG/*�iN�b�9�$b���˻�Ϲ����-���$��^��<`�A�MR�ڡ`�S�J��3b�E�`��I� -���س�����]�{��.!���^���b�EW(2�U��6�Zb�D�?��yj�G@5�Ǡj���؝�*h7b�K�,:���hS'�1�{��F�U!�����m='�$�(?�g(NJ��\�-��(�j���s��%�h##.(#H�a聾$������Y��~��-��(g��5��7���t��x��ݜ|^��6�@+2ik�$\j���?E�U1�+Yd���EI5�����J���u��뙼_�NCd�iLS�q�M�Jߝ�4��ض��� -��������a��|V�a�pW�f5�"re#��p�ؗu�������,����y��P���!�B���Y��:00鿪_DBD����$Rp(��xc��}�k̇vKCCu��Y7V�cz��� -�oU^M�W�H�^! -��"�vV�\�t�1����]�8K�E^�4L{�}��h��FT_*��2��dg��p����$P�^&��L���<bv�V'XB�/�rL) �džul�)�U�H]��#��?�\�'����@�Un���hl��n/�+����L������P�S��:!3d�[��;/<��W�)�6^%/?w �s����5�ā��&q,��".��cI��pQ�"��7�ɬ�t��j�Z�z����y��t_����ˣ�#�4T*5D#�R�x?vP���3�Zd����C�q���/�Z�^7ı?"��5�����q/dl�go-�F[l��T���ر(_S�ɹZ�N�E��M�c���� H�d�&ؓ��eL����Џr�zr�ӄ��L����S���M"?�����|jX!������M(Nz����o3�ܡ�q�����]�w��zzoӟ���y�&�T� <C[$���>D� f�����Mx��W�ט�rhۨ�?J��&U���o���|����NH4���"5�-�>��܁-1?;vZQ蝍&{���m@�[7���|��(~�Br����n�n� z�������.�5��\v2˱�����N�e��u7���jnm��MR� ��@ %�u����P�� Ţf��ĉdl���RT��6�a��gU��o�|�IҪ;�칊C�v�< -K��Xԕv:�Tr~��Ƌ�7�S��Ԁ���" -�+B|���v�St�"��7�0�RЂr�0���v ;,�T�̬�Y���?� �`��Vk˧����$�T6*�tˑvK�$��s����}�3c0縉ȕ�܄������c+m�%?�=�ųɦ=$:�w�J�@(��R�A7���"�4S�AU������@��kB�)v�2����uH��5��*&��dWxf���Fh��ɓ�?\kP'���L�����E�.~�5�c�h�*A�y��w9�9���JZƲWD��}N�q�2�n�Ғ��<��K �]����m�&I8n>+��":�%���j����ux�spDeI�,`{�x�2O~��*ݥ<�F�f;�q�ذS.���N� -�lq(#��m�s+C3�����*����"���QE��Y��ɆX���{������~�|҂rX�*����`lsx�����齇� -�9B��]��$j�1{��MX�z3��$c7`��r� -�?���pO�'����T3Ï4�����8��$y�Ka��;�GYq�BZ�!pk���n��7p�9�9�� Iö�K&���I�E�pvIb��p�A:S#Y2�4̘=.����B�2(��y��I�j��L�q�DŽ���, -�Bz��7Q�R2G�#'��}�Ͼ������!OA�����ˁm�� -�)=��\�P����3��eW�ܗ�}�[V�C_'S�4�e��?w���_�� ����櫝uK~-���Ȉ)�g�8��C(ʺ�|}8�T���y�#[���}O�̎����)Á^S����a\^�����,݈�j��ۼ�,my��x���}�Vw�t4u����[������?%�/u ia����<g�H]���W����(�>��O���²�k��s��B���cO��iAu�#N�o?c6���y�=$z� =��:������H��� a�"�1��Q�hG�wJA*�0tݟO`�,����%1�ư��$�� ]Q؇?y�)��*��/�o?DF�|��NCk�F��!#͇��'��C�;� ��<�+��BZI[��R��^O��Q;��������O�\6�x4̀�͠Yjt�ԸE[�ϔ���B����|^\�0���G��6��Z�'Ez�;PU\Ξg���6x %�x8�z�U}��s4EN��i�Q�\�!k, -K8�W�Qp&��>�%os�G�����M��"�|r�q�Z��^8gyWy�]�y�m�3ή��JS\9dsf���X�,7��=�x�>/t^�p�?�H���!�i{��i�v��7�dgW}0��SUi� �Pq�2yQ w͐c|U�3�/�NH��-dx�m-d�ʗ F{Y3�Afc��%���7�_�s;Wr��(�y��-�#��&ab�S/��Ju��0�T�80����c�����r�*���.�F*z�c⾚��>�/�6�A�ę;�O<���<[@TsO:M�%�x0��p�])�v���DuO���Tq��t��P���C�:���b���8G���?˷���s��]/.��\�n��B4hQa=�]�]B NWM���}���\K�輽#���c�����r��S�<c�0�_p?���k�Qm��\�%?{,�"�T�0�Op�c=wy-�b��Y���ጒq� � ?��;�mt�3ISR4�_�% -{EN�ND��� -|��QZ��z;?��]��c�N7����-��Z��mW��gb��sm��辶�ɸ��i�;������\�u4��t�t$�כP����Y�4I]d_��H����S�%���3�kI%� -���sqˤ]L�S|�)k���d'l�!�������F�����9�%}�O@4,Vf _���Pt�X����Ќ�G�~�i��B�^)#�qE��B��)yw�͒7���G������a��@P�ǻ �ظ�/c�'�ԫ�!�3���-��!KŽ�ݗ>��ő'M���?=�?�v�7̳�9�3&�u��o����eiJ7�0�^/�����*$��P\(��w��E����S��&�+�����V(ϋ���\��X�<" -I����~IY��ߨ^�g�/A��ԁ��ؖ+*�@=��<�Q%�p�H@J� -h%��� �@H� �����{��e�b� -�Z�s��V-8� -EQq{U��~��?�ɻ� ��v��]�U�V�KK��I�嘥wZ'Kl�lV� M3�<�D�y02˽)�R�;�,��@ꮛ_p����AHs:K4@�͒���j`&��^�#�<_IOhWn91\ʰ���>o��:�N;#Lh�Z��M�㬹��mr�nM�;.���d���@S�OO�'P�4�����VV����b�`Kӫ{6?�Gj��UB��� h�;[| ���ZWF���f��*�r9��~D�@Wd�����7�"~[�[!ꭟY5_x�1�C'�u�Z_>ਊx�+e�W��}�R�ӗ�I�^����p��[EfY���tW�m����S����f�{Qe�W�Yx'C -�����K#y��Y���QmZ�!��4D>�`)��ɾ�UB���b�8����� -���7y\���sðZ��k��7*��C�"-�n���(����sds9�4����>`K0E����)MS����r���)�Ր^qI�?Ss>�D��C���A��,V ������8G�&�{@U�dv�qGe_]�_%��J�Jx�9*="��7�S\���=���A�a��C���Z�B��*�F�����d��6�2��ab��n�I�K8�.��P�4x���M�4MU|�����>ղ��z�ߠ�\�'^��*&��^]wI���&J�m]u�2�=ט��}��-�6H�hrҎ -��кwC�1�T����'n�l}��ԫ�!w�Po?��ﴺ�~���p�f�K899�շ�H�Jvj\����� ƱL�Q�V��ՠE��!�g1��n��J�����9�U��:�y��7n�D�;���s�A&��N��4FQ�y��x�oz��ҥڭ���9*O�W�:�+Q{�,rG��A������!눅.�7�zk���:��&�A՚ݢ,��"}+��̏�A6A�c�A��Ҡ��r��3k�s6�Sj9�a�$����Q<�d�^�^Sx�N��˘�]Qu,���[�w<FA��Z��XW�A}ź0{�B��H�Hi�ӱ7�2Y5摈#� :\�h_[��MC���=j�^�,(��Վe�+��,ߧ���uS��p��sG��=������V���Ս�-�s�PW��Sgv0�%U����o���6�t�Eȓ��V�C��~F���joΠa˙��4ςh[-m͎�ZuR��)�-�JCg�e�� +��2F����Zw�I����fMgf��X/���!���4����ҝ,_�q$kr��b�/zp4Ŏ�~���dn3��=� ��;6 as��ۡ -v��Ť����.���4\���1���L_}2��h��I���i`}�r��+�Fº�F���ԝ/�4�K�{��g�U���TS�p�r�(&|3� e�j���ɍ��*���\�ؠ�+6�h�����V�����%�'~^��8���`ׇ���ܴ�,�i���h|f�gHu]ګ�}*��k�d��p>L5L�j�Aq����v�<����#omM�r?{��3v똆Rb�*G=����v�`,4���s�g�)�����A��uI����Ӵ���S�S��j��Q�HU9ua~���aN���4o�`�����I���������4;�慗��hJ��9zdȈ2�>�wy[?����s1����yE�և�@�ũa�I� d�2-d��>Ҷ1O��(��lXQaZ9n=O ;��bZ�'��M�V�U����|"kz��Br2g��Y�Ƴ�Nj�)3��������7r���I���=�ٰ���~.X�L��H+��\������-��u�4���͑�w����2R��!�;��7Ⱊ ��f��M����!�N��Jl�dL,w�tGɁ���[�N�:y -�:��������5R!0�����lQ�զ��?�ٶ�¿]1&��>��#��ϫ���������3��\�Y(��*�HV�4���T���eu��V��gì��Y�L��u��?��A�3���(��ߌk����Qwg�3u�j���u�^4�RvTQ���wEE�ex9)��yT-��z�#.�V|[��}�w��o}kZ�|~^���|TQֳe|~EW�|�9@�y}m-��~ -#��~�+���x���k`���\^��S�yQ -��Ej����9j�<�v.:���R$���Dߑ��Dx)�nj k`�r�)^����Q��χGE����<9���G.����^$�� -��v�ł�xE´�3kz�h��^���YR �͌ZEĩ���9�)��.윹�%��<����$xb�ݙ�k�����^ϵQ��R2���E𨻎t:'�e��/6���%W���JY�}�Hxy�9��k����^ﴷ��RO�o�3F�'��:R�Ҏ�/p�s��%�������:x��ǥwk���_�L��Rc���F#���:g�k�D/�� -��%ϔ��ꎬ��xx���@kݺR�8_� -��Ri���&F)�o��:q��T/����%�`��d����w<r���w�s��x�u0��ymvn��zNw�p�{Px�Y�|vz\B�}�{�+��}��t}��u~��vx~wz~�x�o6y�9X�{?}A�|��,.&�g�Gr4�\Ȼsl��t����Xuχq��w�vnx���W�z/��A�|��,c~���pa��ƾqƔͰJs�N��tn���ju��mw���WyP��AR{i��,�}��{�0n�����pz����q���vsY���Ht�Al=v�� -V�x���Azً\,�}���ـm�d�voz�[�Op����;r���Ht5��ktv �Vx��@�zg��,�}&���m��4n���&p@��.q�J�is���j�u���U�w��@�z��,�|ًp��l~�c�,n�5�.o��:�Iq`�Z�s3�j1u0��U2wU�M@�yƔ�-|�����l�>�Tm���_oO���q�~�r��i�t訯T�w�@Hy��3-|k��l�qY��rŨ��t�+�ud}���v�h*�Cx0RҀ�y�=��Y{j(�}pс}S}+�h}�}<�}�}N��}�}v|7~R}�f�~�~$Q�x~�=$�b;)B����m{3�κs{����H{�|X��z�|��e�}��8Q,~k��<���)�����{y��Q��z���z}�⎬{��y�{��e|��;P�}��v<�~ӆ�)�����x3��� xҜR�?y_��yy��x�z��dD{��!P |Ս1<g~=�()����Dw3�۵�w�x�x~�/�ny/�w�z��c�{��O�|G��<A}ƍc*����vm�Ҵ�w"�h�wͫ��x���w7y|��cz��YOf{ו�<#}h�P*<Q�k��u����v���TwJ����x�Yv�y��b�z8��O{���<} ��*V��� uzʾ�'v3�L��v��4wŴ�v xǬ�b$y��@N�{M�O;�|�*j~ތ��N��p[���qЛ��<s �׆ztts��u�_��SwtLO��y!8���z�&�}4���w{e�0��{��Y�6{׆p��| r��F|�^փ�} Ku��}�8e��~�&w�L����f�w��������t������qj�ӃZ]��J����8=��;&̃���K�ϑ_�₃�l�^����ށċ�ps���(]3����Jg�ڇ)8�B��'���@�́�����Y�.�(��N�рÓ�o�����\��؎9J ���7�����'Q�{�6�}�����t�q���0�.�����n���Y\�-��I�����7�)��'����b�짙���h��9�:f�dnMu��[����I���7ՀƎ�'����n�qg���9� �� -�:��~��]m��{[7X��IAȗi7ŀ|�'ˁv�����c�U~佒�9~��̀~���mQ~˨�Z���H����7��F�'�A������o����Cq%�;��rm|�s�j~��u?X_̉��Ŷ֕c�tG�ڝh�7b���DR��� s7���7���fj:��R�#�z<ZJ� �s{�uij��/�_���(q<�6�C�8qCP�(��Aрا�QO�RG��⃒�D��sG&�Pb͚�͑&%$��*�����O�M��<��� ��B������O1��PUlOC��d���Wy!�4�4D��˔pp4<^iis�~Ɦ�5��`�"���AHZ�q~��_/�>E��Z�+�Z�g�ݺe�g'�;�X܀ѵ �z&r��|Z}!c<����ڃ� �G��ې�@"��:f���8��^�ퟂ��ss�Xx�~(��g�q���7��ڐ�`�?+ܟj}�f��c�HR�D�g9���pkk�|l\w,`NF}(��p2*m�ם�5ypp j������g���u��������ӃS 㢚�щ!��9��|m�N��FG'�A0�~��յH����=�n����\��V��1^!(@�Lm�Y��{h��������D��M�OdT���BS|`�= -f�ډ��A�[z����W4E�`���2��D��Z�8*o�f��?�V��ԭ��%R���m��X�|������~�� -S�j/)-e�4ה���7�w)o��E?�)�]q���.*��9���C�����ǟ(X���*�:4 �)�I��@���g�jt��EZ����n��I��9��YSrK]C�[��X�D\|���}�n4 ��Ef�לf��N'3m#�^��|�$�z`pn�@.�R�/q��s%�<�Ϻ�\Ķ��Nx��v�q��3�vTC�:��H��o��:ӣ��;��w�ϲn W�'�E̎p-}P���Ե�+��� ��s�W5g7G������s��I�NV?2��-��Qw��<����)�h�,Q������luG��!�ԬN�� m)����r�e��0(������=�,�dMX�X�U5\�Qt�ڷ�� [L�+������՜/L�00�NJ�| r�Tbhr����<����}�p�h�e��j3f�W�ڠSH9FM�40��W��[@l���/Uە9��ʦ�ϲ��0S��;k�a��+�km��O7)���q��~m8��� 9W�E�˾��g� ��06�ՙ�����¤��p֩�¯�?���슊��upt+���BfvE���*�a8Nydf�&><�F�ep�S L�(Q ��W`,0B��J�=ʭ��u�"S�a�p���.(s�6I���G�,�MT2�$"�F�_E[!��~�n"�eyS���o�[��[̃'��b��0g-y�������)(�e����NXpw�^�x��^��#,��ɷ���>�%d��H�}�O\�=ң�l���Vm�d(ۻЈ5./�]p}�e��jO��93�p홈$l�*(D�P]�:�4H�m��,}Wx��rU�Ě{n���z_Q��:�Ȗg �\p@�P<�G��1ht��^��="x�ˣ�o��͇���é2; :Ø�;�I�'���03*���U*�A�7*�E� -ycq��-_���x.H �-�Ϗ�dR�b��.��й�)��zI�"���V�&֯Frv����9w��AKS~���b�e@7;[{��G����j*L7�� - u<���H��|�C��a)'-!�4f�A����3_���Zu)�η��X�`�"� al���y�R�9]jN�}��t -eޭ��,����5LV:H s�<p| �mU���~������ϲ!��Vf�Rb�v�D���"v��2�I�����s� �0��'x����q��������a=��Ĵ�X����Z��(,��!?%v~��X+hs�w�0;�ڮMF� �H��L�[篼�sV�1��fOX�I\�ۏ)�6��#����w|)c�Z��g��4����GE|t�s)�kV��#x��)z3���g�$ ���ǒ����O�_.8�~�]),�]s%G�V��3Ye�7a0��(����d^���e_J*��4��ťC �e��Yn�����D��~�/���~�$��r�>�{���n#�G��a7����T`�Ƀ�G��"�K;^�m��/Ӗ��N%O�����i��zʱߎ�m�7��a �~��TD�ljhG��!��;y�~��0 �҅[%��$�)���z����m֬j�?`𧺐�T(���G��a�Q;����-0=�!�%퐄� z�(�Xz��Z��mȫ͙O`ܧ#�3T�u�6G��ѐJ;��/�a0d����&(���ŋ��dzp��mǫM��`Ҧ��"S����}G��_��;��Ɛ:0~�,��&Y���6�Z�?zB���&m��#��`��}�eSˡ��,Gb���;~�l��0w�ӎS&g�N�\/���n��$o`b�apNW:��qhK���r�@-��tS5��v*��Ww� ��y��{�oJ�*v�c��sv�W���wYK��x@S� -y5,�z*ќ�{H!z��|���}�ob��~ c���}�W̳}qL�]}�@���}�5l��~!++��~�!�������o^�/��c��~�hWѱ���L+����@��:�P5��_��+��v��"{���e4���:op��c��R��W개�3LJ�݇�@�"��6��\�v+왅�g"蓯�r��<��o����+cӵo��X�ÎxLa��nA�P��67����,6�ņ�#@��<����o��>�'c��ĖX�"�BLp�m��A$���6Z��},n�.�#��y��i�+��o����|d�O��X,����Lu���KA'�E�6b����,�����#�����˅�o��m�d���X+�y�3Lj���{A����6`�4�,��j�b#Α�� ی��@�Lr�k���s�m��Ht�oꕪvq��wPs�j x�upTwz<w\>�|yb)�~f{��(o�xa��qx煮r{y��s�z(~pu`z�h�w -{�Szx�|s>X{�}_*$}�~s��m����n��ʧ�pW��sq���}s��:g�u���R�w���>z �W*^|� Ϸjܐ���lˎ��n�����p^�{�rM��f�td�R!v���=�yB�.*�|H����i1�E�2kE�'�om2�;��o�{z�q.��fsq�4Q�u�=�x���*�{ƅ��*g�����j��� l���an%��y�pR��e=r��Qu=�C=Wx�9*�{Z����f���vi'�ɡ�k?���Rm_��x�o���d�r��P�t���=+w��R*�{��ɟf7�ĵrhr����j����llƫ~xo!�fc�q��2PIt_��<�wc�+ z��.ȡe��贞g���� j����lX��wdn���c_qU�O�t�d<�w(�q+z��k��{Rk ��{�m��|o��|sp�vQ|�r�b}�t�M�~kv�:�y'� {~�xLv̱Cyw��;y�x2�zBx�t�{ y�`�{�z�M0}{�9�~k|�'X�2~)��uтH�Xv��қ�w��\��xh�s�yc��_�z���Lx{ڀ�9k}v��'�u����s؍���t���uኇ�Mv݉r�x��_yZ��K�zڅn99|��B'�~ԃ�ErI����s����t����u��q�v쎶^_xh�iK|z �9|���(~M�3��q����r_���ds��h�t���p�v�u]�w���Kyk��8�{{��(@}݇��p$���q{���pr���2s��upus��]0w�TJ�x쒢8�{��(c}�����oq�J��pѲϕ�r�E�xso��ovt��\�v��RJtx��h8�z��L(~}<���n��� pY���q����s�qn�t���\;v_��JxN��8�z��t(�}�A����jZ���lm�u�6nMǂ�p'm��rZB��t(GƂ�vS5z��x�$F��{Q�!�uJ�Áv)�%��v�~s��w�kŀ�x�Y.�y�F��Sz�5-��|O$���}d~��-�%~��~��}-~�~j�4pXX��Fw��5���%���I��|ъࠄ}%���D}J�k|�}{�4i�}��&W�~j�.E��?4��_%R�R���%{Y�X�+{œ/�| ��z�|P��h�� ����v�ٴ�Tΐ3�!^�(9���1Q��0�������?�e0�qt�=-����n�*KۡZz��Vmmik��?D�3�\�dD�07�����U.*�%����j6�]�BڞgfHx't^���S�'S�^�ӈ��������{���Gq<���Mn��wM u�S���z����x��m�[5�"���z�.a!���<v��0��kI5� l�'{����s��Ɨَ�TMR�A��]c��$���\Z ��qB��T��t���ܽ��D��I��@��,���cԄW�i��?.�R��[E��S���cΘ���̻� -IU~;�����8\$NlǚJ�\D��Z�i�t�Nԋ���L,�Z�z w� -��ǰ'J]1^مc����W�P{��=_" ������� ����q��W���vB9B��E�e���Yd�pk�'xP�R���kp�(�I �&�L�2<G=��14?D3�W�;�CG�JFaA�PBH.�dJ2%V�{�Brz.9�oL��U1�n8XQ���Ƞ���-;BB�����������.��="s�.q��o�PU��[d@��*�@;�@��\�w��������@|�3jC��X���t���ub58o���$+-�E`�no�P���e��q��rn��F_ԉ�-g^+���6B��n�g��+붧[��U��~O��w�@3�8c̣��G�w�M�p�{��dw>Ձ�O�X��=�i=biݖ�>��2uw���e���V�9��XS�mɡ�:�h<P�ʏɳPسlS�d�E�ѩtGI��?(;v��,��qs��+�� F��wr��Y�x�{K?���\�����-�T�'ug61�����b���A��"U�ߍO�E�K\k�{�`N ���(G�u����i�7?���c��"��J��zb�r�����ķ������1!Y�û 2p3AV^O�������G�\g)}!'�N�[��<�J��#�k�s����w"�c�XF�u�*���]k͑��`�?虊rt9�_��2y\-�#�+���^�r#W���W��A{oD�0g�Dx���[�"��`.s#:+�Fp��ӄ1Օ�F�L#�1�W���7�1G梛��Tu� ��,1�e_$|�f�r��� �o�����fi`9�C���WS�8��nC�N�vj�����; �j��e���$D�ժ}�H����PYz��L��w%��::��/'O���%/��1 �Gmu3k4�g���WK��-���}�^�,��@��ϫ��`��$��U���#��=;&᭪�LfRD�O��>�湿���\�#F������x�bPpD{��M��N�]K�U:���.�7�<�����n|J���Tq��{��I��R�^h*}�j1��H��s�r�0� 襮�1��ھ�/z�~�ۢ͢ -O�T�o��q<���:������p�| y[I��%V -�⍺mТ�P�;!RmS�U,�!�%ҷt\�uz)�p�܉��8����C����iۍn@��L�� %�wK3��h,�=�L���T�lI��A-�� _��wh@�H���4ܷjߺ+y����+������mrh6�J� �"����f��8���4s�\���g�y��j�p���x%���1l�������w�*�����iݔS�Gӹ<����'n2M�[�Q���¾`@���O_su9;�%\aY>m�B?T�/M�E�� ���]�^%�Lf�\�wQ�-e��ݵ��B��z��l���I ���E��� EYu=@x&�(8�s���X�U�3/5��=ߜ1���?�2(='�- G��ҡ��hp"��z����x&���h�+�����l�����/��b��Kⓘ��ې�56�_���?���=�_г�E�!D4����:�&%]� �_���D<���۬�aM -�cO+7�ܓ>�>�~�a�wdlМr1�� �y�g])�0������D�/��0pXg�����3�h̒�u��K�UI$�R��<�b��1f���^&܆���O��|�ɠo�����b���PU��ϕ�H��ђz<���1]�+��&݊v�v.��{͞���oZ����bM���UR����H�����<�����1J�Ս�&�%��Y���[rm��i f���j�Z��Gl�N���nwBӠ{p�7.��r�,2�{u=!���w�t��zfrɯqp�f�Oq�ZƧ%r�N���t!B���u�7�]w,F�x�"[��z�5��|r~��x�f���yZ��lycN��Gy�B��z�7+��{d,w��|N"Ðu}\ی�~mr�!��f1��6ZD���Nj���B���u7L���,����#(�s�g���'q֪ψ;e���Z�̅�NM����B����7l���[,�����#����"ۊ�q��ɏHeڥ��sY��ꋭN/�Չ�B��ˈf7��ˆ�-(�څr#ʍ�� :�\��q�����e˥9�\Y�>��N�/��B��*�R7��-�-R�D��$�p����� ql�[��eǤ���YӠ���M�����B�����7}����-m�҉�$;��@���q@���e��y�TY�����Mʜd�]B_�N��7w�T�&-l�o��$L���g�=��f ��i[J��j�P���ltEܬEnd;O��p�1�#r�'��vu>��w��oz;fʹ�pU[���qBQ�xr^F%�>s�;���u31I�dv�'���x�u�Lzz��,|Ef�w�\��xQ$��xpFQ�ry;��,y�1���z�(F�h{���} q�~"fĶ6#[��4~�Q*�~sF~��~U;���~q1�s~�(��-~� ���a�.�fɴɆ\�݅Q>�ȄF����W<6����24�Y�6)�,�� ��y��n�=f߳���\!�ي�QT�RF�����<S����2k�x�h)Y�_�J!G�_�M�҂zf��ɒa\A��#Qe�$�F�� �<^���!2��҈A)����u!��τ�<�U��g �-��\\����Qn���;F�����<Y�x�12��^��)��G�P!��_�%}��Yg�Ҝ\h�O��Qh�p��F��Q��<G�'��2����)����!ԏ�3����ȺnYeI��o�h��q-j���r�mxCt?o|c�vq�Ozw�te;Fz:v�'�|�y���j�q��l�r�3nYt+�p!uhv�r v�b�t&xN�viyp:�yz�(@||�Čg�}��i�}��yk�}���m�}�u�p}�a�r~;M�u ~�:�w�~�({Bd�me�q�*g��8��iч �"l�t\n��3`�q!�^MAs냌:Mw��(�z�����c��se����,h3�j��j��WsIm6�K_�p�YL�s�a:vV�\(�z�<��a����dT����f�}��iz�WrEl3�<_o"�LErH��9�u���)y��M��`b�Ъ�c3����eܢL�~h��qakf��^ens��K�q��F9�uJ��)%y-��e_|���bX�ꖷe����gҥzp�jŠ/]�m��KyqD�89�t�v)=x����i^ھ���a�����d|����gK�[o�jM��]9m��mKp�9bt���)Px��2v�d���wngt��x(i��x�lao y�n�\z�qVIG|s�6�}�v�%9�y���s"pf�+tSqǓu[s ��vktNm�w�u�[yw+H�z�x�6s|kz�%�~�|q�xp={��Fq�{��r�|EtH|Ul�u�|�Z wa}!G�y3}�6+{Z~H%�}���m�Ѣto���Bq�}�r|�Fk�t)��Y?v��G^x�k6�zt��&#}*�n��l����mڏˎ�ov��|�q� j�rފWX�t투F�w(�5�y��S&[|����Zj��(��l~�_��n8��{�o���i�q��W�t �F�vo�V5�y"�z&�|���"ik�T��kj����m;��z�o��i -q#�(Wdse�HFDuޏX5�x��O&�{��4�h����j��n�l|��z(nj��hip���V�r�E�ut��5�xP��&�{i���<g��"j���dk��y��T��`F�օ��O��H����HA��'C��-��_���g�Ȓ- C��iU�s�lE��b[���T��z�'z۱aߘا%)]*5�%l�WT��+T/(�X�uO��2j����o��%T��(�E���tˎ�xts�.Nf�g��J�o ���u���2n�N0P�@π�dJ��c��B'h�����A)��_S�!�n~.��\�ѯ~�yZuQӨ��j���jE��{��@HR���MAPx��@�~y��h����]魪ï�+��8O������5/��)~�#��Ƣ�Ϡ�ï�ѽm��_���C�����qT��t^��#N.��F���ɮ�;�R"�oq��n ���^��J�h���(m��*U��9�]�f�^�mgφ�(���v����Ri��uD��p��;T"�*�UM�|V�ϮI:�.��D�k�M���W��tx5��4u�[���G��ه���҆�Dy�f<I��>Xզ�� -�z�+��1��\ q�SQ#�>V�rdP'"�䝭�r�(g7\��R�P�)�P�k;Z�K�v����H��1MQ��Z�ߩ��0�)��m#��;�r�sb��jB)� "��o���K������s�M���Y<E��e����I��)Š�3�A�₯��z7����!z\�5Ph��4P�R�U�4Ss|�at�6-�Z�,h;8+x -�������5�[��@��^�!�h��>Ħ{�=���Y�u�t��l���%���)��u�j1���^�7j��zvc�8�~���������$$��E!_�x�O���A�AGf)���>�[FKV@�MY��Xo�=�q/�QfM�l�G���/ݟĒ�|����.�:䥞��^V\��g���_*���CZ�ňax��%d�ΰ��:�$�i/�gP�A�� Ƌ��۵��`J�í� �O��Ss_�xZ�蛤[K��Oh���P9�K�}*���}�@�$�4o%n=�T�F�A֊�'�ט�[����,��x��@A�oE�����)���]����H������ŴK�x<��r��ϴ�&�<�4[$��j\X����A���}%�ٱ�y!K�r��� -���*_^j�8��+�y����ig�U��83����X�Q����+4[��Q"���D���N��q��6^a� p���IG!D.F�A"Sg�=Ȋ�a�Y���A�x;��Hd���b�I(�-�'��_�<"�&�t���+>�� ��� �7a*���)���.y�b�E�X�V=f�����1(E��G�d��a�l�����^�+�b�q���%$UL�~��`�s|�u~�(v�<!f9�1��>`��c^F��y�c牋l)�i> ��W�Q?vh�^���'�X�md��b� -{�~6v�b\�띊���Q�d�9@���2���:�cQu ɛчU�7w�=�}a�h��#�8:���Vb��6R�2��BX���oߴ�MqA��zo�6�Q�wM�Se��a�����~B1�|�ݭY��?8s�P��R�}�֒22뽥\2'��|F:�����$3�U^�d�5�<r�H@���R���X�(�)�n+�gm3w�� /�3���=-{_{2�2���=�K�B�����Kd��y��k�D�Y�t;3���H��^�OeN���w4�8z�QrĆ�!e�(�G&�$ij� ���sB�>_Y�|�j���0�(�K�@�<�!4~P���'ym�gZ]�Ęl0f��/���?�iLtd��:��N��Tj.��q'ð�p�9�2Nb7N�]]E�Δ�Ɵf�O"t��龍̷�R���q҈+�W�MZ#�6��]�S������C��g�s�5c�g���iҗ�0D �e���œF�&��������W�g�)>��7m��-��8�q4���� � |ɗJ��O�tɣ�m�����bn��ƺݲ��Q�dC��hك�ˠt�������� ��E���0�`ɋ�Q�n����gX��`�z>U�U*{���8RH�pe)i��ef�A������$����Aܜ�Uje ��+E�E��:��_w������ -Nʒ��z��x������`�CR5F�=o�*��d$Շ�}q��b��}Õ>l%2)N�,ˎ�0�cߌ���� ��sy�>0p��(]Z��?�����r��Mǘo�)xc� -bT���uGc\ -YP���*�U;i�?Kc1��VI�yk�[��=M*qb�AC�hqI;/ʍz�v�����"�o��)����u��FT��X�+?�M- -WJ�>F �i���C�U�����[�m��z1�^ �1d+�p>���Js�4��븪fĸ&Q��W!�B�i��r�C��-;�0(�[yn��e�L#������%]�M=�n�&�cʏ�g�J�7�=��b��Y��ji���n5��i;�-�[�F�!S�t���`�j!��B����=�E�nu�����OJ��������t|\�ou:�"jw�OG@,Ԯ�G����Z({�q��`w[:A��� -�9� ߋ�YäL�v�N��D���2c3�(v��&��6ܨ�$u�n9Dk�k��ȁ:�?aoaN�R����j#�s6pL���z �~x��.p @A��;�Aqn�.!5:4~�ڋ� #�[�G"�q��A�Ҽ��N�U�Ĺ�|/Z��D��o�N _"��o����Y�>��$FFn�� -��da��)p��{�y�r2��sUї�_�-�"s�2y��eQ��u���$A�4��I>��F�Yl[��>,��3@K���fA=����6ǟ�D) �>�c�l$I{Mv8x�jZ��S��ƪQ�G����M�G^֤���S]H|�٩�k4��HP{X� NR� #�8�$�ؼ�������{�=�L�l�����ٍ�x�y��֡��i�,�_g���6�� -�T�?:X\����u�nTDw!F�����7�6��F�՛�}�#v����L`Ly��Ͱ'�A�9�Nt}/��<�ֹL�HhmK"g���Ȧ��Zjʹ��ں���w٪���r�#���E����T.��}�&L�j���w:���w��Y�av�''�?� �m4X�$��{����8���m{��'|l"r�©u�h���O ��|�������Wjv�8��Yu-�X��G]h�T�$�4��| ?QV�@뫿��)5�T -�Q�\^TU�p-P�uA��l�y�����ɉa�U�~�F)��i��0N�n��"��C������ӮY -u��Q�,��FQ�p\��>��e���P�?.j}%�ޑ�^� ;�~BO��twQ��������_�/;R�"��g -'��%[XѸ���x0R�:�V�\�3�e�TӜA����L�?i�^�>��`��W��"p��{���NyW��\�0�V�)�ek&Ɩb%��M؊��M�|-��G�0(���<�)H,O��}FW�/_ێÌ����ސfĩ�����*�+�ҙߍ��|8����Qp�/�0���wڨ%�ܼ���#������29gr����:��B���h�,� ��9.�9�<�HD�FuI�HX�����x��V��x\�B�(����5�9!!��S!v���Պ��5k��Ϋ��KσQ���+��� ��R�{��,!��]��>(>9�SoOn��o -#����<�3��Y�����LMH�����A�Ud��q�|�XHsSCX���U,�^�])�������,ϳs�2#+F�Ԯ�ȩ3*tx%�@�y�`�{7��v�i�����ϋ�5��Բ�5d �l����Uㅘb*N�&��ڠ����z��>3g�/A}�ӵ�[��6��,%6�J�ak���D�[7����û�WAn�yʏ��NI}U8��ҷ�,s\�8�C����h${+�M�i�%&��%!ՕP6�B�û�F�s���*房�˨Y䡲 �'���o��Υ���2����e�!�Q���Ax$#N��\�L%y0'���YͥM�[l:��9�!�¤qG`���P$�����?���]�������ԁ��ثOlAqTdÏ%D��84��S�C�d�佼���^� ���@V�֪砞W�<��z�R�z?���Z�����=�k�)Y_�m\RR'Z��3u/��E�+C_Ca�9o�҈�;A����ߕ��%�dθ.�������f!�8B�>�xP���`���<Cw,������l��O�����EJr�T�EC���#{E; �;�����o�����X��5R��M[�Wr����-g4�H�r.ՃE�|��0��Pd����t�<CO�#t|��4?t�LuߴN�v�'���c��ŊWm��x7ߤ�õ/b:$�����'F�Kѩ�ӹ���7��*�?c#&K�O��+*�w�.���j�oڈ@��4&C<h=*���2�Ê|����;�:�֪|���pR)*�Ո���gmB�EC�<�8�᤺m�M�9K,k�%<�W�1^p ���9�S���m�t�n��q;����P����>��v�m��ʠJ��!/�A(MA���ӻ����EF�Ҡ�{Lb/Ѩ�b�Z���G�$c�8���ώe�з��b,dW�"#_(��pl=nh��I{����_�y�fڞW"�5��������/�J����r�艢B����g�d�y���� -a�y�k'���:K�����u�]��J�f�'�E���22E�-��ZW�شZ� ��pFDq���$�2�T -���Q��!CQ���Q��9�(4v ��{]~�i��/�3���r8B��B���R�j�G�X ;�K^d>�V���Z�xҹ��9��q���N���+:����������.J��eQ\�}ua��]/o�VT�$~c�Q��%�"�iI|Y�[�~x/����;�o+S"���-��$�P�>*ZV���Tl�r�n�Ǿ��(��� ���1��D9��9b�*�x��O����aS5[�$YJ�P�I�~H����-�9�B��`�ct -�P<��*�N��\�K��J�0#v��F$�X7!i��HsB!��� -ף�.��R��T�j�ʻ5>xأn}�F�**�@�b����)����I���r1"�=F�3)dK��0����w�6�ƽ������n�.$� ���ý�]�ÁR�\m�ތ�#�k�z�+�ۺZa�l�[�%�<��V�%�I� -��+�Ktqd��t��P��H��Σ��}�#���˦���^���G`�5,n�z6 -��)�)��1�3K�/"���|A7`�� -(�W-T)���wRZS����U��0�0l�؈E�e�ao�����H�S�p�}��nu���m"g*FG�W�lO��D -_�f�p�Bk.�����'�|N�W3�����UjS����K��5� �Eu��{^�r\�T7�G��Դ�O�I}'~�q� ��\�rv�L���5Fc���� �t��o�|ܥ�jG�HѬx �'N�a�������p�~"H�����ϘMDr��G�=ﺜ��]&�p-y��d~9�'^�8�6����Q��5�Z���٤�U:�uK�ve��TժtθYz;hXQv+���6a<-�LcG�HV�q`�x��3??U-���$�%��9�l�*) ��U��Yb�7��yk�����Rb�� L9�^�����f����2�i�[-%�kHV�D�����uyh�Mz�n��t�t��Ǝ����Z/7G�eu/��ҷc�$W���yi��Bf��_�Z���t�*�\`&b{L.~�x���m�J�iW�������5 � -�j���OU�����i�s�OF��3�epey;���z���Pe��U��G��!�d�����ĥ���5�X|������0�9�� �wM3B��8/���jx)�͘��J#��6�r�T{J^�p�C��̀w;_�\Gr�����Gn�~ -�ؠnZ�8�T ~�,MoU.e�ј�ソ�x�[��1�lTwU��y�6� i��'��~�:����#�կ��5���o�q���u�j�,\�A�����g�NÒL��w�5k��d)^�i�`aB&�H)Z�da�x� �( -""�����"�9���c�vLjev�tl�SYM�n��f5�3e�����ۣNfs�'���հ3�;�@��H��'n;��!���k� e}�#c%o\���z�c$!�&V�ȵ�o��u���偉���z�eDxI] ��e�O�"M?�+�1�[���~�<Z��D2� ���@=<��N�oD�Pu�7 J�'��7�{��J7v1�c4�(��'���g`9�M��J��a���Y��'X�l9�]�+��-��ϫ������5,n�A��{\Լ��:����f}��s�X�Zu��]D3_����Y�M�7Kh�a]����8-C�'C~�\(�b�/"g�jI�:��9J�[�,�t'mаU�<�;T~Gk�Yی|�N{��;�p|�庘��o&����a�}�����D��i����ڻ���o�*���U?�j�lP��?�1MU;eLa���̷�>�\���|��A�����j2� �T��7�)<���*� -21������z>���{� H�m�~ �9�Q����Qb��ne�W&��t/Xn\�s�د�Ԏ3�uٚ����Fs�D�N�g�wa!Ku�����D�nhH�3l1����`�L -:n`Nj��w�Q����z'�JHSE�$�c0:��R��xY�a'���u�y����Is[5����C�I���kH��=�j��3D�pY���liJI��q�Ԗg9�b����P��<:�kuY��tF��Š�uߎI���:�<RN��ʏ`��V��c��;�{)�����7����mѸ�R�Z|�c��@z7�ܺ]0��d+���`)>�Y�kI�ly(��͗�~�"���5I=�4I�G�ǚW�!���<>�mX�:fa�c�k -��Y��xݘ/��W����Vm�E"Y�����5�1VE���Ħ:\�7ѓ��Uq�]XO����a��'����^�FY��Q7)�x�����t`i�����>zؼQ��e�����{�45j'j ���x���E��Ƒ4���ԭ�-3*Gt���S� �Po(N���k7���mC6�C�J�匇6�� -�:G"�BB�GWc�;�kC������ z�酗����y�Tn�������5�l&q�y��9:��^F|�wi/����/�f�j�ϨN�^x^��A���#ۙ���H�U�w�x� uc{cޕ��8�o�s��ßd���M>eVM�wK���^~��}�s�^�x�m��[g��a�Ʊ�K��^����%W�����P|n�Q��t����ȷ2��B7��MS4D55� -�ÞڱR��j*\G0u��z����l�|�MY'+��x�e�A�G�r�����h�)�vL��byZ�_ׄm���q�]��p��y �e���j;�� ��y�2�����r�$�����*�u�Ư�|���O2�K�,�pV�b��>V�t���H�>�?��#VE,6)�h����s�����d��.����k���6��o��� ����8Ch�N��������(AI=�Lf���Dͨl�и�U ����k6������0,�1@��|wi^8�Z���?dI�i���F��uS����P��`�Q'��}�5� -ש^����|�s7-Ow��!3JF�G�<�I -���)Va*q�2Qx���X^Zp�2@|��7�lnN�S6��u���SK�'����,Q�s��Xr M�MN+�"F>IWJ���.opN*)�$f]-�P���̏s����/柕|MG�����h�RW�_%�AS&e���R�,jS��lF|>;���w���JϬ�����h��@�-5Uo�$L�k�Q柪າp��4#4R^���� ���-.��� �4Vҭ�xBܥv��ہ�Y� �����e��vԔs4���龍�a���ߪ�Z��`\��4:z�\hPY�2?����TW+S�̎0eOg�1'Rύ�kO��a�b�S��W$1��7�O|�5u �����V�:�%KN@e){�� {����f�MH $`@�~E�,n�y�"WXρ��(J��-��ρv��?w�ߗ�K��Q��*�=K�c� �<@]� -e�G������t�$3�y���o�?j����V���ڶ���C�T�3,~��n ������OU��Ga�}DP����Hm�]�r���8� -y��b�H���`��P�(|�}9���N˛dg�5� T8��R������~Y����)�n��( ��1(.`���c���v�d��5�KZ'�滁��DŽ�rK�U�3܋�P"����>���X���X/3�c��5�� Or�xo�m�J��Tt\�j�%���e-4C���c��6�˴:�%���K1�د���&-�t����tG�a�`I���,���s�L,����^�Pé^R~������[������"���OK���$ �!o�y��N���U�"�9l��t����Hw���un%*NB������3*N2�y�3��@���Η��ۚ��K���c����Ɂu�ngL��_���mC1vQ�Ur�4��>@���N��%�SK�~��<���V9�2TL��6I�罫��d����>ٜ%�w�6%�](� -��V���,�Yn&R�)Z���3i?cwA�rE�(!���*�y��q�B}����gR6����n� ��.�:.\����G�Z� ?�.^�U��>����'��6 ��L)�(�n�m�(�G#����*\�9�[1[��=��j.PW�-x�v3�D7���$�Rl&�4�w"��1�� �K�x�P:TY�Ml7�"R�5��e[�~���J��,ʫ�kR�dkI�ZI��P!��xD�kJcՙ�J�Ü+M��V���O3��>�7L'�H���#��A�_���)��7��ҦZ��}TV朽Pb��l<���a��~�p�y��fa�4)�%���ӻ��{d!+�Z��'�KM��_�J���g�����P����5��Oe��;�E� �-�\i���B���"Q@�&�Y�l���rk� -���m �I���u�m?.�֤��L�a�1<ԘSծ�*���˥[�~����@`�Yj*�M�Q�����j��+��(��.�L�!=�FG��[�Z��O�����qbi8Lheh�C�J��Gr�0\���E��E���{� eT�.���@�ˎǻ��ȏ�oT��^�1�����_# �H�E������R�EF����虸^��>���tU6+� �ծ�[��Ȯ�K8��܋�q���&0I���ʧU��X���l� ���TqZ-��Ve��h^�|�"����$�0\�LzS?�1 -ӯ����th��'p-�I���$�h�h>�my��S�'�HtǪ/.��T��`�N��bB�L�Z��0���g3��"�<Sc(x�w��]�G�^��ۚ�P��f 6WX��x���0�c���P��v�.����lbB������ v�����@�}�:,�� -܌���9�l6��4}Ra�xH�M�?�G����t]��i�:�"zFW�Rn��/�Ru�e�ו�Y� �\\�/�+Y�,�C��}'����Ӗ�� �����╔N )�k���{�ۊm�]�=��Ub���RH8D�������\U�t*�+���/(�u8��N�Aקּ�! -����GF�C�[Q����s�m�)eL��tD] �� J������S+����&��~$B��ס��N� [��q��SN�Xq'�>�Q1B�(�D�j��sո��c�T��Yh�^��z���ID��s>��{)��r��(&ޯ�����j�y*�N�zN�v�֍@:�e�\94��1 !U%ɧP���M�M�S�� �� �N�1��jI�/+������}��m̜MpAMn29j��|�e��Q6)V�(��ː�1����?�Z��xb0�9]�&�o�P�@���)Hp��!t��EY��4s�ÔW�w7�U��[�O��5���uբ+Tt!!r$�`,�$���(G�J8 �����?�D)��"�Z�Zu<�]Q�x����8"^TwǾ�w��X���)�� -SoS/d��&���yH��Nȕn���ui45��8\3D����ң���E�X�Z�߭��[�wU����ʫwL��̌�R�z:���(A�;�pq1F���,�TuR�5twMH�y���J���z�:��9&�������t��&��������8�aW -^���i~��������N� 1��7 �����[��^�vH�G�� �~>����7��a8��K8��S0s�<x�Z������� �:��l�.����S�\�!�����:H��C�P6+������nP[�Ĩ'���MQĻ�L�2��ا�� ]��GP�^@8�L��u��<��Ő�]�b�e?�Uo�E$�,�� ���9���h`�c�@ߠ���P���V��6_����r'�i�;/$����7Ɵm��Ӧ�'N$� ��p�`��~IK��8'W��#9������O�}�xYEt�+���l��/�X#�+��+u���Ɏ���E���5�i7#��� -{dEr���r�n�fK�Rr� -V]��� Ҵ��:B+*r��lEFZ��MUt��ePɏzA)j�^���W�WD����?�Im��G\dVcm���eT����FɆϨ3�V��uLJ4Q�-�ݪ� iXA���%����o$I�N�&Ft��Q��~lF_�����dtʸ?@���L����ډ@���>,�N���w,��Ҡ���h�����c0%����W ���tV���0!��ډ�B%�3(9��� n�$������Y�g�G���E�h�U&�&Kdj>���?�dw� -�8�� �X{v�A�+��V�3�#,)B��N�h��H�B�C���ckYN�P.� ���a�!�������0�-g߬h!=���dǞݑH�ێ�K�d����Ϛ�哬��[�/ɶ�����,���ǽP�n��m����d"�mkky����q���J�-[mU��y�a�vo�B{U���[��ԾO x�ʫd�L:?V�G<s\��d�N�%m�,7�.���P�g�K�C��� V�-����ڵ�sZUs�q�n>b�v���-K��-�SBm��[�}��>���xC���J�y��&�;�j$1��y��ȵV�?�0E��V��/����g�ɢ�N/�>�c�hx�Ӎ?�TZIӺ�e�'��s�,2M�$�@�_�Ѕ�5�'DN;̿'.�B��1 �Yq�|efqnv�yP��&�c�IZ=�z(��%Ę���8iM�7�P'�Ĉ������ r0�{?���1�D����E<�%[��߶��.��k��&���D?A���s�A6���>�gm����뇿eJ�<s��/������ -��K��/���_�ҒQ��6�xT������ ��?�������/<�gG�k�HDŽ"3$�5��n��ܨR�A�zu���="4���8�n�?���S�y�H�!l�MP����R��v�4\������MvJ�?,L�{X�JP��V6�����M�[�յ�-���B����75�n��|-�ꓼ��|c�0�7��S �mh��l��ek\��p�S���N���������;��{V�'��[��u���T[�~���y� �7m5�bc��Q����3��Z��g-��.t����)0�O�j@����8��2v�� -he�R=�%�01 �|rq�Fw��um���m9��w���� v�c��0x3�r3e�#R̍>=�K�X���k�ς�6���g�d�^�(6�gwEo�M�Qd�)%[.� �n�ĺ`�����}�[�I�d_G�K�$q����je�Ž����[~#�Z�*�t�@� K�}�F��r���9y����vI��Z�,�Hcˎ)��� -C��b�(�*.Y�[C'���� �����8j�1�D��AAD��cJ�HI {��������aY2T8����z���ʇ蝂'ZQq�Z�����G�Ŕ@��)��O��4J��7�~����{l �~V�*��k>I`5b����}T�lH��i@�wY6lP�2��@��ѐW��:SnH��qb(��ωئ�cx�e��g��(�aZ�o��LA�B�Sd�t�kS��ÙRShb� �^P�nGH5�fr��\�O�&R@�6�T��I�Ŗ���1�Ulv���qۻ�?�x��� -��'�2[���̣�ǫ�țv=A� -=���%� �"�|��g�j�&b�K�#��B��i'�Gf���q��v��)�P�.I�EK����uw��X��d�`��}R�Q]1��uL��N��;�;�녜!e��#YT��ӗT~��qw�����Ղ�Fjt��q�@�u����!/�s����L!o%/�a�7��Z���e��܄݉Bh\�����zB`]��V;{u����{�����@Ν]�Ep��_�^�!�Y.ߵsAU#L�,H�WT����D9v.�(�8N�IH�Q4��ċ��9Gd'՞;�> D8�E�:9��:\�Au��LT?�d�(�2A�{8��,�j�ٳ����~Ṣ���Oإ������A��C�5e�7�/�gH�g�2�%�<��?�*ze\��*�3bT�� FЎ��6t��6MR�9N��i�/�ge��I}�����XXk�O(��X��usG�`�_�S�ʍSœ �� e)�n:�L�$5� Ր����jB����,�̳O���gm{�|���F���m0�.���駣�_(�,Y#;s��]ݟ;�I�մ�� gb�q��XQ{x�ʞ]E��}�(R�v�8xZk�<��<�|5�eSˎ��C�B��Ą]�WT��~�����x�.W.Q:����(���1��Bh{r_��@wp�1M����ح� C�Ѭ8�#9���Z�����]�?�T�4|�ucP�zw�/R\���$ܢ�+�se�b��Z�I:,�_������� -Ua��z��B�7�K���SGk�!r�U�R_A��@�c�3h��'[G�/R�����իu4|��%���T�M���c���g\�3~�c7�?����i�e���yp�`��!FQ�7 -���n� -�Y3V�oA���j�����`-#� ,oҶ��Kg�(p�R�ՔZ�B�tw,���ґzOh�>R�T���֜���j�T%�rƦ�I�Un����x��_!�����|�di����g^�W�s-�=�,i�BZ+�Z7�*����E�T�<Jv|9��aO1o�\캧.��H�U��uѴ�m͓����ש�����B�k�����ѐ�G��G/���-|:�M_W֊�g�� �^\��kCXꧤ��!�Wa�2d���J�������3�$�������?9��ń�����~���=3u"I��L�DQ)w+�&�.9�r�]ʍ�ܔ�I'�lp�8��@h�9�Okb���:�C)S,�|���kT)ԗ�`uW�4i�h#^� }�OP6:t�pD?����#e���U:OּĨ�z�~E����d��N�(ѥ���I �}�/�o��9�u�g�a�?YQ"��E!I���l�b�9)� �L j��愍��!�2�\��|�ĢT��fqt�0�'�ˋ/���Z��Q6� $r�{Qyِ�k����`6f��U���INq�-LO��~��"��K�: ��iO�V�8爙j]i�m��F�&��5�@�^�����T��36�+�^i�bHs�;S�ֹ�j�ꁍ���*$�h���ʼnB��Z�Jd���S@4�W���L"�����eB:�{��^�6�p,�f��|�A��gt���mh-��܆�������E�㕆Y6��xH�%\aN�<�,� � r���Ś�B ���.![���:Xa����w��к�.�umZ���5(7s�QNuZYY� -^��i�*yژ6 -�~A����Ƣ��+�V��Շ_S�`���a�4�@��,��0�$d���^��"a����"�D۫V��ړ����Z��C=����O|_��$<�鬡N˓J=�?�9��:`z{����1��*M�]@���^����콠i�)nq�H'���@������'4B����Ƭ��@��b9zH/R��~>��[�Q�&�U˨n�|C4��?B�̶L��d��{}�V��o��ѯ酊5��O��s��E��?��̉�9���\�����I��6���|q�>=hZ�����듦:�}��<�^�K+��MU�5 -�B��������4_�^/^l��P2cxF�t�>;U�D�,ľ������Dgvp�a"/wQ�-a]�"��Yj��?�+���g�S��9������U��>�=�ׇ����G��9�Q�M�F��R�G��iI�R��C�EJ����) -��C��2�\3�^��A�*G8��~L�UhB0�����i�����U`��0�T�T�S�v�*��s�J�!<���s�K5��&�[�Nˁ���cV[�|��:h2K��s�����?���< �W]Pd0k44�!����^��kZ]z�dȳt�䌺��.+�X�&�5q �SͿ��)A�P��ʹ6�Yh�g-t z�2��ϖH��(�3 9���L������9vR�)ar�/_Z�zJ���h?��j����y�Л�R������.��ž��u�a5@(Aez*�Z�:�y�=蠫��zB�z���b��/�_�P�=�G���$,R�m�b�Q��=�5��\��up+Nr�0��2���r�\�]s��+Q)u_�7m,����y��>���-u|�/�u�Yrg�5����m�IQ������+��J'�i�D��y /�/�H^8��l��\ -!*��aO����?�C���PP;�l���i�j=�Y"u��a��V|REޕ�Z��>}+[ݘ���T��ŢĠ -tY��Π�n�9�X��b�m��{�j� xi�{���"��܂oI-���o�{AIZ��>�=��>F�Ui���h��s0�^��k 墴X�X�ɩ���#���U�A��y�A���)�]��(�1��܄�plΈ�פ.��`%�G�6G�4��}����Q�#���w+��/( ڟ���ǐY-�#U�$֥��*����ɸW�v�s�'�s��'��(h -u����MĂXr��U�D�g��oU1�EZ�����[���O��C"�5�'1{6�����K2&h}�ŏ�n���a�n�,�2�T%�QǓQ7�_p9l�M�3�d��{��v�%�4^���n�,"X�,wK?2ΙyG6�bz�o�46 -�&pɨc��[�I����⎄Kbٖ�_ͤ�Q��U]�a����fc�������$]��>���u�i:�x�B����s��*X��1ЅT�u}�XJ�%�ʈ9y�[,qZ�d �#i���O�У₨@�X�(�YK��D�l������z�S�cn]x����~R�9�B�$�b����mx��ʸ��S����)5��)vL#u��F����=���Ds��z5��1\bP�8��_�Z�!���}='��0�f�:�*��H��Ex�3h����G�����F!�ށ(K;C�A���?02�*�W;�s��ȏ�]���_O#2�����L{�01/o!ҁ����Pt�3 �����؉�OGm9���!�q� �mϩ�7ϵ>/xO4��G#�5$�gI��ˢ7k�L��Nց��5Q�ޏy!��݅��E���)~�M{x�|j�|�{��'a�s��dx���r��(�2�\�J��ص���0�l+o����ğ�7w �(ަs���72��u#�����?�,�J�O�S�,�w���{̈��w�s���gj���1��;�T�U�;V������`���A*�d}�<s�y�5w��&�t��l�����)����'u��M��$�ǖ(�;�]ݬ<��a����^)&���]��ͺ~���7����XT��`�'*�mT���Q/9(D8��������|�ߍw��n�!*j�5��Z�"6_��O#����(%���y��>�+\u��� -�MFzl�1�aۿsKB�oG��;8�3�gF��7�|'�5����y_�G��'\�ˮ��.*f4���UnJ�Z@]�/�u�}���I������`c�y�-{�t�nF����-fG(������U�I�9�>�S#�$��$��������O�N��'�����D��-��(�ta����J�̻C�b�_��נ=�Y�ͭ�x�k���E����}Ml�R̼�#(:�����-̐� �!ܥK�G4N�3���X��l�]8<"���$a��zU�U��� �̤q;սu{D�:}�L�7��o��ɔ1׆� U-Rw��x���x�Ѣ_)=��x�QȆ�@�w�W�@�j���4�A�+�zǓm-[I�T���Y�ƭf���O[9������]ǣ�o��y�ڠz�O�1����K���a;Ώ��U���ט��if�9$����Y^'Db�L�"�TȰqEE�.|yGhB[�� j��-0�4D}럪��`|SE�D���5J�wMj/{�C� -��eκ-�N���2��_��P�v�����U`og���A::��ߕz\�}ݼio�CU�Г���Ev B~�l�k� rK����U�6QT/,���〲�@����2|�&H��EH��F��M*�E�TH)�V5��b�+{PBs��I��h��>a��bG=�9�T3���}&`�;(��'�W�Sʀ<L�����0J���O���$��"��Q�.�i5�m��x��6 �Lͨ~��F�]����1,A���ͦ��%ݖ�k&�UeX<�1���8�^{�{�=��E��+�Y&[Qy�Ce�ݶ�I�P���3\2�Bˈg�w>� -t�����2�8��YO�<�l�H�@��7T�N�$'�A�c7��,�c��`���89�i^����#�w�R��������*M��$SM�Z~A -����� S� ~����;�@f��aO H�/e�=}�tͤgCr���=�RQ�����Uu��[�H�6D~a������j>-�ȗɪ�� �$�7>�n�M[�w}hPU|ѽ_V�O�R��I#E�6Fo�{�:���P~��RȍZ�~`$�7���pH6OX04��H�T�hV�&����I��7T�4�Ļ�f�8��?Z�5� ng��ߵ^PB����f$C�)n��IqB�C��$����rsy��&)��=؟��'���a��:5wJ4-V -���߫���˒�5Zv^��ܛ9o�7�lTخ[�l�iy���[C5 �����C_*L��u�<н��2��}��Eg��Z�6~k�v0�xҨ1J���LkX�̹�h�f�R�&zi1l�U�X����Y����W[�]�%���1�QR�K�7�Nf�3�<V�c\KK��`�.�[���5k�! -ޟ�`��H�|���r[�ګ�k:⍢���=�5��Oj�YJt��u�Jݜ�8���HD�9w�YY��jf��ȧ<�nx)5�� -ޫ��k -��t�D��璕�>fV�����8��z䦑���W�t¹�U�r���r��i3�4r��Β�⋡S�������8=�_�8{��V��>�ت�'4_ʹ#��RvR�t�7\C����2[��p���T�q�>n<n��ߥ��1�|V?�Z�-$5h� �W�����EQ�;����� �����S���.D���|����B��ݫ���n�(��5���p��1����O���ô�vҬ�uE\�B&)L)e,�d�Z��.�ju\�U��B����z�0bV����Y?��pH��1�h0Q�1w���dƌ}ă��7��B���'-C�Pl�GC�� ��_��k���[�(���v��孶��gE%�g8�l�ÏP��9��K�h��~J����?��)Z�"��Q�ѧ8����\�1b�H��S�#�hD�C2�M����C6�P����ܦ���v\�Cx���X��C_7f�����3��`M��ժTQzJ � �H@Hd$���I�z//o�%QD�sT=��箞�|*U��h=A�`�T���sVm���y��F-�ͱV��B������5�һ������������S[b�+{�NF���\��~ǣ�wPգ�g������,R�G���}9�Иtq -ϓ���B�Q��X��Rp!A�q}�v_�ن]����M��L,f�U<u�WWoL��� ^'�Wne_�Oh~ގem��M0�>Y-��*�5�5Y�4��:�p�0�0olb��x)��Z'�(� �|9e�Iߊ��Y�F�^�^�H ���Pj��.�:�/C�����0治�!���`�����W�kr�²ɺ#�C� g��l����3����%�����b�Y$�f�m|�O��=)���)���/0(�!Cָ�Ֆ�,���>�a�L�Is:�ΔC(�w����8N����pƊ��OxFVU�u�1�z��ĥ���v�����l���h�{�\"n�Jv8�%3*#�W(j$8���pKjS��Z���8���.�9��.k�=��kDj�Y���}u�ʖ�-���V�?f�#�>&�)_�&������Fc��s/�;u�?q����}�P�{P�E�9�}ik�K9�D��nT59[~��B�PNJ(��vұ��ce������ j������hM��huN�d{]F�}�DY?�6�x�ur�֧��\Z��X\��x���ږ�o���vp��/����dX��`��Ն� &�/s��[r�I�i�*v ��7��R�Sg�i��7���Q�P(p�5w�[�����cp�����}�Q�~��.��ݘw�D��D��b˟��lhvP7��}�s��2��g�^X������,7<t#���VǠsQ���'�Q�7�ѣ�葸{%y �AO�? �K�LtV�Qm -�%�����S�!<��}`|_�w�l�ȑ�j�K0E(�,�_��J�6��gc��E���Ƨ$�;�JF�BJ�{1=Y���/�CWKv]�*���GgEl�n��H�c>�9|H����1{�`��\����2\ӊ��q^+��g9;��b:u�[�<�V�3s��M��w�yO�xs\�kşiI_�#⡤.�NFUx�H]�T�r�n��K�e!��Ͱ:���=��+^���h�y��U�ӆѥ�;��ō֭�e��Ok�B������ -s3�E���E�``/'��Z�)mԭ�L̷�6�ǔ n��zt �(��c�a_���|�9�3p^�~#�� Zm��&�������!�G6��;�k��!��A�)� -��v�!���t��B�e�M�7-1Ih��@��/��V=��V�2�����K�a���\n��L�>� �B��� %䙪��#�SH��A����ElV�$��^�2�����j{'�i=�邭�SxfB�1o��n���ٝ�Jbf�\�A,�@'�V_ �A���T�^���)*I���D�ŏ����;MO��`7����^�mv�@&��þ.�y"���Zޔ.Z�W8��Z]��խ��#FW���mm9&����F��\�۲����9����/ҙ��+����7�V6UV�9� �!�tW�2*�{W��}�KlX��k�P�����:����\�s�i^uCA��P畇Y�� �`:���G���[䬄bD��u�]����q�w s��L�z+'��X��c��0�a�U��Q���{�Qӄ��-*$�@7R��(��ɽ���?q"�'FOV'��2�'���~�� -��V@QH��k��Fj"4�s��{ܭ�B�����\�Y�i}ɘ���z���F;j�����Y֧N�@G�B(غ���A��)���>�� �����{N`�'{�ȾmT����Ȓ� p'r�+���!�m���@vb�#19��'��;>|r��g��`"^��4H�I�f�C$p��1a�It�~�ljK���uⁿ �}�$AF��n��כ(�^����/9���n)ӆ������k�o��]� -k�\�p@�x�G-�U+k�r�ǢU�j��غ��� @�02��F!���H���$�7�&���9}���-��������4��=CR�H�GD{ʾ��vw��㐫 -���L1�=���j�?8�E*r��9��,֛'�3t| ��m�끍���VA6�L�e�*�b�S�m��3F-g�C�x[�K���۽,���u��5o X�i� ������x���C�t�T�}���8����!���7�r!�}��z�s���Y2�ɱ�4s����f�ct=�~�@�^�l<4�9 �����^��B�����Y��zɔ�r?b������C�3����'y����{H́�M���������:b�Aj��W!]!�Fא�>~�v��}�;��A�����5计���vP�$3{�����1<���1q����+D��,2fw�r��v���qV�"�b�\��*7;���LOº`hc(�~�Ha�/7�G� �� ��Q��{z`�Ad�R��g�& �l�z�����_w �-[�}�]k��>ݹ�����L[g���~�??t��G ����� ��_������gL�iz���������.^2���/_��굟�����v���;w�����C�?,��m=��{b�����N/�]��n��^� �>�~P�D��1X�@$���CB)a��ᴈ�(:#:&6��J`'r�<~RrJ� -=#3+;'7/������TXV^!����K��u��ycSsKk[{GgWwϛ^��m_�������������̻ٹ���K�+�k�*�����-[� >پ]Kk�ç;w��ؽg�^` m`]=`��&�(�*�,�.�0�٘��6gc`��u������ylg�셣3����B�A!*-��d�9\~r� -#��h�K��ƍ��@&� ��@*�:0����c�@����4�nvv�~������������o���,5_ꬕz������Ulfj�&���cbJ萔�'��Q��z�by��,A����(N-�U�T�K�j�e�a�a�a�a�a�a�a���r(���e�\8T㭧��� �48xHF����������n�<!����%�VVf��k�E�o�� -8�K�_fkpV���xm b��L�S�maQ]����&G&g�u�l�$���:�4_�'RU��ଳ*�2Y�~'�ZNI /��HС�����L�� o�bʚ�<�<A �q�J������⬪l��l8�~tX�B\��`-&j�� D��f���Dn~)렳��L�H�e������IJAzMf��� -�4W����W&�ը��R�1��@#��M��K���2$팄��8�P�ZX�N��DFmrn�$�@]6�.s5�W&k1��pO�m8p{/Ѡ$�I�)1U��las,�����ې Ȭ�d꒲�j�r�epX��!&���?��an4am�z0/�8�TI|]�E�-�Zb�yM̤�FVJZCbZr=?�'K�R��Bk��2��R��ɴe>R���hG��ڋv���0�J���W�E^K'x�����9;���p�S��e��Zc���P���d-��`��O�=�[ o�`��[�$SK�"c������Ĵ�8nJs<���N�6rS���TuY-��X�p՞����!�Q�#�7)���h��a"��mHXAO#���������oar9� <v3��j�'��J���{����n8(���zN֏���zGɘ����������N����1��VV"�%��l�q�e��h���:���N���Ћ5#(��q�{��1DH� �*�����HV��쎉�늋��b�Ew&2�e��1hF���佳I�B2��|mU8Mg����$L�0����c��C��"(����(e45BNS���e^d�A����h�z���].��Xd,�_��c=�g�I"r<M�`�G���#4B�p�2� ����L����$�s�U9�cT��}��o����r�/b��y�e.�.�'x��4�ڏ< ���L���2\g�+���G<���x��\���SA�ǩ|�G�`�a*�]�:�y���Bv"-�$.�:��\��T0n��.*�-�*��.U������!��B�vN�6# -CN���C�(Z ��'����a�q\wm�׃^�4yS��KK{�&�^�4!))�$m$��P&�&��x/�Zֲ�e�-Y{X{�m�aY�l�dY���`c����^�W�^��_|_?���'e�p� ��J��j��¨��Xk�dߞaϰg�3����A��`�,���>^w�.-{0�K���y75墒�۩����NX��a�D:ئT�i��0�[�V�8dζo���h�>��_\�6,zq�^r�Cj�P[Rn;p��v��ߢP��ی�j��'�;�iВm;C�`�?�m+>��Ċ���C�-�1es~<|�Oh���� �5�c -�]|Y�.ՆL�Wg���}N�"`�6P�/���s{�uW��e�u{.�.� ���Mر�fR"J���.Olk<V��i0�mz`����MÖ��[k�|�������x�[;š��M��2�;J����#�8��bi���N�gێ��1��>s����~�臟�!oLw6��b*�����(�����'�j��#5��M���*�ߞm����@�W|п.`ggÈk�QTa�]2��W�����(���q�Sb��U�`�^��Z�n�]��e������F{��>�sA�gS��ty+G�%�]�F�L�`pl1���+5a�NP�$~�U�dݎ�t퇠������?,���͆NOD�9�A���QLA4�� ��8W�Ls0x�^�D�͓+"bMkX�MF~�eʶo���/�Ǧ;�?N���&�9=��{�qL�'�krS��~:O���zx2i�H)�(4�����g�����o� -����\���D���P���$�J���;��;S�f�0���T},�,�m�t�䂨Lɍh��Y�m_���r�|�_}h����do��G���4�Y�>�AZ��>N���\�#�@��c�V^�T��V˙Q�"�@�)ȶ��� ���տ������>��� -L6��AUئpH}��Y5Bn� �X�&O��#�� -D�^��S��{��l�NC��r���� W�2�����Bu��g`y��� -��L���Q"Y4B��L� ���q��"��T�ҧfh8 ٲ�o�_��x�����ҟ�Ǫ����}�C�Q�ʙ���O�$���� &�6¡���tʰ�I�fh: �t��7.�74x��t�/��U�9����a��+���yD5'�`Nᑴ "�:N�R�hxR��DLq�I~3!)"f�s��!+��僚�B�ɻ?�O����~߸Z{N�Q�+z-b/��, +�s��cBa�T�4Cm�O��i&;�Ab&y�l۴��̄n@b#������ϕ�fZ+���Y�!Xy��Us���o^��ů�`Wp�˄��%R)j�R�\l)G,0*��l[�88 �����q����*��?*�O0����`e���ن]Gm�n �an�7q�a��� b~�Sr�)����uc��3�?��fo}ϼZ�r;�W0� x�,��}����A�?@�Pu�D�Ԃ؋5 ��j��eH�T�/g(z*�T���M�=�V�~fk��aE��q��f�����Tq(c2=-���B-w�o�'�N��'��z$QF_k��'�m{�=Þa���m��k�:c��%U��p4\~jG��p��AO�Oj��1����̷Q�����#|�x�h�g�dafLd�JwێA� /������MGͧO�إu����Ė-� �v�@#�i���R�IHU���(�A~X���V��= -;&�m���A�{�4zn+=���d�~qŅ�[���,�c3e���LzR+�ĕ -рT/���ʨ0� ��n��Q�o�E�������^wrō�Y�`n-z�������j�Ii�2Z |NL#g^yHjV��n�Ki��j+�[��v�Í���k���w�\�K^��E/��y����P5�%n2n`Pz�\N�M,���r�ܠn�mI��$�j�¨z���m�`��ꆳ��Uw��E/�ܼ��8��3������#���B鴱9>�P���N�NcS����K��D5��� O��#˞�|ȳs�ƫ3!L�D'���YP��������c�NۭN�����:��v�k�V+�Q�V��:�t]9tA�+B�/BB�� 9 !�BB��,7$ DDA��u;}����������Њ;i���������E�E]m7�z�Ŭ�[ �.S�&`��C͎���� ~d�[��?��\;>)�IL����FC��`7��g0<n��� V��Z�Eo0�l���Z��2U�F��4���l�g�z�]��c��|��L��d7��x?9}pW�q��J���ԻDr�C�5�U��j�f��Ū6u�Uƀj@�_æ#�eO��V������H�4<JH�������z�����z2�K��5hk�����l�ۺ�rk���7�;{6��Ԃ�`����Lg��h/���0��ljwaB�o��u+趮*��ϗV{�jU�Z'w��W�Y��e��P{i0�ٽ��~gч<��ut*P�86�930N>�!'�E�y�0��>TQa -2���O��(e~�V�3�^���0��ݐ�m���k����5Wξxk�����#� ���1ܱ��(9�;M�k���Y�T����� -�A�LأR�F-;�ұ�zt�6�4�R~�Ҝ��|[�������P��`s�3���&�s͒��߬�Pi�Q&K6���Eb^H!��ʪ^�����.�9��6��&�{�O�Z�1ہ�!� ���(;�:�Mj��gٷ_�0Mĩ'�T��)�r�CBg@.b�k%��YV����< -����|-a�!mW�c��{`�'A~�F�s�a��_ d��u���E�dQ�A�q���5,�2�T|��^H��h�������զ{?�k/��h��{��mS%7ϡ/:�d�&[��-����S<7ZIfE�4f�O�3�cr&mTâ�ٔ3j@�� O�_}/��0!8�������<�%���K�eT�f�)}\^��Ǖ�f�hf������WLq �I���� -*1RM#Ft��D�sǍoϵg~k`(���q�,�@c��y��3��d ��}�~�X��U�Ip�< A~�@��X��Y.�0+,��Hʱ1%Sa�̉�e�X��XW$��ޙ�7���ͫ�'4[��Š�:gq�r�F]��'-�O)��%�C�F.f��W���/{,(@�%pT\ -9P��m;��曾��3��Dz�}P ���A�i6ȻHEWI����*����'�o�R�֩w����g���gU��5Nb��*���MG`=�`��eXc<�u�Z�n5H�/GX �8 <<E�g1�q��.���2��U�\+��E�~�7;��p ������X��3�%���~*��f����!dE���H�?���O���`N�ܙ|@�4���(gs��CP���85����(o���_C����7`)�0���i�e���yO�td\���Ie���bZ`bL��X�''��r'�:y�~�j��]5�W�W�W��SǠ��h�����k������[���M6c�@�]�U Tl�\L�I��)��9ɷ��\'o���GX݂a֠��5,���|Ǡ��60g�nG�۰�o�۰i�V��%#�$^��Ω�\Q9%ֳ��Zn����~�8;$�����"(��A�m����Y������(9��PvmՁ�������J)~d�cb�\�JZ9!�f��-�A�h��*��d�~Yg@��J�� �1R�ۃ�7��V���8˯,;�w7���)�^5�I��b��R�Ɍ��]z�����)]�~yo@ -��Ͷ����V]��g��+���Kn��'n�.�W�FjA��5fe Bz�F��T�mR��'v*ZDm�&A��!)��2(�A�c0���i�>��,��S7�Ԣw)�!~1�dL���aWb�^��1��:)�U�y&Y�̡tJ|j�8���BJ��_��1���|^���W��xПĽ���^�͘��:�>�P���N+��3�9�Z�ȩ��;u*�̣�ɂZ��Oe���K�6��/79�Yv�a��<q��;���pm�OJ�Q�z�E'��ЛM"�S��kur�ʪ�(��&e�؝�&;����[W��l�f��{�U���i��n8�jW[]բ���q�\�΄�!�}�B�@�.9�BD�B�!��\�o��|����?������hV�� -�u �4��4Gn^�J�x���Q��d3��xkv舝�C��n�d�ձɷ+�����P)/5T�U[��1�ڍFM�ޠvh �~�v�P�y�:�����f�Ao+��X{���.�پ��莶����,Rm��]Q.�+�B����Hk4U��6��Э���@ze0� �Q�j�?�i��j����x:�'\�H��{�(�rzC3�TU�g�VID��<��X��[�ZM�͠*l6*��*c�6��ǰʢ7�T���� ��ۊ� t��<���ه�l�"��j���,K�Xh����lj��̤�/)��4��n}^a�6� �o�� �>�EoxR��u���a;v�p7~_���`����^o�%�Vv�ŭfA�P���IU5*E^�Q-�,��T4�sl�zii�6�^̗~��"f�ㆴ?M�f}4ҁ�����r�t�I�jr�����*��ɢ����E*�7�˥ z���X-��� -�ZQ�CH���!���Қ���u�����v�C=�m�.��.wvp��t�(�j���T�b���Te7��ۙ#��QH�v���f���*�m�J��^U �0N�k�o�o���TK����� P�w >����~H9S=A�(�&=�j�I�u��~ �ߧr��$�^���[�K�}������ F�������5���w]�R�t�6�=�ݍ�Goy�g˦�Q�IR�j���=��#\�-fq��,��Ow�������,��w��өU�l�o����D��p&�n���4� ��$�(�/�����Y|�l��)����c\gDDeyr�a%��ֳ)C.y��Gl(P� �ӫ���r�-����1�7rm�X�k'1l>\�y>�f�;�K�H��)���`�B<���H�#g��)���)�<�Z��r��2b�3 -��y�e��Λ��b&�P�BF��yƅ�et�h˙#&0ghH���F��g���(Ҥ,��U`�^-7Q�ǎ[ �� �ɖPDO�9D���_�{S6V̤�*\L9���\H�Q�a�eL}�E��ƒ�0�s��cA"nV����!13�)Y>m*�gL$(B�t�+����#�<a�����+f�6.%�TC�~$��2B��u��0��p�S���ˬ��zIp�(��\�Eg,�Ǥϫc �?Cx�lA�x�uދ�l�;�KQ����b�ʃ��H���� ���{�Π�� �3�u6��Ҁ>�R@z ��տ�������Q6���i�;*_�W�q��N��%B|0�ѐ�e&`����T M�1$����o��}<x'@x2$�Rs�/�M�B0����J��νdžK����VD~��;�!qw*��AB��$��["��⁰/��J�M������>�/ 8�"@�5rX ��@�!�?S�?�� -����-�1���9I>jFh�>�[�� -���2�j�y�]� ���a2�Ynf{�������� ye����×/z�AM����d"�A����k Nsm�IN-���.c��b����~�]�0�u�A�7��C�v0�E������=5�#���y/��,�h�ӢB�$������'8m�qv/��vq��C�.��c纹��CΊA}y=����%a7XS>��N,[pg̤�O���jN�#�=%S�S����>ƿ��r<�^������ ��xC�&ޠ�uފ!/t5h��s�V(N��Yiځ���oJp�s���;�g!'�"��"����)n���Vr�M�>�]A�%��]�F�KT@/��.|E������ӃmYǞ���̔�.OY)��fF�^�R��y���吔���A��]�7 -��Z�S\@� -���7Xb�<+K��bE���ʬ�W��.��Q"G��ġ~F�NJp(��N���.-�JjD͢I��7�V4�S��&rJ^7�� ��1��`���S[Ҏ����'5��|5�o5��h%%�]����������::�T������qZ�b�:���Zm=Z������ �a K���B��������(�M#�E@D�y/�xN����{�|���I���S� -�6���d�����AU)�W�TNE��J�U;�}*��_�o��` �v���I;�jIf��G&�Y'�q�G����*~L�=3��P�l�h�.�YT�/��h�*u��Bժq({�%J��X٧��_ -�l�2��g���u�Ϧn2���'�xg�.���0aJ�M�t���\���X -ח*˴��RM��Xӣ+R{Յ�>���5h�o(���̖����7)�&]̃cn�w���~7�bW=�R) �*�f8����<�ؑc���l*��JS�w� -�=�]�&_ۧ�Wc���ƕ7_�Ǽ?��m�@�;�f~�kb;�� �m�]lo`E{j� �1��X�wX�ⒼYQn��j���\:��[�k����j{e0�ە?-U�l���8y��{����A+���vF`w+�B���r��5�"��.��4�B�I��oU��i͖z��r[o��њL^���5hQ�9h �mX��}w�&aۄ��kI�;��<��M����t��Ĉ�k��+j��%��B�J�g3Hs����R����i��zm~�Ng���H?E �� ^%a�Uǽ��E��qm�h;}�����M>��E<�Кz������3�E5ټ� -e��L/їZ���b��V�S�Ztʢ��ګ�7��j��{غ����&ݤ;�_�P���#��H9��!�uv�"-�d�;��S�5;�B]�6[]�#W��PI���I+��je�^���2�����Ǿ����i���N�����Ꞗ!�ц~rP�=�ՒNVbA+�bn�u�d�M�XVg�f;��rUf�G�Y�U�˽*9jУ���UK�k���ZR6t�u P7� ���2�8�S.���0�9]<��C�P�JҤ�j���$z���!�{�r��_.��?P�j( -~}�*b��'q�Hn��>���!��M��O|��U>�Oţ�P� +F���+�B�䞄%�V� �L��;�L��r1��;�}�~�y0��@����8xŜ3b��}wc�u�ǯm�nu��j�YG�̟��RC >&F���$� fg�҇TLސ��,Hc����t��W���;P�3j�-�n_6ҎA��c�7��|��j�S?/��LS��OhW���FeM��鸴 1�3�����1���Ƣ��ؔ��ʃ~� j�=�,U�A�=����֡ؕ�|���㔀�i�W�� �1H�@��5O�&�ga�f��iQkJ�c��!�'MD�D>�2QB!��R��i�:�`=��՞D;�������9N��!�.����, ]����"�������h��$��TG��b�3��tARʔ=�0U��70�A�� 2���v�z?qN`�P6��b}��m�JH<&��S�r�Գ,`]L�e�ˌP�qiIN|�������f��-X��J�� rw#��H��&���1�f��r���uKQ[��S1D��:X@8� -�S4`�H����� <���@z.��@{!̗���������H��_���cH�����_�������pu{&\�Ã��t*���t���`}���I�L�� ����8��1`����W/G�:V ��;�¥#H.���ή�¥������N��!@�>�'� @�<���Η���U�Ā��h��Ñ�����n-C��.G,��×��e����!� &\�H��w��5��a!��8H�[v`�ԝׁ�I$0w]������{�A�i����?F@�5�ӯ���ІmCt���p��S�d�4=�ҫ/2 �EAA��4q>�7�u�r�Rfؽ�i�y��������Qzx����7ïԐ��P�5���S��`F &·��� 3���ْkK"]̢0� (I��W��\)��6�4�K~���qR��>j��;m��O�٨Au~=�7C.fX>�0S~xa`-�y!r�9�*��8'qFx?-� ���z�j�>8�:�Z���j{�֮�����xڵ���=N��ݴ�/�r��� �r��y�@ !$$r�@�p r�$��.`Q��x8�;|��<�<���&>�t���w�3��u�=E�i��'��5W E��4v0$�F����ux�@:�����a] +� ���y�&�ؘ=+��.J��Ar���8�˝��s���<?Q��D�Az�%�տ -�b�S�P���JE�K&�E��c�0��O���2��zB@l!O��4���q�ߟ7��1����n��]d��"�C>jPc��C��@Eҟ@U���,ه�T�=6��5oȻ8Sʋ�%)�B%�//%�I+(�ĵ���9$�a�x�=�I�G8�q�&���w!��4��<v�LbM߿d��,l#}=o���1��L���Zq� �naIΐ�H���z�.v|��)���<7�7�9�^$ΪA���W�)�M`I�����k�>�!��SOMYs�3^�����7��p�E*bw��~KV��H�V���"�4���dBX�L�%��� F r�P���qo�[���f�v��9H�Cu�5��cV֕a��F�N��T+I�J-�Eab5ɪy.i��^�-rH�p�t\dG�JDž���řu@��*�.W������q=����?|��3#�y��+x7: �M[@jVi.���,��k� �jYlϿ#����J�����"�Cj(C Uq�?�Myk���ac���f���f�o#9z��q��½�n�q�erR}I1�NUƶ++��B��J�&�P f�1)�b��G�� -Q��k`���Ӻ��î�]sn���V���6��#�ӽN�%����eԗ�juJ��D˩R���بt#e�D��I�>���/�$�[5(�Df#�ſ�����MY�̶��Nzh��:��[�\�K���D�U�Qk�m�F�N�6���S7"��n�Vu_�-�!�b�8�^$��51/���_}Ґ��Ö췦=�]]�}��q��<٧<͔�MN�O�j~�� -!V��A�.+���X�m������Z�T��I�j?I��j(F &���5�,4��>׆��$���������3��V2�ޕ{����^e�s�U2��\�.1��*C%��;�B�GZ��'-��$J���)jPG�f��g���ל��Lak�'�M� vw�`�_==��l�bjKk�ʪ�i��UT��T�Drs-�oj�HMÒ�r"��ő� j�ļ�����%ms����@֖�{ɻ�GR���8���9o����nVjY���qJ�EuLE���o7 -[5[݈�:��U~1b�Ñ���ѿ̺' ���c7L��6ܹ��J���/�/�w2��������;�)�6~zdy��!iP�a��'pڄ<G����y��"�-��5�P���`�Yn�� -u�Ey����K����z�e��Û��e�|�|��/�����!�� 9Uئ�����+�����k���5�����}�>�)h���-נ��Dhp�:�����>I��$T3A<j��-��/��^����*e -�e9��b -�WO�����{\LjO/��d�ۃ�H0jP��Ih��4��>����xٛ�C䷝��-����Ӫ��|<�G��I��r�d�<o1��-ˡz�$�B����MR�ӴHj(��ǡW44�� �7uR��!�o��v[�0��T��D�gI����4;�5-L� ISh!e9T�AUfB�,|ȃ���gOLG���� `? -Ͷ�n��:&�@�v�s.k�5��n�b�A�b�W��)x�x��b�a��0�%�$��?��193l��nNM��a��d{�]8�-���F�B��WP��U�9��ɺ����,�}�2�S�r�x%�w���B=K_�;O^�arV�K�+�2��~�=/�����}Z���}=u�'�@�X�AÝ���.���8T���Z˓�-��ĝ*����|"i�s��K���Iȍ���Y@�o@Υ��XP|>�.��dPs14^�$P�n����֡����COc����-� vG>H�-�g��<���R��::z\���Ԏ�:����T[��V�Tl� X��&�����YH!$�@��а/��B�**�R� @Q��3�q<s�\�˹������z���$��m\��-��������� -��q �@Ɨd���ه���u؎D�G�ӭ�t�q�⋔LlA�/�#&�����D -�V��� 1������t`n��v -�v����G�lW(vG��&�{���7��N'W%�T��@ -'���ބd�D �}���\1�z��%�pn9"VӀ�.����� 1�|/ -R6F���pl -��PH�r��@Ƈ�C��`ȞVUMb�� X������#؇��/��I���@X��@Ē(�Y�eD�-Ɗ`�<)�������Ձ��9�O�l�)P��N��.���_����y�$,M�j�$o �cP&�t�HS��X�`�3 (~�+��օM���O��EL�tE�'����E��c�쑘n�hL�{�7�0� ;>T�AK\ :ܠ�}ܐ����R�'�D���0��I�=�9��8��1ι������0f�5Dz���{9#�.��.o4�[����A�ҏυL�[�E\ ٤?A6�#�1? ��7����<�ĄT�L��TXBC+"���ZH#X��2���r=�.�0�6$���{��x�A��AGxr�+�H��F�_^wO���ը�S�����0*5��H -#<"G�VKD]���87�A|/o�z_��xhm�0�e��Ay|dB.q9�I��L�p27a�u`L'8:����(T��rC�#�%�-)#���(�K�=h���w���t�|�a4c����a����A�Ը�@x��e`!�3e�n�O��Ĕ����wdP+9ѯV�)�a�rst��8����%j�w -[w�΄�X_B�p(�Y4�ly� �a��4pC��d�#�u��Bi��Bꦱ�_G,ɟ ����ѷ=�t�uf�?Ƙ��¸�R톤�qMܜ�&�Hj�&7����C�u����P����� ��Z�`"� -�K��Hk�S7�3� ڒ��Y8_�jtdAw�*�MU6�]�����|KZ%�9�)�%ig]�<`5��k�*��l�t�字7�p�>`������RҪ�2�Oc��4iWO�@���#Wt=+��s��ܬ4Q/+l M��I �zV}Z�Vz�[��ϭH{�u��?�8�n�p���� -� ���PL|k�NZ>;P����H�qߞ����Z�8�5'���k�M�Z�ʒX�(K��Pͩ��x?�o���|���_�/�%��?�t�`� ֠yPJ\����t�I}_Q7�Z���������U�2Ɉ��V�E�Ҙ�NU1ˑ������%��bE�����N#��!nP�o��_(�;e'�>�$-���|X�X�&�� {���G�ذ���4bMn�"[K/��2홅���W��*[�efU�Q��-�W�{ ���m�� �c�`�3� .x\I^������!aMgu����{*gi*��-H%V�] ;�zi��Y����e<Kf ��i�5���0�ƍ��n��^D�Ay���c�AQ��r���j�Bwmq�%Ɗ�&�櫵�=.g���v���"q��*'���t�1�i5��J���JԨs ��A�'4h��-�2�p�����z�$��ԑ<l�-����+z�O �O.V%�)���LZf��lV%͒�c�rs�9F?;lj� MB�����'�e�a�,7ꭗ1nP����o|�,�w�2l�@i^���{���ښ���j�c��䜲;��E�R��HA3h��|#[�W��4��jS�Pej�r{Ejc��q���+�7���xa?����Й�M1��P�ܺvviKk��]��rԲOW�!���hS���D���X�Oi�� -k�0�r�[��scf7ꭗ n�~���(2�8=c�.���H������7 p�G�W�B��������]A��{s�8����i*�ʞ��(���KJ�i�5hjq�Z|K+t�2k��[ X���A���#ȸ�2�H@�[�"7�r���7�7T���?hs���:�j�J8~G��X�ZKE�c;V=�H�l�2"0R� $��d_�%`PRYU�Z� ����GYeȖ�O�g^�+�̋���9��{�sc��%ٕ��.)2�\�H)��&�����+K�K%�2E�kY�!i��a��3$`�,�Nh��'4P�ڟ;��%��ɖ�v�W%���F�P��;{�A쫭S5�(�� U?3���b���L(���u�"I��HZj:P`�����p}�y -�uN��� ����{HfU�T��n���N�ޫm�9�g�3���E�OnV����6ji��\����-��������u<~�OPe:�ݑ��sy��q@���gm6��n����H7�XU2��*�g���e���:�u -ݒ;���m*��U -I�m��en�e1��ZF��Gq~��<��4�3Lq_b�r�k�x�~��l=��>��<ѿ�)�wG�y��p��;k��]� ����s����?�[ҟ�/�K���!r����}�df_u(��e�{$��z�d��B��7d���ծC ]P�=�������ݘ���`��5k��'m�a�4�>��;�ƥ.b}�_����_ı�Y�L��h�-<]���7Q�z����&�$̐g���W��&sT1���O���|�W̑�g(�����Q���!�� >c�=�1&83�\��Z7�1�=�x�\��������q��oS��uh�x)j�4C����λ]��S��{/�<�����z ����M,Pm��8 ���GX �g@��tH�� -i'�!�1��I�N!p� j�I��b��s�xw��-F��/G��mQ>�D��}I:��H�7Ɂ�U;x�-�6�@����TPա0Pۑ!���y��G������P�`��b�* t��bT8�9����H.fI�L ��H��6H�f Tk:0�E���� -�oB@����!iWh�@k�9�~P��n�3U�=�n�!TЊ��7�T؉�;���B -��|��ߕQ@���2�>�z2Do�F"�6��H�@��6�B��7h�xA�VO��f��*� -�J�0GI��`�-��a��tl?���<��C�2"��(BV��*�"?���O|�i� /��ѧ�@f�JK7HZsR֚ -h�,G�A�<��;]�@n$���P+H� *�-(�v Uك�� �~�3����:��Z�y�#�����e�qӌ�8s7�6��p��}܅���t���R̠�}Ɂk 3�i; �mq";��;�TsD�NW]�������ݍ�j��k�݆�a�{O��|&Y�>��9�A����k���� -�! -3�1��e $a�T�%h�_��P�l!Q`q��SFi�����������s�_���i�~�k������9#�o9S���Y�ޘ9���9�+��A6GEao3ĺ���k9�>�K!��b�v�0m��|;c��a..�qF��<%�u{')����u>zA���Y�ۘ.�P�0~ f�˟%t�g_f����C4f�a3L���z-�K�ՠ ��ȯ�/1�4�C3j��TB��D\��xl�Oc�+^o����������C�f�3�W0�-��̜!�!6�fI�&�h���|̐��<���x���!3b�!#z�t:����^�8���:�qn067 ���/-��J*{č�Dm���+���B4�$��ϐk$���B{l�:�.y�2�����Ɯp��쨝��c��cÚ�Sj�kb�����.eQ@��vP�LG|%�n��$?��>���>�L��H�)U�����a`a1fP;-��R���Ci�l~��d������L���t��M�K�:ͣ-1˷5���Ly�آ(i�W�=��P�e�ᵲ��dS�ӑ��������&6O�1����E���'� Xa,$YN]��e� -m�PӦ?�gו%��=]��&ճE��oJ� jP��� �QVDT+#����A�N�V;E������X�5ȏb��Ӌ �9�/��F���N�|�:m˛"���+�m9��g(]�.%{�_�P��>��w��3��4 �����v�Y�:�DZ��zl��Q�� �Qz 1�&�%�@�)!$$��R� ��86�+H ����㞳�~��o���s���O�Ľ.Z�};RC����5�]���̲�Q�:b�Q9�P�_�dC(ِt0�r�g��� ��a���%m�s%��'�Ď�<�#�١g��֍�DJ�0ݹ:>ǣ2VN-�]/��g���X%Q}le�[�"z�%�~ǒE�c���#�Mɻ�9yǷ���ݴq��7C*�y��>�{T��� -�/m~��r��4dFYפ�S4����lOuB�Oq\ Cs�U� ��1��4f�#�c�X���!b?9s� � ?�l��J��U�>�Eu��r�5Ō��2�����gj�֕����T�KIr��")�&*��x [�����7�͍%r��8$�>�%�dC$ِz��3,&��LV;L�t��[�1�[㾺UM߮U��JOk��.����D���4/iJM"�3s�9���\����-|M�fG G��oC�>��c��`Bq�p����j�iOk��vT���[~}kC q�J~�TYA���h{�8�Y��♗.��J�2�S�Dfr=/]��O� ��ϣDz�~�#|�ِ��������>�u��g����ZוZ ͤ���k����JlU$��/ȍs�'{���h�VZz ��V�KNk���H#���nr�(�ϗ������u����,������ -�4�M��^���ಢ�ϖ� -��Z�g�PYJevy�N�$ό�Z�8�)�V�Yռ��~|V?!k�'�� -�G }�O6�� q{����G�).���5V������˳�Z��5uޛʫY��+�' -Ձ�a�b��1�P�R�擘�Èϓsbs+���&� �)i�#%b�������{w���L��7����t���ܳ��p�q���FUc���w,�2���,�r�*�!Y��&,J��BVda9!�rå���o����Dd�(G�%gޛw�����+���j3xԼ�ڏ���z��V��5{�Wh�% �٭�sU�6ɕ�ay�Kli�g�*�'�D�).e7r���9A�Np�;D��� !ȳ�����զ�;� �[�����ie\���w����I��v�������Jn�L������u��-r ��� -��j��� ��.����F�8#@��� �?��r+|(��n������h�XOW=��P��mMn�϶�v�ir�xBK�yAs�uDS�]�I7�.7�H<|屮x�����.*�a�ʫ�Qy5:}0`9y6�@~���r=��Y�:g@�ӭ��9 �}#E����^��Y�|6�<a�Nx�?�0�dxW���N����W�:2(�v�Ѯvb�7��ۻ]�m.�� Wvˤ�>xs���>箂���z\T>��W@��8U��1�V?uI�k�:�+�6��������������K���fX�V��2��ޖ�����ߖ����{}0x)�-,�����Z3�[����j(8�A+(x{m�x�m^�ײ���u�o�&�C�]7�n����F�F&���N3s���iCU>C���=�ԁQKj�N/�^��Ц2��FC({�(7�t�(佳2�~O1N�p��0�4j��6DG��#����v�tA{8���L]��Nt�G�u�{���פ���d������n�W��&��¤�!h�2��fC� ͇�qȞ4��_4}�5���07]�� -���#{=y�lba� C��0j�7���Ĕ��(�늲��Xf� ��#>��a6�@E#����K ��VH�#����fz(��-���� �^�B�J:�i����П�1�gW���)�P���P�t�6l��&��d�������? ȵ�An�H�B������@�L:���H��߹#g�+�.qƀ����F��`�J;����1� �������*(�d�?| ��\w��N��!`�I:ZL���W�h?� �]��'��逴YW�9��9��;� -��C�[c�+�[h��o-0k��ѫ������a8�\{��{������p�������|��5���w�qm��u������P�K�QFƥ���!"d�]��������BB$B!�TP�AE�֕�"�.k�N�x?�����w��^�s��sE��M���#���Z��o����=@�ǗA�7@�� -�[]@�K���"C$;��p��s3vT<�I��%H� �q������j��Ct��6��9 (�AB�-�o;��o�_�}�3r�=�Z�^D-F���j:���Gs�_8D���g7:P`Ӧ@�t���\萖hRR��(����ԧ�UPj�6Zϭ�wگ��9��=E�b_;.�L:.�,��b�г1�=�o��Es�t�Bl�$شK�� 2"v�+��X�H�[f��u���5Z��jR����&Sbj��㸔��?tq.~�i&n�i*n�i28�&�KC�b��]Ё�=,�0`���"җ #�;�cZ�p�Wْ+,���d��%��~�V�ZHjC�S�8�%�_��:OQƜ'(�]�QV\F������DBt Þd®�[�L�� +�� ��Ȍ�v-�~p�DZ^J�<�Ȗ��g���_�eԠg�-NӴ��SI�\&�/\Ǩ���,�M� 3L�_$��I��1��x� ;*�d -��˛��o���R��kٱ['X�`[��'g�e�ө*����8���]r���3J�u�?�<Lq�O���-{>��Gt����W���`Or`K�mR߭@�ɺ�g97��9��gŬ�SB��q~��W��S|� ��e8��m���>ȼ��*y��Y�k�'ɓޏ�>�� ��� �.&0 ¦%@�qx�a�c>r�-@�[�2�yEԟg� �&��GF��O�����\���ie�g��<��n�`u�<d=����_kܿ����Z�dm����:P��<��M ������Eu��Bҗ��Vo�臇e��$B��")����C���~����S�ۛ����� ��~���z�=w���ܖ�ܜ�a@��6- v��dΰ-�-LEA��cwM����~3�J:�2���)߮_��ؗ��ړ����/���^��t3�+���(���:��3֜�֘�V��j@�$����{�Y�/!�^���[�K�~3V��tX��K e�@��~^�]�T��#�angxu�~7�A����f^GX���;Qǝ���.G\�a�y����QЁ���y��]wY+��4W�o9Q�����U9�'���/b��(9v��t�4ۭM���".�o��kBkױ��=8#�yd�]d�`1� Xŕ�"��`�GA����{ -4b*����}7�Uo2bw>����_��GK;֩f۶+��V�حI��]�� -���3�ëD�8��_. �o e�yB�h��FBp�����j���ixP�R �2\�4r-��Um���ƈ?���v�R�(fٶrэ -�[�\�m�)*s�C*$�>�!�T|�P"�'jůI��Yb��DЈW������ G����Y(uFƫ<��:�M���?���鮎����D��:�FM�c�J��Vf{����E!:��ͩ�k�ۉj�=R�d�\�=M*�6U�+xs��ҏ�����>���;!�F2��4�l�k�cW ��Fe��f=�t] U�`* -��˔�%��M^)�0/�]')e�d��UT�l��/[&)�&�9>8$�]����'�U�Yd����:!/Z����+ۺ�C�p���]�1�H������}�P�w-�dziղ�"�2D��b�*|^~)7��,�{%˛��eR��D0����������<�A&+�#C �ȓ��^[�ZC>��D���.�#�_���2=�E�y��9�Ep�Z��-4�sT���.rV�Ө��wdI�)[a"���}3i/�Upv�� ��SȘ�4����a�����r�=xwK+������Uuԓz#�^[���T�e����%E���H���(��&5da�(d��Yh"���"x?���\� ����H��sHW��eۭ��5���b�}��)�DI}�9u �Ii�˫�|r�AY�a"}NPVK��v��I��W7O�kMs�x����o�~�>�w�{d�y/���~��[���͢�N�'5�_nFе%��2�Ml���|�i\�i��VKa��i^5�h���}�� C� i�6-g��k�N�RIEf��i&{4Ȕ$ i��A��E���}��{}�����=�}���]?�]v"8�ZaXZ��RJ��$]�C�_i��/wT��c�Ӂ<� 7#��9���z����C��-�<v��Fo�� As��2.y@_u�>ת�F�_��-�����,�#��*8�T�{Xb��H�ͪ(�ͦh鍮h�uet|�D�t�`�/�~���%&�Yn���g½3�h�����J[εD,=�D]Q��ޘ_/��y���U��#�~�wJm���ީ����`��҃��U�»Ma��0�e��6�O -��B���l�M�zP�H*����[Õ�?����3{���9�4�ku�S�U�xǴ�����Ħ\Oi�)_qc�AC�?��*����}�ȫWpj��e�dnY -��@K�.ܿ� ��p��.u����^P��w�3r^~'嫜�,��7�u�;⭒^';$���^�*�Q�/�����ROv{�7��ه��ˇ�b�'��������d_�O�kÃ[�PY�W[��|�J����������ɾCss��K2z�LS{���^�xE��X�n#Ps�)~v�(�\Y�K�L�_?0zm�+^o��l�wMLf́�_]�}�Z��t| E}렰�~����g+#�+c%+_˔����\�JX�S��㨲,�Tǭ�Bk��Ė�.��UW;ƨ����?OK� ��s�wn���<с��pReyC�pl�W+s4@��X��|<j�t��X8���?)Xƙ�~7�dƘL3�i�WQ4���5g���#�������0MϦ0�?S�\p���]�s ��^hCް dMl�t�+��'�q�n<�b�!c汑�����Q����F1�d�i���l�p�7 �_���y�x}EV��f��w �f%�����@�����q-H�D� ����}:L֧a�L -�̎A�a��D"^8J����!�� 3bޢ�X�x/كW��a��ޞ���kN7d��HF��1p��h 4t�X��^�~��ڡxP'#u�1F�ā^�����7c/�g�a¬�0u�/f���</,0܅̙���;<SO�<�+�!� k/&�~C�\ !�#��]an��Ȼٍ����7���.��;� -�� yZ[Q����1U���L�}��^�z��/X��,�C�!C1�@`��$�d�!��W��!�� H��@�}K�6n�P_XMR�m��o����)֟)h�&���h3D��zQā�C�r� �4D } -�F(�� /�9���uf 2ϯGF�FdTX"��& ���$���qZ��(���0Um�:n�"�W�h;@C��������!�tV,iq��,��e�(��Hf��#+���rN�ӰK6N�J-'�ʭƙ՛Ǚ�l�OlGol��}v*��n�>f��N֠�}?z�8L���Y4�8Bor��<����Q�1B��ŇWj�9�'��֏s�-�8��F�7�GXU6ì:���f{5�p��t�g�:��C�>&:)����D����s �J:��F�����_S�fcr�|Lb�Ll:!M1g���aXP�i�w�Z�-�Uq*앜Z�Av��'v�S?�ǹ���Y�q���.=,t}�B��O�,"��ؤ��w���Ŵ��1-�KMj���$��H��|H��V-9a���<(,��((���;��?��]�s���v�vs\�q�]�r4�o8����[��N $�C0��Hg�H�$�L۫�!�03ꋱ#�%�i<U��|P���cB���S��%����/:)D�]z��]�ܺO���;��|������r�3z<�G#wL���KGH'y�o��1;x���ȹCٴE�Lβ�t�����k�ٖ=�'m�dg;�Ϲ��\u���啨vk��y�s��VQ�G�H��Q8��A���D�9Ff��āFG�D:c;���Z�����y��<����GEf�Ik:�27u��{%/p~�P��Lvykk���-���%�w6��<�% -χ�W�d̻V�>5�;���0қ���]�QM^k~�C�k��y@/V�b�*Z�8PpB�DfHDD@�@$ ! !sB �L02ƨ(���@l�*Cժ���,���Z߿o�s�~߳Q�IAs�z���0Y�m��8c�s�g�QK�x��<�����=J���L�}/It�Fz����:�Kmq�N���+��D{ʈ�6eܭ-e�I�p-���+`�H�)��bP�ICs��>��[���x1a���9�9�� -������o�rw�,����C]d��vR��6��D[��I���B|�є�£!�G}�FԦ;�4����f ��L�����&/D~�O��3�g� �W�b'Z_g����e��1����m�sKf��&r �!�գ���R��{א�y�Ho��30oe�U����m��^<�'�3��s�`\x�I�L�J�HC�}[x�R�K���&n�Z���-��ؒ[�Ԙ�w�ϖ���T�������>U����̧~���|�Y�O�W�B��p��2�e�'s/��;/�.`(= ���X�����$rE� n�բ ������X��<RK/r���U�Tx(���r�֯���_JƗP�S�2*�'�b����!��$�K� |f��k�#<�:������ʃ��J"���c�k�Iۚ�H���َ*V��*�U�/ĕ��<KsU>2��_��'�sD9�m/�a�B�g,i�B}3�2{+|,� �}�G�~��� ����zEп:�×��b~h%n��٩�(������r--(��2/q��W��u��G����w^�G�㋆�o�"_Ҷ�$�g���|/ܯ�����=ʀ9Wg���E���%lU���*��rͩ��t���p"�ċ_���1\FW �1�f<G� `31��ፁ�A+f �%�}�+����~��J[�q���ͬ��0[�[�XqaMMY�OU�ԝr1��A�aI1�UP��sE^\�]XG`�2Yw���?o�����ፁ�! ��CB��o o�7ße[��G譳�����:�����je��ʊ����_JJH��%��Q�O��q�O�̗QTK�+�ҹ��r�O�����#�q0�1�h�C�����%��#���z=��BG�o&�F�o�C�j"W+���JI�by�>A)�P���̖�ܘ�b�<Q�o�P���P7����_���τl>�7viH@���>�Vè� -W/�ކ���lWZ]L�Z}f�6��_���2�ō��%qOQe�� -�QF9�8���S"��Ȕ���VYz= C:�!� K>�3%�Q��(�hVf��I�%<+[���Aײ�m�@��(��^ӕm���[�-�M�� ۸u�v���|U�S���B��eV�=3�>i�~� -=>U�O����?"0?c`��Q&�YЗ��4,�f�7�~e%4���P��a!��+��Lp��Z�&n�%�6�����H=@i�;J�㸦�E�T��#���;�V�X3��ITM�$Vc�����!?.�`!K,����ZL��c)�{����~��q�2=~�@���eU�k�בd�Ӟ�+KK�']� ���$k�.�5��5���V��K���q-c'㚧�/5aF��#/,D{r&���A��Z���������nك�︩��o&����0{#V�o^\K�q�G�u�M�>kg�ܽI� -t�tG.�4���9G�<q��w����݁�K���a��U�@[����߂��Z��3��X0��}��� ze����āԍ�������q� -w^�+�}�b���MQw��Qw�����nO�4�1�Y�7J�������U�MAtw����(q�|��Y���Y��$C��TC��$C�U���f}�P�邁�5�Pni��a�=gxd{nx����{��'�Nc|��@����@э��-3�/�����h/\����,c4pz��9�c����b]K\3�fya�b5��&b��.|\�!l\��̄fS�č�!ÛC�^my��(�t�4�T]_(�g���@���W�@z�Ŀ=M���-�ߝ��.|v��sϿ��9����$iq�d�����?�,�?�� -��� -���:�c�����5�cF�&hUTk��:����9/g�?|�{<��ǟ�E7g�lmm�n�K.�.%��u�A�$jcf�1��a���[4!�ԲIٕ����:N7�"�K�~��{g�x������=��;�O��*��0���m1����|�V���`��� M&��c�p�Z?��oGQ��f.����>X��V/�Ɔ��e^ع\�+j�Ko(��� �\���a�����X�t:���v�����8�ܙ�6�#g{c�/L���s=0g�Χa�*�-t�Z��\p�� �@�#��88�0Q��3� -�qx�^�B8��I�x�/��1�!�.ț���(Vs����P߃���X���TRq�L��>������Y�3j�/����4�BflG� D�^t��H�]D�z�-��6�<�Ȃ�[06�,1,0E%H%��N�[�Xz��y�t=� ��L�i"G���!;u �r�#�� -,_���Uȸ���`�����u���S�^� ���}Bo�����G��ё� -���L4P��"}��H� ���L�4�'�Bn�b�ȗb�����Ye��yi 2��~ l\7ئ�%���$�e��CeR��O��?�@Á�?݈�o�I��%}3���O�0 cj"����1�1D�=r3W`p��נ��_�Uk�ص����IVe�uO�٩?��6��0d�|d��&�3��} ���\�O���I�d;�l���#(��p��Ei#?q�4/�ǩ��U�ܢ�� -����Xp��h�?F�� ��O�>�� �?��G�����c�q/MzXh� -t'��j�mI:� �ؤo����᫁Q�(�j}FhO��/�S��� -X7�+�|�d0ĭ3��m4��Z?��0��t���7�ጘ��L�� � ��q� 4'X��A4Ј_s��H��M���c|�1������Q�ڣ"�����Px��AA��@�9��F����߇�n��b��{h����5�o�+���!_̟��e'7=%�*p?ɳ���_�I��E��+����t����R��a ����I:���u}J�^a�Qwx��[A�ٛ�k�7���3�n���|�������(�������=I�=@:�q�-�ė��>�I�=�6��7k89p�@BȢ~�hY�$^�;F��::���(��Ed�ٳ�*�N�˧�%���C���n�{��w6���M��[ -*@/���t�@S���hr� ?����Sk����Ƀ��N.�J��<���:c3���s�:�-��7ߏ���Y�����m���� �l[">�6G��h�@����_�^$S&}�M�!���_&�Q ӽ��ҏ�y��Z�R��S��81��0^a|/.{��3�oŔX�FW�܌��m�j�k��l=�o���&�]/��W uQ���D�y+9d�Rk�J�C���>�s���#3_�d����#E��]y�v�ܤ5!âY���w֦Qr!�z�o1���ĭ{���"��wY���W�W�1Q z�&�3y#BH�"�H���1�zN��e����S��=<���=C��z"�Ҝ�dzCv²!)۪>�������H��/�]����ɡ:����.���A�*ɔ� :V��O��CD��'�x"��#���m�}�=t�v��3�j���mWi�fV6eDS��M�SS6��3�k�smk� -w��X��b|�CU|�S��ݥB����kY��KY<:�ƣ�*Їh`��OH�ô|#|�� -=J[x��:�z̸[�ٚ�Zܤ��ؐ-�\ˌ3�M�m�Q�Y���c[%Ϸ����+O��X�T�R��F=��I+N�%�Q��Е� -�!�InCH<!5�I�)|̶��yV���UtD�� pQCo�5e�^mv�IuV���t�ueZ֎r�i����}����du������c�|Y�{�l��"��]U���6�e$�G�>����S���:��C{ Z���X�m}wEm�P�Z)6��I��8�b]����$]i_�V�pVQᜟz�����v:��{nJ��2u�]��4���@_�`��(��$=ɠ@�iC�,6���6�v���|f�^B��JgyM�`�żh�\�E�)�uqv�maV�}~f�C^F�snz�t�i8h��9�2�T���z�T�[ӞV�B8L9�C�N�(�dɾ�!��'K�$��]4������z��Tt�s]�W>�><������z�Yj\3/9��2)��UR�ˤ�q��82g�M�2��?�C��ya>z��N�F�����|]��ru�HS�㽠$���A�r%���b�f�&�'g�NM�7IJ�0�y �{V���w���1^���'��ؾ�|2Q:x���g5�U�n�����=h(? WSj�TQ|L���k~a��n���UYY��gdFoI��'�Kv'��ħJ�bSx�)���S�����ZF�|eg2��g�כ���X����8OwK��V�Z4V��b�l�%���2nQ��Nn���+3rCץ�D�%e'�L�J3�ͼh�Qj�Qg.�i&yj&ym.�����d���W�c�8� �x�������j��r5� P}ي#��+W:��Iݵ��N.���Y�Z�&�0bs|~������ܬ=a9Ŧ���_���~��~���"�,2�r�do��W�P�2z��p�D梾f%�kwBZǓ)�=��w����n�$U�S*N/Kϭ�+�]�5�$ygh���"���������fP0hP���l>�����=O�8���A�T�e?.�뗡��V_3���h=5�����A��Rwb���ߒؚ��"/�_V�)�2q۹����#�K�{�.��^�1����2�+#�� 56��1n*�3e�Q4ת���5-Aa�&�#������~Fr��ZB��VLө��tCCV]�Zs�A�ѿ^�ů>�ɺ�]>u-��u=���W>zא�d�� -���{p'h.jj���i�o_��k�q�i��$��O���<3ꖻF�M��C:�,��{�F� -���է���y��o�j���hk���ֳͣep����mʹ}2�_���N �Z6���Ƚ!����ڹ�];�m���6rݎ�C��������:�u�+h���}��zu�������[g�jQg�Q�okE���7�Ft��1�O��b��ՠ�PQ��;$<TẸ��|����&�=$�k/��Uѿ�Kٷ�w�Oo�Ɖްy�q:n��D���\�*���Թ�W]���.O��:?�e��,�����Z Pu(l�=�%��>.B�X��?�p�{q��~������i�w���g���{������U� Dq]5�.�=:X��0X��p�p�����8�&�A"p=����Òk�� �>����~m��Z�7���v7N��������G9�p:��40�q(DI8�,NV�d�ڎ��>��*���]u��g��#�5���'�=���l�����]��+��V'?��sd9�F7��g}�|1�ѯVp��øp������7wY�7o�#�/oM�SQԴ��������l�%�ϰ�%=��G�'���@��AN�x8���x~V�h\N�!�;҃ ��0��p�b?��\�#w���3 -�1��2���K鲻�@ֈ*䌨��*?���& �� ����8�L[��t� -���m�>�2�^2�n��!��.:����r�6:�-�=:�M������u��(��B��ځ��n@��X���*���\�����V3��Lg�ف�d�4�0 S�e����YE6��XN����f)�� -�b:�E1!������]�@��@V,��X�G@B9x˓0p�GN'A��e*�]� -ٕ�"~�*�g�]����8��+����k��w�?���'�����Ζ4'���>��y0d˜�,o��+�a����zɓc�9�� �w$̘I�y�ȾL���z���AmL���*���E��Q��#v�4>� k������i���9��h�D�.�a;�W{��@�w��a�vmk��9��)O�� -�t~�W�昮2:�P��ա��EX��Yx]cTxK㓰[sX�\�pP�pH�pL�� -i�=i�ۓ�D���w��2l��d�7����M�c~ -�\C��\bf~qNQ�씥�7�e�����WP�*è("k@@P@D��'���i�T���;;!$�@H K�R�A7T\�HUԊ`A -�p\�;�t������;�����g!��d>���]Ddž��fo"��"n�φ��O��2��3��|<Y�G�c�0v�����د�c�;�g������v���(>��S<u�2�h�l��wqEk���L�b���1j���V�?��-��/Y���i9��j"j�j,z����ϣ���(�q4�=�D��@_a���� �qb'�A�>7���{��}L�Z�����m"�h���f&�`�t��t�\e6yVe��l��D\��x\��˸�q#�gq/ O�flž��09��6ݏA�wc�f]���5��q{���X��D|7R��G�z��s��˧ShFS��Ư�����fc�r��(5V� O��mG�{7>&_�{D��������i�a��M2�2t9_�?.�@{p�}x��,�p'�q'�$��� ��/��<�d���D:�h,�a�<��~�Zh��\��Hr������'j�R�;S��J��4Dy�|�2�|�2�r�� -r���\/& 7]���_�9N9����w"-2��l�A�d����93�O23�fdo��Ʒ��*���Rn?DU:^?�q����r%�w�`�5ׁ���� ����=�z\HFݘ���a� -�w4~���H��7�0��샱�#zOs�d��c�����2���%\�(�L�:�*�.��m�Oiq�K���C�@��y��bg'uvg��W;y�� ��π���?��7 -w3�����0� ��y{�i���wr�07�p(��ګl���,��E&ߡ/�xK]��;�ڭ+]�љ���A��j� �j���hi�}Zh�}�iȧ) �4~� �p�f,�w�� ްw��<_x�����0,<��:?�p����bn�y/�e����ԕU��3S��ΐooͬ���[��3�}�2��j2~�Sg��ߐ��W���T����ڇ=�$�<2Β����0�� -c��� ��_.�\�/H0��O5��c�v��8�q.Zv�[3K��1K�f6��3~�S1�2��d�)��P��]������o&�,����SyN����x/��,�/_qAD6�R���t[-�����hrD� ��*�ʻ���d�T�����nWf� �Ys�r�_A,X�F�@�p1Ix�0ϱ� �<*��K|�t\��^�)�[�&J6���4 -�6��.�<�{-��S�[��*��O��Vd�˲�He��13�R�'b)c�t�a�Λ�g� �.� -��n�z�@�W�'�^�����vi�j�8qCc��P��T'�qQ -���bO9O�S�W�W�m��v%��CJrGCŹ���Ib."b�u�c�9�8G�%��,�Y�)Km���S��z'��I��QJ1m(I!�g:T���+ynB�g���G�W���k���;I"ޕ�B��B�k��!�01��@GL�ۄs��ßl3���c�J����z�B�S�ݢ֪c˚��l(�__[F���f�W���ʋ�n�E�%"�Oqa����!�@�A/��#a|�d_�>�/D$Q�G�� ���C� ��}�\�[B������V�Q��G�"¨��lR]�l%����ʲ���\W�D�C$�*(������Hy�K�\у�\��a\�BW�H�.�q|'p� ,pW��h ��%pIa�5۠�6ZT߂���RU��je�Z�"�BVE�(�36�s���=eE���2?��60G�1[��e�L`�C�K C�:�'1�5�; �x1\�Їޚ�Сrm�?h�{A���e�镕�1Ʋ:���&զH��X���ʯ�mϫy��˾̮P��[���}$F�pC6�yGb��)C��@Ƹ�k��W��@��zx��KeZ�f�4} ��=�l9�_�|byYS��M�i��j-�ϰ�W���u\WNm�V�tC��+�ҫ{�Ӫo�c���� -�^�����0Xp��Uc�=Զz��-����ڏ-+i�0��� ����4ۜf�&vS��Q薩�x�k�v�ԍ�)�n��f�����j���� -�D^��`���\�`�T�t`��h ������ ;O���.Ө��,�?�vE�k�]�8�wG�E��&I �bA�m��q�#u�S��uCQDP�},�."ܹ�~�|������{��='Onv��l��F�^Ժm�1z��N�^s!����뇧��:*���qIY'����Řu1�p�t��l�Ù�)��=S ��,�h��5���@!� �p���䅽W�`W�t�(����7hDk�&��]]陚�z@ʕ��+�l���Ϙ��脼����r�uy�t��&�.�����D�E�P��mn��;8�9=���*tǶ�/��X���K�`���[*��[���ʒU�M%k��7{�w�g���)��..�S߬���x�)���N��Q�p�w�� ��;8���`.�����!��@���������b�<�b�#�Պ��� ��E��]�e=����.�g?U�o�c�d{G?)��.��~����k`�l�2���}@6�����������i�l����^�B��`x� �п��Be���Y/�0٪+S�c+� c*�����8+��FV�vST�+��yDT�{(*:�#*��9��-�rO��d��-7�X���*oĿ��کP�̓�m0�߉U���^�e :�����1�:�q����G���9H���d �Z�ŭ��- "q�G��e~~�����+��" �0�оB�41-��|?�����-�w���~#�#�(,�\�%��~LAH��o��X�k��ӿ����Z��|�5ǹ߀c������k��[�h�QDPtyC�퇰,���Y�-�bZ�o����$XH�OQ�G�1��C�ce`&m���t:ƜG ��ę?3��X[����� QHɁ����� �F�����L�l�ˎ��A��B0�ĘLrL"%&��<�8Z�1��y�Q�F�a���7���$qԏ��K �|Vk������\Cxf?v�E�Md�vb<�e,�c�B����P#)|)�I J��J���m2M���'��1s�q��Z -�r��� �?R-)l�5�wؐ�'[�bG�c�$>�d;�07�⻂.�A��F�.n�w -Z�$h#A���$2���gN��4��_��� ���-I�aM�m�$�kG���=��ݒ��$��$y�.�u�G�a�买]�F�*i6K:��R6Hɱ^J�wr6M��g��- -`�,�/b�R�>J�\gA��V�ֆ�[m���?�3�d� ->�N;eY�Y��Y�c��ԱM�T�"�5�E�d��r��ɹVF.52r5M�9mh�]s�L��kv�� -Pd�-K��^�n��ٮ3r���~A�Mq��U�#jQ\ujV;5E�9�GT:�)��(>8U+ȥ2��*"��uy����3FsƀB9�@P�|�=!�;�艉��1ZwĬ�m��`��]��ױ)꠨1�S��w�w���o�y.u�.�ʇ���Q�.�Q�n({<^(���e���2�4}��n�w"�LDp.��KU0z�Rtj�,�4z�M�m�f�C�z��m�.Qm����.ժ�U��n��\� -�u�r�=���r�g��e���c�=��P��1�u'�����?��f(���윉�Y�[��R��GZ4��Y��O��ѥ �u�+�;�_��sy�/��ˏz<ל��L�����������˾w5��J5�^%�Onj�熚|��4�h�gїw0��?�����S?���'�Qc���L4ږS/ N�������x��}�aϻ�������*�^P���]�اH[�sM�2�@�58_KC�h�7��l��;�'�o߇v� S��4�IAx���)і/��m˒��V�;�1mr/M�ާĸ��M�����x_O89�P�=�@_0$_�7O_>���a�E��a�zvAO_��?�<����9���$a,:VLBcr ެZ�W��x�:��Aj���U&ǒ�T����%m�[�bg�|�~�<�/�.'��5��"�xyX��d�y�s�,c��Y���4��gi�I3�B����F ��.L#њ<��(O��'����Fnq+=ֶ(��X��l�kTSW�QQ/T@� �:����֎�j�T�PD$B�BIH��"�j�wb;N[���U�Tk�3֞����Z����}�zY.��<��yb�!��k��^ڟ[�s$Ǹ�7�÷�1���1�����θ`f<[�ʠֶ�`�����E�f��� -��]��|���*�X�p�͈ q�ɢ�Y�y9s����g_A�wO�ryW�veG����gf��0O��֠f濃�̧��`� -j|#p2�O�"��rx^�|�pn��W�(������?V��tT���W�^���{vr����>f�zuK�nMs~S���h`n`_ճo�ճ'��ؿ���S!�7�'�6��G:�ŠK�,�w���N�K�)����*��8���t�Xڅ���E¥&�tE������k(l\[�i�qCk9g�k8c�j�#���pm!F�N���t�=��=r=�pn��u�2�����}+ߊ~Y�]OY����g�$��$.�l*.ZjJV�rߺ"�-_X�k ����5�z%�j����xAW�(��O�M��F�f�#����o>�E�qI� Ky����Q��*v�u�ͲtgS��X��h�����+j�e��"��PT)0��}� -����J���~�\�K�\@щp�����ɽ��u�}�v���)��s��Va���UmB�*f��b��I��'�<k�^��Еzה -}�$R�ʒ -��&H)n -�w�d�Ñe�_EIE������3E'hS��I�M&{d��g6� �q��#�E8� -���wѥو6M���r��A�:G_��S2U�9K4��L�,S�˥� Y�!L*�K$'"K$�(��.�<S��MF���J�8c�p�O��N����ڷ�V�-՟���g����i�Ӫs�+U�^� -�r��d�LQ�/�k�$� �bY;]$;)�]���!~��QBE'h�Q_����`���V�-�T����վh��B�n=�u�m������Yښ���Z�*�b������x�D-��ԁ���PA���WFp�"���Q\��H������ɨ�9$�N�%����e6���jgtԭD��C��7nEm�n{mC�Lu}���.˭\���.-��UK�hU�*]paUKX��(=_s.����_��xEϯ�hD�dT�ɣ��V1HH���f��q�0�5� 5͛Q�3M՜�hJ�#3f�J����/a�`9_/Y]X��+�� d�5��tG�rugh������_Ù5T�T�d�[\�3��P}�{��ah���5Z�_Pٶ�;m���K��;��3\D����|���]`*�a7�W3��~9MƵ�M��Y�/C�C��ː�F�w��Qivx���9��j���tOҿj�B��e��(�������ډz�=g�z��r����]\VW����ٝU�2;oet���wX�;������H7SS"uc�\)p\C��0���z]P�4�߇ph��bl�C ���TG�P�l��%g�3?{P�9P�1P�}p�q����)�W�\_���xeʑ��R���Sy� g N)Iu@[3���������X�p -O���������g�kI�˶�;�0�Y��[�\�,��������$K�g����}��^��'�O���7LyOe�d�,����4F��Z2��tP>��3n`���|�.�C���8ti�G���h����)V�d+�1�*��Ϫ���Z��r�ܸ�'��]��g}4/~�k�%j~�E�m�Qћ�����7 ��d���g�Y���%H�W ��g$}��W�bߵϑp={�`�X�M�8{Z�x��7ʦᄀ�s�����[V���;���1z�r��i9�u�?0��@�/"�����@ҷ�H����~�����y1w7 ��V�C��gߧb����~?���a�ͧkl��`���� �͏�&�mz�_��Ԑ��h�! �ůϟ̿$^b�9����x vL�cۏ�`˓u���c|�t+6>�Ɔ����gIX�,����s����t�iX�y�����)����%�#L�}k�i��ӥc��TG�����2v�$d Q����¸����E�r?������\������������;1�K+ô�j�Ӿ�}��߁B �XsH�����n�������fx���Wg�m�Ym�0�}:�����/|߱��H��h &P\�e�/%a,e`4ma�0�N`$�g��q�$�����y ���9�&��0��F��M�1��` -9p�pL�1O0�&s�t��9AN>J��ȳ8P(�)v�����~�})��~#�4��H�D]��Z���x�'8W�W2���ׇ���LNF�p��L.H�q�Ι����FOZ��<�9�bʳQ2x/<Og4����� ���@Sݹ���f�f�sL��Ѳu��[A���\�;� -n%�v� -a�[���햰�퉰�������[��; �I�w�$��G��p�tuM��9�sp�� -�.rOgk@[�.A��~A�g��ͳ@��yF�������g���cQ�g��g��W��o/��"q�I�t������pДq���s����,�g(h~KE��j�yo�zg [�s�_�����|�g�)�'�9Q��R�(�.�(}$j���KE�}��u>$y�C:/}H�Eș��r�t��c�Y]��y�I�@>Jt,�D�d��V -�e녟dۄM�=�FY������H�^V&n�]�-�]�V�@R�['y��Q�ܷU�/�>�%�G���:��=�{�!��|&�A��d�D��@|Y��'�r4�W?��E��;�o��$��#�7�:��W� -���j��{:O�t�}н��U�������v[NƝ�h'���9<����;����<����ϋ��B�"V�V�&|�X%�Sl�Tl��Pd�<S�y���}�(�}�^���}���w/�o)��P|5�]A&� -��5�Vu�\�p���9d�?>�+ږ�§���}���J�E -�%��iŏ���<ܢ{/p������w��(4�PfTp��z�-���g&U� &W��]�n���b YTt�&��{ЗϞ弓%#�4��SѠt��_< �cU��~h���P����z�!�o7� �6�>hR��7�MYlvEy�[���������z���f˳�ԣ���UIhR~z��.�y'A��2�U��[:Oå�����P� u��F��{-\kp5l�Qe�&��K�L/.��vAu�⼪��Y����?�Ψ^�,V5Y��"�"V��T�����.�B��7���I�܍�Dm�5�J�o��$�Q z#5�"V��G�7;�l�y�z�e��@����=O���~Ѷ(��WA���'�z���Sӿ�v�fpוZ�z����f��G<�v�_�q#n�~DeB��">LR�_�lT�5=���T�Fˢ�V�Q{�OF�͏,�}<��ﱈ�~y���|��Pdk������(����+cKx��^h^�����0���]P�4����d��,9Tr&)R�����E ������{��b}4v�m^Ln��1��Ŕ���\p ����z���/����Ҁ����y{r߲Fc�5����N�������(O��T�XP����.�?�g���bv,Ik�������6�zH��o�Q���b���퉿�;��aW�g�] 4hg � -�sהs�U�,��h����n������(_1%Z/���Q� �X�,=��HZ�١�t�ܔUV����I��';)��������e%Vޑx�q{�K�m���%�`�5�uF��@�-@!�h�4C}�!�&�⚦*��(]9�2<P����+Uz��2��&��OO��Ѭ왭Yk�;ms�����v��ܖR�5��qs��!�R��F�M)�1�3���۔�I<��-��� ��T �h�q.c(�W�� s�3}���Dthu�ށUF9q��+�-v�����f�n_����,�-��n��ޠ)s�ES=t��)�8t���i]:9�����1�'�a��;F�;I"�hĸ��%�N(Z;��� o�Z�/:�V���Fm�'3�l�wT�g�/��h]P�TTFh�*"H؈Z8@" CA��B�@�@� "����գ��p��n�T�jY��w������;���}��~�����gP H��!���.�"'M1M�Vn&��s���\��J�`-俦g?W�g�g l���{#t�.�?��D3_�3 ��2��|�J=qH�J�F�2i�N�d��¬�Q -q�~�(y\�0}�L���$3o�XPj&�r'������WD7=�Y��0?=��a����x��T�R�N�ǠQ6u9sqH�J�J���k˷)� ����5R.��ϖ&��HҌ�Y"ca�|�@\l�.���E*n��u��u�����RE̒���F�v����4@-T��h���P� *�]P�X�"�:���!y�!�r�"?����%O4��(�NI��69 S��Ȫ-���-�����w���[%I{�IfIp�6邅i���Z2e �NK���uQ�0Fe��(Q�T.����E���`]�2b��0ftfA�A�"e_!���/��?�`zb^�yBn#'>�{n\�-��'ﱌ�3N�'`��� ��Q��Sb�}9@]�*��PRb��R�y!�|5��5���t�eaz��G�K�Ʀ�$&�OL,�LI(�7�+���Wy�<Fy�"Zy�]��S�mS�>b>� h��YL�2)�~G��"�e�PTZ@^5҃W{CX�^#�z�vZu��������J�J�O�⏋���̛]Qf���azT�y�Ȋ_L#˟�F�u�4�t0������(:���@-���r�~�#!;<�Z꜑^��_�4h�o��>4�>fDl��Q{�S��넆Qu��%�w���՞�Z�����ϧ��t�$��`��n��;�Sr�[�WU -ꀒz�N�q&R�9 ��5�B�j�T[5��B�Ĩv��V��E6%�$֔��Tl�T;~{ә�AM?o3 -:�i��&���38/�)����Ҩ��?i����=31g]}n9v��E���D��5��Za�=CBԉC��izAj�mj�-�1��Sc/\�T����x��e��c�.�����*j��#@f�H=4��0D^4F����쀐ˋ|�ۛ�!�9ۮ��֫Q���i��h�"��Ԓ�������*=��V=���z믾��o�0�� -1�����\��2�C��Q ��r������?`����~�a��^���7���c��@�������{3ko�a�m���ۥ�����{Y���-�ۯ�|n�j��d�Q<�:�7����s��f �h}������|����_����?X�e�����,}�%����q<?I���,zV�gM�lk�G�x<}�'��P����+���ɟF�؋��}o�+�`Y���f��/fó���p���/W���Z8ul�c�� �ױX������0��rؾ=F\Ƽw���A4T���#��������-���,z������ x��X�5��v������s{�����{}1��7���o78��a�AsV3VSv�hD5������(������k�K���p���6��_�i��8��f��}Kfƣ��0�-� �1��$�XƱX|��D6X1qxE*�A��$�G��?~/�;>l��lȩKα�@{2&�L�w�¬a�������#���J�2h���`��eF&�2l�Ls��l���%`��D(��6l~&�]6Q�����������?�>��xoߊ^�[��w���ƾ�:2�t�4̘��l&��C]ӖGk{�ګ�M�"�! la!��|�;���W���:����t -�Nߡ˩�N7��� ^;u�Sڝ^�0�����0�Y`�V`���Or,�y�9�s�@l'��\����{W1z\s���D�[:�j���s?���j�qo�k���p��/�ܣ�<�.bxB<^�4 ��Y�XP�0G��u����20O?b�<�����<��3��7'�9�H�i�tDZ;��v�ۮk�XDA4�AMjD4� !$!�$���CVY�����M�v뭕� -U뱶����L��g~���g��yg��%^,(����x�Ё'��8���v�a�n<����E}_t��ǝE�p{�}܊~�фqm1a\]L���;�z��wi�i��'��B���Mw":���x������� .1���b*q/�wc��N�N����q;v�bGp=�&�,Ƿq����e�=B�]F8�Ȍit�"i����3Y2$��c�|�����8�I1�,w����1o����_���Ƹ�ɸ����]� �� 7��V��N'>c�L|�N$����5��|����������d� �効x���_��;�kq{en�R��*-.�L���θ(�d�ճFD-�s��Q7����sRt�sb�%�����<�$��N"��Iī�7��B�4�FG�,��X9?���$}����M�Í�d\Z#�Eq&F�j�M)d���N��aq5縸�;$n�8*���J��wD<�yX|/e�������S~��I!����S��;�Wz�<�w��^$NIz��30�2�����u�qa�J��HpJ��R%sH�e J��#�R£_R��'i�:(��> �����H��uKn��>���)%�HI���@>�D��OkY��S�����cT:���#��8�1�SS0�����,f��}(U�=����̫g�˻[��w�l�߿d]�]��{d�wˮL쐍j�H�Z�L��J���C�\"�� �&���M�qb�<��y2�7`�\��+9�[�{�=�6[�;7;}w����Jkؙ�ؾ�'�m����.�lO��ߖ��ߜF�Mom&!��gt����u�x�!�S���9'�gb0c.�3cp kz�%ؗ��������T�ve�:2�}�����鮀� �-��m�}�&���F����hh��ih}: �K'��I�n�<�7��:��Z:6���&/�����џ�w�*�ѭ\�.�Z�Q��:��vE.�5G�=��גm�М�ؘUܐ��_��9�.�oJM����[����\Y�)2� -uGҼ��֑⍗<q}�\g��r:����}yQ�T�c�z ��X�yrn�*���n�-�kP�+�k�!5��IU�]S*9'���r���r�/��J�|A��J�{��0.c�-,��b㨒���Н�tj�C�6m�k��m�4N�&��5_�]�.�Q���lA�<�B�0�\�3̡� ���m��6Ճ�R�+a������;��Gw��7n�Ҽ��� ��У��.ݟ�Q�9Z��hыФ_�lЧr� -2x5:�w�V�W�- -pj�A�'�,�>Ԗ�&��w-�C%��"��ǩ�E�!���̛��3)�i�<C3א������տ�]���j�-EK�T���Ek��E2v�A�QY���ԫ}�����$�V��[jCKt�f�^�I�U�Q���"��2�HG�T�;ǡw��� 4o��y"ȧy��{����8-�9h4G�ޜ��b1���%�´٣ܔ�Uf��&X��A%;�l�5� �]�����S -�R/�?����H<�釛4w�d���s���� <t���^�!-����j�r����U�rXҸ6K���$�X;�\l4�m|���75t�=B��?Rc�zj������J�$2�Z\�s8��h�^͝&vX��T:u��Qe_� -{��D���g��S�%�tO�M�c�i� ���z�5Dg����6��-��*ˡ�\�9ꇩ��g��A ��_#"�1���`����>;���v��I�s|�W�Gp:?G�3�+a�HaW�8F��3�[_��ӕh%�jG�$�c�e�NAN�avٙ��шl�OR�9nH2p[F�@wqP�7���n��s�U�U�`��Ku4�5 0֬aj�,}MWW�婩V��U��V�'*�!ٮ�ə��)�^A��T���{�ixz�Azy+�����u:��y@?��>z~�h����e�����歟��d�y\����O�L�s] �1.!m��-�:RZi�H�N�}���\��2ք�Z�T�2�BȌ-k���y�w���u�_�s~���;�����O;X�Β�h�E��t$Nk>o�\�9;�����s�� ���%lm75!�����wN�q�p��'o�q�6���hyf�Y�8��2�'� -��º=V�2b�S�%ٱ8y�}���ܔ����=k3Rf�MKY��c��SRֶ����� �Imc����|� &�����:��{���V_x(�C��g~��Wf�Yf�6�I�t_;0f�Afrff�'�S��N\Z4S�&iM9<]g��y�&�-k��Z?&mS��ݭƤmu��MTZy����6Q����:��pC�b�<�k!U�?)Av?i��'��A�s�3�}G�qs���gR�O'�d�3F�ø��Dg����X'*s����z��;� -�J������2�뇝ll����\���S����}�OH�� ^�й�`J�f0�1cO[uf0�s4D��q6�� ˍ%47������DP�Z���۴����u�jtrtΪf�ˋ�� pP�;E��!Xu����@x~sB�Td���� ~|�-Ƨx4�Kb�.����|�]�����^ރ��$���ͥWhJ���L���ߚ*�@�}�\��lV�������h��^��?���@~���Mo\n���V$N�c|'�Aw0�|%�w7�po�����m��>þ�A4}⨜�}2��_w�e���2�\�8'��\�:z`��G]q�0î�ҿ��U��?�OB�����_S����E� -�k6c��������Y����8 �/Q���\f?[��_~��� -�n��*m�=�6���[g��3�_Zc���WN�|��ko��I��Qt��H緳��n9F 1lH�c�I:4��O��&鯐�\���_$���v �o},߀YSL�LJNtW��z�EY�I�`��i��1P�U>�R!�P�4W��S�i�֠������'�H��H��#?�/��2��}@�0VHS��>F�-T�u�h�z�2�+e%�i��rBW�/"��(�SV�C(���'T۾���(#��e,�fO�/"�T�b���k�FT�mbL���$�&�4�d�`�ϻ�%����+�'ԙ��Y�F��*j���_R�Z�:�����϶B�:��݄�C�ňib��7�W��|=��[h���;�d�Z���o,�xm��+�b^X��e%ՖuT�i�I_E���������ʮk,,d��k��v�"X�EYM������z%���xc���6�ye������=L�m�msxf{��T�>��_�7�`�➝⮽��3��Ǿ캦=�'3�e�$<�>؏��n"�v3ye�����ٯ���9l��!�j��rL��1�J��<v,���:�+(T˭� ��(sV\W]>��K��{��nҔ��3��(�ir���9��N1�:���y��˨rYE��:*\~���$q�� �\�S�z�ۮ��r-�l�#J�j���@���Fq^}F��*��e'�ù'j�%�}�M.����=�j�1T�O��,x,�&�;�5������\��kC�qe�J=���Y@ɰR.x=���9�ߑ7\�룴Ί����Yg�M����wGy���˚��y��N��?�#(�����i� ���e\�Y�E�u�n�����(�;�9�t -�sȗس� ���|�����)2F(�ϩ>Qߢ�Q^2���[�Կ?~Δx�G@0��c)1�#gS8r�-� �?��'7x9��Zg��i� ->�����R��rC'=�J�x��c��ّP�����(�v2������M��S;�'#-���� š���� |���ɉ���edG���-��̈����u�#�tOD�=���hd�ޑȫzi��f�ܣ�>�8���Hȍu[�.=���u縋ݺy�J��EA.$����$$&!! �@ ��{""7� ����Z��ug�t�yVמv�έ����9��:���ߟ���}��}��턣���H(V�K�O@��Jz�j�eo�gwrV�F�x7�g�"ٌ�q�0�E%�-�� �oHʹ�R;}R�Ș���cEqNJ"�Eф���LbLz-qXz�=X|�=P�(���J�{ -�/I�}�C�URK6w����/�{����tΕ��|�N̔f� Y�d�(Sc��@�YGeN��7>&kM�u��d������~�\r��-ND�'\� �������t=jɼ;H{�x���`�O�?�se/c��L�oÔ|/��*dQ(i1��>�03���yCB���ꕇ�yoR�|��-�Y�%���)_��s;_p� -*�iPI��$ud'��|&x�8��)ŋ���&�[1�܃#�<DU�R)h* �OedF�������Vz���I��=�Pe4%Xy��^y>�M�{^��?��7ϯ�R����5�yI�}�K�_$�{%4�U��� -!fT/`B���͈i21�ُ��"�V����U�nM �Ks(�S�`u���Au �Mݵ�U=���'y>����uA���������� �#�.�J'�WL��$�-J�k�]P�0��aJ�F�?AT��5;�W�� �kd��5��P����Ʒ�l�V�����8>m(�Y۟�ю��� -��E �υn% � ��PtP���3�~)p���^��5��|1�*֡ט�����K4*�mF 3`4��V�����9M�`J����֏ -\�9�SMT��[����a����jY#�7H�\���kH��%�q%�L?B��W�6��Ӽ�C�h;$E�\N��U�fsM��lf5�����2�q��0�a�M��:��4����fz,��(����r���&�%�35��!Q�7�gy ��W�nA�-����� -�l+�{lJF�U�`�e��u�zk#�n p�,�<�%&�XN�Y���-7�f��%��"���YZ��^�������?N��Q�g�S�}�쯠ͱ ~�N�8�ୗ�����P_�p94q�c��ac���d�ݗr�~�g�j��"��R���A��G"C%$�C��ۤ���y�xp��O������|m�������{;��b���l(��7�v�:���'XܖD�ۙ\�jI1�B�z� _�:.ԺDZ4��S���P�uR�'��H/�y�$�H<�&��u@�8�JE[���Ҵ -M��p{^�ӻo�E�z�-^�����z�,�בT��r�� ����S{&U��B�gQ�j�DX��@P�H-�_�Cҏ^�5�E0I��N�;�d�ob��y%}/��{v������`HP(�}����&hu��@SrU�m���˭�����|���|��c�����$����36��\��Y�;�$w���n�:�?�5�掍���CH��Pt!)�Cr�&TŨ -��!kBe�!Q -$����h�,t�+�ϕu|ĕ���ک���ޮN��<�������w�d�NL]��н -�����C� U8�H*#2TD*i�^13�"��҈�U�bK##IE�����������(Na�WY\���;���@+�I�=$�Z�C/���B��Q9�s(�6�|h�Q6���a)J��(��!�iEQ;� �e�u��Ǣ y�ل��;��臬�����!��;H%.�%,ăF������K�IUEi���P6�<J���ckP8�c� ߇��7^�܉ -�Lh��U�d-k���or�.��f���2ē���1�c�I�,ΐ%�&m� � -TJǁ�)6�EΉ�=�Y3�w6�����l�����'K�{N��9=v�ٱs�����85N��m�7�1w'��p������D�����#��dт��y`��T�<�m�8�2ίƶ��~![/db��>��p��/ʰ� -/�b�%7�_b���^9�5W��l�yT����o�Tr�Pț�2U� �CQH �T(%���X�^6�m��2BƔ2d��Ȑ"Z�I�e��>�Y��z����w���<��\yո]���E��1i;S��;vɳ�_��4��a�)"NAP6�\��˺�o��u\ -�3�О��.���Aߛ�8��DZ(��(�o�����]O�;۱�{T\��Nv�5�ݮo 5A�@��rY���0�D�ڃ�I�Eɿ -΅`[�l�ӳԚz��a?�=D�2/����z���H��ϠS�:>YI���X<=��i�����7 ���yȾ/:?����� 9������&����u���L��܂�[c��/hW�Bۗ����ի�Z����8�Q3��4�ݎq�a��r1z[��v�_�����H�����\ɿC�C�[`��*�U�y�G��&��К��iQߙf�a���ON|vEO �?!����_�ï�ū6�="�� rY"��fA��>Dj�#�,�w*������m9��A͔&�C�}�e.��Itv�I#������@�K@�a��%��6�mQ&=PM��Z�F� @�*�d"�P�3��&��Ub#���h��z�}|0K��Qޙ���E�� -yc��j��<7��������<5oH��D��^��5��J���]��,��w��I����O�5�|Ь�f3o5I�ivS�9H��0o,�xm��K�^hP�yFE�:�;*wR<�(�Ґj�M�$�������� e=H��q��e,���xo9����Sk���ռ���+˭���E��~��J�� �Y�Piu� -���YW�k-��+Jz(�꩸��F�5��v��!{bk/[�*�d<�z�P��L���^��C��B^�,�M<U6먴��S�$����6�Ƕ��ٞ�m>�l�s�W�{�r�GE����whHi:��d.�P}d?{�-�_��w���!���Sxn����*��S��;����a5��ﰍ�d�r<�ǣ�v̦��*�}K��Wɕ��9梋"W\����^�n&�r���,�pu����\���yΓx�M��LJ��p�e!�.K(r���: -]7s�m�n��:0��O�7��&g�κ���gN{(N}1�!e%�v�$�-�Mj1Ě��v�җ���)�ý!(�Mw-��6tW��F�Х\���a�๕�ɜ�<H�W&g�r�^���8>��F~∷����������Fy��gG^{u�goJ=](��`�?WGN"�;�\���+gG- {���YN��N�&p�7���86�0GGgs�/���Ա���U:_���m���.�ޚ��m���@�Ȯ�8qm��Fs~L0�c#����Y7�c��8꿄#��d��'= �C�IL�`�)����:�Ǘ���LwO�{�� ��Ů�>��F(/�˨�����2�V���ȕq}� ș�ޜ -$3(�#�ɘ0�� �I�Oj�bR��q d5�Ct��������;4S/94Wogh�~��J�����MT�[E�w�S#���û1�FP�g��1�\4''؆�Pg2'zqx�XM -%uR$)����3�&�e����S'9,^gg�z�a������o;�hk��F��7 6�?1H�Rc�i�2���z(O�˓1�o�R؈�3�&w�Xx_ҧ%u�h�GL`oD8�#b�9����I������:�"��&F����E?aj�����7N=�x���uQ� �F�1\��Z�K��|=Zj�� }�uy��d��$3���D f�Q$Od�I$EG�-:���Yl����9z���%z�W鯏�d�.fG�51���cN���f�2�q|�+����V�*����[j�]��BjPW'CN8��lFFt'Rb��'֍]�G�4}[���EA�6�Mڙl���Y�]��V����F��� Vj���0Zw�xY��&K�JM�W�dI�G���ѷ����W�=����R]�aQ�i�͉��pՖ��ںRn�k��V�i�)�J<*�T30'`�f8���"���� b-E�U�nWVjg�J�v5#�}��K~潮y��}x���/�=_�كˡmE;��ȶ�'iHG�i25��T�^c�)�u��ͩT�m�5�,�j�[�3��������b��0��]_d>n(4_6��0��,(z!�'�d����NK�G%�v�M -��ԟF�0jӟ#��"U�3Yg��Z�b�X�SnI�g���,��K�����y,�E�@h��M���jȷ3�Y?ϵ~o̵�5�Z�{�{��7�������kK�]?U���~�Y� `�z���h*�1��/�̾��*��f���P9\�B�G�v��;�Cs[�.�>c��hx��bx��;�'c�C1��(���� }��p U|��<M�*�mQ�LU�(����gN��|���x��x�+(t�(p����lm��P�r��s�U�Y��ӹǘ�<��|_�V�6f8���2��g��$�[��� -[m�)���"X�=��9�)͙Lq�l�rP����J ϕB�˪�q95�.���* �t�u�6���]��P��uA�&t�9�A���W��O�<��>8d����CV��L�����]Y�7������O� ����u/��^F�;�,w�*ӝ��p����� ��bq׆��[i�w�f�y����ќGP��'�d��H�p\�������l��s�w慰��?ŅC),K~�\�h�=s��Ē�YB�w%v�Ye�:�V�K������&���o�>�{���=+\5$y~}RQo���.�� 9h��w��ېUBy���/%&�t٥�d�M�Q6[���8,��|)�}V�ɗ�I�j�}�A�|�����������)}��˰��ۂ�X��E���xPz`���$���*<��J��� �<��5Oa���bikg`Z;�ԵI�/%ٿ�$+����nMBe�vyeU��ʭ�K*�C�+�����VH|���d&�t8��ҋbE������p�ð�@Z�PR�Ɛ�!�U������P��@��, �Y�U�(V�լ�,�i�.�i�����>��V�[��A ��y9��vh̓fѯ+�U���!�ZM�ƇXY� u#XV7�%�'�y:q�1,��eQ�֯$�!���x������k�S�mܭ�i|W=��:���:f���:Eӓ.��E��.��@�T��*��u�x�zX�EOܛX�m(�۟���o�̼���m�KL�"^m^�+����ac֎<f�(cFK��-�D�bZ�E�5_'��'��+�8,�U��$���V�Hj��m� -K������� ��Ĭ�Ù�o,��E�:�i��x�uS�/f��^�obR��ۊ��V��[�j� ��Qm��#Q�J/:�vJ���~�Nr� 9[jm�;a����t�t�a&���'��"F2��d�zh&������<יȳ��s��3G|�>��QG�2�h#�|��÷٩�b�O���n�>�B�v�v�^��9��;��0明Q'��t�@Ft ��F���q?9�a'�1����^��y����g�<v��G�m#�\'��]!�� "��%���i���K��o���"5��SZ፷ �m�� ��� ������#�2��A��h�h4��x<�.�D�˳x���<t%�>��x��"����?��}�� ���1~r��l���P&���h��;�=(�GD�8�> ��£�B4�w�x��}ݗ������_A����t�E��~��rߐ�C�� i�뻅��%��ol��+�%= ��K��$�y��c��.u�\�?}��/���!����h��;���a�0R/L��D�p�\��r t -rػ%����˧��M~&H�H�S%���������h����%h�Տ��u�$�E�EyT��0V�(ȅ�ȅ�� T�"P�ɉR� -C�U�u����+��(�@�.�gK��.��<����/� ����]M#?i����掺���N����������\����������4"��er�(h�,I�Pl�`�"A�"EAdd`h�$����ł�XP�!�`W�-K�5�%vc��x�1B̏g�o�?�>{��� -n��5�*���^W�1��3D{��ߞ�x�r/a-�x��L�e6/,��Ͳ���<�\��<�Xͯyh���ܳ8ĝvg�ٮ�V�����%W���/����6�w������uGt�9��_2�q��k��+k-/�g�����[���z>�rߺ�{�˸c���ֵ�b���V��nu�V�3\�j�bLJ����� -Nw��*8������C[Dw����%N�ۍ��&��6��6�<���}#wmR�m��-�|n�sݦ�V�%��T��Z���s��;.�6q��4��]�����w�|*8,9d�wD���J�ʵ��IoY��_�3�zz��$�څp�.��vq�ؙ�j��e�.��?�����l�e��Q�ɞi��q�W3�z�L������w��~�4���_�.����#zuD����k���y�{����Nj�>\���>39c�I���fr�o��q�_)��/�`�jXǏ����F�ʋ�����s�ol$hpl{ѭ�<��R[�¡�����t`O�8���0.:xp�q�C86H�a�X8%�48�����;$�=C�iR��C��5t� �cǰ�l~�m�ϱ���\�Q7B�Q�a�`}�'� ��@8�X�X�bhg��N��^��H�p7;{������0R��# �r5��5��sip+`�[ [�*�<���Q�ԍn`���ls�uc���� ��5�Uo!�T��R��ْG.���܁�\>�k?�F;�g�'��}��B�G$[=�l����3�:�L6z�a\�Ǖ�n\k�Z͚���Ɵ�� �X����^�Y�%�z�1H�pQ��UE�H5�G��ؘ����9�5�ư�˛z�@�&Nc��hj'Ʊvb"k��X��*�V~]·_WR����>u,��^դ�,��b�T��b�����6�~���K7h?���쮢i|Gv{���Ǒ�I�l��:_?���R��J����͊��,����ɹT��Ŀ���+�XϢ��f假��/�-��( ��lA���A��"�/�a��3���>��� ���S�̚@V�P��p���Y4��S���FŔlʃ(.�4��la�Z�! ���&�7���C�(�^H�� L(�"�d2�ܥ���!_�+�1�]ꃺSlOM�P�Cǰ,t"KC���"LKyXea�Y85����,�:��i̊�-V̟V�(ߢ�ߧ�~V��*7�D(s��M��.K�yBj�~c��~[��c}hWj�~Ɗi�X�Fe�x��R�DD�MDE��?}��sȏ,2�Y�ȋ\���֫�j������Yڛꬨg��(�����w��yo8 =g��[�Ӱ&�3��{�4ҁ -�ʴ��h}(�R5�¨( -��̋2�7#�����(TdG�+��W�2�7�3t��t�5st74i���4�x��-b$���32�a�4�9v�F(X��@ՌO��@�n8tc)�M�@��<](y����f2w���Id�d�e��+�cJ�s��Ui� ��nM���y���<I�D�����d��e¤ߊ�MZX-Y��3?�4�/����G�;��X?���Nf�2bg�>+�9���R��)q%��*�)�V�h�N�`8f�`�fn4<��^K��-�dWd/�"�ۤ�\���7�Ĵ�lV7�㾠��D�����qd|�02�0���ZRg�I�7���)>G�h,V&���Ƶ�������"�U~��ⅺ-�e/\�c�!�����k�r֗,���8�+����NAF�s�IM�'91���HL�1$��I0�o���m*R�*UqI�ձI�5��������z�#���D��r[��YY�&�;�����1H�#)1�S�ؙܤd% =y8��cIN��GBJ0�����r��8��[axiC�T(͐u��3�A�"*Z�h�E-SJYZ�Z���H"�lY�lC�3�g�9�J��\�{���>������o�@W�z��ˢ�e, -o� 8V�'x��wp��Wp��g�QᆶgP��N�3H���D�x8-u(��H�)�"[~�$a�2 "u6&x�5��b�_�X�8�:���.,��O�����g���I�y���Ỵ�Ïh��_�r{���VP4s����~o����d�L �x!2Xz��6�ῢKV�w�-#F�1xE��3����̋��]����5*AcN��KT�欨R���?4gE��ϸ*�2y -���O�zi��C�/�h@Ds�� X��9>1}���G��Ŏ�=������f�*O\V�2kU�3�V�ό��&��5���p��R9�z�����S��gd/��>���w�x�Ļv���,Ǫ��Z�xc���ځ�^;�����0焩�H���w�}�O�?SC����d�~�!i?�Ig�O��}���دU>�B2(�����ĻAƲXq���ܵ��m]K\�w�y�ӓ����-SS옒2��)�8lt�~�l&m�`�F_&�1>5�q�I�M�bL�>F��d��:F��dL��O��>��Ŀ9d�w��}d4�##��4S6��akW&m�dBz?ƧǸ����ǘGFeL�LW�Ϝ��L�����gmaX�.l��a�Y�m�sl��JER�<�>K��k�Yw������ f��}���9?l�g��vX1<�?ò�b�=��9��.Ǒ!9�|�;���� b@^��R藗���R����&�����d+ ��_ſ% 7H2l��=gL�cr`�o���ܣO��&��k�;~��7��F�U�8��]�e�����Y�EQ��Y�ӽ� -��G��Ƭ�C9R�YwJ�d�E�A���_`n6L˃w��=0���C��f��iiL[`|�k�D�#�0*M�2:u���K�/�F�X**v�SQ�α::})�i`��7I� ����=�=C����� ����1ЭT��Tڜ��_g��>kA˳}hq�[�87���'��3��Ѫ -B��jT�2hr�P8 k�?�*���zGK���-ngY�Dq��u���.�N��sZ��ˠQ������`*��ܔ���߇�4�5�ܑ��]y���K� ���Ֆ�H������� f�I��?(~q[��D܆�n[ͯ��:�j�;Zr5���=#x`�J�*MꓱP/M�S9�� q�\B.�y~�}��7_Y�Y���>�T�,O@7q뉻U5h��m��@�(y�zmq��k��Kci%�׃����P� M�"/b%JH�P0�}���W*�&.���X��!�?Y�+yA2��D=�r+�y�NQ�CJ�O%���V�����N�S���F����s�+�[�{�^p�e��C�!�X��F��$�Tqn�h�+�\��+Q�q�]��zM���\�\��PՈ�(Q�J&ͬ��(�v(Z?�^�� W^���B��3U��p��y���*�{�djU�����*���|��P���� -Tu��~ɩf -'�+T~�p�����uPԆ(�$��=����PëV�<U��ڃ��E�S/�-��;�Ԩc����z�թT�ӹ��N�z�[�L� -N�����)�yEY�úr>��>�����%��.J'ɤ��|Z��p0�F�HouzӸ���M=o��-�^����8��Y�U��_�)� Tl��6*�(�XHY�2w�@�Q-�����B�P`���ʗ�Qt[�ti�b��bڕצ=�7��^ס��2��.�Tu�əns9i��&?QaD�i8e�Q1���{"���p�l+��mg�y>E�%X�fo�v�z�o��i��g����CS�f(fj���y�Ӏ��Ԛ[r�l��,��9�c��(��M��'%V����?�{Sl��/��4��� -ÏT�0��]X�P��C:NNp��ر�X^�M^dK�dɒlɒlY�,ɶ�˻�8��,�b��,854�4 �@)L�%�Mi)0Pv��CgHI~<��|�{�9�/�&���f��k�X^3˩��8��8�םai�s��u��c!��W8t�M*�;�BY-�������o���WW��Ikyz��ݧ�lr �^_��+�7�xd��S�9q��)1�R9�2�э�,n��¦�ٴ¡�f�����{7���=W��&k�a���j�k�����'�)�9�)�GS�X�\��-��b��-v���YLkf!��#�Q��r0c���)�g�a߃�e.�'�<�Yb&�m��~�έ -;�@��k}Yiɲ���)���7p&��~0�Y�ۚ����d8�ma>�Ɂ�&��؛��\N7��ٽm��Ӛ���~�۟ ���ڋ�k?bL�0z����_���ϲ������L��,��Βf ��S9��ͼ��}�r�Ff�6v�5�+��t^���Q��{I�0Y������.1�{��ċ�b��C��@I����ol�%s=%��l��\�Rޭ,�f�0�}�LfuZf�J�.2���B���d����Ɗ;-�f�d�xi��ҽ�e��1���ӫ�+=�������N������3�{B���x�5,��|��̕&3S���2 �2� -��FF�6�� ��}��1P�I��}�T��Z��j�.ót� jx���?�B�[(R�������OH�)�S�?�_���3Wq'�*�1U��DU6�U�W�1Te`��B��A��C�!@�:Lwu/]5�Dkf�6�����*dzM�nzO��6�r�����9�_�̳$.W���쪾�DMc56f2h��o,��XI�h��h���H��L�6D�6F�y��y�v�Aڬ��Z�O -Q��Z�_ --�����k���oD{Y��C�Xuv�|�)ӭ��W7o`��N�EC̢�Ӣ'j�!l��aq�zi���V�E�6D�m-��f�I��~^��U���K�\�+_�7�� .h����=)��0g��c^Ÿ�'��~A��>b��tٲ���鰕�n��ZZ���=�Է���w�s&�:���\'T�9������zG�v~�r;�������y2�R�GE�aHTa�,^����7�︋n�:���pf���tpV��2�w���6�����؏�=A�{�����vy�^V;��>U9�˸(��E�VjpZ����.�W�#���w��iL"�N!�� ���������zj�x�p7�hl����������ۣ��S�}g�6����6�'*�W��7�e/��N����b?g� 6���\G�駴yW���K��������/��o��l�����쥾� {K[`���n���T���U�֗����U斏EenV���d'<S!��*;Al�|��d�a��4A�-��[h܍��^<��4�p�8���+����k�cm�`io���Em{Sh��5��Tա�������nS.����5�+��G=ҋ^�� �h ^���F<�;h�X�#��=�A]X�5R�9��6R�)R�1�@u�C4BU� ��;��<Dy� -��煷�G��>�\��2�I��)9�a�����������ߡ��z�n���,�djc�1�l��'CO1U=�T��R��@�룬7Di_%}��@,<Ka��|���Kt1��V�=��/��~ѝj��#���w�3u}ߣv�fj�jp-C�eP�Fi���xE�t�: -�����F;<F��^���@3�ۆ_G���r��I ����̳�F#�V�vI1I����R:~��������T�&3�N撛(B��`[�����섟���Sq��!}�p����%�%m�3�'�K�i�^��3�y�Y�>m� �GDB���O�"w�Ml�����r]�a9�k�d+�E�h%�")�R1�P�,�1c����c%k��J�����P)3H��̆�af�c�1��w.��t������}������<����G�_BS�NH�h�S'��6�i��O��дE���08=�A����cJ��)�>�;�����坶6�j/(�r�U���+bh�A�m���Ь�����A�Ĕ�O�h�Ld@�d�sg�����+�kٌ�e�-�p���w�mz�<�w��:�T���ڏ�a�&�@��rϐ���0.S��l���7�_�-���x�w�O�'�&zг0����V8��)�͢K�B\�cq.N��$��j�o�X�#����a�?=^gR�j ��0{'��-0j�O!x��[)8i�cYG�˺Щܝ����U�}�0�5���D�T�K�ʹ��ZF��x�Vec]U&�b]y������ -��U�T�G����{�Q���`:$�a�Z�U`�4=٘�ն4:�@�Sn4��PX��tzԫ�����Y8g��<�����zQܬ#M�-����X��)�[r��;D9��@���z:��֧��4p����R+��Q��|��Ѐ�������j�7Ԅnh���e�M��V�=UǶ���T�Y�z���C�5�ܽ?��'��ih~^��s�&nX�96��jr�o�4���;��iP/T����{��6�#I�ױN�/Q�s��SB�j>L57)ow���nwl�����.�)TF�߉;�by�T����3������y�!�����Z�ڄ/7��:� -�uq���-��H1^L3x�\���_X�ϬBS:?��H�{�yH:��}�r�B�uD�W�����<S -��g�u=���8�n�Sa"R�ů�'�l9?��ȷBO��o�|��%r�|K���r�T�r���w����e�("�(q�������U�&,"�$�=E�rΓo��[*_4���o�|[�%I9�)�,9��Y�%*�;�Y}r�{��1'���x=^�[c4��h�5i��h��K��F(�q��my��9[���"�\��z=㸢5��5�@ -�vq��B�U�V地���8��.G�<��ڠ\��������a�l�a�Ѧ+/Zx�/mB�m�M�I\���63��t�~����p�� -N7[é��nωIo�Χ-s�jU@e�2*��p��:��p�ï���zM�`��ưk�a�A8�]w���f[?�n��vQ���&5vS���>�;���NSe��J��T8�P#�[9�B�S&���S�\J�� -\��@���s}�ޮ���h�;odžΊ�s���箓��=��0�3.���b����:�2�i�6��n�P��t_Bq�h -{�R�c�=9�3���r�۫��1r�/���;2=��� ����z���p��Y�F<p�ᆋ-\;S�Ó*w?���S�II�7(�B��t�<g���|��[���X�V��G����S�쿛]���9����H��E��Ol7$��z:ڌn�ߏZw+���愇=^�(`�h`y>f��&���mr}���;��As��!�-"c�r�ǐ6d#�CI��`��~��&ѿ�힀G��`s���z����U��RG������oH�O{v#�ϛ������ɮ�I��i��H �)A�����KH�f[p ![�2,��a6 ?���'�z�ua�{��0��z��q�r���_��\���C[��%ă���:��P3)a�I{�İw�6����>����l���#bX?r3�F��Z6����u�5�Xe�O��+���`��q�5��:Dw�P�u�� ���@��^���Cʨ@G�$���$6�'���"�g]�\�">b��%Č^Ś1X=&�豻YYȊ�c,��K��$�0X�;��0T�[&��`�3t���Z����4ۑ:��c�I;����l��`}��"�$6�b���:j6���=n+ƭd��8��O`Ʉ���o��=*�:�㟵�j��z��ڪ�in�%5CME�+*���00���3�0w��7@E�D0� �R��M��e�X��V���㩭�.��~���?>�}��3����~����"E �J�-���4\��$�A�Sc�y��1�:���EknjFK���L�1~��.>�Z�j� �<l�Pi�SaȢ,!�҄ %��'��ȸ�¤���(0=O��.����~S�U.NѺ���U���F�!����^mzo�BS�H�a�q:u�H���R����xJ��)I�Q��IȔM�)@0����Z)��7��o�$��>�;�Z�E��?"L��*�U��'��=]�]���=������`����ԤL�*e6�)�(5���G�l$h�P`v�xȷ���V�km ��ٶ'�=�'�-�%��q�~�~��R/��/)�g��&�@���'b��VjR�Ri�L�u&E�Z�(�ƐoM Ϛ�ϖFn���4��!��Ux�����r�������8C��;q�L{��3s�f��yP�O���r ���!u�5���HCI�B�(�G�o_B�=�\{<�^� O�wFY�A\�8��t���ڃ�Ս�����t�;��0陿���k�ã���T谪D���~UڇR�1��̻�w�O�s9�Ex]+���rq�Rqfe���%���.��YG�gi��ؼ��z_%��9���ˤ����} ��C�b��N��[� -�׳*J��#�5��.r=S�zg��.��"3;����fҳ��帱���斐�[��ׄ��N��)�}��3�s��%9'����˪�a��T8���Y��}�;���[�ν�oμ2�"���������7a��0�H���(��!)�cA��H(8���S��1�������X��rpH1w��{���#�'�9���'��h�{HN�Z8K�B̅�I��2�J�� �(CQ!�EU�)n ��1V�'��eb�OSt���O"LL᯼�^<�tI{�W~'[}�+�ᓾ,�;��`l%#���#�l2����P�C�*֔�W���tb+ܬ�]Q��� ������N�*_`Y�)�U\`Y��,+�s\98���U�ۥ[�W mw�K�R1S��$V�A|�����Ol�lbj"�6����,�5U�ʲ:'K�|,�+aQ�Z�mf�L���G�_�7�מc~�%�����ۥ�T �U�>��O�ү��:y�u7�a+6�#j�$��G��~.����a%�0�!�y ��m�2�1���jf5nafc3��h|����D�O��+"�G���)�GC�PK�P�^�T������`��߲�i�$r�x�l����3�����%�h���[��ߒ�Ԗ<���3���I�;���$Z_cB�&4˄m��/}T�w���2�@6�L�y����Y�q[`Y3��3v\���nf��QLi�3���cR[wEr�%ܳkw�Jd|����=�mqgGc:Z��c�;�3������5��~�����*�I��Պ�P�^i�I;Q�+�;afL���^��'n�OO�dL�8FwNbT�tF����3BCwx��ۺ������ۿ���p�c���.��}?���x\����Nڨ>hT�����; v,~�+ܷO�`�!���}f7v��1�{�r]σ�Y�����11��.�#*��Q"�ݯ� qZ��ء�7mV�l�Y�vj;��E5���N}R��Av���������Ѡ"n�%7Zh)>��츆�����'5tzu�{�� -�� ^yJ���?�hQ�������,(�Ŝ���\�|�3җ��;�E��8��4d�����$�������ZJ'jG�{Dh!�P���.��T�7 �|�?��%8}�������ʁ�]�6I{���-��p��p��0��_�M�xO|(>�j�>�EK���2 g�iY�����}��8����y%��D/(���V9/��rn�筒�<iO}�)�?(�!o(��H�}�P -e|�sqF|)Ίs��������r��d��E���1]�QU�i~�kj�L��~�{j�x�Tᨠ�p����"�xAA! ��3L35M-kll�Z��4��,3k�D'��&]���{����|﷿��Ԥ�Ҥ�h�z�/z������x�1��1Ex�<���#������� -��Z��z�P�m*��j~d;7����;5��\�CM�M������rM#�M|���E7�"���b����������9��[�-�4b!?�N��W���"_�|����(���|����F��Q?������<�w�E1T�j���W��)�C�%k�ŗ�7��+V�%E�m�b�,S�|����ck뜐�=>�/�4���V���>�Ag�^t}�}�����K^���l�%(��2�����m�b+V\嫔o�|��.ߛr��=�f���� �xK��O`�V��h�4F���)vW��u5)� 9#�Ul��J�+K�\� -��V��_ΙU��b;o��lj�op��[]�X��9��G�?�p{�C���v6Z��xJ���]�Ӻ?�[��Z��\m��Gm�x�]:X9�����8�1�w:-����9�y%'�^��.h|��c��p��n�m��9�� -�������!�\v>��c�Чmtk��nm�ۥ�vx��:�/O��R7w����K�'���i|�ɱ���NC�l����P�U�]́�e��S��>u��{�]�NR��2u���v�mj>`�@��Abt�_G�}�7�ӧߣ%�v��e�^��=�S�&r|�L� �Kà�l���1�1�}/.fϐ%��î!��]M��v��v�6j��c�F^q���רu��QMl~�`�(��]s�1{��������K�[sv`N��_��Hw���ޗ�k���ёԍ�e��jǤP3&��c�Q=����ױ��2*�US1n7�\ߠ|�96���� ?P�������� wu�~5��Qq4���kd�����Q�?�� 3�>a���v㵉Ql���Չ������IY�Oʣl�j6N.�tʫ�L��x����a��Uָ�*�_)�0�`��#��������z��J7��ߞ��z�s�0�Oue�4��gS���f�`�=,�y������dֿ����9M_��Ŭ���U3k)�<H��)Vx]!��[�~a��Cr��f��W�Q��}^}�qq�]5��V�{�`یAT�C���==��5�����B)�vV,kf%�jV*��Y���g����.'ϧ�\��,3�$��٦oXj��,��d��fn�>T��,�iO�J��u�9C5�gW�x�c��H6����w:E�>�1��ʴ�B��S4+Lq��If��r��3w5��6�ԯ�L��d(.�L���I����������T8Y�@�>p�W����m��w|;�iN/J��h�+k��Q��E��\��X��2+9��d�O$k~�r�X��%%�V�����F�Or�?I -�#�E����������o*��;t}ml�R�-̪�1L&/`���d��40��@�6҃�����0���,.&9�U��;I4#�|���o�-��48Cq}�J �<g�lPٳ[�_�ʰJ�oXКu��)\8���Q����AV�7!�H $5$��+����X�AbX a���i�#�r���_k� �� {���W�����<�uAP%�t_�P=WHW��}Y:��0W��M#-̓�0���YLbx ;�D���E��\K�uv�b��E�':ꚸE��I�|�2�I{!P{Q�Z���$X5�Y�FX'r,=ɌBz�XR"'�9�D� V?�օ�Y�qDو����J�m6�j�cʈ���j?L��]"��#��{XlF3�i�����Q�=rn S���7�ƒٞ�(R��l{�D��1��x3�=�{(��(�b��.&ґM��K�F��js$�y��S�q71;~F3Wg�Y��Q9wʹEe_�J�B]s� �֚{w���w� .�{�4lNO��&��DƇ�%>���$B21���2���L���"Z�(�((R�:F���* - -"�+ �&*�E\� .�����6�dfiSSMNN&��T�v����;t���{8p���{�����Y�G�gZLa1{�s�И�F�� �z&,V�jN�u�W��Z�t��� ы���hO���gΒ��Z��������۱3�7���h��%���x�������'���7�H<#(�b���Z"�(�q��^ys��K�c������H�&"����7��8��I��&����wN~���&&g��τ�RƧT3.�)�|�����w-V.hV�*�7+�A��(�|j��u_��$5gz�3�˺��E��LY>�LJ�Ĵ`Ӧ1>}���ǘ���Xͨ�����Ȍ#�g\�?�6�i?��Wa�rZsp@�+�ɻf�� �#qr/JRϡv1< B2���#Wy0!ӛ�d cL�hFgM`TV#�#�ϞLj�h��NfXNCr628��A9f�r.�}����e>+GU�Z��ɽ.��䎖{��+`�ڣ �v�Y�̨�n���dD~���1�`8C�2�`�t����װ�� K��ak�g0�c���w������O�3+f�w�]"w^���� rG�=S��\��a�Z���6��B7^/�b`Q ��|��.�oq8��ū8ϒTz����d3�%{�QrV|I��{�,zB��߬�O�Z�{}*d˝*��L�'w�ܓԖ��(�����wkk��:�Y�F�>x��n���qn�����Z>����T��kE&�+ -q��M���8U\ǹ�.팏hW�����u.f�LZ +�;A�H�g�=��n�7�4«ۡ�N�\�k�����R���j���f8�GS�M�he���i9v����ؚ`c��M�Mlk�?[1*�l�I��䎑{n��j��� -���j�k��>p����i���M�.� ��6���f]�̺���1kÙ����s� -d� ����CqSܱ�)Og�A5X��@�te��QU�'��~��o�WC=FM�h����Х��.ħt9��]��"rV��6�9M�9��~n�. -��q�J��:�X5П���k��́����A�#�zڜ�fg�?/�[���;�B�]/�>pe�\����qM/����?�b�^ ʄ~��8oŠ�^Q���L�2�W桇��=�qڟ��M��B�B���qC�ҧ��gN��j�.�7uA�f|�q����;~w���h�5j� -�Xo%w����Q����Z���C�].��������K��F�ߊ�D��rWM�=]�xM��.(4��z)>��8��O�؟�Xp�D{�&� -?1J�<eO��z��r���D?����ޣ��P�]�hdw0q�����%n�7��~�o�3�f����$^jGW�G�{��c���9rF���$ɕƿY%W�\�*�:����Y���+��;��zOe;�)��U��~�%���_�Bk�B8�N���C�w��NQ���7_�(e��+E�t�2�ʕ�����(�m�|;�����i-�4��9�g��,�9ل�&�4�Gw�̾�:Rމ��4�r���B�b�+AKc���gʵZ7h�n����rU�S��8��K���Q�CJg�|4��-#���Z��tTnw��i�j��*�de�P ���"�k�r%(�2�2��R�5g�|E�m�jaf�V��u�>�a�f�V3]+������q�R��j�:��^���r�+�eUUg�5O�w9��q�&��6��mW�`���fy�7�@]�R�5�`�� ��!jZ���� v�k�:<�����/`�7v���-f��Ϊ��ys��`N؏��$����,�<��W��1�}���i�Jmە��r�v2P�\�.g#;�US��@��9�����C#[\����R�� ��A[�^���:r^�Q`ߒ�;pҩ7G\��a8u����q2��©�<�����e1;�ı�Kۻ�Q�5��nk(북�ݷ��m'�nu��8EqϏ(t�������N�m���ܷ\�画P��d�1�����͛ڞ��r���Tz�R�+c�9��^���Ql��3�M���܃���8��iM�i��6�#�Ȃ��ZX]`�]�7,�,,�]����k|��5:�m��ijS�I2����f:u��I3M�6��X�=����7�����=�bv� �7N�w�f�1��/3��MF�o1�����C�'"�۫䛕���i?��_���.f g7��t�FNlN��%���,ė0�`�@B5�]�K�c&���6&��������QF��#�|���3n��@����ܤO�/zw )"�R��|����]��{%I���D8���J|�c[c8�����̦�7��i��IE%� -c�nFR��N�d0���C�/}�����d�@w�5��7إ����?�_�i������ܗ�3R%o�zr��W)�c>�If��ٙ�dF&cjF2t+�TZP:�Wz���ћ�FOV�YteO�+g�`�I:sB��*��?Ҧ��V���"��-g��~UZ��n,����Gv�\�R�e�b*;�����s���eOn!}�zB�&�UVv�\t��� ����]=J�z�ւ��5hּJS���]��FM�>���_��)��ɞ�sᨬsٰ/g1���cPO�ZA� ��5]Z��4f:4v�5� -�h)��_��8L�v��cxu�i�]���m�JާN�]�>���C�ಸ�I�<��gd�/L��<�@��m��8�]�; hUthi��iњ�k�4���t^K��v�P:D�~/�j �QS� -�۸�>�e�J�D��MF�,�$�3��J�<(cش�Gd?��6!��t��(M��4�V}6~}Mz��r�z�'ue�x�Z�-b�w�4.�a�Ƴ8L/c7���=l�O�{X��(��=\U�h)��Œutr¸Vf���%�2���,���x��S�U(�V�Q_Q��BO�����Ƀ�Ԍ�2�ü�y���-g���,���o�ǘ���H���~U oA�'K%s�%o a���ɌU���c���⫌�kN�ΜN�9�Y��R��b�n�b���Z�����*k�8f�<��a��������Ra�� -��H��r�H���yL���r2���n�;̋�[��X���� �Z�p[�pڲ���XmZ���T٫��]T:����!*�c���(s����9z���]�wޡ�y�RG$�r/�_b �ŹW��2A���~+xm�q���\���͕B�K�ŝ��]��m�Xc���AYm=��V��nJ=#�x��;���"�uoP�E��#�j�"Q��ۻ(��⟗�w�J�a���B�]�N����]�ճ���8*�a�O��>�� ��� FJ�輵h��5vQ�8��q?��}?#����!��o�K�"Q^�w���|\j���G���Y_h������]����M��)mVPҜ�֟O���B~7�->�Z�Z�i�!��Y�ϓ�zM�If�?��Afs$�/���I�Gl����!��C�F>q�6���͒{Z�Ei�J���(�،�#ug:��9�uj� ��0�p�h@lgg����$;��I �~�"xE�CR;?'�#�E�?+��K4w�D�Nq�Kp��� -z�jE�o��z���k��%�;�̞�=Jv��*&-TFj�E���!?ɽ�l�#�w��$�^!���C��� �_E���~\�|���% -햺[�-ϊjq�%�H<��e�R���6���F�%�<�ʶ�,���$�?lb˰���F6 �"6|���g� �̺��3�>1���~�^���>$�Iq��! n����6�['��=��$�&�."a�;l�X��ɧ�4����[��J�S��LiX'����*����f��ǧ�Y=3�ʙ�������,�z��w��QN5�[w� ���&��r�6@A2� Eb▽�~��:�]�\�sO�zn#��X1����l~0_����Y�`瑅F^��҅1�,���,��&����xh��(����'��L�yX���?"َ��"Ce��R -�M�eK���M�$��,EE$����%�ı��2�9�c���8c�s�Y���4���z{�~�����=�}���c垣������c#ߤ�S��u1X�]��ӄ?�����- �03���������O�t���Uh�+\���,v��Z��ՠ&��� -����i��;R��r���p� -�ZB�]Ъ -L��?�Nt��r��VV��F�p�.�m���B֬U���j��q�� �bF�r w�ܳ�T�5ئ=P -C`��[�N���49���=q��.\�ǻ� �'����:�k�ҟւ�V�Ok�:R���.�OX��N]�����m���hūr�~�Z��z�츼�D�8+����Wq~��.��*�U��Tq����qE�UxW�ٯ)�kZ��{��~��L�u����{V����}@{@��?*�����g�;'�"��eqE\�ŇZ���� s��������;���U8���B�����Q)5�*w����T�<V1�h�N(����bm���J�O)T���@|&�)���~{5��C5 �T,?�<��B~�C�^���K�rGF:���������A���'����>W���%<&�oI�V�_��6��M|��b���J���|�~�_�T?S�z�{�䉦��B06�q#F�3�^t}��!�Ɲ�1��#�9�勑+A�$�B���+K��6r��A1�R.�^����V�U%���K�^} ��?`T�k�D�]䷒{�p��KO�o�*�P�z�F�S\�Z�e�P<�2K�\m�<� -�UJ������ m�8�,�i�ZEuJ�S��lĨ%��7���Kz�m��U������h��+L�(yȓ(�2��B�4]�lm�uz]���*W��߫�8���G�PY��CZ����F�_�}&Z�Z��r�V�v��I/9��,�L�.O�<8�y��<N�*W�X#�z� -8�\d�2q���ϽZ�*m��<ҷ��%�o�>m_����-���h���}q_�2^��r�R\�+Z�x�Z&W*�d���^���b*�A3-S�J�EY�����ȶ7���*n�LqwP�=������b��`��T�Lc���mAU�h��Ƴ�t1��˩h�Fy�,���Sڢ��;��r?ŭN���e�Zߡ���lj����F�y����qwC��%5�L̨mb���m�t���+��z��} �&a�By�Pvv���S4%�����;���<�"� -�lbs��V���16t�g]����x���Yca$�����n騭ױZ��w۶��S������0ʻ����/�-�Ql�g�XN��r��l�C~��z%��W*뭲������Ŭy���>GX��}�F��C�l�g���7�(��^��z���-Q�kpoSvY�SfՇ�>��u��ړ�����L`�����`�m9��X�/��~�d�O&�:rY5����It���I���r�$�'�,m�a��_Աz���b�(��;lڳ��%��?h(�F�k��Z�@��'��~*��Hw��*�(V�'u�RR��$�q �7�4t'K�U�ة�D��X��5�_����x'#_u���;c���[Q&�� -.�oI��X�hM�P������A��iN�Hu�D�s�Ρ,w�$ix,K�'��%�D��$��pD �n��s�%��:1���v.~"����]+]E�tT)��FW�6Q��y�L�u�L���I1���$�y��-�$� ,q�B����%ad�#�`�r�Fg3z�ۈ�b��I"�\%b�=��1?�il�rP���=�U#��M���#V��g�HV��e���$y���ÃE~$x�9�8�b=C�I����Z�<�UD��#�g+s}w�w�9~W����~O���56�ʍ����r�r0Zu��*�I�hMʘn$y�e�� �N���$�Nj���|&0�g -�|g�N�_,s�.&�%��9�(bV�.fcF�e��ez�cB_#!�;��go�|e�ʁl�u�-V�s��K}�Y�gE��Ďu$���=���#�?���I� �Nh`��������+�1n !� -�6����2e�%&O�����+&�36p]�N���'���5"�O��3:dIt�-��NX�(B���t�GU]�a�1GM�C45JqWA�E�{/�{�/rAYD᪀�:����.��z��9��iFyl�f�\f&stLӱ-��dv���s����}���}�'X�QJ�6�cQbL�bg�j(�ŰPf� -��g�.�鰌q�e��(����5v<V;cp���o�"��g�cD/b<�������t�H�^Q�a�l� %�d5��b��9.Y�q�2������\��Z�X�*�rPQ�S��\@7i��:�=?�`z���a.E#�b��9��9��ʌ��x%ſ�D�8Y͓e6OS�e�L�(�&�6�&d*:a��K��T3m�*¶E�� -K:�P�y��B��:�${p��V���d �� -����g̉�2,]�l��Ą��$W�m�L� 2$�)6)X�I3�lPd�U)i -O�UX�<��V*$u��Ӛ4=m����ASP� ��C?��`���ߌ�W���8j���O�>1%�����ΊK�'c�Ť�TT�x�L����@�g�)4#F32� -�L���e�U`V��V�?�A~�{��6�L�Y��y=�oF����&��aW�-I�rQF|��9���]�Y���=H�F(4g�Br&jz����! -�G*�n��M~�Y��[�)y�49�Z�6jB^�|�O�{M>�o�CM�w���|�p%�2��h��B�mf��%E��F3f�h��� -��T@�H���o�M-���0M*���B�| -��J�l�+rhl� -�)Z/��U��H� -�jT�]y��~�~j���y��ni}@I��ۘ��6�P�뤩�\5i� M�?\>%�5�d�ƕ�ili�F�Fʻ4N��z�a��c�F8�h��NC쐧�MCg5��<K�������Ԃ�����]��v2�x�Q�C�%�R�gag�)s�w��F�{�� -/yU�ш�I�8@C��sq�W&ȣ2S�*5��\/T�Q��f�W�[e���K����+�m�w-�eْ#G*��;v,�,v�Bi��{�4lyW ^�"���4h�` ���Ucտj�ܫ�V.�j��T��wM�\jȹ�Z�jԣf����U���QuC=V~��+��*;�1O�;v*��aG,�aOZ*�^! ���Nr]�U�k]�R�.�:���RϺ�r����uA�V�.�u���3�s9�,����.�:�q��ڛ��fs�E�G�Y��D|�,���IS�����k������(ui|V���ɕ�_M>Mc/�&.�&.����v��C6�/��M�a�� -���Up+�s'�;v -�M��`��y\���E���R�-�3��d��$�b���:v{�<��ai�%����k��Z)n+�hY��D-Z>@��KZwQ5�w&lk��F��g� �Kp_l���n�V�{�>��u��zd ;�@tx�?�8��p�|�C� o��m<���ɏ�<���9-ay%˩�N�˶�ye ����?�D R�a�@� ��1���G:1�p�`t����8��4��=��fϰ��oC�v�~Be�Y!���MR8�}w҃�����������$z�Fg��K���o�s���&���ٗ�\�_�h����D�/�������΅�;�� z�=��0�����i��'���������/���t�a�����\eo�3�ޤ�x�%x���A�C��a_�4��z�g�3rGC�8�O� ��2���-���H�%��E��*uM��oU�+��ՓW7�j&��aI�X�1���>�/�>�,����>μ]P'����z(o��}�r`���K�5�ri��Iν�2Rl%��j`���V#�����N��W��:弧?Ay�)��y;� �����>x�7� �M����p���q��K`��� -V-� p�uB-:�ô� Z���RmT�(>����Nxw�m��ZO�=��M�_0�h�f��'N.�8�<�A��Q�%�V�Z �NGh��O9@�i�����k��u��O��h��+=�7�jq�Z�Q�N)�������R8"Y�Ʌ������t�u�W�5�^��L%Z��o�d�����Q٭���n����"nNj��>��;��x�3��̄e����8Y���ɇRL:`�k���V�F��� -x�Fvp������:�랬������Q�gg���;�8���+V,��Y.����;���3��'P -�<�CD�3&B �!<�C H@ρ(*U@�j�a��t��V��ڹkoݺ�ns���n�n����n���v�O�g��x]��������y�[k�c���qJ=]��װX��ɘ߬�N��sO\��7�]�f�~���L-�� -G�G��������w� ��B~y�ԻU���T.,S��r=+Jx~E%ϭ�rf��gW98��ż_7'�����c����f��SL�>ϑ�rB�nrh�-l����{�<f�o�/�y{����J�{�%yu��W�$k�]��� �� -0p2����*�m�rtc3�Z������y��<����p`�����\c4� �>���;�}�'��[�%r2r��Q ��pI���r�-��LP��q̅�0�t3�Z�8V�dX5���q`k3�m���ض~�m���~���Y<�g���@�k�G����������P�-rg�\����V�r�_��pF��Sa~̅off{$G"���J�`�����G�1�7Ƃ7���X;�X'Cqnvx��1No�4��t'^bO� :�ޥ#�o�_�T�|8�||�Iƀ��2���?�C�-A����8��[���c,!�}�ٌ$��I*d(���� -���*l�(Z����:�G��ug�<���)�#�����{���w�o���d��HH�����SF�0-qs �)�v��M�³+���4��*z�Z��"��e�I��#�W�g����^Z3�Ғ1�=��Y�e����_А��_�g+��%}3^��=��0#L*%���}J?�ӂH�FoF<��2����ŕ��=�D[V��ZZ�m�s�i�qc�=L�� ���ԩ�a�\�V�65�`��'��"�I�_�q�M��� �� a_�d����g�� cON�� -�U鴪T���4��hR�Ѩ��A�@��A]n�yC��O`��Z�@��T��¬{_�T���������W�ދ�;-q��n8$�~T�}�%t��ҡ �=7Gn�<%��,�s���c�7Q���F[�Eg����J� �q̅G)7<O���~.����kL���H�{Y�?/�y��:F�~�+O2N�?�� �m� -�/H�Z�A�^E�^KUa1���Tj05Q^䢴���&�4F�s��Qd����'��kFI(F��r/I_�q�MJ,�%�ɫKⱣp%M�@�°Ec)N��8���ʍy�)5�Rb��T҈�����^e�(,�B_~����3�������e�������\����Q�<z��+�����6�2�L�) ��4si<ee)��eb*�P\���l�`�������Vt�n��^��W�,��+�������c�+ԕ��\Ε;A�S��D�.��_*;��+�f�A�V�c����j'��t��* -���,��[���XɭiASۅ�v�ݵ��X�ɶ^&��:����Y�o�j>^QË����^��}%�.4��_֔�*���Uk1Ԇ��F��K$�NI^]6��\������i�!�����2�H�$�v����*)�ߑ�xG�KJ�}� >^����I�R�G��f��⮓���F�Uv����גoۂ�i;��8r���nN'Ӯ"î#�n"�� -eK#)'Ɏ~v:�Q8���z����;~#�&��K�ߑh�U�ڧ�ߣ�헚]���m���d�u-�*��m�h%�=�Tg")N%����t�pHr�I�#�����b;G��<JT�9";LD�Dt��H�·D9qI�'��C��[V�Vq[�]!n������vBZ�Rvu�Aѽ�Dw8 �Xv����ӳ��Q�&"z-l�m&����>/�}S���%��:[��aK��|.|C��!�������U�Cfq[Z�ĭ���emU�A��J��6� f�g;�8� � dXC�H!�G��Գq�I�w� �I�yO����o������5�{����/�e�����RT�MdGAME�Q�M�T�J)�fJ6�f�X�>�j�R��qN9�8���I��tN�������~���{�����w%�%�ة�caGH��a����`���lk�5��Rֵ�dY�AuX磗����MRߺ�2�OT��L��_*���3W�3D�1��1��~��˨��Z`��.](�gK����; -��eR(l�J�c��Ĉf�J�h4�YS��db���a��<|.}���n��$�y"��&���1H.!�Ǧ��aW�HK�\ة��q�������j�������e,�IfX��^��o# Ȧ�ș�������K��bo��(��"���Mh/�km��n��n�b�$|g�N��L���<f�4� ܁�^��3�j��k���}��i'�N��c����N�����rd@;�qt]ES��r���1�Ҕ�����6��kw�vɔ���s���#,i��O|�J:HSx���0�r�8�r���`�;H�1?Ƃ���S�2��r�j1�|'��N����*����K������0bK<x��������I3��@S���4����Ogy��q�γ'�1{�M���"�xN�ئ�ɰ�aG�I���;~��< ��a��N�3�}t]@����ƔX>0�>��>�F��KW��5z��s -���f�&��E"oU!�t�<m�;Ϟ�$G�Z�5�5�|7��0q��g�)��>CA_0�ܦ^�bo��,�`��1��������= �o��o��NSd���`䄼Q��2���h=b�}�l�e� -ԩ"f�2��*f�&�}͌���m&�/)ڛ�x7���$!��Por�>�����~�y���49�_̠p'�q��dX�r�w��/�/o1�� �ψ�V�5�6�c&��@?�p/��ٴ�DrVwI�#D�ңw��gض���F>x�;zf4.�4��2���N1�28U�C �� -��Ry������u�"~��N��o��1\u���G{�K�C����p�9x�H�S�p��'?Ŭ^���WqDja���:X�X�eGNp�.���d�G���@�����{tOư_��0�v�郯Px�ä́g~�`,���#�q��8H�cq�U#� 0�P����d�dp;��Mߑ���k�3u�<�s\�G�MN�ȥ#L��c��uHQp��I�O�L|�B) �Ez�8v�V=,�M��oZ��F��*�l�j7�>���"c�c��}�z��rqR��ş3���W�&��hV�' N*�L���Z�j9��T�X-0��^N� �s���I�tr���D�EV8�=������0�u�,���� ��x�`M���x8IP�`ej�Q��� -�d\���UZ�B�V��S��ՙ�QM�����%U���*�NU����>?�.����r�k�8O�~�ǔg�Ȍ�e��Ǝj3�3駖���|���#��'Fk��kM�$��~Y��Hu/�f@�V�S��A���Tn��ʬ���꒖Y���T4� ��Rt�{�^Or���m��َ��s[��z����[8�`�F�@��S��)�<C5Cfk�UIV��tU�Q��b�ٖ��v�� kT��F-�K��*�?�|�Z�Щ<�Q�rѭ>\�0;l���}��v�e3�S5 ��j[�ٍR�p_Uۇ��>BS��a�JcU☨b��*��%N�T�T�����w^�<�帼�l���r;�L���p�7z�t��g�粒����IKA�ޅ���W���r�N����Ξ*u��2��*r����MS�[� -���>Oy�i���l�Bey,W��:e�Y��ە�uP�^g��uU)��R��#��������6�7�n�k��]��_�ו�\�f�壬U�ᨢѣT8�Gc���3\y�S���,�-�JP�w�ҽ3�66_�>���[�d�f%�۪�~������?Q��]�_�T�~]�d�>�� �Փ��[j@+P�}�����,T8�N�c]���l_?-� UƸ-7Ui�fi�_�R���쟮yy�P����jҜ�͊ ޫ�!PL�E��=DOܥs���(��+>R#mi �@%���|-��\��� -���-�Qj`�RÔ4Y�"����DŇ�).4G��K5{|��'4**�M������S��'E��A�)2�'������୧�h��Q)btQ>_��̔l�!��ꪤP/� �S��P��� S;!J1a�OQTx�fM,Ԍ� -ENZ��6���������[P�F�D� ����P6����;�ɐ���0##8���G��Pi-u�j�u��16�:�hӤ������cژVm-}i���s�#�}}�����o�%��U�d4|��*c�#ԭ���n?�����0i �EU��U�pN?�E�*'�]�Q^ʈ�UZt�l�!J��TRL�c���d1�(!�H��j���e49gj���S��c�1�Z��O�W�1=D�:J^f6�9�C X�k�,�0Zʍ�����0�Rj���(�8SVc�,�śbe6'�d�������&T*�Ҩh�jEY7)ҺCs�("�7�=EX�nb�Q�M����X�kQ�X�qx��n [�p%%��%a����d�%�%Lk�b�f�$&+*i�"��5'�\� -KiRhJ�Bl�l;� �Yt]A)_*8���+����o���f +h �y���*i|���,I�2'��1�S�Ť�+��H[�"�>7Qa�� -M�UpZ��0K��+5+�E�u�!��w�5���E�@���P�ej�� -���� �(ۊ�L����6'ťRL�HE��לo�g�Ph�l��WмXfZ4;s��r�U,��E���\ӳ�kZv�|��'���f_�O�����|3���~39/���R�Q.�t,c2���e�e5��rQX��Br�4�K���(`����h�(��5izn�|�2�W�)y՚��L��/ib���@������wG^��hb�cm������p+ɹp.=�m�mɔ���נ<g,,����Q0^� -��S8]S�fkrQ����Tl�Wq�<��4��B�JklI�<J:5�d�F��B���St_�E�����}y2��,�9Ƞ��`�`GbՂ$�bɧ�)y??T��ȫ�9=W1E�+fj\E�<*#�^i��J�FU�hDU��W�˭z��V�kH�n=[uB������?kH��r�x�꾆�/�]E��`���;>�;v��է�Y�G�jt�0��s:/ ��[��\�C5�>V��[5�~�6ȥ�F�V�_C���˩���ԟ�S� 9��Qߺ��H�+ɻ.ߛ#���$�$؆)�,ؾ/H��/JÖ8iв�5�>\.v��OT?������d�R�3�v[>���h-�g.�8�1����n�{j���?�Ε��{�T�6ÎĖ^/M���0l��M���5,:k�^|^|.}G������7�\3^��al�;�����$:n���ZI�u��"�Gؙ��;f3� �X�4v����f��X�6��m`�8����G�|Z�x[���x���6��F-ږr�17m��QtaN[oj ܚ2�G�`���;�����7Ꮕ�w�F�m���m&�N��$��b!�-ı����x���x��H��Zt��~����It]U=��jj@��`G�d�y2-K�np](#K<g��h;ځv:���Y��a��Ep}������Koþ�D�s�<@-�ߧ�%-b�Ja�P�d�%�����-�8�u��T,~���|D� �K:��b�r|��c9<�b�:q�������&�~�dO0L'9��.��atJU��v:l3�Vf�:O`���u�O1O�=<Z� ���{M��s��B�1gK�қs���r���{\~�3�0��i�N��W~�Rj�; v4��j=���������h/��G�0��2=1f��t ��.�ߠ�]0o��:��M�Ϲ�oq ��c�Cv�F�����?�ۍ���|�XA��\�U_����|�U!��y�i�n��>�_���~�U���������c�C�e\�{:���<������w�����=�t�� 7�n\�L�����k*�S��TéupI�NVQ�q������ahO��S4����>������w1H5@��'yN'�`�1��,�R�d�O>��O9-�����q��8N� N3� 0����h�*q���J������C����/|]uUcaN���"��H>I�N>90�a�(�1�rD�p���&8߃������:rP���e:�C䱾�c��������E��H-G��7����"�)F�rɇQ����U����Ǯ�Z g��Jtq��3lj�6�u�m�v���1�C���ߎ}_��0=�k2,?X��'N -�9���A)�S��k�� -����-LF'W�.��Co3-2���VߠǨ�o<:��S���A�b/s�[���,Xpp,pR�d�K6�<�-�S�Q����-� -M��ѕv���eg}��!!1$���3$���%˹��.��˱�����\�%���5j�N<�1^�j3U[�X��ر�v��N����i��N���j���~���w���y�������^椲w\�8��������W��>�����y��#�I��ޑ�$���Hr�+�)V�b��WLqzˀX#bM�5#VX��N��sIO��ڱ��aE}8�#��\����ב&�$=����� ��ݦ;���%^�x%bUr*���5^�G�9����^n];ȑuA��M�u���˄ןd~�9����e&�UB�o2���b>�@�W� UGG���k�oR�S�O�o�� ܱ6����8�!�c-�F;X��bys��xY����-����|�0s��n;���E�wg*�.&�f<���%��ф7I|���Oٟt]��o�j~�V�]��ý:�������ߺ��mIّ�R\�6'�q(����z&��Ijc:����~&����f,�0�]���uû`0�;��L�o�g�}���k�n����XL��~�ݻ4���i4ZIѨ�����8�SR��ebfwӻK�Ju2�Z�xZ#ci� F��f(}���9�3�Зy=Y�6>I��%:L�&`������t��*��Ŀ��G���Z�QiYZ��yC$����ғ9���x���L�,��*�r3h��o����E�i���1����|��S���Gk����~@Kޯh�����/����?���P Lpb��M),�ek�4�w�62n�'����^#��|��E��ћSEwN=��>:r���і?Bk�4-��4[N�d9��z��El�KoQo��t�� ~�|?�x/�{�fl��Ҝ4�uB�F1�������3�)0�U`���N{a����-Z����=4ن���8LC�q�J��?J��yj����W�_T�?�����b=/�)�G -����:!�H��H�Yc�%�a3�n3�Z�OKQ��2��UxJh,i���I�cw��e�T����n\�KT:�O��5�οH�,�\�����~��ǭ��5��+�Z�6���wg�F��q���92�:rh,�PWj�]V)���r���U��*Ʃ����Z��u'eUqT]�^�����q���r��~V����+�Ǥ!��Z;�����sn��L��b�lj*�,��*��UCE�gu��}�� 9jg�P�>K��alu��Z�s,u��~_���k<m�;�|�&���ě��_�����~Y�&W$ U�qW'P]���&���\�km��K)uWa�k���Oq}E #Xf�4.S�x;����<K��g�z�H^�{�g�5�P�S�O��$c.�^�S�˪xe�eߪ�6RY�gC -���8�R�)��c���ꭣ�����.�}C����i^��|��-������`l���b�}F��+U �)���?�x���_���*�W��-ۦ4S���Kqs��4,-F -�y������Z���������d�/�8Ez�{Oc�JZ�������){ھ�����+�yN�qq��jЬ-n}���le���mk��G��#�c���;͘:1v9��r���t�b��#�g��=���`g�$�>ER�+$�����wH��D���U���/)��#��O��]lO3Ԉ�]+�}-�soƾX2�%��/ C����<v��k���n��I�&~p���Yve��=�=Ζ���z�-�o�u�#b�r������{P1wɎ��]';P)�Cl����2�"�=�����$w��C�h6;F�>j'v�Ŗ1��D� r��4Ɨ�?˺�K�{�ȱ�ź���O��N�r~P���Z����]-vYX G)3(�)� ��ӛ��#:��M�6��D�����%"�����3�`&,�[��8��8�d�B��ߕ>�rֿP���-{���>��N����;kB�s -f��9�:���p�.V� �ᄳ$��om��.����y�)I&1��� *�+b�*�Qq�{t��UΫt�}�'�8 ����*�,�/�ʊ|劆�[wHj��c�̣���0���{�;"d�?�c_APDE��K�"n���1&���c���%5�&mZ�$�Ukl��F����x�?����̝;w����@�k��6r�9`�$T#�ld�FZ�1�6G���MtG��{����D��;v8��WJn�lO(�I�J�C���B��k��J�f[�l��˶���B��6���maі��&Ĭ��0�ُ���+̓[2�\�I�y�(<�ϣ���p����vZ�^C�cC/D<6҈l"���u��B�i�D$[+ne��ză[���."���nq 1�/��ca�q�y�r��R?�f�`�����KF]��N��S��G�M~�!?���iڹ�9x�$\{"��,����f����ȳ��<vl|vl&����9��lC�:��Eo���%��15���1��q�����Iw�3�.yq����Bo�Ӛ�߹�<v~`���&�7qwus;�Yo���x�@@H>�*��tӰ���'&h�>ĎK��e���*���j�tm:��{2�x�!�=6S�KV�l����n����t�{@������5��J��ً�Mr�K��9�W���I��8~���K����7�d���-r��#��wJ`*KcJ�f&��=橻�S�U��1��br�R���՟��~�4t�ĹN�\�^]eƻL�>`���;L���i���������'Y�u���Q��3��a����pr�G�S�(����[츊W��.a��w��3��#aϒ4�i���֞d��L:֭_��,�w~��tן�u���y]�x"�d��T|)��bf�R��/����Y�v-���k�M+۸�H!eϱ�7��k$���9��'3}��0m�� ��`X1��# _�t;�a�Q�x;��N5�Z8K�,g�U� ����7�y��h�d�ݡo��s���d�=/���!'�ˠw'FG1 ?�Ԏ���l�Q9�j8�p��v�y ����ާM�����s"�O�#렯E��r{����cf��uX#�9�� 80�`L��4�0ra�A� g6�r8�p��4�~3����ƻ7ɔ�d��Xw�r��h=E��;�cgt�,�0SF�s!�}��eQ0Ʊ�i�� 'N��D6�P�N=�Bv�ŚmD�`g�ԓU��V�����I�{W�9�]�vrN�������U8!p���Ù�' J&�ND���JaUi1{RǷ�<��(��j2u�*��\N\��GOȿ��f�o�U���J��kӆ:��e�����%�Q����;jٗ��i�f�*�Ug�*͚Un�Ys�����Q��qQ�z�T��}[>D����x�+p�%W!�q�v%�A�Y/�����t�bso������� �g��*�I��k�*zMSy�B��)Si�j���Wq�*�I�/�S��1��Py���A�h��G�)g����U����� -jL�7@���Gu��iA_{U�sUU_U�`T�U���U٠�=h�JOV� )P������4ݺIS�7(g�neۼ�)6�isS�#��~P��g: � ��\uk�v�h��:����k�+U�ќa�*�k����*��S�M��G�j��,M����QE�U�)/-T��rM�_���mJs8�T����R����t�� �j?���Z�:ڐ����s9W_�m��S�(;��;o��h�}�r���3:^SF'+�!]�9��8C��ʔ:�F���F\w(���ƻ�Z n������S!�m6� mG��w�ϧ=��l}�#s���f8R��HMurT�wM�Lg�&;G(�%Vi.�JuMU�[�����>[��J�|Y�^��M��oj��9���@��C�x?ѡ��v�]�����k��>��\��|��j������6\��vJwwV���&z��� �QJ��x�%xgh��4���R�_�b��(��" [a����*,�w� -3<@�u�=�B��})���r4��b����5ӫ�ҽ�(��V�>�J�q�x__��i�_8�X��OR�!]Q��,RD`�+ԸR!F�\�2������.z� �c �����í�Ji��t��l��wi�=�lxQ�k%�+.�Ec�`PT`�"�bn���4�g+$�P��r��)0l��_�x���ߓo�'�ᅰ_�O��~�'����ª� �E��rx��R�跍fJ0�U\�Pń�Rd���C<꧐P���"e�WPx�"�d�,���E��'�I�ћ��.Ϙ3n�3���G���Ļ~ �2Ƥ�`�^��RQR�4.�~��L� P����-�py�* �( R�@��eX��Pv=�EDA%Q���Z��&�Z��$�i;��5�i�t��I�8�vc�I5�h��&�<,��~�{|��{R<�:�_sS����Yiњ����JLOQB�|M��U|�Iq�k�)ư^S��):�O���+*�5Ef��>R��&�R?���|�÷ë��^B�=O2�I�ط�7�����(�0A �HMϊմ�D��c���ٚ��H�1�Q�+��N9����W�9/*,�U��\A����³j?5� �:�K�&&�yp �xl���3�M?Z���@��(&g���NQt�tE-���E���3jb^�����_��v�+�PH�nSP��5��2����{ -��}�^'�_%l�2��p� ��H��s5���{w��&/���cQ���� -+�Մ�D�/����� -.^���R�1UkTI�K�P�+����-9��%ч�3}*�����fr�H�+`W�5s1g�N�=;�����ER$�1l��Ɨ(�,XAe�S���� -,���s���5�\��U����(ϊg�Q�/��Sr���������cy����o$�հk��na6}�M̀=v����XǠJW,�e�FX���2I�,1�N�G�\�W�Z�/��r�f�?Q�y���Լ�g��jn ���Sub�w=�*b.!�������� ;�L��"�W(�u�w��ܬ~r����+C�JsY9�V�j%+I��3mx+��ߵqK�a���!+7(�]���صp�p�����3a'�N��������e�ƣ>��ec�p���a�Ȱ�3h�r;��NC��Uv��Pf�i'?A�E;��莚a[�V�--f�6�N��D�Sk���_I�k$�Fɥ�KG^n�7t6����ku\Z+��䶑�6hkBԣ��:��0��E7���n�6��{&�X���>%�l�݊6��� ��N���>��������f�v�h��͢��݆0�����YD=v�/mc�+�K��� ��aO�=�.����6I�V�U�R�K�Z���.����ݣ�����c/�>�N��G���E ���C�4:���j9�*��{!�� ���9�m��u��d������\�y�:/��\� �;.$��Q�r�}0x���I 3��ћ�UK����;ig�]�E�l��;��N-��`}�8��.�.� .�'}�S\O���x���?���:H@�����8��.�όĝ;v(_���������s��%z�q^���0��.�o��s��y������-��I�E��X��K*�ދ�(����Dˌ&f�CN�O� t�i���"ƌ~��9M��q�FIBFk��{���̍�����C�� �|ح�!�����X=��|��{����{/�us��V�\�u�{8���x�Ѵ���;z��]�g^��l�U�y q�m��㫾�����@��B��Щ0���RuK`��)�Q�c9��X�����i#-�p����t$m/R�Ӕ��7�w��Q����w�o��̻ 3GI\�J.�(�x�u_��J��x�}�UQ��Lv8-p��t��Nڦ��9B&^�Wh�;��z��|"�kt�D��EG��7�x�x:�F.��~)�W褖���`�����sDZ�lf��� 59J.�@<D��������Cϣo�~��;[�M��7x�_)��EFK΄�JY������k 뛩x��F����A�q@Y�C�Q�}���.v��^��C5w�_d�!#��>�˛Co9��+C���(X�� '�li0��?�����0*a��Y���>vi�VvХ|����Xq+��A������}��������9r���~� � Xpb�$Ê�0��?F���L��G��.��j�����lb��\6S�fN�:��N~��sO���?���1���^ �c�H8qp���%�tV7C'��� N9��U����fj��_��ZV[C=VCYũYI=l���S`�WC�e��α�u��m�!�1�B�i2+�Ùg.�8F8�� -`�`�aY`����g������Ct� U��w����>�~�'OƠc�e�>�k�ma��˝�|Y)^8�)����LX)Ĕ�+�x -�¥T�]����Zո��ŵYUn�t�Q�G�̞'T����x^U��m�x�G�Ư#���b�w����Z|�<\ ��<����ָ�h�[�l�1��H�r�Y���LU{-��;OUÊU9̬ -�j���Tوu*��"�_����UpB��S~�����Q�A_�z~�X�ën;ײ6^3�Q#c~M������[�>�T7|�jFF���*�DU�'�" M�����.��3�?����7,��r,���.�.7산hxEH<ɨ�*c�ښ����њvm�iҚ����Imƶ��4����16��$�M�I?Ȧ�?���q�{�������^u���ѩ���j�Z���-j�VC죪�="O�I���D5�o���}�k���Y ��K������5` Wp_�l�F,Ѫ��R�mRgt��1�j���-�R-q�j�kTc�O� =�&�Q]���Iޥ��r�<�*� 9 /�a�(G������O��#�=�z�5��u�/����!���0u&Dȗ����4�$e�))O �E�O.�'ť��j�r�vɕ֧��A9�;T���3��t\��Ub���M������Y�,��>�v�rlJ'#�h���i7�Q�a��RcU��"OZ�jӲU�^ wz���NUf��ajR��Se�^�37�$���'�� --�T`yA��7П���&��(y��k;�&�I���U�6��ƌY����js���ir�3U�����"�e��t�[%�g�d�^� *��.kވ��\�3ʶ�����G��Rv�m=E R�a�l`%��5����J, ��u|w[�Ҳ\K�ʲST����EE9���U�W�|�WVk���{�S�^م�d)ܫ,�W�i{Z��2]���zOfۿt��g�ى����e�[�+u�V��ߵ|�X�y�*�.R�5JE��*�OW~A�� -�[X��Sٶ:�(jQVq�2K��T�E��e��*��-�ڟ���������V���Q�����nŇQ-�*����R�]��)�8\y�q�)1�Rb֊�e��d���TV-cY���W*��_��MJq+�qH�� %8�x��︪�ߕ��� -�����`��Ư�կy����nŬ�vvβ9��/UVE�̎$e8�2:V(͙�T�])�.%W�+XLp�*�u�bݻ�>�h�QEUO*��SE���ME�>�!fpT�Zr��Ӈg#�5ȁo1ؖ��T�~U!�p-P�;B��x%W�*�&S 5���)Vl�S1uEյ+ҳZឍZ�١��Z��6���B�z�h��o�R݇���������^�gK�������B�sAجj)��8�;W��K����$E5�hQxc��5�kIS�5�hAS@a��5�yHs�Rh��|R!�/�yI��n(�������o ��x��mķ9@�bP�Zw�m��>ƶ�(�-L��õ�=N}��̚���\��R��A��<��0~��ϭ��v��aMۋ料��fr_�w7���z�مw�س��RZ�v)�G/蚫9�K�Ҭ@��@P�� P�@-�3�i0 ���B@Z�( -\@��_��z}7��RF��s'y㝋��U2��N)�[Z� -�墽g! �b�����c��y��ij?�h�]������~��~n�~x��w���J��Vτ�F���ۍ7������R��x�(���� -�j�l��~/�F.ڍ����/� /� �$�A��G��c���E@�����u �og��L��x�����#%�����N�F��C�Xr6S�-,[��rG#z���ѓ!^�!�C$0����|7t1�/�w�ߞf�$z݈���K����H� �q�FKg� -�X����N�]�� qs��&�=\~{9�G8pF��;�CGHb��<��ٽl�{�$�\V/3����F�ux��m�۴NJ$��M��mA�]h7��,�����哾�/��A��q�r���ҍѓ1�;F-FId��F�Alӣoh���Y#ޕ�܆w怔D��CҼ�}�=��Q4\���x���kf!�:s� ����q�`6&��E�=��<d|Q����a��Ȼ������F8bX�;rg���.�G��ptz D�FO�c������Ǣ̌�b�$�I�Iz2I$8�Y1I�ϝ@gԉw=�\-�b�Q�<�@0��AߣA�9�N"F|��� ��M/�?��6�ij����b���b��(�FS�J癋�Ϫo7�R�Ț�;��9� -�3y�����KA8z��<z5�?w��W��%����q�K� -=���C�6�W�������p�W��D��X�>�u�W-d��u]�J� -��Y\�E�-�q �{�����\��~5�J��AQ����ކi?�+?#��A_��S��GZ�g�F��yW%��*]��.��ՁG��^R^=n�q���^!���я��h�i��y}���s��)��O)�g������-tC�6J���.º�Ø��7%��I��������c�>�8��>��4`�a���� ���lNs4�HB!a%M�D9�tm�4Z�j[���6i�VE��Iݥ-��i�6u[��R�e�mS폭�pL諟m�{>����>σs���p��2�5O�8I��'m.�K�N��&�wu�H���7��_���>�zU�D-�m6ªė�n�� Y��#؟!Uf!��X�x,�X��A -�"�.�WY� ����W��G�}� ���M�_ѯvR�;[)��y�b�S��]u������0�'82�0�0�a,�X����L�g���?.b��{/ϓY��9�����;���Α�E,n*N� -,�`4�p����Qލ�0�>cJ�h�Ga,�8LF���'u�_�⭓xp�{�:FVo��?�rZDh����o])�b��8U0������`���"�cp&9Q8�p`{U����a�kx�J�����I:�����_V�������εx�X�猜Q>�2����� �����3�����X#��� -��0����X�"��g�ُ}���_ 7�ż��9ڎ��ute�$��)'��'ٰ��V��N�v8>8�p�)°�ɈX{a��*�Npz.�ϰ[��N�'{Zo����G[1��Ε�y=��,Ɉ��qX�πWŰ*`Y�V>u���k7�aXXѭ�L��$^D��8�4�i�f �u�c��b>`��cp�k�e���(ZI����,&f:����uU�lD��U��th:�[S�}���Dܘ��g4�����e��64�|N�)W5���S�/���V`�íRp ����t��"˔�<�q��Ӟ<�����LE -4�X��$�&��5�ܤ��FS<I�+�k@�� �Ok c^��C��"k�ϩ'窺 /�g�#���ސ'�}��VI���g)q��O+�T�����Z���M��(�����,�pf�BY �vjwv��9]�3�)`ɟQwޜ���[����3�(�"w��2��V�O�b������<�~�!r��BbO�_��͢i>G�m�����+�����| 䕪?�R�|�z�)hUw�G�"�<�Au��^��dIm�G�b:-g�e5�_���-5�,��u�.��T����8-� �iӧ��-��ݬ�ߘ��1G��"u��-����^��r���2u��,���3j��/G��UO����,7d�|SVˏTk�=z���{p?7�/�\� ���4�i׃�0K��D���Ӝ�����+�Zn���Q͕mj��* �%���)��,�V��Z� �X����9Uپ�J�Ua{��۪�~���|\�s�^Z�iF�Q4����g->Z�NK�ܖT�Zr�.���L�j�j�U_���!�կZېj�"��s������Ϩ��Y��������[����d�@���)�^��=`L � �#/�X;�V�lKR�-S u��Օ�Z_��z����ݪl�VE�̎q�5���|H%��T켨"�*t~U��)���Sa��,18A�W�/���F�AZ���v�¸�K ��:�T�0��dTe�Y��52;�29[U��UIk����*j������]O(������qE��W�����]�����.�������'a�a�3t�N|do��f��i�QUk��۲dr��eR��JE�:�;��ީ����a�t�(˳�LϺ2��潦]�/+��]�x~�T�[���w�%�։��(�qx���0�����V��������3AFo� -����+�W���Zew9���VFw�Һ��3���%��)�Z���翥X����3������)�`�>|��=3�8� �n���í�`��H&�T�#Co�2����W��>�R�,J�WR_�����*��!.8�P� �K�&b�%�D�u��_���#p�`���`7��y%myY�TĨbJ� JJW|(Wq\ 1!�:D҆p$ċ�^6�A6̌��3,���a�S�n%L����u�K���Gy5����I/5���K�>fع�R��F3N1��Lp�O�Xu����� ^�`ΌL�}��5�C�P)'������lj�<>O� �krc�� -�|@*fD��Ox�S4͜;C�{�a�����\Q�;JREqf��͎"�cv���%E�w�t�{~���A|���9@���� �veH*a\��ϸ�Hh)�l^�fA�.��b���˾r;��pn��,~q�����qDN,ұ�gX��*ゥ�m -f�%���F.�v�ոP�?��(���@���.�.��˱ˡ �r��WP)���rT�^��XMLĦ�4m5��db�D i��I3��d&m2j2�L�1I��N�������}~��<����Ú#���fɛQ��{���bS�GW��?6��n��GNz�d�,�ý,�w%"=�5�����&��D�0��Z��o�l�����@~>���D���N4v�F��M<����c~lÏ����~�� ��Ģ� �?�����;t��覷]�R^�ÝK���\ة���#Ys���\op7��*M�Dӹ�v7���^�c?����:Hm$'Cz�B�� ]� ]� {����M-a��y]{&l'��Rk�<6����h|���ҺO4ć=hL��Q���c?~Jm�����0��k>*A���3v1u���6��=��Mm���}�'F�{�B��f�=������܃ur�ܜ�;�et��z���ac�nC�#�yA�����y����c`O����h��0���ό&�=��@�#��*�Q1FM+� �^hGٷWٳW��+�[W�\W���,�2�/?��5�G��I̭�9�����ic�n�I�u���78���&�E�]C�;#��'t��|H<n�Wn�����VDq}DR?z�iO?�' -���4�����y�f̻��dV�;L�3-|ʤ�1Ňl�l��L;�Q�d*y��Ŕ��r���2�^�Y\��?�{�?Ɣ1�R�x3�>�}Ü�7f�/��n1�|�l��f�*�1`Jy?�����q?^g����~��)� -�e��%y�$�%q�ys -'�}�L16�_�������3��̷c�6��=3�U��)f.��R-�X�&��a��h��AitQ"�06S2;)�������9�z������x6�?��tqzÔ.�_�%/��1�1S�%'���N���د�L�`}�f+at���n������l<��<9D� R����<߭�0�b����x�{� -&Nf֑#�F�J�_���دg��a,���F�׳�62�����z�o���]X� i�n����q�E�ۉ21����寧ί��"�0�YQ��_J�˱_�4[�j�h����k�wk9���r�H����>*��J���;�v>a5����G�7� �0,X��}'kȁQ�q]�F5�y숅p�B�a���˥�O��K�Dn-U�F����Nm��v|p���q6�%��X��G�*�Fk��r.��(� -+�S�u�щk�K�{;�V�_���a}�m�ʚ��&vy#���n|�8b���N�zp_O}Ģ��b��^�p���ɀ���N1�2*�V�d1��0Vb���݈W��E�S����2w o?fw}��?n���I>~E���Nb�F~��`Ea5��5���dM�p - ��*'5���4�i�~����Q�����x;�*�G��@��k��G�Z���NԎ�ܭ����/^4,�dX���`屦BX��Ͼ��V˛"Q��<�Q��v�yj�'��kD%^�T�y]E�����:���?��_�u��������i9�N���X�g�g�|��y�k�g�j��U�U��Ie��Q��B��Y�R_2�Y����P3�+�i��*w� ��A�Ư��������N+�F�J���E<[�O�~>��Z�U�ƪ�/Q~�Mvj��J�T2�TEU��@ӖiFp��B6(7t�\��� -Vf�i9�F���=Г�{X�� r�5��֣1j�]���<��ş�`UN�S��`͙�Ҡx�4+8U�!Y�����b�U('�N��eE�*#�K��J3P����ͯ+�t}!��y������w-zCkG�|�21��|�oi�$��0"\3#�ʏ�*7�.W�S�Q9ʌ.T�i��͵J�Y���9�:e��,[�>%Y�V��yY-�d�\Gwd���!b�=t�%^���xQ<�GU���Khً�g&�7�)��ls�2c��IRZl�R㲔7C��Ke�T�f]���f%$��5��'�U���bl'e�]����LI�eN���`k\��VxKi��i{jPb�U!���'��0��-�%@i�09�&٭V%'�eKt*1)W �V[���goT����أ(Ǔ�H���S^S��=tKᎻ�K6EKkீ��e�M�D���l�g�T����&+99X��Q���d��?=M��bR -eJ-Wtj�"Ӗ)"}�����ܭ`�9����*��.�\��]��A9h��c!-W���3�|�BN�UOf�ML��%-P�i�2��Ȕ��(�C��L�g�PX�l�d�*8�AӲ��ݣ)��w����u����m�f����5�M�W�_ -{>�J8%�R�A(�=��ؚ���)S�dE�B�V�+^!9� -�qjjn�r��W��y���"��p]�AQ�g?��!FD�Q�"��uaaa�Y!VЈUT`4E�ڑ��ƨ��$M ֻ�iL�5u�8Z�f��fL;1�ԉi�L?�����v���������\��9�~Y~$S� -�`1�pCX�T,)��m�}�� ����s.z��Y -��A�f�����#[b+B]9E�U�+Қ�pk��p:�Z���*��(��]�}U��n��q#����Ip ܦ؟k=��2b]�>�<f��3�s��,E{�K��##�f�T�}�L�d�I�Cv̷�������;Mw�(�Tt��&��'OW��A���-��F�Poȁ{&kyJ��T�j�A2�1��h�z3좠.��x�e�`s�n�|�F��h@S4 \��ۣ��yd�����G��� -'e��p��ΨF��J��L Ǵ�-Մ�if�q���X-��M�I����L7�pw�dt�bē� �=�o&-7x�S���?���:�\ã��|�Ýw��:��'��.�V.ն� .�vr�$��v�\K ���<����ԣ�ll �h�������_�����n��g('��<�wz���\�^$�-�I-�7��e\v]䢛Km���K�t=�����J�_�z� �'V"T�D����j5v�Yg-�R�b����E�����%/ie��>�!XMM�p��ŏux}x��d�!��0ڏ��_�G�O�=�� r=}����l��I�jA�pgD�Ѥ1��by�s��,����?�c?6��0�����ʠo%�[��p;`>���a6�͈���M�����T�m�;�/��W�}ߐ�l��3���%Ȼr ���������Nzc���S�4����[�(�� ~{Q�|�5pÝw� -i1s����y� ����MY�E�9c{�+�鑽��~��e�*C�*C5AQ'��>��%�&�����3'�r�O�3��!���3=��;f,�<���/�^1��X���gƢ>���\�Ǚ�c�Q��8��C<�yu�����<����:��6c��i�hgD��B� ��G���+�H-�TzG~�t��9��t���ȼ^��? �����x��}S���R�KΓ�;|����X'��8V|b��<�R���5 �#d�t|Ȳx{����;����4�-|sJK�C��C�<bw�z���W��o��l��ج�r@�fw��r�a�� -�Ɛ\A]FW]�A/P�s��ߠ��#Q����DS��<(��F������z����T�r�1��,�=:�*j�2��~\@a�Cq�E}�Ai���ƏS��+�4� ��Mss�����7-㡤<���� ��ɗ�HbJ@��"�ypE%v,�K�N��N��1�#������x?0Ho1�o�P��L����܇�{�g�A��y7�V�;8����<8�xb�IA�e���QE�_��F,��'��pt��G/�P����6�u���KkOjQ�Q�Q} <x��~ '�8�<��� :�-r�}�_3���~!�Q��Uطc��9Z�h���.8z���#d������7��I>��Q�)x��{|�γ��§=�c��\���}�����k6q��_�}+�k����k��j����w�х�Uda���7�v#Oo ���_�s����~�����q��5����0��b��~*�3�#3���a��5p8��\�4s���Ӂ���^M6��0�ū]d��\w����t��[ࡇ����8�n\�#�ifdPS�Oipdau���(�:��?����fx��X��'�ude����'/���t�I�����n�/��?��3�����W�zK��H�#�lb���Rx*�1�xԩEp��oV�� t�3t�8N0QG������<��lW���「�W��oEXA.���.M'�dxf O><��J&�.',�k1Ky����1E�t�(���ӃL����|��nvW���Z���x��,������1�a=��f��W>\��*�� -��i��y��);����V�:�e�}[̨��ʢOTN.v�{W�>��V�.��_�|�`�/�h�_*\ĖWq�e!��9��F��TЭ��^���TbQq�.�: s�)]Q���>�]��kj#|k"���V�Š��YY�Y�B���!G@���� ����LU�f��T���Y��*��4�Aš�* -�9�Ws#��9���ݚ5����ʎ���ȿ(+ꁯ[�u���hV`V�VV�&�b-s����l�����54Va�*KWix�J"�UQ�y�*����).�OmU.���*{�����(#v�f�N*=���b��{�ƨ�f�\���<ֿ�/�J��5�]Ǟ�?�)�U��ST45N��T͝���i�5'�Py�˔kSv\���[��@'�Vz�Ғ�+%i���(1�]%$]U|�'�>�+� 1��c���ō�`N֟^����J���q&�EhN\�f�'*'~���������b�L�*=٩��f��v(9m���.����;�$ ��PDRBC !��!�@@����Q*�ŖY�=�2+ZW[[����ֶ���j�s]�N�^o�9�k�c�v�m���v��K~�����@��}�������aT2�$�x@Ҍ'$����~%Ɇ���gن\{���a�"��b�-�L���3��}�͜+��bҧ�Q�CV�d�k��-��2����4,�vY�}���G$ټUZ��|�3r��eњߖ$�Ǣ;-[���^|m���u�yP�@1p�X���1,Պޔ*��t��6ʒ�\I7;%��ԜJI�m����o]'Z�I�MH�m�̵�ۋ��͈�z]������_[��`�F�X˽� �mf~7 k��ʺ�DI�.�4�I�eI��,��2?�H�v��s�J��E�$>P✛e��=��Tvr;���y��&b0@����O�p��"�6`F�H&+r�]$�/w;�r�3M���2�`��-�J��%W��q�˻ �,ͅ,��"��"��B��B�B4������>r� -�&xB���.��\��t˂[�tZ\ �ȭ�y�I�P�"qJ�z0��xѻ^v�v��C Ⱄ)\ʍX�T,9 ����d�X���W\ep'���[����킟��nd�֫��2���aVNS���D�0��!��"�����0�H -p;X����]��%������P܀� � -Lp�1��{�%𗣁�詊��𪢨�U5��&�ռ��b6�ޭ���&�AS��PBl��j������ �Z���W�)�y��L��K �RpMK���!�[-å��^ƀk��i�0N�q(̋atf�|�;�5�V -#, �6���҃�+�^���VB�A>ȁ�v��J�)�x�˥�-Фa��.��V�"BrۈEF#��6���D��l�^�@��TV�k|!�|�C����&�������D4��Zl�6b��d;����Zr��P�&������Gw���ܐk��.zc��ݐVx�s%9.� �f��5��s&��ϻJb�ҕ�QYrD���.��ա?HN�(�!�kg�w���XϦ4@M��A�#�O����y.����Hc>'����PyN�y�u�Q�.ܓajt�s�r�1�d��������qb�i9��H>6����X/����� ���?���m������xa����G�آ�,\c`���(K����m�N�S�8�jc ��`?F]��z'��3,����0A)���m�;����!�wL�ڢ.=��%�6��[�qp��L�/�9�Q���?ES&�Ob��&ya5s�%���p����u<��W�;�.�[T�=���(�A�,��0��r�#p��^Oq�s����y��<?��<C�`�4���sR ��:�s=~/ؠ������O�B�������7*����G�"�8��Xf����pWp�M��4NMO���q�o#1O��G�8+bk����1���7xh�@��rQ��5����k�kIn0��@�~�0����ǯS�PD�ۇҒo!A�-�r�����C�|�6�%��-��OѤ��IߣA���5����K���zM�&ɛ&i��k^AS\�78����h�Y9�πo��_�/�4�I�|������@�q�'W��g�W8�e��4�u�%��+��e�q�s���x�{�:CO��'x���/�i�/��t��9v���_�M�>�*s�J%�Y�8��x��c)�����X G;^v�S<}��� �)���>J��{�Ӽ����r�C�g)�Y�i����QS{m�cbq=s�y --s���@G�#G����aT�!T�Qd&P��9�4�^���4�n����v���.����G��pr{�k��N�%�XL�u�DcN��J���/���A|�%ڍp,�)�j��9��Gh[�� ����b�����5JՍ¯��F���?���^b�G��^��,l���n�{�:���_C���h���Up���~F�we�' -�Dj=����:��w0ū��S��yLߋ��l/��2�o���v���0��Q G�����O+�D�����'�Jt�O�>͵s�o����k0-G�1�CMʈ����� ��^t��9p��p�ၣ����'D����OY�k����4����?B��'s?7�?�ҟ���C�#���k��%=�g�����e#9��N�]��Sk<!:��l�"*먖!>��wI��%�5t[ &�$��9PF��?卭\]X��#_����QOQ����a8"t�Z*w�O���P�'��Kd����Z��f%��*����kh�x�&���x��d�+?�p�����+O=���T�RJ>�T��J.&�E�D!�������ܫ�@����p�FXϖ�F֣e����!z���TQ��h��>�Q.� -���*અ����� 7,�T��<����QV���&%p��~�s�,窫5��ը����8��m>|ip�����v|N)�/'O�sZ����ŵ�U�Nr�7�%~B� {Ŕ����2�m����;���qb�ljs:�NH�J҆WI途� -ʠ�:ֆ�Ѯl� -�h�JK��Swh������ -i��M�*��vx�7~��Ol�����=ߒ���~ o�'�|>��M�:�N,�)���� :@+���E왑����L�S��>ٮ�d�jR| -�T�R0�A�� -dt�,�O��a�X��k�����\���>����*ʾ&��� -���%�@�W�?���ރ,�3Y]"��g�����ScIUuf��2��,R ˭�,���J�Z��ˎʛ�)wn�\��r,WQ��춧e�P~�����ue�n)�������)>/���3��� ��:$YTbWE~�����ϵ�$�@�<�<�>Mϯ���V[DŅ1��se+RA�2�9�*DZ]V�s�r��L�e8�W��cp_ۈ��z ����˚ҁ�hq�?�u ij -P�M��kO�۞!�}���v�UX\&�#�g��\m�暣�郲�+ӳZ鞭J��S��������a���o��i31ϓF�_7�A�kԃjP��2��.7p�"W�l.��۔��O����,�:e���Q2Ki%�R:�d�*ɿ 0 �P?������UzW��Jb<�H���XZ����Z^+A9(�8yo���d�d�R����b��=J��+��ZI��W�,+� -�zS'�r�ADh7u���:�m)�}{9?�#�j� ��g^�E|.`��)KVV Ci�%)� T��+�'���!�K�#��\�X�AK�p3T#N����w��+�|1?����[9f� ʀ�y�(c�*��U��+���PR ����j��!@u���Z`,�,N ,a��0S8̍�vl8���PwO��t�~8:�h���cB �{�+/p�0(��ZXY��h��J���AA5�E���9J>�h�(�S�塙[0�Re2G��TK�鞖�?<���O-��L'�E�� -k���Y!��,Q�������h%�8�N���Q�/��G�%.FMĸ b�J1�vv�6��6�ֻZ�||� O>��AP�g>�|Rl��tVǤ��@��A,f1D�p�$��X���!=�V=�|=���r���m݅r���ϴ������I�2�k�Ց|.�gk3�o5x���K,q��r��q�����)�� u1H,���?d��6@�0���wQO�X���h�n�Z ]#���Z���>���J��v����Kܸ�9ϼ�%'�c!���Fc��Qc82��c;�eN,B)-B5�П�S�7�oGc�� ���=�Ҏ���:��đ���S'!6����,ǎ���89Y�0y� ?N,�;�U��|<���P+YZWP���n��H[�A%�>��Hcv����k��Q�o�)v�1n�/C��Ŏ �cvl�O�P�[(���b3�4�����?��:qU��x&����p;����!��%&�����5%x�o�)¶���D;�s����^r������gH�nT��-�W���η#ϭ�����.����1��U��O�\�L�m��]J,{�X -�KS�����C��e��e�0��6I=�S���(�Ib��iu0N#����{�;n�OBX�rհhhJh�V,Ub��Sb!1)e>��XH5��=s;�1����o��S8y���F��c�G4suH�K1���V�>o1}������u|G����u\�E�u@�u\�\ ��ט�?�_����� -�uq�ޝT#y1�Zgrs��$�z����&�Ip�o�F���J�� -��%�kQ7���q�9���w�m�-��Er?�ǖ�)���|�����o�F � ����x�q�&� �� �+���?�`J�Q�W��)�K�;�"Iz���3����ngˈ��%`��k�W�;�#��������~��v�5��#t�u�����.v\w�z.�8h��ة��8g�� ��u -���C�@���s��CB��T�Ii��)߸��ܷ7���g^&��#�f���v��z?��?ð>�'�c^cXgz���6����=�xE�(���줄��a,9AI]�����4N������ -�=�@]�O��N�������#�7a|h&~�Dx6���[l`����7��7h����~�j���g��.,���;�����p?��O����>a���b���AY�u!g���Ts~������h�9d���w�4���h��X���A{���K��SXv�(�'c�q���w��~�/Ϛ-�Ki���s����_�� ���V�� �]p̅c>Cp�q� -F�"���<�X���1�u o�/@\K�Ys�L���h˧̑��X�g�}����O���bt����L8b��.�Ďر��e)u���c�0��#di�NX@CQuCt����#`�97*q�ʸS*g[9ۆN��#@G�ਃc-tG;���\��RG����P���O�QEs�G/��������Ę7���&7��ĵ��X���+��� p�V�S�xZ��<���S�CT�b��j6���� �1*�����e�t}��O�6i�I��h��I�&mB�^���6�R(��r�r9�9��ˉ(d��s8E��c�TƆ��M�s��]�o����<��|�ߓ�������~ޟ��i�˴P���nɌ���SjD+�@OK��b�T�;07~,N8�TQ%u`5�� -�XΟD���d�J${6�[�֝��RK}��j�7i1P�ϓ̨V�tSp2rMɧ1�gu�F���?>��*##�`%��z���gwQASy:�.�JP��t�8����ZA<*�N*��hTc�-�8"���X��q�d�H��O86p��`���(X ���i�U+8��?Eb���2*�����;LG���fz4j9��D�K�)#��M�^�<�U����l���<XE`E���U V���'�� S3��E|k-7�F6�#��`����7�u�$�pz��$lC�7c#�և*�%�S�Q/�z��t��`��[XA�J��8������B���0]�ƅ�6��|�Vq�n�<�aq����C�Q�g�� f;�x��)V� �uϢH�>�d$l6K��*E�< =Rh�K�)$>SL��*�d5�;�M\� �g�.v�"���m�Xl�$�vX̶�b�yNL���7$.|�r~x);�gM��*�r� �y�;lz)�1J�%K���-Nɷ��e ��Z&��J��(6{J��.�v�J�s��\����,z�ѹ"ND��=+�\:����;���h`EH"��X�}�煘/O'n�Q\v���fKnn��n�����lW\��k��n�g��=��R�>�|ta��Gg�>�x��s�f��d���jF��y��"X�g~��}��zq��bw�$'?K,n�d�]b���[,_LtI4��B���^@��nDw��8�sv�B�u�Y3r��S�5"ǫ��q�a��07��<zqxb�%�gS�U�~����{Mz-�~dq)��C��7�R���!&dX���z�x<G�� \��X]#�-� -YݼX�O'yذ�X�1�E$�B8VA�ˑ0��[��VJw)cA+c��2}�h�S��[�J.�<r��;�h�WE| 2��x��x'syoA"�Bz�E(�R��R81)lj�b -�/N<�h�8�C�Ο`�I0�(��A��b�@���l�� ��n��dq���q�<�⦩DD��"�4�8�c8\T�Lg��$�ģZ-��D �U yYCV3��QII��� }H���x~6���88�Y�(D07��Rzb���4I�P͓����z[��p��5�&v�&��&r��I�����b��B4�T��;��H0��~V��x_�yy��9iN�_%�isa���F}�����V�")Mq��H1mR,�)r���lc:��XF��:-�rA&�c8#����V��b�(�5�.�1�z��c�e`3X�G1�$���� �;����t�"],��`,��X�ҧ:P��/K'xͬ��`%�hE&��܍϶�G\�ۚ䙡ީ�lZ`�L�S��T���}8���q�i|i�h*9�C^N�>'�+'=/)��m_�XT�6�J+�����8"&�9Y�J��j�c���=�(��$�|�p`��#���~T�\���g��k�aUX��E�.�ہ��6������fhbc�&|(;�kF�-!O�s����b���Z��pd�h�x,'�v���"מ��z�%�u�~��y�����0�5<%vI/��a+41��8ǧ��Fje`1�J�77"Qf7�37� ���툸��������2s���L w�&25,5��kXJm¶HF����f7e/|�w���'��ȋ=8�ն�����y�4�N�������j>h�j�]�am��"B�ee�,Ej9ًQ��%�����+r�{<H��O�9F��t/�ݍ�?�H-�ۅ\/g���|^���p�k��ְH'Oae�,hwixG��0RMr;��!.�2���؟��f��"/N�;%A~���u*�Vj؛5^wk�/��"�4,��2��8�����(��؋�y�5z����W)�!���� -j�%�ٽ��`4��-֜a�W9�.��[P��C�ҲXyE|xeU���nDCcSs˨�m�1�c;�wM�4y�5S��N�1s�����,���k�.[>�r��5\���>|��M7lm��7��y筻v��{۾�>���w|�S����=��=r��c�?��C����_<~���O~��WN?�䙧�~������7�������}�{�/\��_}�Go��?��/~��_��w����������|�����}�������Y�e>ە��x�Sn)�#8U�'����^����o������ `�b�GQ�� ���a��a�"b-L�WTl��-���(6n���*>>��F�T�ܭ8��|V��9hyD��"��0�UE�3���C����=�������`�NJ�7G���?(���h��[o�X�N�1K�˓� -�3|�����ew'����(�/�]��K��N�7�i���-Аc� ����v��t������'�+��4/b.K�4/�2�[��4kN���E��,[�jͺ��oڼ���f��bۺ���$$$����IH<���vc<p٠cBB�!�@��i���m�4�-kӵk!I��NZ;��}�c�o�����>vl�v�~�u�����jU� � ��������X����?�FƮ��0OZ��s��Z�L�3���� wFܞ��s�Ia��vm��܂�o߹sf�����||�����������>�Ow���/���h�<�*���`�:9_��ݘ-M�g4��(9�*�M�,��h�)��1�'R�%�N�����k�����k�� �có��=��'�g�7k�7j%��������0H`� ����h�Hr�\N�[�� �����=�徾�ʹ�Fm�f�dq`�� �A�@ � -S,��|���|�m�� ��ߜ����̉���{W�`�����P0p``!�&#�G�H����?4,�a�J�F�d�6h�)Y�:0�`�Y��9���?�0�#��t>�t��* ��4���4����gώVkC�4Ն�`�yO�͠YvAB��d��M�UdI?�̗�|�Q��U��s�V?=Z�Q��T����8��R$��L ����,�d�JP��!���"s3����^�=�_m�+׆'���%��#�c�M�/�a�L� �bt���|���� �ſ ��ߞ����Z?|��8sͨ^��� ���aJtE �EIx'16���P6DLjN[�~���Ш��<�73�#��Y�^�*���kV������d`Ip�8_��T� �L4�i��ϐy�m�6{�[3�ÿ5�G.��je�*��Ѧ�ye:��]$��Q\��1M8,��"�N[R�4�o!���o������\V��'��5k�]�>�˨V4�Lc� -I.<(z"~�� -��[�㝶�����C������A�?6(�/��*�mT�3[�x���� L�Qʼn�dO�#��.)�;�p��gv��}�8w�j���ž��ś��ɚ-O���-�n���i�Gs�.Ս:T_`Z mJ��ʳ/"Fs�����IK��2�gO�.��j��De�.߰�%��kL�\ƴ�Qtx��n�U�z�t�gѱ�ӛ?E����$/�Ů����|�`v�rt��Xs�䯍[��<e��lk��,����i�]7���Ӕ��e����k�|���{={������w.�rl\�Z�b^�X�1͛Ǯ�M]�֜6�5�� ��H��q�dv#�d�"[��o�'_�%����.s�L-v�2GZ�Y�k6��U��L �"X�G�E,�ԃ�����õ]��5t ]C��d���<6-�3�����q�ŢE<�C���e����<��c`�"�`�T�^��1\Ø�Ł�C���h%� �R�H1�����"���B���>6�n����;�c ��7�'��NY=U�(���czKiI<���%(��<���� ����#0���0�,��Z<0�Vo���(O�H� ��2J'�t� $�,� -� K�ۃ��6�� ��Gabr���g9���:0���tM)T���(%I��b6V��a�h���]� -T0(g�\. /� M�����2�~�0���]��"����NQL<����I�:6FL-i|�)�A���p�QyoBd�,W���S$���$��#M�d�=2�+Ǿ -��v��� ʄ���5���%gX�]�$�"�/�H��s4�n/ ������3ۆ�骩��,M�l/i6�,9q��$h��̥���$Ne�Ő��c���6<���xq۰&�?�"�ZƘy�4i���%���q��&s�H�>���i�� -�|�@�����-0��=O��aM�|ahE�>W�z�\�8eՆr�#L��h�e -qΟ���t� gL<�g�m�Q�y䖰cxjU?�{U;�oU\ԯ��j�fͰ�x0d;|�N<����Ex_"ġ�����l$�a�mw��!��U���b�e�����`�:0Z��LI���hS~B�����ݑ���/�>�p�����!���hG��d�>�T<��V��+�F�I���X���듾�fEc�tW�&;C���$�P�%b����]����Kp��&8u�p�aBI(3��p���pdZ_�R290�i'��Ǝ�6���?%[�}hڕv��V��jW���"Y~ۊ���mڦ���t�(8���f��;|�k�`9�tl�x���3��e�֤P�X�E��PwΈ3�ڳ i������"fG҄V(T�C���9��>,.�+_}mz��Ye��?Y����Zs�4 �U�΄ʛ0"g��LX�� �b$�!(K����~����`�SՋ��.\=����Tt����)OX�A�^0YтE��̡�9G�I��a�)'���߁J�����_��������[�Z�gvX�Li�Ty҂����d����b. VS� -8�/��.o��L�E`���8��S+M���w�)��}���}�Z��B�����ˬFc��M�F��j6LN�Ɖ -b�VP��L��ˤ���ř������6���|������瑇47�:&��?�:�5�2�3\��Z���Y6 .Y����E�N�"1t���'���Y �~�{���W]�6�N>iy�k���ˎsc�����ݗ��7����l�����D{����k�t���yk�yk;t{�)���x��plWl{���yz7�we-��Sh5K^�n�c2ͱ8K��4eê61��d��ek�q��%HRO7�(�L�=7<7<7<7�?s�)��L��1��_����S�C��q7�wu-����ڥ�N��S �a�h�&���N�I�Q"8����E4��!�[�`I;����7�slg� v6ըf�Q !�~>`4��,���X�,3���p:�$�*�B�<��d$��!����� ���ݤjH��lW��zU��H-�������2� ULT8�*�(��d\y�娐/K -�1D�a�Uo{�g��G�6��+���Z\5�'��Q�e&l�Na[ُ9�^�ͻ.���3t($Q|8��*q�쪷=���������6��Fk �v�W =83�S�,�0� �9��x<!� G��D,l�>L⼨쮷}�g�aG�ʐ�Ѹ�Լ��A��zp�7c�d/�06!�� T���� �Q&�SB�c�87.��m�}f�t`�yj���%5�ŔjU�`���($1w6A���3"�9>KAw<���K��NR�����}CG�vR5$�GjfQV ���T����Qx*(Ƙ���p@��b �+f蔏�K\�B ��x����ӯ ���=qT��h�sX��ӊ�s"O�8�F�x ��p,���+��L���=�I������Ѹ��Q ������̸~�0 �sf<��LJ���ф=P\�c��8�#'�Iw�[o�[_��Zք��{���R~D7]�[�93!�>�z��$���p��8����=��b��|��o�4�RMG6�S�R{��a˚��нL��\ep���7��k -�,b��1'^6I�i� R<"�h��~,���ۿ�?90l�����ӛbU�kp��=T���2�'��xte��U��E����H�|h2� -�I������%��H�!]}aCn}eS�~���AS-�^ou��pv�QR ���e̔7 9\ -��2�R�"R�M9=��y 1�SB�=�~|m���+/�+�/o���L��3��۹��q�2l�:$�7`ޜ���X(: �I�J� -Π2�Dd��\�E�f)Rw_xO�c ��ʷ�[O>H_��|�ݷrs-]��[|e�)MB����捘C�M2��BZ3(�� ;���4�0)ڨ���3����Dp_i��j���B��s�gSK����=�HEk�ݝ�ْv V[��byf��8��`#�Y�͔��Y'�Ϻ�������j��7�宝(�4�./��K��w3��;쫎�ي�l/O��zh�#E3@���[�+j��!o�ty�]w������P�^��L������%��� -�w�pk���}���QtvLOku���`�2���P��2lѕ��d 'J6H[��z�H|XQ��,�م?��7�6�����������t����H� �!�v�b� ހ��M�-�vaŶ!m�Ms�9�ˎ��9����=�����G��v��N�4�}��.羛�}�}�裟��3��?Y�{>3��4 }�ǃ=ҵ�Ƀ�a�}���hL�N�e�SK���E�?��˨`����ڽ��R�@�|�T���y�5���k����C���+��WE�ejs�ubc�cl}�s� �k䱨{���g葬��CE��C�`M30���Z6�0��>@W>=�۬;C��{Qσ���2��f�kǟw^9��������w���&�{w�{vD ��`c������si��D9��}���x6��,G��g.}_���G s�>S��Q�鷃L��{��������ȅ[��Ŏ��O�_/���\n{.��z�����K�1�9>0�������'���?�6�N�]Y�6�Ɂ -)< d��� -\�#�%�*."���6�K*RR�4E�_S�EТ4����iq��k�k�k���%����iP��K� )�\.1������ޫ�O| �cWW��}��}�N�&����>���q�|Q��ʨA]B�HQmETn݂2����ج,���F�y����X������5�yG����HӺsj�� -Vm� -)�.�b�@%=�QB;�X�sj�aN�g�IcF�6Ҳ�!�>�\b4��xx�����X���f�������58���L�D��e����כ�sZ+>�xL3p��C)s\�"��ό_�ø������_w�(��;����yF�ֽ�ኗ5�E��T�hS�J$�,:�n4��S���c�k�TJX#��ůH݊,�%F��+É�B�a�5Y���v����ኟ5�D�e(_t*��M��'��,�� -8e��=���vDa����[�69�Y�K�̬�}��5�y�a'p�i=05\ ���P���e�\]tA�9;��P�9G���q�uQwPC;<p�r@i�U�5p�����;�o�vp�q=89T MV����D^���/��uk�y���R�3e�z#X���ƜM���i¢���o�����+�k�����(;��XQ����J?��9���vZ<q�7�7��N,�jV�&EH��%F��XX��B�~���A����� - 7��Ɔ�"��U�5�E�-E�cJ��X2!�3�'�7Vc�e��V�.e��Υ�>��՟���vC}5[�O#�w�b�S� �h9-��� 2��YZC�a���#bs�f2�6��6�!i�t#���#ְ�~n?�½՛�������5zt�AzB�\���28�V�4��B��2zm*h�FG�c i�ޔ$u���K�$8����{�6�N��gс�'�`�Zrh�~~TP�ヹ���Tx8��ި�nc.s��Ńn�xtڡ%J�.1��2z?>��qv/��ێv}�A��=J���f����s|y��7#�| ��������ɩ���pإ��vđ�`*e��g k�]<�i8�����?�'��V�t�*��*�����ie4'E��LAF2������p��p�cWG�6ȗ� ��r���+8�_<�m<�k�=K����L��ٛ��r�X�xGc��@Ը�r�~���ڈ%P�c8n&!�N)���&- -��c��5��|j+�|f�n{�I��7�3�3�����������\ legAd:, ahJ��)�JM�$E�nB��`,m��xa�{�O��I7���h��\��gK��V�[#+]��q�{V��Έ!��"XN�j�*-���PF�)��,M�� ��}�8�0p�1~8q��.��Mo=J��`~��'镖�h�V���]���rbV��f@-�`8�D��Z��iPYN��fq�$kщ��0�c�p�z؊\�&��\�i<�]n����j���]�����1cyrZ�0 �sb|O�R�( ER� -PX��#���I� ����5�����T27��_�;O�5�,��f��i�5rc�{4؋<�V���K�I�P )�Ӣ�L$,*��E�����S:pr���iL�����j��I���TiʤIӪ�m�&-R�Mٚ�kӕ���4dIڜK�\ �͍������������c��/��� 4@!�B���j�+�n/�����G�����o���F���-v�f����������<��?w������i����݆J�����z]��͆�S��In3#'�_�!-mY��5���L��� 5�����e�EPh���<��u.���?��|��ST�V�b.ӊۗ�Ϸ-�/��o�ܼp��4ϼ�x�S�p�WQWXY7�V����5�ĵ�sm�&CPh��s���Œ�t�%oH���apq\.��?kU��A��Ƶ����-'kW�k�<C}�:{m�[\�,(�~��V.��U,6R���tk��u��w�O���/%��`��pag3��^=���P?����ՠ��J�v��,̢+�s���*���@|䫧����ȱ��_y���/��'�����w�#�Z���@�4pqG��N��yԾ[wZ�_\�����s��G)T���B@��>��G����m�.����������� -v��ܞ�J�Y\Λ�4�q� Zጠ��ׇ�qB�;���Ch��B��[÷��;�g?7���`xn� 4Ԣ'���F圊�4��o����� �Z0��lAH:����x}�7,Js�8{�g��P�}cP�0�lzҠ' ���E=�jVˤO��̜Bĝ����J��1Ԃ��Q�G<���#�>�M�aNR�����8iА�[��4���i0��z��l�� XY5ʝPH��u�(f� N<-쑤!�?.�%�~���������4��nFʞ�jъ�4X�3ff뤑���� -f�rtT���͒4�݆�İp@���~��.^R�o@�uݠ% �/�i0S��٨E��ͥ�v�`# .+k�y�Z�pL)�F5���d�"�<��*pDD�>AL��O�\�$��3 $ �g��u��4X)�W�����E'i�d�M�9��E�7`�F*J+U�a�Q��]��ȯ #am/Qwc -�0.s��=Ͼ1�H�噡x;�Qv�8i�I���z�E���������AF ���FI�FUR��%$!Cگ� U:�8a�a2��p��m�₧ʮ�n������۫f�H�-����2���a�\���q����q����Z;UZ�8aD%dx>�4�H��4X� kNʻ�]�������Y�͖I���f�2�h�����*a�#��G�9\���.�̢�ʈ� =:(������{I��o=5���i:3�m���2�S�f��4xyhڃH��RMԮ����.���i#��4��K"j-�kD ��$$��� ~�n�nX�E��o�?|������g{ۚ��7sA&7��F�A�,����^�;`���U��S�4ĀZ%�)�xB�o��ɺ����(-x�]����䜿��N�^?ji�F��d�`0������{���`����6�&dT*� -yT)��e��C@��`' ���=W�r����=���P���pu"�ؒN�0b�L~0K=�b�����{.���V��6�2�ta�LQIq"����u��z�8�e�yn�_�;�~j�l����@]�x��<5RWm�����P���ƀԨ���d>�C��I�!n�p}D��c�|�!�xf��9NnYu_x�~o�[s��a��㉚C#�K�Lm�?���&9�+&��P�&$��I�Y0�ߌu���#��,5��*0},����`�=�<��Q�����U��N��]�k��F��dh�D�s���NrQk�/�E�"��$�J-4��`�A��=a-�QÖ��L�O@� -����Zu�ڼ�-��\���\���T�rw$S}̟�]��Z\�v�m��<�����X.�+�N-�5��O� FԼ����*� �@�/�#g�>h�}z�B_�/��+~�N]-�f*�rէz�k+\9:�6z�iH���$#B\IѨ�D�r^Ĥ�D\ -vį`�br���'���~�=�uX���IB�I�t2�&��t��Mk]'��6���x75^��u�bc�lv$� Z��ru�� $d�Y�n$�#6�ـ `N��~I_�? �����9s@�,y��!��d�.����ԗ��R^�N��>��ˮ��t�T�6���1�G��p��_���i5�>�N�k�a��zao�^�ۮt������{���CU� +��AS]I?��Kx�c"�ݦ�I�=Z�s��l�c�0�Y,�8�H9"CbL�%$"� -Gu2������<w��㾥,rw)������� {����|k��;zG�ص��3)���3C�˴p�*-ް�����˖M�%~.�@��"��厪xE#z{�,,���CN�5Ԁ��j��9�{�����D�e�{��3-��^��z���z�t�z^���X˾*�OK.����T�=^{�)����g:sZ�ț��1��L��ƢMV��&��[� ��o"��ǐ^��?&�n!�դ��ͤ�ZH! ��65����^�G��☫E ��u�4�v_���,K���L�^Ҧe-��3�2�6`E�Uׯ�^�kH��i�n!*�|-zg1D�UC�qHB�E�z�����\`�;���f�f=�Q #([������MEdچ�r�W�+��sAzZ��4���Z�����|Q����=\�:�W�!��H;��S!�`O�g��>�ܐ$�J��뀟�ʳ ��<�Y� ��� ���#����������܋l��+"�ȁ��T�ߟ��Bơd�>���_� P��U��Ap,`Gc��k4(��cW���)��F ���{�'�G�s/d ����W� �����xH�MP��@ξ(`�u��GBсK���E@F���pP�!1����7����� P��E�P��*�(��v�:�%�H�e�.g+ạe�9��`�_˞�5rf�m�ino���q��^���7�� �o ��n� B� # jҠ! ���Mm�i�0¶T�qk -a�}B���k��3kNdc�`�ia-oJ�(��ۄ~^/:��b>��/�:8�% iА-iВCR0I����+�_U�ӗ�����9'3�H��)q���ՠ�h�hB�.�ܕ =�~�O�%��>N�M���9iО i0D�3i(��snĦ�4�K �R�?#7r�d��I�C4!������q��nԋw�>� �I�Q�� ���g��@@At�A�*����%1JI�%7b�$?a�ĥ,�E�9��pZ����lB搌Kj�1Q�l�Cx�A�ꕵ�5��z�H�# �'=i0�������(��-|��JX)�PM(cVO��4Z���D<N����bT�"�+��AE摷`���\�OH���c��4���4�Ƽ ��#ࠜ�v��7���[e�"`̚�"�^�Nh̒1�]6*�Q�HZU�⻪Nр�E4�ly�.������>i����4�bނ���-'-t����`S�����3iRb�:�tTU&�ת���I��ح��jŃ�*�����@���H������nUQOlT��g���Z��X,��g�(g�B`�&>��*}�Z���1�I{ �~]�ԭ��*�%�ċ~���4�I��4��G�ޭ��������+���Z[��T�͝u -��v 6fQˆ��G�2u˻- D��F֧w��>���� �����4b'T\ݳQ����[��W����7�G-7���nqn̹��*T8��-f]����E�YV��SR!�3��w56b@UJx��WH �!����D̟??ۮ����+��JC�'�My�\lfE.4^�k�P�M���!�]�K��:}��a�5�;�n�M�k,U�kK��bŠ�,���k�b�Pv��7��)�-7�>^la��w�6ۖ;��N��d tp;��z�Z_i��얖r���Z��1���:��M>�� -��C�ϓ��4h�n^zy������w[h��Z�!3y�'�Y�F݅�/3���]�؊I*�z���fvԗ��6}q�Yo����n�N�_��4��3��c�uY�ήUۭ[W�֝�]��3��n��vv�u]�֣*�� �����/BB!�ʡ�X@P� W4r���>3���w�E_����g��<�o~�̮t���y�'�W�����fʈ��ӷ=�a~1��}`�������#:��+$���<R��Wm��kֻ?Z��oX��Z�V���aʹw��h�ᙉ vl�;��.�=Y���Y5}��CƧM�=��9{�{E��%��n)��O�/�Vj���,��l��������k��G�����V�9�=3#�###��o��5��ؐ�o�뒈��U���Z�&w#�/���[{�_5��OT{������b�T��Rjl�tSsV���ةmhr�V4�J�iE�Y�m������@� ��7��̖F.�=HZ=T����N���blk�g��p�Tx�q%�b�u�X��R�3�i�6�E�\`W5��R=l���k�ҕ�����l���|�ԥx^a��C�L�G�9RK^��D~��IR`C��j�P��z�)��?*�;<B��W�0>��t�V��=�,o+��Z-���VUU{���L]q�u>��Wx^aC6�\<^y)`�>aiG7aYcec���랗|�'�`�Kp>�ѬC�i@!N�MU�����n�^�U�&�0��Z���n��A�IQ�+���>��{���o��\^�j�]𨇸��C�螗����<R8�wN��2}J����qUn�H�o�Jz� -QO�J�*V�\U��]#h��k]���%�^n��q��{y�#P�n�;�^��R���Ijp�Kڹ��h�����(�Q M�U3�C�`���y�;_��#f <�0��$�~�|����N�Gpc7z^y�wFu�ѿ������/)�p���9G:d�wis�p�#J6ɽ,�$�OG�<3%�Fm4�HnJ�H��-eR�լ�a��oSo��^�ށz��������K���� -��lɁ�� �3@�j��H�~V8��� -/�gdQ�iuLʔ>���G}a'$M�)��䱻 ���D�X�߆2�k���JT3�!*���]ȟ�\��Mf�ڞ -��+��W��� �0�G�;A�I*�O'���G����͡ s�����p«��Wf�.^�-�[w6B�������fT4�5�{u�v8���5��"��!��, �J�n*�����$��K�сx�C���+�?t���× 78��D�M��^E��l�ټ�]�ۄ��K����2����5,{��(����yh�〹#��\�� �[(?������~�/�S�-��krL,A��`R`��/fá�48�|�����AĚX� ��( �?^�Ɵ�gS> -�ρj�YH�r҃NC�ǧ�᷒ -�r�J�%H�֣Bɰ�`�B"�cI,[�a��C�0���Ŀs����S�|�$�ք��'@��h��i}0dm���߀�w^�:�_/e�rP��ͥ �$�5}(x� U�IZ�ls«1��E�_�J 3���iN]�����Kg RF�ÔA�����tsG����jH��a���5�V �`��ؠ�m�:���>a�����{��3 ן-�F�J�q3�ⴠ$q��<ɫM��'�q��O�� w����u�f��_��.6H�A� ꐷ@� F���H=�z�1Ј��6���R� NKn�&ŷ�&��qA-}�ߑ�� 0��nf�`��-b6�Y�B�߀�^ )6��A� Fl0cCa����u��s?jU��jSܴ2+aR~=iBZL��<�1}�N�����/ts�<�� �^<ĭ���6h�A� �@�Fo6dR@�8������藺4�F�P�Q��ϥ�l����T������O���[P/�����*��_���O-6�y�`�=fdwB6�R�����$��1��O�,I��ܔ�ʛ�g�2ި���+v�z%}�6ـ�^�T�*�nI��-��#�E �6C~�������I��1�sB�,��:��L��0��y��2�W� qK]�Ny��A��/��<��;���O-V�(���nG�U�u�cu�;�j붵ޭ�Q� -*�r&�r����<� �+� -�T� �!�EA��>������������g~���m'j&j�r���w���#b "�+@I�'���z��PJ;=�c�i2�����=Wʨ�R sBd�4p�zy�����\Q=�M���F8�s����'���@��z�>����"��7ک�2���bv�s-'��N(ԩ������7���8�v�CԄ9�:�)2aN�s �D.�� }�O!�%a��|ߕP�>�m}S�k��rp֘rr��u~ʐ:Q��X�Q�U�.E1�*6 �n�;1��Id��Evq�ȁEN�w �K�.`��P��A�*r�\M��Sҁi��T-�w�:=h�2+jP/�شr��<MN��R�.n�Yp�܄�dF�]R!v��ST&vbz�Kஷ�,� $2 �����6��$�}�>y���i�G��73�Z����qjG��kQ��u�F�Iگ��Z��R�T/��K�Q�ԉK]BwA��� r�P�*�2]�0]O�t�)eϓ��=na�l�<gkJ���&���F��k*�I�rM�r�ݼR�e��&����"�/�;�2��]o ��D�A���WϘ�?|�D�he�mg�{x+�{G'�t_#��>3��(`Ֆɸ�"���Ш�QЭ֪�U�\�\��K4*�X�t�4 -'�.�\���0(�.����T��q�!�{�%������.Ʈ�^�W���?wu�[���&3�e��pu�|Ia�A���P�i�s� -(��6�Zm���9��\��]o ��r�.�ߒ��P�g�������G�h[\w�����}w�I?��$�;��*-F��sqe�^)5������ -��Rk�� -m���8W���Y��N�0��,�r�wg�a��� zԙ���G��oK���~�l�O�l�Ê)��J)lf)�~�D.0YT��~����BZb�ˊlR��.Q:pwA�r��Ml$a(8��*�<��u�V�!y��(+{�)u��4я�����~f��7������ ->�E'�47�8M6��&Vؤ"�]".v��N�����AB4�Ѽ�oє%�c�+�]���^gҪ�A�G�é�hx�v�8�:�`F��3Ȓ~�W��ի��4b鷝X�Łsn:p��!�;1a�K�.�,#��AJ��W���D[�Wo�G{t Rִ���4�2��L�1.<b�ˆ���!-{@���*�d��9t�3'�� 7�e��Q?�Ϯ��T� ���>� ���;���g4���YP�@�Ҷ�w��o�O_�=����J����� ��FgO�$��Ih��f�HuZ�p��d3�f�n g�Yf����Rb��@���k��rY�Fݮ`�6��q���4I�Q>M9��M�Q9�t^4C�`E��sbXO��5%y\��4�8z�N�'?xDO���Ioe�K�7��������/�P����q~��Ʊ�?�>%m*�S�&�$�lP/f̱Y3�a�ص�ߔ�)�nĒ���-��I+)a�9�9NJ�u"�]�-&��Z�[Ѥe=q��2r5��x�LE�.��ڤ��ڡ��_� �P$�dC�0�R�3������A������Ұؗ��1/�#���ע��#�F&��E�F�A3FO���G}�i���-`q��Յ�Q��1��́��� �[P�Ѐ~" اȐs&DgcA� �_���B����|{@�kۥ��O.���1�Bt˼����������f!�����pa�.y ps�ngA�ND�I�>2$������:�E~8TG�A{<'B�|2:~ -�)w ��2 T���_����)��"�,��j6�{��� -�nI��m��=?�����+2w��`���/��f�%(������q�]=7��C���|�D؈����7(�z0��2*��J������H�� -��A��` o����8�ҷ\�� ��d��@��9(��,�nwW�!CB�{�`ʄu�[�p����F� �p8�$��B��%Z����|�F5u�q�.(SGku:�.g�,���3z:�eڙZK�]Ԫ�� *(�}�Yn���a a7��EYD�(օ�E�����<՞9��"�˾�����>�����!eeП?�_�|�> ^@���H_�kvAầU݄a�.K��006������)�#v6b$x�Fg ;�C��08�� D�Ң��8��!������Z�������z�'`|v+�/Z���f*ڛԍ��`� ё�@Į"�U3��p4[�g��������~?0.z�a��F�m�PG"g�QӴɨQ�t��9�b=���~������?��� E�O;�!ؕ{�Yw��O S�S�?�����T�7���EAH H#V����@N{$�� R| -x�N���v�85a����H`~ ���s��c3�q�s��cM���i�]�4�6w:�_@C���d�!�Ƞ�YdP���C ���X��y�@P<��6E��1`���Θ3G�O�Fc���.�D�=��^�u��I���D>2{A�:dHOx t�w@� �xi{@l>TG���9έ�Y���{��g4�������L�_Cu�~{l` k�� G���` �2�������@#� -U(H��@R "; ������'gy=q��q������G)��X�5�XR�p<�]4�7` m^62�Ƞڳ��A�%0��kJy2XA��w�F���p�Y���������#�R�CAO�4���`0�N���t�FS.�GS��c�-��s:d� �4��!��u��g�My�� ����^O�{��E̪rH�'fG´�%iRx=ewS]�A���[2L0Z�QF31Fo��ﱁ�\d#�.xd�?��P�2$� ��G������� �#�iY�quaܨ�LҰ�!uH�E����}��d�K��-�av#�|�U�����7y�@ �!8�r×������%�o�16�X��&�D{GM�#�FC�O���֜Nq*���e�>�=�72/�]��]���+�u�!�W�!�Y�0�_�~�����^��PL~����t�r���K9��!�|Y�+[s?�L�NWB�U;�7�����;x�7+��s*��F��Ϫ}x�z�F=���cȠ@��������_�����'�Wp�x����a}-�';��m�ҿ�ٹ�Fa�ꚸY��i���#��z�v�WT���6�O�/� m=TA荿{~l1+g�^�:��ʈ��w�Y��U�m��$n��]y��K�\F���ߘ^+�O��j���4�̮sI+tn��#.�yEe:�/�.b"��Crv͇�Á`�Y����f�6��z�g=�ݾ�'7���.��۬����lf��w��*}�®���靊r�SV�wIK�nI��#.�{��zl��h�"CޮyP~x�\u̲�Zʪ�����&�+����9������o���K�I����\^a���g6+K2o��3�ENEa�KV��&�F��j���e�. ���܌�e�<���:b������5SW��w�5���f���u�}M�R�M�XUb`� -r�%y�2k�y��|S�o�W�g(�MNy~�K��鑠D�zb@{S��0(��ʰjcG�(K�m��w/������ٚ����Z_+:V]���l���3n),��jU&�U�)�_��;�4�8�9f�4'�-�1y���Rt�A��M�@�����s��������%��O��r�䆶�䠆V������%W���+���Rc�C�^ҩ�����E��)��sI�r<��#��A�2d��A�6����x�����}�q����؋�O��|%isM;#��,m �,u�ɑ�3TH��*���]��PjK���"�Lou�Gl������Ȱ)2���A�g�ïB��h{���G:�N��x;�����W]��,�����Od7�� F���"��� ��MFԸd�3.�괛Дz$iE^����pd!��/h�cl�~'��ݺ���]�֟���{)/;�$�g�n+��>h�����%=]ޚ��T�x�/ϰ(�,�D6��ظk�$kvW&����RD���0Lcf���0C��� np�lA�`"�������)<�^�O~�����<��}�}�y.�d�4gg�i��;ْ���yVaykfQY����za��c�A':��^T�ӣ[.T�(��>Y�4+��>���o���X�ŕ�ڀ]��"��)���]�}��p�B��qE��o�g\n�0^h3dU�r�k�ȫh�[9cY/0�ҏX?.��Ӌ�ɵ���]yB5�R�s��O��goU��p�f��f-��)+��qALn�v��a�X�P!�4\V�5)4��Pj�u������N���2��f�T֏��?����t�]nr���mٝ�M>�%��H��iO�rei�Ƨ��!0�=?��^�mۗ�j+ONk�!E��@$�)T>��nuK5u/S�k{d�@��USX/L�ޓ�TՖj�:й�UT�n�]|�I8��I<{�)u�v�bya�zMN�:c^��k��7O��u��T#6�ljz[㥭/��{����n�%Y����.u���U�T� ��H'{VSE�u�9v¡��i{ ���E�Hur���Z�x)��WƖ ��DP2j��x�4�*��?�6 �Z�6�L���]����o���Pٰ;��Q�>�K�N*A�fgC�H���KӐ钊�nb���wM�q�Q��:�X4�Ƣk]�����?d�Z��G�j*������F[:�9�V�np�w`Ә"���!f��OG�����P͓@�H��&�pq�/��WK�P�y�,ǵ���m]�<�Y���KD�/�� V�/��h>�-�<���D�X-xo)���%:!����?���l�h~���ðoN0���r~ �.�Åh]d��Lj��}SG����)��H����jR�w���Sj�GԸ$L��`b4$��P�̓���� y��u�?vM]������֢�=oܘn�������cߠ�>Q&�X�I$�`.%b �����I��֡�(� ���?����!{cTv>Џa�p�'��y����88a%�ߴD���?�B��J읉���N�p�O��7� �g��.� w���<��<�O��D��#7D�+h9R�2�ϡ�Z -��36[/A�(K �B�zql^ı���kE�4����B�� e�ǐ��=N�|�D�\�r����H~����w�AҐ'�����A!8�"p�$�l���<^��O���A��a��'] ���A��������pZ��CsHd��!�9H��:��{�S�A���aT��B�.g��@z��� ��ѯ�>� ���Ao�����O�.)ֶ���,�o��g�7�9$0�D� �n���!S����z�|�o^��ϐv`��WAV��e$��!n��BӺ�0�$�v���&�Y���i�����,o&�cO�\kC���푓�l�#�����m�2h��AY��)��BV��G\H�o����,5��Ȇ��#~����X��*�j�C2s�3�d��B��7�e��|�K?@�z!2���P����H?ʁ����H����=�a��@��gc�lpc�bh�3�pP�jx�}�p�m�H�M �e^sH�K-c��� -y�aK�D%MC�����58#��Ɲ�8������*�!��@��v�I�ܛ�ҕ6Ң6�4j�a�҇x��C����u��9Y�0�T�O9l��ƶ��P�8%bGlM[�-z��<���R���}��c~����t?��_��4�:�/kL�ځȻ������:�9�R�9�6c�@��9�0�gl��d{]�-vD��� S�S4;�%Z灢l���b���/���G;3+B:gy����V݃�f][�c]�]�)�z�)��a ��8�h�?o4ǽ����9�>�b;m�B�� v��Ǟ�I#{�3w�����|����ֹ�Ы��� ��6=����4�t���F����������ğ�}�K����,�\���*{0��l�;�e=1sH���ք��WG=�g[G+���j�m-������SAQ�@vH��F+�#!,�a7�@ -©g�uB��zo���������~���s;ٯɭ�b� -�����?��Ԣd|;���U��]�3D������ -���E^wb�(������{�O����`}�(�%� �1f�Z;\;T��6������d�t��J�� -)�F�A����U/��*��4d�:ݓ&���*;�/.��%���8]��ث��1h�&zS��^g�ՈFi���P�� U+� -҉�8ЈC�@�٫�L���5g?6�~�����i1������y�}�D�i�Ǖ�J��8�=�6�5^�h�f5Ŀf�ď�*�Lm��Q�`��&��~d�&�@�ߐ�s3Ʌ����0�W����y�4j�?���P��z�4l�uU�n��.y�G�T�ӜZ� ��[�Չ�I�q/&�8eb�D<�*����^$�Y��L�M&q����P��(v�1i�LQ�?�qm�gwkBVh�[������.m�bO�\�[+ͣV��1/�4r�$�x%�'|�d8�H2�UKLl�d�U(13 -%�5��?�jrom$����[`���$c����z���?���������<bo�&ڥ� ѣ&7�W+� )�4�a��K�i����!~~�pX~����6��K53 k s �8pɝ%�@�����`��OxY�9�Q3��N���������+�{te�k��=���%��u����U�˕�#�{c�"s���s&�"����"l��[E��{@���N��+���F7x��9���cε6�ez}cs-ϡ�"�,νD#�Qʨʼ|Vv��'�i��*�I��L�O�5�͒�8Y�#�,��i��8p�ް0i��/v����Q�3^r��{���Kz_�ֶ�?�븻+k�J+cN�ʓ(��`�Z��(,K-h �܌���S�C���anF���!7��Yf�5����:��Mfg�������{?����v���q��NO]_���U��8���*����i���9tIi '�DǏ/��y�OR �%相�;%��LU��@q���?��$������6�mp��{�ֻ:{]g6�x/����b'Ӿ���?�%1�+�.�/�&;$�JÊ�l�FW��+^�Ŗ��"����a%�Iy� w&ɅE�.��X �5�v ����ӻ��@덣3��O^��M�5����<���H猎8��6������&uh���)����/Y�5#,��̌)�0�4Z\ѨU0�8����̭��`.�C�|��]7���Ah��:�r��_�W�6e�qwg�&w�]I�^�Q�w�"��y�=!|�K*�u4�\�h���7!Q�7���A�G�I>��`���t�Bg�Th����j��r��\�� ����uһ�����o��������������^N�K��1?�~ܟ�2�0Q7�o ��A�ħ�i��j -��ـ�k2�nN��G����!(~zJ�s�����%�g�Ւ��M O��~�x���`�A��5��� �nLõ��/N��Mx0�ߞbu�;�ѿ?�� -���aX��[6 �M����*/���v@�#�2���;�37s(p�d(t���Z/4� �v� c�Әw�f�8b���6��s y�.���(m������5��ʩ0��Y ��3��o -�� - -M��g9٣�Ӥo��$��-�/�g�>7�[������ΘH��P�5h��!�m�^����|�^;���S�+t -z�~����ѶpGp�@�DPx���5�#H��T<n����b�2?�c�9+X���µTL\�}1�Y����N��^8b�h� �S��t�4��T�����@ʿ�Cn�xt�t��B��xj)�����T�. D�R?�YFA��3([y -U�ݱ��xi�>����\���}r��U-�����$�\�p=��=����y2]m�xjz Rf�c�,���F�\/����<�������X��'�Z��/���/�q�!��F���@��Ib ��L��b���t�gT�g�_,cI�Q�n�h6�]A�" ��"���C�0��Pd(#EA@�E5� ��vcIlh@E�v��9�m>ܯ��w���]�d�e��������I>���OȦ�#|�b?qF�t'd~ꈂ�(�e��ٛq�3k\�����I���T���>� ���pLeR�a|,d�X�8XöØ���9�#�083{�3[x���2�dl"�4̑6���LP2y�&�y�:5Qʻ�}�ާ�cL��|0�y@�9�����k�ul#���Gf��l�!�2}�0=x0�0m��JH�r����e���-B�Z�3�a�L7�N�_vP��i@�Մ,p&d1_"0u�y .�º�[�O+��Zޯ������A���C���AwD���"����N��;�'1l'�m��M�B쪁`?M�Jf!4jBR�F��[���I�R�����6�WCxk �u!x��۵�M�����CT�a1�%0��P��M���:��7��/d.��D�x""�"<i>B�����ŐU-G`�6�m:�\Ѕ��Z�ׇ���ACǍ ��8�Fo�0�ĺ^֫|������)��!�YQ>���Ft����DD��+\���UnЁ�U�}^1������A�f=ģ���0�(��w��� l� �iw�� ������8�1��2Ne�8oM$g#!Dqq� &�D�/GD�j���!��B�#��z�6�z�aL��t$��p ̆d0��yo,�Ca�{�z�O����G������2N���8�H��FJ�c a�b�)V"�DQ��h6�o3�`:v�|$���p�+ˡ����!�|�M=a�z��� u�|���x���,��:����D:o�dZ���ɉK���=��\;Wi<S��C�1�᨟,�G^��]�}���^���w6/�͞��Q$l�F��F4�Ĩ��2�}K� ?+��E'eC��Td��D�TY����|LM_=��o0���䭼�| ��7q���c/nc�"�����A���q���p����b��+[;��shR-V2RƉ��L����>y���9#��ω]�6s�Λt�aj�體��/�mz䭶��oy��q�S����l�7��V\.�õ3���p=+��:�G��%��S��?BN��iː�6���G�DR�(�^ԗ���7'���B��r�ǩu���:<H:������#��>�k��?��q>g��ў���pW'�9 >��pڍd���O�h��zOR >{���{Y��J�����w�m��Uj��*�������l�~5�⎮仜��/9�?xu���=^'v��x*8-t�Չ��1|C;Iy3��2��3|�.�L���|Y)���"l�#U���E����s,�+��W�+�~Ψs�Lk�<��û#��3����S�sO���.�M���4�4��K����')e�X��tC�v�lT�����������5�/UϿ[���*��JQ�奂|��*玜j�3Y�ާ2��N�_8���ג>�;�1�k�@@c&� ��τ�:A��(ʼA�(g��YЧ>ld����n=}�>i�ϼW'��Fu�������t�sE -��{�\��*=[��-9���;�Y�MY�� �ゃ���쉀�l��d�O� ��AH�t�i&rt���ݾ��e�6���9�C�3n5��q�.|YgU�aGE�e�*��D�ז�2Ρ�Z��ܣ�z�9Q��;��+�Ř�Z1.�RL�ؿ\u��D�7��f$�L(t�X�:6P���4سGG\�[8�^;,�w�1tIGm�A{U�EkE������M%��ze�v�!Au����_�����ʂQIe���"\P�7��ȃ�:A<�/�H��4y���2�Wcƺ�����v�F��]-��� -^|�!F��6i㑪���\��eŜ����7 -ˋNI�*����e*�TU8*V� U{&���$�h'�!���iԏ�el�B��4b[L��6v��cZ� ��ٖ�E'E�mH4o�M�����R�Oɩ�(��-;(,Q��*K�d��ݤReɨ��xLXT4�/V"@� �� !�8�WfP-������O��+.�3��0�(J��1��FQ (��a3�UDt�"e)Q@2�����"!�� b�]qM�����X@��ξ�7{�\�o�s��}��;�I� ���P�ʎ�5zLi�Y�q�%�iMu�U�c7�U�o+.KuU�f��?}��xQi@VaM�1e�<C�((C�Fv�`T��?&��e�B���y�����T#�5f�`J���ʊ�7}C��;'��xs�T�%���ڔ�Ƭ+�pd��*�%�"����<n�baZ�iJI�,��<�x80�hT�V8&N�aB�qu@��.� -6�����@�����^�n4-��֯��U7ΥV��-RÊưOJ��(/)6��uʹ�����>�UE����*[������$�?���J�Eɧ'����A�C8�sq�G�.��f��sS�S5��ZM���:���q��gFe�X��-Ģ�%�6��9��m i���ǽ�N����+j��k�D���D���#�ė��X -u d:��Y�Hb��A��hҭj]m� U�!����.v9Reמ�g:�:�n-��\���6�-~sJ��Zr\7zl:�s��/�rw�j������<��~�@(���[i3��I��r �Z?�Tm���K���?��m��_<����Ϳ#3̹��2�1�%w�}�Й�Yq3��7�.�7κG�h�~�3��+��vx�k�����_��k��I��35�vQ;랪F�� ��3�J����};R>���{���y��~?�$��~��ߎ�R�������ի��������s�1��ch7��:�#��-pS"��s�Po.QǏD�uD5WX���P����g��Ğr��4��ZiҹI��q�Q&��1�I6��3m��� �/�*�_�!���Ơ�7�c��6�`Kp�ՁX=I'�u���͠�͠������t�(��Ք�̞2^��H}ɝ�80K14��`Ģ��X㈡���4��ֲ����W5�$��ۊ��X�b�s�3�?��c�U�����]�'�hc��D9����҄R^���7v��Ε���;��Ό �6�<�Г�'H�3�& -Lf<����w��wV~�a�{��{lN�TMt����u��n��am�YB�WR��p�H�NS�i"L[��YB(��4���>P��A���y�g�^�=�#u�( ���蜊����w%��6�!����XM��Db�C7`�$x\H4}<��S�;� Z�Ș�mgT�؎˳�p{�#�8`l�:ˈ.�3(ne����E�' -�4����XJ�XA>�����v����D�p��M���D��9mCmE�$;��ٚ�P��5.r6�e�z������f��l���{�dψik�/�'�̘�5ӱ%{�%;6�o��6��l���N�{���-Ĵ -!�7D� ���d�|�D�Z��w��� ��a�]"�"�"{�)Lwm�"�gJ�aAkaôli5�dZ�i%�c��X1?��Lf̏ ��|2@ �#�����Z��r��O[X��[Opd�םu=oo�{$��EL��G�O�n�<�χ�9]����W��sk|���|�3������0|��7���qa+�y�똧o�ۙ�� �a�4iC�:�����J=���ۼ���b�c#p�2��[���q�;�4�sa2̃�����Ñ=���%��u^o�7�Y�q ӂ8v��B���B�B�APc�~�!���^�#�����?L�#���yX:̇٠��υX�!��y��r�6�5��n��:/ׁemO�mȃ� �����y�f�BrR�38oQ���L!�Z -�}� ������`����oD0b�K,���V�,C93߱L��vu^!�r�?y�ʧ#4j6B��#(]�\�N#�)�uf�6�OHn.��X����x+�|-���rP�bX?�`�)l��a�β��7N+����g�Wj���N���������ث�Ax�"�~o4�4 .3 ���}�����'o��֯e����X�\6��/9�Cw��5aea��h���XK]LJ��UQAEDP�AD�� ;hՖqA��!B!��� 6�A@e�eԱ��U\Ǎ[���}:������,�|۞�'Ah��y�y'w� -�]�@�����n[���'BI�d�����N/��p�ߙ,Ïт5��F�D*�g��M� ��2�b�6�w����/C�>}i�$�W��P��0���0�ǽ#�=�!5 <�<I�8���;@�+�+��aB��>rC~�g��K\;+^��)��*Z��?���ϣ̞Ev�z1�{:������"�[L������h=��W#Ѻ�3��%i$c���4!�d����H�ѝ�Z8�Z�i�*���G��Sv��Ǭ��bKw�̬5��nq�ѿo�q��&������(��B�~��D�m'����&<H� =qv��G�UIG��SgV���ע3�_×?I�[�0�o|?Q��.'��'vў,;�ئ�c1=6ט�vC1��菙����PO:t�bѡ����Xh� ����#�p��#��#z��%|J>����4@�4P:=t�}q��Ia��m㼬���� q����]a_:�7��͚:����R�oNmqx���N l<R����}�o��~$:��Ύ%���"[x���e>���'��2���f\�a85y�@J��+���=�"��D�C�٩������7���ܵ�3�Z�AJ-]���I��� � �#7��p�I���C�f%��e�#L��`*�����gFe�Ճ���WғL�����m���-��#�I]�y]nu�cԚ�Ǵ��_��W�Э���eI�Bp���K��Y�ԃ� ~n'셧 -;�_��T��� -��d07lu�<fCWV�i�Thٜ&�i�:� K��$W�i�� -��g9��WֳT�jRK�^,@�"R�5d/��C��3���ă��(� -&�`��[o���/� -BVu)�7��pL���3$�5���q�K��ҽT��Y"��.N��Q g�U(D�!R nWm`��p��!Dk���a6}<S���j\/���2�yWKO-�\|~e[a�q����.'ɲJ&����r,�湔Hʨ���q�o�x�O)~�LCo�=s�H�!�� <���ٕ�D�x��f2�`:�~Ro��r诤�v���K{ٹo��#��+dm��K�[�ڨ����s)�j�2��;Gz�O�~�_���_&E��,=d��%讍?4�";"��]���I�^�V�}�*��� 5���]���~K�YËe��U%1��U ꂔ�*e��<E��B^D�ɪ�3�;��YҬ���O�Y蝑���H˔"Ux�d�@2�Qdg�V�����.�I�"�C_�tչ��[�P}zyue�Q��Vu)Ǽ�Xp@�Js�Ȝ�� -�R��;-��O��OU<�)~��w�=%�H�d!U��R�3�'�D9��X��@�t�Fghl�_XS���6d��������Ky!�Z^*��,�:*)�w��=S -[|�#~���~���>��蕢D�i�9H�� ��,R�x��*=��Aw�Zhk2���#P���Ee�KKϯQ�GnTֲLeՉ��mZE���\I�_(��ʚ���C>���>\��WB z�TH� ��������"p�;���Ё!�.���@[�Jhh7������_�q�KU���V�zYs����k.�Oޟr1�>�V�ĭ)������VմK3G��Dj�t�э[�����~�3酅pG -p-��d�v�;�/}U�M����}T]U�q}eϙ��a�vEouě&_X��$8�9���GfK�3�y��h��0?P Ha�"%V�.� -t����aF�0!���TC�o����Ս����! -(���� -4H^��e$��]$Sv�xOl��:���&��ޞ�3�@�~�@�s�DGz;:F6�cT�v���x�#$ٳ�d�F�Ae'ɟ��@ym䌙C��#d\��'�~J?�_AM��X�gō'nd��L��df��b���ː�A��kO�BF>X��<҃6�]h� ���+�X@w @s-@U@1ɟ9#z ��5�oo�Իv�r� -������賧�N�2��DL��Me���2�yO��S*����:M��"%*Ko^��}�SiO�C�1�q�`d�c�d��-K9� )R�:Eڗ�<f��y��������c��[D7]��lz`��&���MD懿�}D="��5�^�#��3�ZAt�2� �yHT\/G{� i��y���N9�Q�ǭ���A -)��*I)�Y�1��Q%���7�T13���4��Yp{�Y��}�I,[`)���D�87N]�Dt�?��� Q�� ��aL�]6���Jɟ�)��{�ecz�䣾�)F�������i��j���4y�����)~}c�~_�_��Q���1�J����DG��c��gDi�*RR�1� ZQԐ#��xP��Z -Rи?I�Ce;A��~�V�Hq3���'T|Q�&�c� h�X�� ���q��=BT�#��+D�����r�%�"�U�pQ�R���6,"<h�i#����!�|Ok�N��%�%��ql�.�y�F�o�>@*W�9�?�������눒~! -m"�@�u�h3f��%k�ggZwZ���5���,�rE�� ��G���iJi>N2����F�����G�WK��w�H��M|Vb]U>�y���0'7ذ�=9�L�������&k�#+�b����X2a/���4��8�TH��3P��p�#>=�7m~G�3J|Vb]YZ5r�>����Y�7�&s,��p S�3��L�b5M����=i!�TMJH!��HKx�]�{�=�+gg��o� -�k���s˒��H�R�*BpZ �K��P���*?3�j�����C0��w�|�6�uFc��Ҁ;�o.�a!{X�HX���������GfL��ByN��%˕!�� -�55o3��6h@��t3Ú�Bh� -�1��w����4 �����K��bK�w�%Y�;֚�����y��t9l-P��D��!*S��D�5 �� �#�-�Z�_'�m�4,¤Af��A�t{EГ��N��\��y,`}�/�_O����n�v�! o"��+���*���!�Є�R⛓ �ц�^gLܬ3��Kwpۀn��5���E�ɟ�A�1�cJ���{��x�6����6p&6{�|x��#��"8I��Jګ��R ��5X�=x^gTrUwDrGoXR7yPҨ�'���"���@�K�N �>m�Ԏ��*��4��;��9��3�c����;�k�[���"2^�c��G�K��¾�;��zF�/���kh����G��C�~ �`��k�1t|j[k�`��`��eBa* ,g˸g�b�X�s��;&�2F�;�Dv,.Za(.Ue 6_�k�~�/���>j�uʰ+������:"��G>���:�]���͑�&o"0�!�_�cγ̭��VN�������q.9�X�3�(��ف�p�ޔ$���lͮ�"ݎă��ŗ�Ɲ��w��]���اf�c[f����,f��i,�����X=`��J���,�+���ϳ�\��M\J#i>ԗ�U�'#D�3#^��ΝZ�iz�R� �S7&3y�tz�ʹ��-�j-�$4Y=N�z�0d}?��&��S�\O�<i����*S��� ����hG㩮ԟ�Mݹ��#O"ך��6'U�uV��������~���#s�R�,�8���)Wmj����K~1�vr�������Xx���\N�}��I���Z����fF��7�Ö�2��'ϋ�wo���m�o -#��]���4?ˠ6w����f��<ly7����S���+���v��F��k��.�}u�) Ε��48�O���t82��p�\Ϲ�g��>����N+��kG�v/�����z��g�B���k=,�9�^A�����έ�=du#����2�+�眪2�/��x����&��=ng3�v&�?fb��L��g�YX�|y���X�_���p�9u�P�^'j,��g�Ԗ*��h�ٗjX]�cr�p�����U�/��w��Bn�˹�*�3�w�+�_-;�ݵ�<{l��s�~2ne9p=���I>�3�L{��Y�A�yf�QdA�Ŷ���;Օ -��t�����n�$^ݟiR�m�ť�}6�K����vy�E}ea�e(�X�!k��"Ql(R�GbŠ�`Al����J)Cg�30tADE@�h���u�%�Mbu�Ơ9{���·�s���s�}���7.q��r� -i�KYL�[iL��$�q��_T$%ע�$TKɹPJ�ڠ��v{���� x;O��q[���8�+o%:�6j��5�tV��ƌ���iR�c)���I62�]i�ƹ$�ε(�U\{ͣ �_�q��Uq$Rœ+�Be93N��kX�Yӟ3o� ��2��<Hϙ�� -{\P-G��[�3��s�L�g���dF[Vf$X���ٔ��ګ����5"�쌇BvU�+{ļ]��Hb�-GF��P�rߍ�yFs��~�(��4w2 �b*.���\�'����Ol���j�E�YEN��Yq����,{UZ�0/�R��rjIVr�Ry��D�B�H�L��I$���6 �^���H��ʙK=���7C�z!��%h*Y�W_�ըV�glE��)e�������s�l�r��L�0S^.��h\��~iiJ�=ϔ��%)�䑚A��trKK%m�j>_ν����$^��z�=t�'�f>����P�[[�kX��=ZS��Z}�"� fV�J6?[�n���pN�ш���=�/,M���3!�D�E�l'f�[��\�A^�G?ν!��#���ux��/@��M�sP_)±�5�ʪ�CJ+v���MPi¦�GYe��K/L�M)�uL�/v�Wչ�*�%R�m�T�r�Q�3��\r��!���!������ <��u#���_�9U1��_��V�ʺU:�u>�յ;F�j��V��gVDX���Z'���$h��cK��ZQdQ�{D�-�u�{��đ��U@�1Jj��s��g<pUtd�R�~5#Q}�� (i\u�� �Os�����Zwdzb��*�&i��:sQTU�Cxe��Pe�kXE�(����P�2.�5$/"gm�F�����A2p)8[����U� 4'�A}��g$P4���n��q�dʩ��Cf�'�-�e_F���=ܐ��`C�]H�Y���=N!���ב��c�RC��� ������(p%h��,j�8�4 �j��ܶy�jA~�K'��F���F�큣���F�EN9�?=�%}������yA-�6{��.l� <M���b��6��i�v/q�܃��X-P��O����cR/�DR�#d]�^ ���`�b�nr�+vbpg�y`�b��� -�ݝ�3�;{�v_~a��f�� kf���1�� tq����9��s�����C��g��n��b��x�p�F��ޝ!���{ �����g��f���7��l�u�l[O�ٶ���u��w�,�~G�k�y(p�{pN4�x�(<��og�N ��B�1!w�|�A�<x-�>�E���]��Bv����� C�f�}T<bˣF�͏�������M>��x�L|�=���;1��,����=Pt���9 �2p�{`�-]�yd��Ofb�ϋ������O���߶b���M���<���Зh��/w�C־l1�~y{�w����F_?�aڸ��A3�A{@���K��v<҅_�)6���Ͽ`��NX���oVb͛u�z���U�]8��)���?O���Jw�sz����I֗�#}�[��)��@{0�H���W����ۀ�}|�?_���4+h!�"'H��bZ7��� -g -�#��XLr�����2ϰ�H+�d*y�ك����������*`��Dֳ�+��L�@��#��p-�k���3)�t����4:Ɯ�T�����j�`A3�z��b�k��[��{��g`]@H�Ys4�e"�M�\���V\�%-f-g��f�I䅿q_Lh��Ѝ�H�T#��#���f�D��[n^w�ǀS?X��̦A�?c� ��d�s-�i�Y��5F� ��a(�f0-�>���D�/� -gx�)�#h.��8�Xڀfq֜�����-��n���@N� �tΦJ����a���dZ$�����e~���.�>蒘�~1�����vyF5�fq�B�DE�� H1��q�36F�D%�V@�:=�PB�&"���bWtAVg��.�EdE��9����s�o���w��y��@�`F=����ԇ�z@�mT�p�'� �zq��[ -���������y��)�;z�� -1H<`��sbLa�iJ�2&��N�@0'�Pq6Њ��В<Ӛ��\s�:�_��~���#�\,�g��N�G�ct���.���"q��.0��O�S.��.ߘ_��'>s����J2�E噠 �`ihE}��<6�kn�Lu]�.�� 7�<( �5[]%t�T�]��/L�uJqz�y��n7���&ݞ����g}r�d}܍���Q��!T��l�-��:rL;S:?����p�Hu����o�Pz���]Șv/T�r/g~��)��w���ס4��Q�»�<λ���7����v���ʯ<p�KT��sj��f�(��ʙ~������ wډ}|����g��$?Ma���8Η�>���G�|�P��?��I�-@�5���(���S� �>j�����q�CO\����g�{#ʟ2q6�m �aOu"�����I�C0�)?������z�+V~�[��ʧ^��O����)��>}O}ni��<�|��Js�w|�?}P�j���mo��������it7��k�2�sS^���F���~{a,���2^V �P~���$�T��5����}��>ؿW�����=�?�=��Y�?����%��p�U� -qip=���p&�t�Yx��� -���K�V�� -����<"�1��8<e�p�H�^�d��ق��z���V���.�[A��� -��D�J�����}A���K�g���@�߲�v^��O��R.�f0b�7��(gx����ߋ�`݉IP�ν���)չQ�w5�I�rx��a�+���W�{��'����aS�]aht*�N��h���g�?��=$�ܳ����_ ӡ��)�^�8�H�6N�w} �J��x�k�S�_�-�'�;[��7�n����է���������}Mg�K���I�Qh��&�D��h4n��Huw��x����,���d�*xkO����R]�V��Zj�ҥ�h���$�����g�����Wv�7��u����3k��m����x���84?�fMq��h<�6ƣ�Lp��\ӓ��y����X}x!X �Ma(}��p5�S�B��R�0R�L�`aWZ�^gJ���dɪ�$��� ���̛=�M���F��A�S/@v]Z�&�avd�']�rM���G�6���H�.��0��Y�pE��r����R��i�̊_��0E�-#ˠ%�а9�tMS�������ݜڤ��5I�l��>�T%���L�u��,�g�[��ɵ���̃o1s�u�:<LU���Up5����lO����Ց�q"7fa�(I�9[hp43ϨAXbR�QeQ��ĮJ;i-K�l[�z߮<��]Y�Ve��.M�u�ih9ܮ �W����*��+Ã$&���F�\(���E۠K�G�C|��V��R�u,?A�17ݠ.'ǨF$6�ʒYTd6rʄ��R���o��7ᴝ$� +�-⺙�ϔ�9��O��oi�f�傥�+fCW�萺A��K��$@�I�� >�W[�jP]�m$�+4-�-����YIDm�E�s���p,�~E|�/� aEp -��=t��_�v����ΠP��2�X���Q���]���T�Z_6�HI�nUq�� -q�aiQ�iq�Ԣ���UA~�m^^�CNޠ�(�%1e�����5���E�L�K�i������l�K�rpF�:��B��G8^Ņ��� -��Ujd��*+bt���IK3V�KrM -��-�$����6K|�AX�wGa�(��^X���ufr2�=tU�o��Ir>���r�<�^ yW�h�Z�5��h�3��z0jj}�e5��e�Q��U��E����"�r�YV���Q�d�V�e�"��"}FLڥH�6�� NZ1�g�n4��r0N��{I��A_yW)yO�4�BC�=�6:A�ѽ��X� ���ºxݼ���#Y�BӴ�r��FNb�IA� ;A�S;�쫭@�� 2�J�@vb9�� ������r�9���w��U2�����P(92%�t���hoR�B�2�0���6�Ԥ&J�SB��tQ��Ҡ��LI��f�<�y������[��]k���ߚ͜;9�pn��c8w�)">q<�%���ňNv����G/�Q9ta×���F��9BkW��v&����h���� ����/2 -��1���(4�f��Ѭ�X��#�篿�끧<�;{�{�3����(@L��J5���8����^���J�҂Tw_ޤyy�fD�.�m�脧���9%n�Ɣ���Ʌ����;�/�Ap��HH"M��M���łC@V4� H��u:8�����������f��Y�؝%��� -P�� -�����k�#C3�Fo�8>nCF���3.O -�(��Q�x�C/� -M Lg������>� �8g�s<�D��K�De)��l|w�;s-���yނ��R����J��C��Eݐ�wXP�ё��Z���%�+Ж�Uk�r:���4^z�t�7h����*����L�AJπ��8����l����"}l.6��k��qņҕ�{�ZaPY�����Jk�v�H�Y]���<E]R�?LR^��Wڮ!)��Ť���F1���{P�o����������:��A�J�7kBVa���X��V�*��o�<�$�}!ZQ%�SyJ����J�U��UU�<+[�=�`�_i��G4�Q��}~9�[p�w0)8��#����� �q���O�px?���X^m�;�ո���/\j�\� Nu;��� ��R����"��F�}��Ւ#�(���f�$_by��ؿ���R@rp�\F���D8�1�]�)l�Z��;X5�bq��-2,z����aѺ����- ��e0�x�V�Y��9�O�I��?��=�g�:��d��c��5`�>K:�a�5ݳ`�3��,ļ^k��sĜ>}�`�1��b:Ec�B���:L�^�>#��@B:������?��wf��s`a`F*0%M�%��a@s�1��,�d��Dr���8��`E�+: -MJ�H��<�>K�.�^�7��q���wd���� �;E|7U��H���.M�M�6`�����d U>�2�@��B�=pO�=]`2?�,��-�����o����~�:��|O��b,)c4���4�AZP��P!](�>D�� c�X3Ό/�?��?Ĵ�9����n�,�6�c�04I�sdV0L( �z��4�hz"hF*� ��2,b�3�@3�6|�I�e�g}�k���$κZ3��9S�ϡ�Ysg�����b�0�\?�k͎a��A�W���9���܆*��Lޣ��:Ms �#��h2�a�9gN寁 ��v\ۃ�g���v��4�{7�&dz4?��Ι5�)C�7O�c� -�f�h3�C�9��B�l����g;�i|����g�1�7�6��ǵ�@f[x̻@��dΡoQ"z-S�c��nq.��w�)~�6qމ[�vq/��^[��ъ� L���CSt���s �k»a��X8d��q�,�;!�X�}V;�c�.�c�=�v�x�-M���W�j���"A�]���aY���}��֞D5�$WmO�/�����ҟȳf�u��=�<�y'lq\�x�w�]��hw؎V�=hq:�f�h�[�XA�K��K���庰�����}�K��s�?D��u�U���on��ȍ�*�H����4������'���<~N<gkt����U�� 4-�k��P�y@P�y\X�#��JU{]�{�uM�w��Wyݕ��B����=�.�roR)���w�I��)���^�/��g�gq�u�7�1��b��r@���W�E��&TKv�I���J�!�D��/�8Ň��J$W��Ir�[R�R&�R)�{3�ЯsH� ��#�\&GBj��f�L��ys�sZ�g��G�J#����Qb����㙿?*e�H�M�@�[���b��r��J�4A�H�:�P��E��ph���Z����lY�� id1�2�hdH?���k�Yw)�ƽ���Ih]=�ҹ�^#Fe�3*%�$( -�+کX�oPA�����/r�Ū�Zw�O��4��3 �w@� -P�EB��3g�v�Զ�}�:��WT#�$1 1�����a �*A@ �"�V��NGG��xt��ڱ�tz���C�/����~��~��<��IA�� A��^���n��'u ^�t��v�J� ��ѷ����-$q�I`���SL^���H��:0�IkpY� D�gE"�~Q��)�j��d��-�ȳ'���xRݤ�Ķ��'|�/��%�hM|М����D�ߐ�~�����r������9�N��}��$a -n'MÐh.��s�-��8��&9�J�c�%�Ěw;R���Nj;h�m5����D�A �[�zы�:�/�Z�E,�� $�aS+ȵ6�%��_���e»���k)��B:���T�F���� Ν�b���r�6�ګ�P���C�Fie�Eb ��t��gBj���?�UI~����Dp��q�@{ا�`��y��{]�F����θ)rťC\�g�/} z�ѥ��pL!pn�����hJSz7ȵ>u��~�Բ@����*Y[h��/�\6�/�=�e?�����0!�\{�z�����u����9bH�A��e�Dϑ��T��U�ݡU�ܤJvk8rȣN��mVd��2��+�K���L!ƴ���mZ��rx����"����i,��!D�=l1݅u伻F�8�$��9�t�� -_������OaլA�v+�Q�\�It�͒x�2Ӽ+ժ�媜���DYjP4������#�o��)�4",W�B��������`�9xB��Op�֍si�PMB�&mٟ�E� -�9[8u9{�̺�1��)��To�F�S�� 0dr3�C��~��k�Nu~F�����u�V�� �V�xD�=l9���֙��}��a��i����ߝ��n�s��u�PS=�*_�Z�'r/͕yr2|������|n������5���Y�s3Ԛ�Rk��ְp�O�25,���#��cp;��(��]�th���R8f�2Tm@�a稲�x�}�B�gAA����L]^n�&�$$3��W�X�+sf(rn���}�бp�O�����4���xr��͠O te��十E����QU��kQj��`(��/M�_"�S,��5�|3�t�*�a�Bo -K/<./<�Zx3"��;��|y���Y�=,�� <����!�uЫ%�#�l�;�T:e3a,���5�WFq -*�8�V��]!r�*�y�˔>J��?ݨJ-� -����IK��%%_߆K���3���B�a��ww��#3�5��Z��*&��z����_���Ȯ��1ǏV���*k���M��rS�dYuA�����R�"�:�V]V�� +���rBL���O��t�zrȻ(��������셼z>t�9�4,Ef�:���q�M1N�\�)����% j�K���R�di<`��&X���_p�XpB-�&�#Ȟ������4�C4Q~� (�%�it�����Y�h��4�j��o��=�Aھ�I�.t9�.sK�*��9� �� ���>���X�_\���V���������n��=P�H9�R~m `$�im@j�uM���C�Y -Q�:{�!��I�pL����t�x��-�f��V;~���g��gt�7��=�k�q6aW��6�������@�� 4�V@�H�I'����`N�ľ�y�;��sv���Þsɜ�s2����Q;�� �\���p�:yL���1Qgu�2���r��������I3�{x�����k�Q@�A�O���v^����<l�4Q��c˗�a� �te6^���CBl�c�p6�]+㬽��y��C�'���s��ʜ"�0��F�sq�=*�{X���i��N ����R����QX��D����������a��X~g=�}� K��b�=!�OǢ��X����{���-,���cXx�a���T.uPAP~���2l��4��=?rXv����<0�q �=��'O��GO�a����g���, -<ߋ�~���/����<��������W0��/�����.#�t��)���W���S� u X3,� |t��+7|���ٮ��>�ߩ�-S��"�,�$$I��H� -�Ɍ�]jZT�ҡז�J�J�QRGԱ�\���Û�K)$�������k���{/�s?��Z����c�[a���0o����&�c�G}������9����!:�a�~p8��������V���ˀ���-`�C��0���i�LI�ad�!d��4d 1�B�f@��B�7�~� -�)*��^��NB��C�^� ��T�����+� -�R���#���L`)A�ԡG:�g�yC8��4��!M��&[��3>��t�dg���?���/������M����1g?��w�q"����"g��Lu(��#f��06�Y���8�Ȅ�H ��˷H�k;u��4��������������CY����@:{��n>;�;�*�+���=bM�"nGw�C}f@h�b�-�q�hnӜ�����1��I@�A,�m��d�d��C?���� -��vd������Ѩ mF�V^�֡���� ��pty.L7���2'�Ζ������� �DzA#�|1=�N���lv�����%Z����H�ks�_��F���������5����H^���#s���bR�����6�s�6|��O?e��Oy�`Q�v�Sh���{�Kh���ז��dيV��� O� ���� ��CylF�a��u����1W� ���A�d�� h�ي֟w�����vb�L,��I�x5�,�'����[x:��ض����T�={�v��� ��2ȷ3�c"���8��ǔY��v��j�/�������j�f4Mێ{�����N/@��<q,�c�Z<rl��O��ܜ�7� לIp�]v&����!_�-��l��}r�gd� :f8����x3s)�f���Y!ht��� x䒌��Sp�5w]��4�9��s.�����6���� ��Q�0�k��7V�h��~��Ar2����uZ\m��:��x��x��;�#а ��I��p�-܋�Y��� -�/:�P��J��k�5��T��|#����Y�IJg�iV�Ȝs����;�x.��܆�u�(4-��'���ᆆ��)�եkpiY�m\�JV��JQ<�.��:أ��h�J�r� -�ߕʽ�)��4+���P)�!�c����M*��=6\�M��U��X|�0@�b<^<�����l\�Y����fe�%r�9I��YI��\���I�R�$��)I�� �I�RIM�c��>G}_�)���V�K}}I�+��Owd����T��T�u��-�F㲁��=�Vؠ�� ��<pN�2���U(����m�Y*KV>&KQ)���>"�S+��[$=�^(��^ m�'k�+��9,[F�_�wh<�j�2:��B��*�{��u_c�J�P���P����2��PŒ���G�+LR- -���P@Z���l�������9��������h�� �����@l�������s��&y -��Kw�+�^��:�!�,p&�'C��x�2]�'(��P*X�+?tc�}sBv�g�dj)��>%�\��/��nZ�[�=!$J !m��4w��Fw4���$���ʵ�}~��� pE*�o�b� �aSP��O�}ra�<L)'"Z%;"�ρ�-�2����~��ay�=a���a5┰;�]a�w��ގ���DL�{d����������_�+����� ����8*���(gD{ /f� 'f�0+&T)3:R%=*^m_�f������i�)�\ݝ��vy��6y��������/�d9�1��r1���N��G��x�u� �@�~e�(������Dn�;��}��Raz\�Ҿ����X��؍��%k�Im���K�>2pkt������I�/�g���$f�L���#{�7����/��\�C�r>r����L�o����8�0�^ؗ觘�����V%%!Fm��D�m�h&ǧ���e�%��o��0�w}І���:�H����������2�A -��\{�����:U�7���ՕG�CW(0t�"�"�FW7�'�`I�&"D�����>u�iR� �uU���1v�d"�DP,�9��g?����s�{�{ޯ�{�AE�(�&}����(L�Ö��InJ��*y�AVr�azR��2Qa�HT�''l�JHP����m�?m�N����bE,�ś��ohc/���[G���>��:NI2��x�H�!�"W���4�fV�J��}�2��"5�(Y�l��ȒƥYŦl��I�+�N>e�|���E%b�$,�ś����U��s�C=�&�N�*�$(I�FA�'��'@�1Y�DC�����M��^\F��������ȴ�#���mH;)O�)W>� W -� Ja�!UX�7yݏ�_X���@4s@�j���T ?�9Yn�T�����H͝E�B$�Hr�u�r����F���E椙D��� -�u�z����5�m�k��ڄe ��LaI,��u?�^�:�����1)��WA����<g(�}�T� -g!�p6�56�Њ.Z�Ya��p���B��ڂ��e�oj-B -�Y�_� -��c�EX�� ��+�o�ZY����A=���]��~.�Z���b�JF`S�DD��@d�<l,_� 做��!Z���놕���'��e�*+��������fAe�̂J�͂J�Y�VR,L��1ׅ�W���}���+�]�(���J5]a��J7�W�ź���>k�`u�ߪWh���$�ԑ�+� zA����� ��w������\}�H�����J˿#��r���i栉V�>�9�~)�U�@�6zz�0����tD�./������#�vk���.�u!XV�!���,�Wj/�/�]X_����&}��+��u]�~�E?���o����(���I�SQ�>���ڛ�ԧ��IJ��C��K��`��IX�0� ��w`�:�C������1 �s4�6Uj�ij��6]��9�X2���mھ���ᄒ�����f۩��Ȫ�w��+�7�_4�a�a�q��GGcV�$|�<���O�/��じѲ�[�1�SOnŔ�����L9�S[�'DŽ�����d�3��@���A kP��ҋ���>>��I����W|pv&��� ����}1��?�]����0��1�r6F�R �4���v����/ -��@��e?{`W!����;�c�b�!�y#��P���0��F�SïZ�Oל��:��0���po���z#�.7�0�V��s{ ���.8�p��.���*�Q?��a졌]~������`�`����C����? -���!{4��`�1��� },�Y�j�>�à�|쩃���F]:��f�KX�L�>v�� ��a�3��O�ޗ�_�6@��<ׇ�SH_����_�������#&C"fs�&(� .:��`�'���>�3~����/c������_smlo��S��Bz�ZB�yl�q'�d����P.>��`�� .��a����� X~����O;�c잌}0c�l��/�8M���C�SbM��.>����uN��7�P�AH�q�=9�0^s�����B�K�/�T(Ǎ$�(�<�#����T��d9DN�K� �Ё^<Gg�-�Mt�"������zn��2�����R>&Ἆ% ->�"���T�]di$-���<^\�S��xbЋ�~�:XĎ��L�"�< ��s �H��w�Ǒ)R_�x���%��=���\R�Wf���L��:<�6�Wڌ��g�-mE���?�=K�����ܱ%v}�� ��t���c�_ �?��Wv$��E�,�e -�ʲ�T���Rt�W�ˡ�����09����p��>n9=Í!mCZ]�� -\}a��m�a��v�pc]�Gq�Hf@���+���uY��ux��N�$t���[.��{�ہ;�����ny�� �_q�����^G -��8��� -b������zx�#'���s<��G�W �\��#7��;�F���l���Av�F5�^q���[��tz�Ӟzf�vNm� 7\Q���,b@ @�@!$�@� ��"(�".#Cqju��R��z�V[�9N�u|z�_����>�}����߂�>�ĝ�>���eS���<�.����G����_�0Kf�0|F�_�~Ju�~��ݏ��Y�ә�+?³���{`���J1��*wV���|�������AV�.Ȏ�����ø�~�7�`v��l�|��� -f���p�L��`�ޠ�S���R/�ҙֽ�g���帿n#�Z�pu�s�������qnk~��3!-�q���� é�386���0��k|����N�%#��+ػ�`K(s.���>`�^Ƿ�?Ɠ�wpo�qs�\ َK��8�#3;�p6��g~S�S嘊4�D�~LF�1Ջ��#�O��8c�9�1w��<u�Ÿ�d����?�=��\�5t���`�Dx���&���s\�[p6* -�c�0�+�cs1�[�cqz��U�h|#g$���H|w8�w(a��P�,o �6�/� �7�KO"s�&] /�b�r[��@���ó0<��V��#af�G8�� �O��؞T�&e��^%��Zΐ��iPl������v�Aq�K�x̵'y�ߝ|�͑�W����n���͞��m�5��.��灭�k{�mp����N���cf�[8��{�0��#)�NMơT$ -�I��J˸�ռi�K����p됌��%�=�$W<m҇�-�{6I�q�Γ0��؇ԃ :C��߈.Ӯ}.���~��_bT� -��mL�F_Fzei��p�3U\G�ιCfti����d�.�ِW�lJ`�}.���Ͳ e̫A�<�G���/��>���ԃH� -e��=�w�Ww|�0��C���Fov��p�%�����p�r4<[���%�֭I��c����,6ʏ��������M�g�Z9�ș�xυ�r:�ާ��n�ҞO�ϦОK�g,�C��O��܍p䅣C{~ -Z�e��|�)_ͳ�����*�F�ٳ^�&0)�D��c>Պs�U������}� -&"B"��c^�����)�]��g��9���B_�"�(>Dg�zؕa�ƢY%F�*��_��5�T. *����n*���)� ����ʣ���?��_��eJ�MDD�W2�B/W����h��Rޢ�g�V��l`0� =�?AG�R��֢E���hX��`�H9 9�N�������+�M^�f�A���W������u���Z�?��j�-b""$�����?4wh/R�P�;��Ѯ��C��M�/A�f,%[Ѩ�D�.&]*��4�[���U���2w���K�� -uZ�O����F���Z{�_]��O]�|�7�5L���<��Dzԃ)9p�rπ�RqЪ�!���`.]�z�f���Qc�C�!FC�S�!��`(r�J�t�*ϒ2����û�lȷ�촿�캿R�7?���(K�7�B��� �J�s�z0A��z@�Ϯ���h0�Sy��+6�Ҹ��`�܋��4Ni��Y[Y袩��ՕFUe�@i���>yƓ~��k�on�!��r&$����{��NHN�;���%��(5� -Z_.BM�"�?@yM��!��FAkJ�Ɣ�bS�S�)߹Фq-0��)Lu���V����;�v�'�� -y�Yü3������`��h���p.8Q��P��e@eІ -�]�|Lo��~ JV��1E��P�wCiNF�%����r�,E���5�R�ii��0� -��B�yN$5�E(m|!�60���y��q8���4�����ﮤTS}ʁ�'h,�Ce]eS���#�9�-ѐ�$"ۖ�,[6GfS:e�t�i�*����g�qO���.{�[y����71����w���t�,��z�WEs`��(j6r�?CV�R�:V!�#i��v�B�#Ց�}�<$ws�]ܤ.�sb��%��k|��x�����q��N����f(~�S$���n��-@�жP��gHz�@ʁ��~�����o�"�������gb�@bJ3Xlj�;E v�<�_����2������$t�LbQ4q�DAQ T� 2@ l:6�i���8Yi���������"�a�nJd���%��)=�+;'qf�Å{�s������<O�uĶ:� Y�#2�MD�li�m�r5���ߐ��U�=g#��qY�m���D��$2Ǘ�]��� -%,w�s'1./��y3��=F����ݟ�����}�!y ��BvI��<�\ς/t d��i�{�X� �)�e'n���!���#ÿ�"�З��a�ZJ`��ObH�tJ�ap��*]��&���e`��J��_l�/�¦�t�h��(?Eu�ܢ�Pٳ����c� L���E��|�W�]�9�N����S@�����H,=�L���l<��Q���W9��>�{�M<�<*����K!K�OU�?2��l��NxKٓ����Ka��[���B�oZ��Nt����O���@ڝ���h��L���8�[�K5q8_Jۚz�6����S�uݧ��ə�U�_ra��>y��!x?��C���k���^�m�u<_ߑgt���\�ťQ4���W��:�k�bu-˵�p�*\1p��-��z�L��S�T�S���2:*�g5��� mj�U-��m���g���"�k���o��k�hAԠEH�n��墐�ǃzy����z+�n����U{�j��_����w��g/�mXn?����؋�t�������F���h�M���&�t�}���Y�>M�qP�Gt����'���jWvKesIn<��$���\:���?^�MF�Y z��&=��tw�-�$D"%^ޖwe�,��H�dI�N��G��G��)�Q�n맑[:�����C�ϥ����h��p ����%�d�,�$Y+���P�ve��beW(�������}��\ё���?K7���t�����z����X��{�:*Qf�\�@�-�Ѳ�G��xhI�A�x�V�Yr�c)T͇���2k�����j���P��p��oZ��4S��:��Y���qy��z�Q�(}�����a��4���}��ܵ[��$nٯ�}*��3�f��+�\��Ϗ�Ǩ�������\�FN�5�z��/G� 9�c��_tƴi�qn�qո��i��pN�S ����8�[N��\�:}�e�E\t^ΏΫ�sI��.������.{�i������d�|��՝9��pD*�;��V�;�Q^kLg'LW�G7�G�W��m(w���z�\�<��.3��2���Q�ug�.�t�$Nu[ˉn��{�z�Qݣ�*�**=�S�u�ލ��2��6K�S����n��b��P/z��x�����M/?.y��3��I��9���3�����̧��'��)�>�8�/��~�|e��IY�Ô���p�U -���_��I�S����h�������rç�>}�� �D��|�;���R�7�C�fq`�{��!��)��������a[)�Ϟ��'7�";���yA��֧�N-�w=*��`�<���ז��93��c�P9d#(�Lq�t -��d�w�b���3r!�C��7j��R� ����\��.%{�1��ԑ1�>�c �%M6�iʸ��:_lŝ�[.n���|5�3��>��R:�/�D�?v��f�k�,v��a���m�+���̈42&� }B�#��y��Ȼ��Qƒi,�G�=���{�Ǐ�;C�|�ָAVT�ڒC!���EAX�y��L�b{Tۢ�$뵙dN�Ö��'�����)iѫ��jI��fYS`�Si�.�����[�ɱ�zu��Z�ߌ���?�bj��:1 -���/��8̉=��z͏��� +&���X�'%�6�m6M�Eꔿ�a�G��[b�<.�j]\����L���|���lWş�M��a�2�جx�zy\S������08��jMu���* �/�-91��6�?S��?��S��0m*)����7�a�sY�0ߒ����ꄕ֫��$%���=1�ي��͗'~�|Y��KM�%��vq¯l�dz��^S�I�;+�����<��3���ag���w]Ū��*���=<�Z+�ZPDH�@ I �p˭\���ZO\w��kw���j���ժ]�Q��cU����������}8��f��ꃎ�hY�#�!j!j���:&�11�P�c�ڈ2�ͣT�%)ָ��4�nM�4_�Y���/�ќ��47dٚ�B�F��Lu~{꿥�8vQ1o�d�%��J���EC�;�SOG�&��!X�2�Z�h�(�%�P����9%��|�<]�4GW/d��Y�} -��¡����~��u���������^��}��>`�Vq�0��ֵ�CQ;Uڏ�N7��KQ��Ba��cQ��#Oo���gH\�\�,}������vE�~�"]Ri�_SZ���DQF"�$�@��u���/���P`/��E3��&�u�!����ĩ(��Aa�"$�@�a5r j�� �6�xfm�1��n,ҍ52��Ma1�R��'�&���"� �R�E�H_F��68�>8�>����8�-g���(K�"��(0�Bn�B�R�!۴ -��(8M:8���Eb3gz[ͅB��Jf2�(RL;�F���U���@aH�ɩ�,9E^����Y���`$s/�;�F�^Չ�� Q�: -y�Ip�g +-NK�PdX#�n��Ֆ�a��%f��+�V 5Z+dk�"ɺ]���]�h��L��W$XDyB�(#�������8�`�����mI��d���m�\�/�l�H��sa�X����W�lW#Ց�G���������;ʅG�\��VhS�9�U����e�2"ĥ?E�w��p��v����nN�&��94�&�i�t�۰:�� R�������HΎBR�z��#�e�Ļ\^ZW�4�U/�][�1�^y��<:��,:K�3�"�v��G�ɳ<���A���H�z=�R?�;<`�S�s���;��@$�/E|� -��h4�+�CS��vgJ��E^k�uޑ�MB���QpQ���YX�'J��+z��U�� F�}쁭�#�ԭa-rP���9��3����/�C\�-�uIbJ�!�d%֖F!�T�5�)�(�{��x�*����ux����R�]�R���R��^�bRԟ��������]6�"u˳���m��Hq%RĔAԺ7YᏈ����UUK��jªW#�ZUuV�X�yM��T`Ym�GH�^�����շ=��D��JRџ3�y7����ks�#Q�H-bI�T1��@��+6����)XV��4"�!KUX��ōZ,jJAP��J�����;1��4�7�B`���z��?�4�=�����[Q�Q�ZN*��Z@�L��Y���?�X���7~�y��0�}>����0|��Y���bfW>ft�b������8�u�����EL�H�^��砛{�V��H�� -f?֝RG��f ��l~'0s�Ӻ_�G�F��m~�`�TL� ���@L� ���0��#�v��N'��U����᷻~�~�ߎ'��#b����dv��&z^Śs׳�� ������t�vf�]���^�� ���7��1�����}pFZ����0��GLޛ�a� ��ѽ�{�?��C"���f�]��6���`��܃M����BjP{�^`�`��O�<0��z ��|>'��w_O�+�fc��EP���L�gl��-�pn��NB8{�3"���������ug��j�P[�C}jP�}j�M�CN�>�����y��� ^rÁc9���x���^�q(�s@��^a1ߝ#w��؏&�W�r��fz�? � ������=��ÿ���$߀Z�2�������G����uE7��\������q� ܩ!,�.�{��~N �/��[Y�v���_R�00��cX�Pj<��/P�R�.��\'�:�$?ѓ;��`<�7x�A��\�1��'|D6�H?D-����)�~F7{�������}�Q�]~<��C��ʿY{_�W�5�#���G��!��<��{�Qd�M� ��D>�"���.�<G�82��z�sa�ŧ$��"�L,$��<A%c=���\F7����:������L������"7��ߗ�a΅/A�"��l���HI &b�n���.Ӡ��,��v�B�帢������PQD��*K���Wz����ED�E ��"JDTF�(��BRƨ�єK9qƭRS��˒���s�����Y�YHs;��饤WEzH���D����H�g�xHn{EO����� �+1�CL%+_"��"��H�&����Kz;ɾ"�+'�j�;LV�49Czt��:(u��G��6�r����� ���O���ҷ��'oNv�� �f&];if�^ٷ�TJH��q���m3�6��ׅ�zn�Wy�N�?���qz���w�� ��=�����3ţ�'���N����O�(��Z�{�4�뗃��v�N�"��W���kp�=����5�6t:_ǥ�A��>���P��D�0�s�`��}6�#0�˨�`#'���yx9�^���E�?8����P5n3��a6\���#��ӥ��]Jpid�u�qqt+�\�����h��n�&Po���D����@BH�}���s�7nc�|�t<��\=q��?� �Q��-�MԢcR.N��mJ6�O�C��B|=�-�kpf�14�8�S3�ljYw���g86��~.����0�A��6��� �����p�8ܙ0]����5�)B��0�Ε�������V�^���sд(������*4|R�c���?�@��m�.y�Kj�jb_7K��\8�qt*ml6����x<���ƕY�1!�/�B˧pzIN.�D�G��� �L8�܆��8��+ -q賽8�y<Q�Նj��P�� *V�F�*�����\I{21�J�<�*m?.��9�ퟌ�Y�98�|=}�� ���P�Z�C�J�z�q`��צ�zm����K�T��Fź���C��u�����������.����3��ΧG%���ͥe@�R!ZV��i�t4�qG��j��������Ea߆8TmРr��6��g���<�~^�=U(��nQ -E�(=@��Ol1�u����@w�V�h1��4cӾqa5��T�O�����qh�"���ľ�?T��`�&1�6KQ�9{��Q�ł��t��Ů�](�@~�a�nF^�e8B�#7�r��� b����������Ej�-����}�o j�P�i*�,Gy�:��P��/�Q�E<v�j�3Ԍ�0;v�mE^�N8�ː~�������Y����K.=�qi�iaT�{��W4z�� \&�V�7N�h�w��?��AE�l��z�8����(#?";"�ȋT�i��(r���U�ْ.S��ϐ��Ӣ��F�*H�<�m� ��Л�\|�N�7��O>�6�H;�az�9�"t$J�3�;�Q��C�yсpĄ#7F���R=�dd�ґ.spi�b�.��ȏ���B����"&H�1�,c�Y�8S/��C��.�A�&�9C(a�o�Tƣ$b8 -%S��yROl��"G� ٱ�Ȍ�FF\<��H�7�����\.YQ�[_ -� B��Ш�%4���`��8�\b�{�BM(�)4�4�iΎ�9�(���0z0�e�]�����R� C�teR���'�aKP#Ye�U��j+gV�Fu���>*ԫ[�t�NZ�S�V���k�=p�xM�p��p�r�,�\B�F4PA�Ь-�m���V�E��ijo��a�!Y#�E+E�V �.&]2�,.Q����� -��#B��k'��S��a�� ����f�;�Q.�^G�4���d�o��YJ��)���vE�v&�:w$�W¢�CR�f��`4D�`��ި��h�֘����y��L�4*L-�8S�0��Oa�� b߃�q�xB�p���F��I���ƾJ���P���T�� Ӑd\��3��h�.)ڤHh�b����`1CiMC����YKrk�@f��Pj�&��<�$�!m&L�{�C/�*��9�A#��Q�LM>���Z�w ,�0Z&!�:��e�ؼ��mDBJ )�O�"Ξ�X�2{ -����I-�%�����ӂ��NA��� "�5/��n8q�{��P.пpFJqPQ.�hߠ�a }#�ڻl���CB�,(������.��5�^� 3ٲ��%!�P�����X�/3c=TR�Fi�ޒ��-v�ƌe��cf��1������"|���z����w?ϽƓ�k|��fS�'1����11`�_`�iL�^���Ӧ��M�0�Zm��Q��qM=�X�@��@{Qވ�ʁ܋u��Rsǚ*|Аik�1%ȞIA���n�� eܺ/����i� �è�E�Y͈�0��n�7t>�%x���w�s��x���8��xH�p��m l^��5wʽ@�������tBhm�ַ`̆Ό -s`d���wgx���F��'b"�G�`X�|�F��+2�O������#���_q�x�{�����)Q�U�,����H��k�h�{nL ��ã*㽩â��c�g1�xnv�c�'�>�ŎaH���fP�\������?�s�M�c�?F7���9��o��]�X�?�T�^��)�(�5�y����j�lm��$�'u�9ّ~�.8�=�c����G��8�̣g�Jz��a�j���B�R�bg~D�d��Ib�;���w��Qޠ��L��=5F���\����>�:v��f��kfW�d��s�@:ey�1ۗ���i�3��Ŵ�D����C����yH�,Mt���ʑ�ڧɟ�|�E*��W�ৱ�;�l���K��@���<�*M�գ��4����4�w�A�;�|�[8��9�)ZI���:��GK�U���7�S��=Y��ʁb^���a�b�n���v{�f?4-�����ъX�E��FT,nK���N��t�5]5�S���\����%yj?��7�#E9�OPO�wU�zA�j��G(f�]��U�����hd��q�z -L߂�&8SSM��.@������/�2tI�e>W�h�*�W%�zP����x��-�G)���B�=]�r{h�N`���9 -�'�f)X|'�9qQ\WMp�n����n��A���u�� wu��]�K�^�{ -�x���[��u�b_�3��ъ�Sq;��U�r�W���(�e��⦸%~�ŝ��T��V��G��?�����s����R�x�|�R�����C�@���^�������Bn��ʽ�ȵ2��_B!h�S<*3��3���ǼtQ4tQ5������ڀ�6�����Bs.��>5�� \�01VLs�b�R�P^�bxN"�H�)<a��r��q�?�Q������������[������wi'����)&�Yr.��r��+H� rE)�<$I�4�r��/�1=������v_%{�*��o�獿��+�6��pn�G��r~)�\E�X�r�+�`���� ���P�{�;��}���<������z��8W�7�:�CQ������9C�h9���7��k.U\����BK7T��l�rq����uR���i��DY;�'��e/��JT���u=l����ʱ���s�|��#��\QM.�&?�&�U��l�{��3SJ�|��;������`�'U2(�0(���aT��a��aQò&F5�j�T��}��M��0yq��(.V��9�|_�����Ҫ+8U՟Sl�1�������;8X#����SP�yu�����v��]��Fe�"�Lu��Z<�Ӑ{5Zs˲+W,�r���k�p�ʏ�zS9Z���`�%Y���Q���C��$��& �i����v���09�ϑ��6魞���`�H)�0,_��ڭ�X����|�M��`ՖR�o����^�I~� �k�%{m���kr�-cg�U�t"�C�c��)��N٤u."��w����֮�I�5H�"���ڌ��P���V�uuN�l�![ -:8�������c;�,�)dt�Iz��l�_DZ����'�g�^I��V�$:�'�w ��7�q��M}��Y�J~��'j����_Pk+��*s��5yv���@�� �{{��g�}'`v�NR��l�7�D�$8C|� b��y@���4p/Q.'�p�N��ֻ�����r���ow�Q�ٳ���&���"�O��ٓ:�?�=���K �� �L����K���l�d9QnD��'�=�0w3�=v�q���S]�AQ�g�%�$�f��M�6�hR����D�#� �²,�,��+�"� F��4$QaRM��qr֘c�ql'cM�6m&��h��c�ȶ���c�g��{��}�MWh5���BE�Mb������@��V+�h̜'��~���Y;�C�2�� b7�oS<� i�mΥgK!�[J��Z�έ>v��k�=������$�@0�� H�����LJ��I\��<���I�o= g�=���:�W�r0�~7G�w�J���I�Bdvnˢ3���$mI.Z��iI�Ln�9��@�����_����ƿRc���^Q�&$nh���)��V�S��y1�z�0�����IXHOR$]�k�L�D�1���tZRMӊhNsД�1������3����6s_��x3/Q�y�����d�p��?�Z9"u��iO$��8�:�ѿ;�tÎ�e��?IK�F���4e��ʡ!ۂ?�N}N�95��6���Aun?U��xL�p�.Rn�W�)� ��3;̷�ū�wb�UiOV�CZ ��ow��tf̥5k ���r�Ӑ��)���,j� -����滨��RUЈ���y/��a\�p����|�҂��~�$/̗�ԃoȃ��KՎ���GtfM�%wMy4䯤�`-�f>s"^K:U��Ex -����(/��*j�iݍ��,�� J�oc�^���[��!��E�������K�̈́Y:�C��n'P�+��E�F�-���Keq��T*�s(�Y(���,��(��^�B����V-hE�7)����̶�ba sCo�:�s�`\��9�[&��h��0O��h���T�<���� �J�RV��ÑE����Æ�YN���3HaY7��g0�'��u��>����#��4D�=�u�������=�u|�YX�Y�C���x�(w.�Y� -�+�ˀ�<���t��&�*�X*�0�����swa���y�l���Wɪ����]!2��\� -��V�S� -�ʠ�Ү�A��]wଘ�ݽ�'ke��1X��)�2�_�M^�Su)9�J�� dy;����#�w��c���$�*DJ�����s��>�P�G��/ݝvy �j'�*�sWކ�z�| -j��W�8���ȩ�Lv]"�ud�哮�Z_����I�}$�GI�%��'��"�V�r���y_��yp\��K�{�����]Z�m��]5`�����l� =���(���$7ő�d$�)�mͅ�7;�k��5����>�ab���LLӗ���6��0ou:�1i)v��A�r��K�m�W�({���2�m��o]J\�*��=���X� Ķ�ӑ�Ǝ�wxX� �sOwbM���������"�U��9�{0.�GT�^E�v�<��GڶF� B�b�E�؝w��k6�w-$�{k�W��'��O�$�do&��ZX�[ƪ�:V�u�X�~����Ѿ�<��7��|�� -�r��j?(��:�N�<���Y�@Z�u��^�=�X��V��!r`�����j� F��AK�'�d�/&b���CA �e�1x��o�`�(�����v�t;��Au�U�U���N�v���S���D�Ăg�f���<td/e��J���sFR�}4���:��zf�Ō�a�=�}Ǯ1s�{f+� 3V�^���mi��t�K۸�a�3�hq��z�yqw�ɝ'�qlj���d��5L=i�����6nfʸ���ۙ41�-�:��ƿb��SN����N�G�6�@�n(��w�G`�t���0s�| ���^�.f聛�˥%��r8��sV��\��e{�'v�a8���D����8حwQ^��f�ނ����'���1X����;W�3NôW�V�+�x�V �iz����k-�5��ëe�]��K��}5��} -/��o�k���OJw�t�u��.P����ҟ�����W��s0�����xO|(>R@�|'\Q@����-�����5]��j���T������/��Ve���_ޭtJ�Q����2���`��"���v�p888(� �rQD��7Լ!^J�H�Ԭk|�f&s��K�̬�ɯW��g������w�k�o��W��P���L�NT�å�#m7i�=ݏ)��+�'tψ����Ph5�JC|��tPs�&���� ��?k�LJ�m�m��{�c��g�����-�J�]��+�n�~B�����i������;q]� ~J5wZ�o�v5���4CM��������P�2nl���� snW�M</� -O�/"�U���"G��7ͺ���_Y�Vs�~a����[��v�rS���E��&�������<��N���"z W�!|E��� -v�M�V���U$�Ri-�V��P%� \�VZ�k���\�_�Kz�K�tQ|���:�����sC\�7�-���v4?)7�d��i��O�V_(���Z!�u��$���x���g��_J�'���Z�����!:�GEˆ����!�:Z��/L�b��$����%O9*��(Q��Ik��V�d�Kk�t���9�����vu\�ӪG�;��C<&���mC\\�?@����-��J�|��p���4��+�8�|E�\�m�V�7�vi��� �<O���_�|C���?� �'��V�c�j�*���`�=Z���-Dz&�J����!_Y�5WZ�RmTN(�������#��i��5=Ϯ�שky���jŶ֍<��E��Yk�W{峻��)�n�h2���8�"�C�b9�:��s����o�;������<Y�k�*�վ��j�{j?�O}�֎���;��bP#�⾾�9��>m���i9�4}��-{���j?��:N`_� �v�浿X����Χ�Q�mۻ�Pۣ��=�噥lzv ]6��e5=����Y���ƪ^wY�۠��A�Cͤ�'a:���ۏ���v�x�SW����g���e,;��Om�0�>gf�� ��'��/���o5}�Rݯ���Q��l`Հݬx���gX6� -�]u��,e�1�t�|�>�Q���ϣ���M��ґ���b{��l0�W���5���X;8��!���d��V�4��aT+e���,^�b�:�����/S2���E��n0��-y��c���]�����̀;�=�V�g�8t ��FP�����|9�J�=�,�`ɨ�,�I��<�F���s <�(���1�z}H������`���{x>�Pܯ��?'�������U�]����Z�+U/�j�0*<ǰ�ˏ%c�(Eٸ8z'R��d��J��P�SH��" -�|�O�L��>r}O���%پ?1�� k�A�O#�bpY��i��Fj�PR'6�ƣk<;Q9�7K�S�3�2_J���N�D3E��̛�L�:������gn�BrV0GM����d�Ϭ�/��Ō���{�7�s7]���?�6h��=o�5z^#*�w`��e�����(�D���D�7��ܠ$r�Ҙ�IvpY!��`f�2B_'=�8�C/�rg�n�`�� �C�O8��k��fNQ��J�xb[J{P2��A��C^���S� � ;l*��md��2+b&3#r�QBz�2�E��Ԩc�D�'9��p��������\������YG��#J���8�+a}�JN�(�#}Ȋ -$3*�Y�&2��̈I&=&�i�9���H5-!ż�y'v��簙n�c���ȷ��3��Q�;{&��)�A�f �P�� ?�3�Q�Ɏy�,����d��I�����L������;����8��a�+'�R��R��r�x�Y,q��N`j���U-��Zcy��Y'T=n���ՄSG���df�@fĹ3��EZ��� R�#I�N�aMĞ�$1![B>V[����j������O� �1Y bⅥ�K�O�W-�s�f��HX���T�W��1�knj�LK��6��h��'���-�DB��=�}&q�<b��+19��8@����kD$D$����Zt��o��Vi��QJ�Xsh�Ȍ�t�c8��&���c��$${cM��%%���hbS-�S����8s�r�'2���̈́����1�Ϋ��%8� (���cT��7�����.�,�+�\�����PGP�E5�qbP�� -j0.D�,eO� -2���H�8И��"6�����Ĩ1�iMԚX������q�=�s�s��=w� -�`9��'1���7�ڍ��:�J�mr����� �,��YZ�e��Z���k��E!Q� -I���\�-ӻ�'5/��<B�#�?�4h�k65q���9��]�;o����wc�F�Ձm�2�L>��|�0- s��w�>]��+�p/-�X��~���0yD&hN�v͎,����� ����=����r���uj����]���o�)��&f��'LZa�EQ}�p���:B��c5/z�<�gjN̻��Y1���N�!�������G��>�[�5��>�[��r�j�ۖ.���������ƀ�`f�͒os�V�3���ěiV��f$�jz���%��Ԥ)��<[��hb�R��� )�%��4��iLj��S/�9停�^�9��!�����Z���y;�P"qG��N��?��ɤ4c���S�3�5c��f��K�x�Κ*笹��%�,9f��C�f�g'jxN���T�.�l���.��2M�7q��WsQ(�#��Hr�EZ��Kq�چ?Srɑr;�~g/ �9H���d�?R6�e]0UC�\� -��V�* *ܤ��Ѳ,�V��2�+:��E�eQ�T�t�;!��Zbߏ7o2ިX)��{��I��lg��)�(��3��5V���Sb)��.uT�RW�(������l��Y�Fo���sY�L�w�c�q��ݒ���ԩԠ�%-T�.��2�w|"9H����,�`�4=_���U�K�A˥�$�J#��2S�*������[E�Q= xث�s��i��� j�P����J��d��(ǹ3�Z��!m̖V�J^�<s�4�Dr�k͈������dRG#�!�0ѓ@x�}��x�}����?C��$��9F�t����jd��O�!�+^K�Isˤ�xGUI6x-�x���i�g�c��s��^�+]�!�/�+.4��:/��<�7x���E�Ѭ| ?��F -�w[9 fr�{P�O�S�:�5����o��ދp�� �n��>�D3Fs����s;�C�����7��R��r����a8_������@�C��1{�F���l]/�e9F��&�k��/�7�����>{�� �Cr�����1�x�%��z�g -�)�~V�0s.5�J�����"�v�=�?[�91w��� p����J�A���H�~���<chyA���Y6�h - ��5��(8�b�l��l�4���>0� f�"X� "�J�zɌ����Wf�̺��.=W����m��g��1��I����'���O���`P��9נ���=�wX��q�f���B�LQx��$3����ѿ��k7�r֭&='���y�?����f�;p����1���Q�^��-��;�No|+�k�@<ax��{ųz -���J���؇��R��:�����N�� V��Z��o�L��sy%+��w���sq-��GLX1�8��l��ϕJ�d�ʥd��ve|J��zA_�?bG/8!��@}+��j���gK�ώ8���s�-��CL���'�`]�6�(Z��@��t����3�H{�T贎q"�p�������pjZ��M[�̔<�"^+�ʁ�p<Nw��ĵ��V��b�HL!��+W�t�L��Z�[;�d� -�x��`)��V��7'�oG�]8Grj�י�&��l|q-õ���W�jۄ��m���ũ�]�*�g��C���Q�ʌϨ��S���@{:=Sqg�vu1������7_9�Ԟ<��@�m�Nvp�1�i�5�Pu�E:���:��Of�u�{�tSy����3Q��ҵ�W���ޫ��G�˼^�}����r�=�v�r ��{h˵���� W��7�1M�uʴ�� Uu�Q�4w���3Un1_��/V����X�=7�xP�v �P��X���|��YkǐC�zB�C�*�������T�R ٺC�R��̹��/��g���u{Լ�[ԁA�*�r�ޡSUl3WE�^*�[��a+�7|�r�h�ۡʱߢ,�De��T���9�)ձNɎ���xG�NO�dPD;�`�@x��u0�"[^��nj�^�zȪ�Jm�i�p[�;+�a�r�fj�HOe��V���2���>z���)uL��]��M c�?�D���%�̃�.�0���#"�J�4cE��p0!���d�lv7�d7�c�96�d�M.� �� IP�`�h9Ro[pTF���th�vh�V�ڻ���G2c��L63����}���~��4o�y���?��Y4�/r�6]����`�b]��vH?f��cn]��Cws$��v=�px�"�42e�?����.쮠'���F:�tб����Q�b��p�&��h4|��p�z�W�b7�۳�_���ш{G�/i쟒9)���1�߉�W1��C��2�'�>��q���e�o�3����2��iKl�%���^��ٛ<EC���R^�.�xS��&�T'�����ݣ�Y�Y����{1�_<�[S����!ý�%lbb]�1t�$ў�I[j-iv��JhJ�do�F�=�dDk�Ǩ1>Eu�KTf~���G�����P�����j ސ�従�Sb"N�b0�V$ѝ� �!�eD�bL�)3�Ƭ�Y�4���Ln�L��f7Q��Eu�A*s�R�3�;��rߣ4��d�W�&�Id-����]:��;�'������%I_H]Jg�ݴgn�Ŵ���p�9�sS�嚨5[�+�*��J�^*,�-��Y�Pj=���<���8,�Qd�Na�&�y��ڇw���{:U}��=�P���^ѩ,ؖ���ܵ��6����ךD�-�*�O�����U�4PZЎ��O�}�}�B�<v�;�O~������X�|��+V�X��Ҟ�戬y���R�-m�[A��^j�7RSJ�=Oa��tʋr(+ʧ�����b����"g/v����8�&��;̎a. -�[(�\Q�_��;g�y��8$��f�/��+XFMa��R��Nyq��\�T�]&�.+E% -Kʱ�Ԓ_ڌ�t?���e��[v�첷0�^%��d�d�F�^���������n��_��+�*�]wQV�W��e�(*���< ��H�ی�m�RQJ^E5fO#9�nL��dy�c��&�s�4�oIs�����e��}xEk��j���p��Ю5PL�*�+"��ȳ{�F�v`���R��:���l�kl�j�ɪ�`�6��� �{��$��S$z�$��)�5'�*@|�"o�_��l��:��GMҮ,U�t��R���6,�A���Ov�VL�]d�b0�'�^o$��LjC!� �$5ԑ�o#�?���({�O��9������Ѿ��u���>8��O��qi��@�ꮗv����:��%��"�q �MIn -!�9��fq�)ZLĶ؈iq������f"[{ o�`W�;[_%�� -a-!�)@X�"�)=�ii.�nE�&i{��U����*{�@|�2b��&�c=��Du��ExW<w���+���Bv�!!�~�ww��g�-=3��Ȧ���3���;ľ^P�s�>�Q/* -�S� ����Hڹ͐��� =�_�+�⡾ l�����P�D<���#?��ࠋ��?����C�z����Y3�k�q_��}�w��Z�'T�W�Q�� P+�R�T[�/��>�������+Y?�ڑ �7̚�P~0�=���f���V���k�����j����㗹c����UÁoxF{}L�=$�����ivh��$��!��l�uʀG���嬜\��Sk�mj˦vp�T4�L%q�tK�|kZ�M�A�#bV(@N)�N)N�aF�c��n�>���v퇼~��A��P��MG�? ��Ê'`�2���ؓ|O����|�j��j�̩��t��Tܜ67%�����u���4)�vK���(8�=Ы~��A�tם��I�}�<#��8-�ܷ�j ]��32`g����ӄ�y5��j^��_��/��L���m���0�G#��i�c�t�?%������s�~^�(^//�%�^���コte_�!{S�qA�.ڋj�*� -�tV�]qU|y��^��ٯ}.�Wl�if���~ -6�����˾�}Y�&��%�x[�s\�!/H�p�L�6��L�]@�����_�]������e�� ��ۮ���١�1i�cO��� ?��L� TT���G\"�� C)Q ��(DdSY��a��af`�e@v�YP�h�$.��Ւf7�Z��Xk���1դnhbL���39z8����a�����}�b6c�����9O������c8��&�&sr�{��2��q������!���A>:AN�QϏs�*�#�`���x�i����iw��������"_�;�����4� ��6(�EP���uL�ph�d&�Af��ď��X�L�$�p,��̹�����3�w��iv�E7'�<�9¢�� ���ri���'��w���&_�͛��)�9�Z����D~��>3�KO16<Dg�zz=m��������o�N�7���L�?���x��� -���@�3q"��T2�x�@o8c�glJ��)��<:J���*驥���Vz:��KG.1���,�s���\�Yf�S��9�v?����s?��RzC��h:e� ->G=�,�|fx=��u\�:Σ��&zvг��n|�A|������~���� '�p�ԏ��<f.�Í�E�1��p�ǘ̪�3c�r�|T��#�Ӳ�Y�㣻��]果d.~�\��o��Wo2�#��03�M�9xv ?���H��m>W���b\��I�jPѓʻZOc(����rF\��X�o�F�pO1C��A���x�w�cV���{h{��]Ã��{챟B�z��?��鋡KƘ4��ɠ'���UF�&�����3���ڻ��}ܕ���.VS�8���t���d�g�]�����ω1OgN��b�Hg}���'ƛ�7!�סgR1^{Ά�ɕ��{�[�5��S�������S�5�*Zg�C}��m�������c�3_��I<J�Md�.8<aN�����N�B�L)�]��7;�<��.�lt��c���>�msk�s�6�p�@˼��>�(��?@��%���j=~D��0F���5����_�g��&�x��i�pqC�>x�m%�������eh�����XhA�g�=��� h�D�W�.څ:�n�xB�ϻ����͋�B���a�b6��<}���x���Qs̓n^���w�W=�Ǟ��h�Z��ޡh��B� ��*��C�R�e�nYj�JP�|�,�C�Vl^���a���?�4�J�Gq��"������>�G�ܓK8s����+[#ty�������h��Eӊ@l��@]@,j^��z� -[SQhFŪl -*By� ��jlގҐ=( 9���(��Cob]�w� �, ���� -�>�#�8���قt�N���h�)hz� [�Q���0T�Fas��ظZ���d�4<%�e(�&�v#Oԏ\�o`�E��d�##B�n��\�"c~��#�={�0{\^�W�AQꊪ՞��CyD0l"��ơ42 %�jGPeAAt.�K�.f3rc`�#K܋�qX�g�.��4�0���~�M���0�C�=�y�w�mᵁԬ�J�,l��[�/JcQ"GQl -���D^����ȉ��*)B���zX���.�)�->A��:R�������9��%��J�}alQÁW8�\����Z^+"���LGY�;J�}P( @�4����(�5Q��D-2e&dȲ`I*@z� ��Z�;�*�^~:�)h���:�!T2�N��k���!|�3�{��>���˙G2E s�/�Bn�rX��Ȓ�����%Ys��JL��yHU�!EU ���>�Շ�T����ȓ$���s�m�������6���{I9Y϶� i�)\aM^�L�RXT�0�� M�&m2R�z�f�t����C���n;����A��� ����@� ����܇����}t�mh��}.�l�r�N��̂E�4�b�HM CJJ$t)�ЦʡI�BeH��`��P�$cd�&$� 1!��>b�W 6܇8U�8����>3��������ލɜ�HG�{��_�`��q���И��J��2MEz"��*�� H4gBj.�ļ q��-���"��.�Z.Ad�Q��Q1�9aG��Q��[��_XR��ЧM��� -�e�H�\���0$dEA�%A|�q�z��-���C�Ն��zDX�n=���a�Z��t�G�|�q�+AP��cI��D�,� ��f(��ڃDVY.�#�M�r#�l�A�UbZ�� -S�֩v�R�ѡ��h;����&i'|N�Mr��y�����sI~����B~+��[��jPKݷ�s���G��?�K��˘=��5}�����Q@� M ��䰑�>^�'kB�t�>W��5."Dc"��Q��z9�Z��G�yQ�w���|B�r���!�M䝋;��ȰX -ý�� f�) ��+��^��W���?�]cc�5&�O�c&hTl�F��Ԉ���]!oC��R4�`�P�V 1|(w��r��Q�1����9�����X�b���; -w� -i�J�0ij�41F��J#�:kD��|���� ��L�����8M�s䖴T���咔������YNɵrL>/Ǥ����"�9�zoŝH/2 -�&�p܁���~#�����K>�'���f�A��>P����+��qr̘����c�/c�z �iT��Ru��#�̳�7�V��g��4��H �rr�ƛ*ES�F������=b-��9Kr�i��v�렮yβ��]��:�����u�7��v�%jk���)E�LjizOV�pM-�U�Y�r��]H�x��F�w �q���_6�_/ dduȗ� -��%6�.�S�R6W.���WʋV:h�2�Z��rٻp��3��2�<sxsX����{0y�O�����Ҩli� �b�e�M�-̔[��m�µ��6��FU�U4U�l�4x5��N�"`��>7��cx/TD�������»�NR��9����F�x6K��H���;aq��n{�{���%�}��'�~�6�E-8p��_��-��W�o^������ڿD�"_�怷�R��{��|���\י ���1.�ǹ������|¦s*X�'�z|��O�si���{�@��:�u�`�S�7˥W+�A��x��ƿ_�>���N�)8 �O�l�\[�5����+�#������#��4�7��+$q�/]�n(�������[ً���z����ձ&��&�y�h/�@�P���5k.��[\��pI��8�rߣ7��x�I�����x���i��*���gR��8�:���:����ͼ�79/�ԍ���;��XbF)`�,��]z�"�N/8��K��7Sx3b���|a���B_�`̀w �y4�i1Z�)Nϔ��0���u�&=U�R��0�=b�{@��gZ����Q�"���=O����'�h�7-�V�^>0��s6�E<e�p=V�*O -Sl��x����X�[L��`Z��s�캾�_��|��ņ��ͽm�����k8g��<����L�8bu�8n�5�¬{Yp�0�V�*;h��,�)��K,�:�O��z8Ռ����s������]��O^�4�|��z�ĿG$�X] �s�qVF<�x -p�� -�t\{x�>V�u���J�r��M4���Dm�k�tַF^~�k���7�g��8���q#���Q�T<F<�x�x�f��{���o��J;�j~����h��������ٗ�\��9�㚂g��<u�e�%�}ı�8�(A5Z�+����Q����3�Q���ߥ[6�����7����| ��zu�;��P�6��&� -�3�|�&�Ex�� �� -��7"W:ݐ�k#�J"��C��tA��GoP>F�&,~vmֽq��l�e��#�:�Dn^8����k�Y<�<�yV�K(�(����&[�S�U�6XU�d��r[QN��Z����l���Ƭ��ЦK�-ۛe�e�;�R:he9Z�g���uW��U��W��d�����v����*l�T���c�L�b�k��[��ۙ�e�I�.;���Һ^Pj����%v7+�������q;�1�������*�6�l�]�:8���SE]FkC� 2uPn�7��s���Z�u˔�;D��QJ����k��7G��J��] ���tV��n*��S��7+�,8�y�r;�=9 -���6s��Ֆt�Ua�~2�qWN?_�s����)c�4�;��ځ��X�.AJ����N��u2�)�m���j����"ܯ+���B�� -�7����x9�ȱ�5d;�{��_29�S�s/erQ��p�u��!���<f(~���@��*�3D1�ъ��U^FEx(̻R!�{�һ^A�W�����y��j�-���_���]���/�|��s����.�(�;��Am&��zKO�{Y`AXXX�X��(��&5&Q����k2��z%M�1�[���4���1�N:�6F�}L�|�ї��}�}����e~��\Ⱥ�� -�38���d�B-���h˧1�wx5 ��E<IM��#wP5DE�1ʣޢ4�*%Q�)��P9F�����R�V)g��`y\]7������)k���@kd0�Q�4E'�1S�I]l.���T*q������r�vJ��)6��x��_q�"��yq���}�,�������n�۬k����Si��Es�?�U�#��7R�WBU�9T$�49)3�Qjj�8��¤m�'��#���g�I��-�K���m�8�U��������F�cG+s����ڥkG��~�7��3�O|��r\IaT&�X�D��4�R�)IqP�ZFaj �-8���5o%Ǽ�촗�J;�5�2�XR�cI���H�,`�~���-�g�4��n];um�En0M�6�W�|*R(7S�MIZE�Z��[rqXJ��pa�h��,kV�sX�H�����'R3� ���>ʥ�������h�-Q; K�F������qT�M��2���'(��� 3G����d�Yr�r�e��]���&��$�M�m�$��k�s^'�v �����H��9�E��z�_�;��E�ݲ���-�e��P�9��������{N(6{,�v��iXs���擞W�9�����Hr���G��5��c��C�7�Qr<����^��Ks�Y=PLj�(� g�r�M~�> {�6�|�X�D�V�� ��B+��H**�TTCBQ����wS�+��_%��"E�Q� "�Gy[s?&�=��.�^��!݆,�+�ٕ�۲ƓQ4���٘K�I)]Ari��X�L$��c,��P^Dly��f"�=�;�!�9L��A��,������z*�����w��-��nK�z�hR,�<E�L��RHvN�T1����0TSI�+�(W -�Lª� �v\�&����5O��f7�j^&��]�T_#�u��*���S��/�սQڝy�iWJ;_�6i�)����P3���iD��"�~! �n!�!�U�DV��Y�����Z7v���Ǣ��Y�t���0��c��ÂzR��z?�yoW�=��Z��.��]���_Q�P��ը�����V���aI�?��V���/� <ޞ��+�; -��QŜ�Vfun`f�N�w·�-|:>į����y��:�aվKu�iK�wC��]$�Li��)�(���(���k`~�d��ev�\~�����L�ǯLjOO*?��aZO���������x��gR�i����o����<xwyا������*�N=/�v��Ӥ� �Hi���b��y��O���>o&����7�I}�<ҷ� }��K`ܖ�˥�nq��E�'�U�\�f9��rP��)U�V�vI��V���Pڙ�NҟG���n�?%�M���*���1Q�T�d��ӯ߀^�Q"it�Tܠ8��*�ֿ)<|$�����aP��{ԭ Mꁴ��N�vlm�~�����0e~������I�'z��{t�=:ǐ>~C��kه��V��ʘ{�u��u�d��\�L���<-��6����ҭ]`�x���<s�G��/JwH�Q2��P�����;��\8�s�9���D/٣�QyT�8�Kחu}O�kB.��6i�k��V�Ku[�f�@5G��ҝ�_�>��Oz�!q���W/X��L�Io�A��S2�oȄ�V�8���>�Z��*��zqN:wF�����Oi���1���Мͪ9�yX�z�v_�z�|E�&N���xS�Nh̼���v��Cc$S��q)^FM?�?k?/k�G�#z�������T�[s.�e�愽����̐���z���̘�C��![P蔆�,�HR����_�D����q]/���|��oJ�K=����u�vz�z���h�y��} -&������sqL�-��Xh�|:�n�ۚ��2�wd�����A�V�q_�@M�N��9��3E<"Ƌ�Yb��"]�E p)���G+�UνK���F����K��k%�*y}�e��zS���y]w�CO�&>��b���7��r���!��Q]&�Q�g�UA�� �#!��d�I�ksl6��n6��n���6ɒ q!$�` @����[T���e�P�j=������Nmkk��^��a�>!���3��������{��{�t�����Y'�O�vq�ʼn��ϟ5��4�?h�2�0㸈�(<+LO�K�]�g��O�����)�!�zU�;���M��W�� G�/�F����%(FX>"�Z�x_�xO��9�)%��8�@����6�+J��E|Q�|A]��"<'={��(�s�ՠX���t������V�ҡY�E8,1���y��y\�<�g���ֹ�q��yJ�hK}�m�9�+��Dz�*]͟�=/)7>a�~��� [<�F���:1�h�C��m�~E}H�{�8���������r��V���_����JW�?�>���s�� �j�ҔY�b��cåQ�1:��ֱ��~q�� ;�٧��p����{J���V�.E��V���wJ�S��_��r�Ml���y��S�3I��s�"�1�c��zyi�E��!NX�'�52,֨8Ź����Z�Cʊ�J+uI��f���Ss�|j��W�t�^9g��D��4J�|�e�"O���'N��t�u�X���*�N�nW��y%"�3*g��Q#�;���E�а���c��)������0M^o��e� f�F̗�2��9� '�i`��~Ʀ��oz���o�����]��e����yóαm���z�[n������ύ�eBs��u*u�=5KW�l��zޭ���53�~�������� E�kfύ6F�ձ{���753����z^�ǶE�Zt���``��lY�(��>Od��.���5=���4q��#�3��1]���8�D��^�g���f�|{�atq���1b0��[n]�`�r7[W�\�����1�b���Ȫ���:AO�27�Bq���':�/��N*.��tս�@סA%���S:�ǥ����Uv��{�BFV�2kd(.���BVWп�J_��� "��ܜ��Ƶ�I�;i��1:��"h<C��<�F�.�?0��1�?yR�zYW��Z]��U���J�ר��gë�5a9kْ��fc�u&zS,lL�N��;�G(����n:��h��-c-��d>�/�4f��'�S2��teL�}��9��t��o��u�K��d�����,%�Oo�:z2��^_D(���,+�u����К�97�?w���=x����ݏ;�������{����F'��{K�>���a�N�j t͎f����J�z�)s�Y g��M�3/�`~>���T�R� P�(@SQ'��^<Ń��Gq��Yr��'�)y[�o�)E�.��[qʅ�* �?� c٪�s�_�-RD�ɽ�P�|: -��^�@kq�%��M%�J�4���-s�P��]֎��g����]�T�Zq/UO`�xs�'�˿�\�Ti�״�O��L���{�*�������]Ū�K��Rj P��|�Yx̅4T�S_Y��҉���ai�^Ս����,U�0W������V�S�o0Y����*%s��Z�s��i�=$�n��CbnV9ڣg�I}�J��y&M���T��J�U�A�5�Z� ��B�́��@uM3����>*��)��d?E������?���آX'uAkp6K� �~1GT�����K�[ծ�-�^G�mu5+��'`w�bs�`�-���i�쬧��OY]'��M�n��5F��$��G�v�D��#���A�S�Z�������L|�i�Z}*����鳪穁:�5؝��Vg����ވٝI���2w��jJ�y)������"ۻ���o��=K��ER=���wR��I�Gy:Wg��T�Q1��,n����n��j۬n��̤ܻ��ƕ�4%RܔF�/�_1y�Jr�v����[��I��ú�1��em�����>U�MRc�'ĿG��⏈�/nءt*�b;Ŷz�\-l����k1��GVk2��2�h�'����v+)�.����!��� �&>8Ϊ�b�ϲ2�b�?'�-Jl����O(���6�7���f��Ů۬��X�kN���Ι�v-d]� -��5$�RX�!�� E��`&~��Uݍ�tw�"��e��w�$�����Y��.�6��š�Yܥ -ZzH�qy��[���M�/�e�y��E�BP.D<UTDDD�[`Y�`Q<�R/4$ -tc����x�j=����Ա����4�cc�l���4��]v��=��}���2�d����`O��/�1.��o��|�h�a�4�0F^�Hy'��8]��s�n�VߊR�V���&9W��g�q9����k�4��^�9�YԆ����v��R6�yY�L�ĸ<i<�%Ҩr�?F�j���-��r1 �˦r6���)Z=L���i�M�r0Ȯ����[�1[��j�@�p�Gd�Nb�� nܼ��dؿ`D��;� ��a9�Zɕ�ѩ����\xffK3������n��eCq5�X�5`x�*��10��Z�to�O�E-��w%�&�ZD�SaO�= vD%�e�뤁,�� uo��X�@�xl싸x72[n����u�4Q�M$��Śj�me�)��}�3tqԆ��b�2�.(&�R�8ؑ<��>f���}Y�q�m���J㵍dvlǎ��ʡ���J�[ n+�\�Zѻ�����p����?U�V�I��l���P�9`�VJ�yl�:�no����F�A{Q�h�A��|��� /���������I�w�N�܌������x���n!�N'�3�w,>�n�|y��3������~DZ��߃萵'&�{s�� {�f�8� �l�8z�N�Bt�'���9��UK�ˈwN=�1y���)`;����Vn���6z����@����f�֔hN��s���h/b�%.��l�+����j�t���kt��n� -� -�w:_M'���9��_���l �q�>��.a����� -�j>A$?�#���n���9��p�}I���2P�����e�7>������j�����q��?q/�X[�q��3�������� -Y�����w4��i���}B�?��gֳN��1�ǟͽ=��G�(%�9h��+WO�/�0�����;��GLMըLU�d��O�~�d�5��=�v���ͷc�[�/�3t]����%�=��P��� -3�<XY����N5�:8��4�Mp~c��p�!\��N������ѻ�t��?����ꍟ�V��a�L��X!�|�Ɇ���A��O��u츊��l/2��g�c���>$�������N�t��?6<G�䄟�a����E,nh -����0�)�|%00�`Բ=�a�e�f�;t����0�8� -)���,Y�f��O�g��+J̕zS�#� ����x8�0�b�g�(y9B}V9�*�n-�z�@�:8��� �x����=du7D�^�����"�l#[�_�6Ok����� X�8&�/��}�:��;`�~�x;ڰc/��G ��I;�]�X����ꬶ�Ak����o�lU��nQ��μ8z����'x!��Ym2�I0R���D=N1�epV�Y��-X�����-�|�5dy���J�����m�Ӷ#�zvR��s���+F�g:�φ� -'�*́�� Z�uZ -��j��a�Z;��*YNvk��D>j`/�������v%�=��p�3���W,�xX3�ɰ��O&�ETB!(������ڨ���oW��qU�_��;*w�Ve��u����؎X�qx�%��n��\?���s7r�כX��.D��dv��UݦjE�D-wJQm��2��RM�|U�*U�s���^)�+T�g�J]�U�zBE�WT�����>V^_K��,/��<�����E��`�&�����Xݝ�Ʃ�̽�i�+��s ��-F��ũ�=A��IZ�?U�d��U�G�JV�ijNE�kU�Uy^o(���r��,�;Z8�2Y��ݥ�\w�ަ ��AK�� 5zu�i�������L�U=�_�^cT�)à�*��!3U2t����W�,�(ϧ\��M��]�,�e��)����]P��m��{�T?K�|-����� s�fNZ�F� -�ю��Y���Z:�C����/\E�1*����iZ<"I�#S�32S٣k�R-�Vz�YiA���E�?���[J -~�YA�.rg�c��!m�6_Z�����������]��Z�OE#� p��B�����=:^��Tf�e��k~�"��+5�R)a��֤���J?��g�~S��hZ��K��'���u���Bk��5�<>2�R�/�S�h-�RN����������S���7[sǥ)%2[ɑ�J��Pb� -͈ڨ��]�}P��gu�KS�,���Yt�6� ����9�>?�i�Ky-By�ω襅c=� �G�Q�����h����91S5;&QI1�J����k���:�Ǯ��.��3�?ƃ(If��h)*x��C�w�e�W�c�eU`������m�A�Z�R�z�&��z�I4U3�cSM:MMM��C�9F�#j����1����������{C�&%'�Ub�JH����R|��Gq.�{��&���-f5od-��J��S5��fߎ�RQ����#5-a��C�����e%�����d�L)6��T�h���B%V*���fأX�qE.�/�]uxL��~;�fx�bv~��ӆm)H�#K��K��eCd6Fʔ:U�)JMː1-O)�EJJ�RB�C�&ŚV(ڴASL�a:�p�y��_Wx��Rx*�of�7�����7�1����)��8�}��g����9}�L�@�e��1#Ls��͉J�LSBf�� -�U���ZEe�Wdv�²;���&gQH� -ɺ���� -1�)��`{�� �6��c��6P4 ���u3���g>*c�%g�+1'X ������q�!Ȩ�LMɟ���R���V��A�,K5ѲN�-;5�rXA�s -��\�y����X����������l�pM��RF�d�����Q�e�b� W��Q�2}�"�a -��(̚���&�)�Ц �v�+�Wp��.Z���N����g5��[�{��q�K��� �WP�&�Oî&���4ؙصT��,lt�Q�_?/�P��&ق4�$DJ"5�4N��F�fktY�˪P6G�e-Q�����o�A���'�ҫZ�|K\����ߚ�߂]�KR;v:�$�1���ibEoW>�1U�]��c0#T#gDɯ:IO���j�|�e��j�}��W� �Vy�h���T_���[z�ʥ��6��ɽ���Ro{5�PؙV)�T�Xא*��]��a��[;\>u�\7^���4�.VO8�vd���H^����hPoG�z90n̡�R���Z�j��6��r�O�up+��w�4lb<�H�!��fK���\<�</y9���)�q��#N���v�X'I9y���!�g6,Fk��y}�p��c��g����z=ܙpˊ%+y����(ؓj����S��6�^Ȓ����qٶpѴ�/�)=��BS[x`�/z�B;������܍n�bri%�&��Ri\y��6��5���( i���A��t,�Ņ��m�W.�P�#^:������S�v�_�afW�����+|���V��J�#܊j� v<%�'���Bi0�|�ǰ�q��/��,/x��'��ı�8�G�^l����A-:xȋ������-� �e�_W`ΙE���a'Q��7����J���ss_�{��0#,+K5��B_�җm,"��z^��:�p;�N�����\�����Et^E7�H�p+)ךR�w9Oh������������R�|����^�y鹗az����g |�����E]�F��d�H������q�-��Zt]��,�]L���9n93�Z�eL�}�����܋?-�`�t��Д�,��z�,�o��� �������w8d�@�����Ig��3��E�!��V)��R�q9���C�������=�Y�ݜ�3�Q�x���E�z��{Q��z���8.�2�'�%�_4���9����fӢb�fر��9��c�}�!� ��O9�<��]D�<��3�͜\��~�Bx���9��|ަ�w��]@w�~�=ꇆ�@4� #�A���r��O��O�V=ni�n�f���|́���r G�9N�*�M�L�.�Eu�w��҇�,:������ D�h��}��<�MY`é���?t�?�q�8.ǧ�� .�c��G��8�s��x�wu��}�w�����:��_"��x�a0G�[���u���e���B�J��8��Y�xO�P�ɫ�Jk�&�8�^���u�:��c����:�~�?1p{�k_rLn#��x����+�\r`Xɡ� -W�}9�:r���BKy�sta�ߪ��}|c��E�~�%h������F_ �h0�����m4L&�hXIA�\ⷒ���Rî���1F#�f��� -�֯�Tv�S���-Tg3Ym"�_B���Ow79��������� �x��د0r�%�\�0�0lt��F-W�\�0��L�Z����r�aR�Y7W�=*��A`^���=W��H�����k"�H*O.Ff�0�\��0ʙ�;�ZNF=�&��ʄ�&��j�2K�T+.�ˋ��E�[<��9�'=W�ˈS�<I^~���M�'N*������±���3a9�4h1Oo�ӅD���=KEu�O�!'�9�����[�y���w� -�x^�5��|`��ô�;c�Ѫ48����V �*X5��g`-��c ڦ�騃.��߸������z(��v�S�khs��W��.��6�+�?�����������6�16��m0��ƞ��� -v ����4Y -��hz�*Su��M��.�m��mZ5��������:U��li�x$��ǣϠ��~�9�y�s�5�"�=�7�YNf+NƷ�2�z�̌��qv�wa.��c�A�ØI����(&��Lق?�:�p7�|������ؾ���ݽ���֛J�A�l9�����?����qYXJ(�b�'Rp<U��4 ��@8݂��3�zb���f��������Y���+��\�;�6�r~��s8�91�b�%3�6G���s9v��q �V������\�f�!t��9RL T���"�gB ߊ�|;� -�)��p -��y���1t�G_�@��/�[��-�z�R�}���u�홭幂G#�r1ټ�s�-䌓����,LE�Hq�J�-��Wj����r��N��^8�������*/��� -��k���E��70�� -��?0U��u;����L�Z)-�|�v{� -9���I�?P����|�T��[Y�H -wU\�Z8�Mp��`�f����j����n ]�g`o�(~����/�*m]�0�op�y��5H�rG��j���8[��:�5�kK0$����� -�4�k4��т��~t7��%�Y2�$�t:�e�K� ��h��*�PI��bP5�p��~���L�*�K� s��� -P�ׇ)wC��Yl*B��6�=2��*X�u�w�,���p �^�$��h��Ъ|-ʫP(o�Y�+��hVܧؙ��a��W�� �d-r$qJȧ��{��AY<���W���e :[�`R)aT���j��� -�zmj/��I�4�PjV �|2�k�h~�F�/Ѡ� ��hl���-�`��~��!^�43��*���@/-C�* �jL�bthDз5@�ތ�v54Z�ZTZ;�:� d�9H�gѨ_G��U�鿏����� 5��FCm{�a^��� �)����Mp�Q��Qv�G tqL7j���eA�/D�A���R�t�@a�Bn����ɍF���c�3/�Ƽ�*�*�߃��s��>���/�1Tt��s��u_%?B��~2=����Z�3�s�h7�Cm�@�9��RȻ� �j��"G�E�����v;Qm��B��ʭ�Pj�B�������Gu��Q'���r �c�Wȟ��9Ek@n�����Mo��eRZ�f�Hz���[�z�u�:�ڤ��SA�*�-���߇�4��'Q`��<���wq��3d��9}�D�-�Ú{��?��gs�\c4r �� ����%� �P=��#��;D(s֣�)�Qg���(�Cސ�$�]8�:���� �ui��H��_�#�t�%�!�$�ạ����b ��#[E��VI��t%_OA��a�#�[��9�&d{[pاG�ϊt��|~��!ɷ����ј�~x�8�g�� �.�e��dO�=L9 ��K�Ev� ����V���1ox"���@6��H ��CRP���qA�� ���u�b'�F��>�85��Q�Ƽ��#aڠqr�����vDK����������Ӧ��$!.�F�!bs ��X�!&6Ĥ�H�I�g�i&��-�m���2� �`��8Ϝ/�=E�����Y@v�*��|@5٥|U��ɰRf�l��+O��,�5Vz�E��<�#��n�*�@"#�D)NF���(���/�8C�,�'k��~z'�md�Ɓ� ��0�qH\ �bzq�^n���6��l:+ln+{.7Z�,ʅ��Q~���x�ό�&�?�}Q�S�!w��Q�a�u@��l9ٵd�2� �$�ܳ���y�^��/2�����5�u6� �\� .�s���ֹ;�rr^���F7�vsdN�r��?`�Z�0��s@�Q�x����+8\��-��ߢ6�+)<p39r�z�߱��w����b�f����-z��M����>����9��ݼ�;�5��6F��2����*wsoश������|��0�:k���MD�Y���]��.~���2��|�wy��-�C�����^��y�|7�SE���o��U��c}��g�����B��c����[\���/wٌ�6���0�w�ԻW8H���2�����#��L��Ed��X��E��E.�}_1j�Gb2�Fm4�h��N�8�m��15�M�4��c�cF{�i<�N3�~���i;�����y~��<��It]�tz����.���E�g��ˈ���}��m�� �:g���Ay���C�����.�O9��>�J�]���$}v@���o��v��@U��<ة���2��Y�d��0����eè]AW-���6�+=��q��GM��x�!{�!�}t�ߞ�{�FCQ�DV�D4g4 �؉?��G������q5�p9wpX�i�����6n�s��AҮ��+8ڏ�5�yӇ�W����y�[�p��o�3����Nw�u_j2ެSb�X�M��u��5�?��OpF�i�h����.�,pQ�I�9V���=��B�����g>V��+Y_iqY��k,�zM0&��1��?IL�)��b%��p6�.���>�� ��,+���~��PN��� ������8{_��էxݏ��Kx̋x���1�������:`t��1�b�x�z����R�����!�q�H~HV@j?��k��81��-�0���S���ț�����a��t���NQ;�i0f��c �^�W8H�R������`���;����@�^4��E7�%c���7O�����1� 3+�R.>�a�Y�.��� -c�y�)��F�S�-|����H�6P�g��� -h����6z��~��x�F.(V"�d���m�T�F%�z2�c"����1 N7�_@W����"�Ud|%]���YF��;i)�%��'Ƒs���Q�v����b�U)ē �B<Epp*�x���O�'�i�<�K莧��U. �yD6�z̡��>�.��u�;e��WS�[8�8ޖ�LL��J�� � ��O���y�k��pV�]��������fW=��=�l����5��M�5�4�X�P�QĖ/���aYٍvX�U��x���fx������Y�Y��]�tS�=��o�uKz?PS�@����i�:��(��+x-Zŵ���K������B|���m��>��ѷL]O�4�_����WgH�������&��I��9t��B��q�k�z[�A�U��۪{�ڰ�jz�A�:�w+��:F��\y��|��������_�f�WWh��2kJX�:��mh�Zë�^�I��٢�ȩj6[����E=����>���T}I����;:�S�#�w��ĵ��Qh1c�\���F֮�^����h�G%kr�H���j�p�&/US�5�y��!�Y� �M����ŪN\+wҋr%����˙�k9�n�,�1 -��O �I/�ZǨȕ?��c��ڝ��v�����H5'$�)1]�If����K)V}j�jS��Q'ψ���&w����"}��Tj:��i�.���9z���@�N1 -�d�Ywq*#w|ԁZ�<)��G�S�ȟ+_z��L��f�ʓaS��2Ue�T�Y#רq��"gV�ʲ�$�)���ͼO�S�3�'���,YP�Gǩ�����9�Ѹ��t2��ױsj�>3T�QQ�d%i��$���r�.И��gWȑS����s'�8w� --sU`Y�<��Z^Q��̖ʲ�PV��2�d� �(9�ψ9�9�����P3���߾,��suv��9r�ƫ�2BμQr�[T�_�����TXP/�u��f+ǶBf�&����H� �l�n��t�}��et�lc�YG���w���!��_��ՠ*,Ø��rZ���6\v[��G��0G�"��ť�/v�R�UN�x�.iW��[��e2�7*;[��7�lWI�ϔTrO��������} ���xm�&�ƯTi�����z��$T��a��&(�4]yef��)�Q�ю -e9=�t�S��U��3�Z�D���X�K���[~N1��4�yW�����~��9j��\u�ق-�����%̻�xO���܊peW��<&EYcF*ӕ��M�J��*��Z�S�{����^��U�]�Cê�*��K u_Ux�E��S�+����F�ػ����>��v��X^�_!Y\���[�c�46Ji� Q���YI�|%z슯q)��V1�fEy�*»@C�k�ݦP� ��L�k>ր�/5��O�V��~[G���}�V��p���MzU;�d��{0�ԅ(�~��|1��%+Ɨ���E6*����~���� -�wh����W���z�_��g(2�xß���H�b�L��>�0�������"�r����ܛ��XD�%ފ&�1F�֤��������8Sub�k25�UjM��L��4V��$�xd�[�Okg:�'r��{��{��{���n�b�7��"�E���V�-�C `gÞY���?~�b��5>g�c����D���j�=[��By�+8X�X;Ύy�c��;��e��=R/�������nʅ�;v��y�k�X�Fo�c5����Bu�"6����b|Ё�t� L��8��W^ʦ��} -�H��ȻT_L��wY5@YVz�t�6����ؔ�-��V.�6�6���L>XN��dA'�;��Kf����ɔ�dbicric�l}����I�̈́]�4n y��N#�$�Q�4���/�|Z�`�y薌��<vKyl�q�uqٻ��]�C���Y�e�_|f7ު�e�gRr�w�>@w���Ϝ0`�#l+�'��ɰ��I�,3z �e��2<1\xI��c5q�%�u��~��Þ���=,�ӌ��V~OO�Ǵ���n������f]5-��vV�4��Ű�!��G)��=L����C���z|��g3��V��Fۉc���=�es{Y��m@��==�ú��B����}�Eϳ׳a�R�8>�%�#_����b��QZ��p��i�q D�'�C\��8l�����Y� <�:bz:@O��L-Ժ�_W��m��mvIa��=��$�����~c���a<��Q��<����<'��N�t'(� �ĿL����%������e���w2G9���9J#{��z��CG<�:�N����6ӟ���3��Y��\�����x�H�ɝ�p����.�+r�ו��a��"P.Ӟ!�1��'�c�m�:���aJhqL��r_�7�a�?��.J���A.�A� ��3��J-���d�Ȕ��$o3���O�Q�cF�?1��֯�g ��E� ]AA�5l��t�8>�?n�(��θK��[��es�S��o����W�(Ţ(��z��y��� -��%N�s&��L��pw9,�q%��ݤI?�Y>$���k�����'/�һ���n��[o��5 -�H}��z�t<i.�R8��Z�±|L�߈�q\�u]!�K�q�~�&�-���NQ�s:O����{��F��O�V_�i���>U��8�,<�]���Q�0j��`������*Z�����^�s�6= -�M���l�-� �H����O���蓡��!�IzO�+M��9r)$yT�j G`>�f�0:���QYc�� ���kT����1��}��en1�߈��'�ƀ+x#��5N�d8鬔C��o!� -�i�~�z��F��pG?�q�Kt{���v�Xm'��A��I=4r�l�3�18I-��G'�t�Ȁ1� -����a�Q�����`�nm�:������6�Kd����G_#����� z۸b���L�ƑK��(��ᤒ˳0ra�(��Ӆ68ud�����D�Bk���W��rV~��vCs�m.�]�>4���q�F��뱏Zl�D8��� -g&�L����*�S�� j=�f8N�v�Į%�-Tf/�t���I�G������{H������i�ʢ~&xc�)V�'����dAɃS���K'��A�.�8�V'���DT��:���h�"�O���rS=�Еwڸ�"�{�I^���a�&���B�E���tX鰲a��*�j�V���3��-����6r����0��1�KN�u"�K�!7;4\�~ ?�/��A�Q��R�Hw�b���=iaO�'� ���YiP�R�}D�j��T��Լ�ݪ4����'�黲x�R���>PѨ;�!r=��p/�-\�ky��:I�[�Y��[���f� 5�"����Q)�?����Y��ɓͧXUO[U�[��1���m��n��۠�q}*�;�|�7����r����{�1r��Wyjw��ȳ��Q�3�q-�k#I�8f=ߧT�;A������?QU�g�bB��'��:�@���J'U�d�|�hN�2����^eRf�)e_Ь���r+=�=�,���vz�1�������:����� �-`�*U!kp�,!�* ISIh����TV���ʛlWΔʞҩ��5�ߦ���J�8��o+%�}�D|�i��!3q�s3�Up;'S�prgT�#_W�0��0Y&OTIx��"bT��9�3���ܨ�ʉ.Rv�U�15ʈqhVl�fƮ��حJ�;��q2ǝWB�UtW��od�q�#�nj�"9/��ɜ�(X�fC����ۋ��U����`��E*'.A�ŧ(+a��M�V�y�f�-��hӌ�FMKl�Ԥ�JLڬ��}�M:�褳�LTT�E'>V�٭�S���\=S��A+v�1�ڣ -�*�������1�N��̤)ʘ���IJKN���L�H�Ӵ�R%���2��<��7��! ���Bp��n�%�,�.$�lH�@���́�JU�`p�T ��� -J�H�*-��NO�Z@�Ա�z��ӠX������&?��|'����9����f].��Q�� �a��֣i��J��W��m*��/%� � |�?o�����YÚ�gE���\�,q=�j���,[�23̲eZ4'Ӫ�Ys���/K�G���43�Fw�W*žN���`߫)�^���)�~Yq�����&g�tt��o�P�*a�1z-�b?�g}�͐�O��#d�����J�;M��I�]9�J��Ҍ\�̎B�8J��W�ļ���)U��݊q>� yg4.��;�5�1���!��]�=�F�w�\���X��Ȗ0S�\)�!�s�R�MҌy 2�5=ߢi6%�*����Wu7��Mp?�1��h���L�S�r�����2�RtAH�ɷ-ĺ �=����\hgֆ;�����ʔ�h�꒦ϏR��J,����dM.LU|�l��S����b���5�S�hO��<��٩H/����yE*b�-����/�9�~v��^��n���΄�{�|��+ ���F+�$FK��%�5�ĢQ%2���-�p_�"}5���c��4�X|,#>^�o��р�ɷ���{9� -��������9��m.f���J��Gh�����4��'���� ?��3�,�KV���A�β�� T���^�� ~�aW�,�s6 �[ʀm�m��;�>�JT%L5 �採fZCC�!���>�����K\��1�T`J��)�#�s���ڌߛa��\����"�����`π��q���V���kyd�h���hj�C�=H�)� �Ę � �Bd�2��g�[Sl�m�ϧ7îa*�Y��n���s`Ϭ��pil�4f9; +#� �5��W�h��k��7��[h��w+ƵbH��� �����haRj9��2=5���j�]�����Z�AN�6س��'�&�J�k�J���VtjC��z�����~t�G'��IL:q�&��!��37�Tv0)v���,�?@���#D�I�2R�`gö���:������b07V�1lfm!.[G2��.�������t��n}������������F�M5�*|]��*�E�i�;�'5I��������94l�0������z���a�!?�s��h|����_�ǁ��x}��4�+��~��_���X�ög3n���L��an���m����1c�O���a�g�Mo�t�{�Q�}�q�}�D:��29�2���Z:��[A����ߩ�N<6Go1�}��R�,81��㈒�~`,H/��DIg�ۗx�^�x�b;O��[�p�9:K}���jb텝; v�Vi�÷=��OC� �s�+�]XZ�~���:)��%�q���L�L�/.�܋X,9�"s6���3�ی�'�2l��it�ƃK�9�2���#ھ~�~�Jxa�����W�]#_��K?9�O��'�㋏���r�����eB�Ȍ�Q������`@�����3&�P��0��`����g#���y���=��-�x��\�1����k�0!��B�2�n��;CP"�,��0�� -��|����*���x�{��{��=~�=^�`/�ӼJ��F{p����0�DžW��>&|!�Bϣ>��5w��>�(�g+4���=�qo�x`��%llՄ�� -�PZ�m�Ns�Slj?�pHP�'�O��>nu��ߤs� QJ!���w@������^��T*���90\����>N���Uz�-���v�Ҽ��Ȏ�#�O���q��I�>R�%���Sn���q�[���?�}��~�/N*N -��pQ"����V��J(�0V�6Z(����-���{)�Cx�=<�~�5D��} -�]���>i�{�F��D옊w�H[ -��h2�F������z�_�٭��ND���ߦ�}�� J/��o�����n��?m�en�{ '�S�`̄1;�`8`�`A�����,% ��Zy\�C]˭���"� -��"n���`��Fه�����n���b��c�d)�2�8��?�g���N�ep��rv��MVq�JN��dQYVO�Ց�u�����^mq��<���NE���C�'�1�d[�X��'��tQE�J��rXUp��4R5m��~���*:H���?�co��TWhP猖�x�i�?�᧺_4k,�8NM¦��`e��� �Mu�*U �V�e��R��sZ9�XLtK����K�/�u4�B�m���,u����S�!<.� �g���K��T(�ؖ�m9������+'*���5�h�v��T�q��{�]��\C��e��z�1��'f�HO�J���kG��D�9��'�L�g�gQE�Ue�Z�'�0�J�k���*Q���z-�j�{d� -Fn�<S��LG�k:������);�3~���p ���2��>���!!�@r��$�&l6ٜ����l�]�@"�A� x!��"�@���ZNǖb;�T��TG�H���h��CR����������;N'���fw�{}���l��~d�}w��\���E�xdIgh�G�=$A�gX� ����l5��1�L���G6h�L�ܳ�L�RUF�Qy�&�ƌ�8f� -b��sA�1��s�� ���G�� � ��B�-I���q����;+T-Q1j�N�'&U��͋˓;�X5]'WB����ԫbS� -��4oS�y���G�0�W�yLv�u^o��y܅4��:8Wbz��]�xA3�k$����K��ZS�j�U%��J�VER�ʒ�U��Vќ&Z��t+J���rX��n�-�R�ge�� >_)�Av E���Df�Y� ց��F��Լd|�UY��ĩܚ�Ҕ�*N�TQ�SsK�[�\[��mXi+�f�D۷�jߣ9�CJ������L��ʜ6��4���۩���ч]�B�̷�{Ap��T|����E��nR~�Uy�����QvF�2.e8�d��ʖy�R��˒�V�Y�e�~J ��(.�b�.�k���Rq�~����y=�+�醧{ВN�>n���ȁ��R^V�r�╕�,G�M�9����˖[�ԼZY�Z4�٩D�]29�U|�Ê�ߥ��*�yJ��W��Hy_(2���<B�ג�r,�"���Ո,�ŶU�2�q!��>e�Ig���1���Z����Y -�\T�Ģj��5��C��}�.Y��%*�d�BK(��yM/�>Pp�g -)�ZO��������� g+|�9h}8�A��)G!XJ-�.Ki����X�,S�M�˳_^�� -��*��jS��[��� -q )ȵ]�\�7Sů�{<�pK�3kC�5����g���q�%��L��H�r)Ʌ�� -Ulu�bjL��IQdM��k���]��Z�[u���FGײyk���܈nn���U�筞�Vf`��W��"x}p��Y�M+�p��;�N�;9�-E�MWXC�f4�+�!YA 6Mk ?��%�ʃ��`�WL���s�AC�4��!��ff�>����B~�g-�.r.�X�� -t?���+�ՀjRP+��X^����%/�x ؇���|�G���_�P>��S������~r >?����w6�ipρ;�3�X���� \*8X����9wj�:I����.�y��=�������o�a�������g+���JP -��N��7+XQ><h;�Y��Z�YpK¦.��\o`�0�}U �Ճ@?�P}#|�b��u�����{���5��8� -�v�m�;���AkC����S���8V�M]t�X4�,�z2@qxp?�_��fb5Ji5��*L�J��� ��w|>Bn��n^9��je�$9�Ī��I:H���W�Z.�u�1H뙍�,�!z2�`m��I`?ܰ 0��A��w>���\4�i]=��;n['���'�m� -b�gBk�!z7=IJc>�/[�c�7°�0Tۨ�6fs+>sx;8���K[8�|�����Z��z]���ҙ�9�Q��姈�)���|��#�; 1�T��g���8��x�s���ܽ��$2�F�{NJ�Q�����Z��zz] -��>'!�fR�i�|�7x� ���nC��5�1e -�!59�I!�g�|�ӓc4�( =¾:�C�{�]U�-|T w.����yڃ�c���}�D(���M��9 ��5e�8~:�0z�8^d ���\��/@rn�t����g^U�2��.��F�qIӷ��c���~?1x��48X9�^��ĨM�^�������;��?0��'� #�1�x�5�]���赙�Cw~#�@]�9r;��.�7|�2��?��,�S��qV���8�w����P]�Q\����_��08@�0__���Be���9n���O8,�x��8�k�Q���ߥio������+��⤧��8�ߎ!�p`ҿ��x�� -�դ���/��'�x�8���,���� �k�� -�"��W8��t�V��ecx��(g�O�#?� ��o��%@Y�S,�Т��+_W�c8��w��[��%��%�\�9��)��9�$�|��>GC���(Q&�C�㌎�����i�ql������I�'�1("y�c$����x.�c��K��9&�p��3�堶p���O�wL���^<ި�`��T�ϧ~�0b�'�����Q��8�\�_�,��ɡ����pp䅣�;��c���u��by��������O�J��u��'B?���9�>��1槍#p�X��fj�G9����Q�����Z�h���n8����?��`x�(6�!2ـ�]Og��A��/�e��]G�4��v$��ù��v;��؉�p��j�&�6G��iXWZ�vk�1��: 1�n�ch�& -E�PU$�u0!D��I\*ELZ);�P;����$@�k�$���}��{K�UG��G�9s�=a��SJ�#�u�� ��q���x@ pD��rUt��<�t�[x�,�X�>�xj/'�Mwѕ��<��������"XQ� -0��#J�� -�����QJ,^x�����G�6��t��<c�I]浍S�$#KL�2�Nr����Kt��\����p�\_2W��"Y��k��O2<w�� O!<n���SO��+�tt�5�8\S���4L�!m�R�dw� ��� x|����1�� ůFOL��J��� <N���Jb����f��W/\#dc���ѱ�9��S�^�����b�;��N�M��3|�\˟5�7b7�!�dH���bJ������p�0��pU[=���k�]� ����|��c�,�+l۳��nҫ -S�s��u�Y#�{�vg�E[�$؈t�@_�+ �L�r�+���>?|A�"��ο>8G�";��-*�����GH8C��S�+<u��~�(>�͠чG�������b6�q��>��w�W�?Ѫ��z�J,VǪ2�'U�-�N��!5���)�G��5�M*�6�:ˢ��#����g=� -�y�Y��c�&��]�al ir�xw �&m��!ЇD��g]H�Nk�:�V+j�R��@��.5��*�ƯƻԐѢ��n��j�N��6#�}Q��#r�WTjZ�����S����[ࣘ<8A������U@�#�{A'�G�����3Elij��U��P�:�Ye�˪Rmv@5�U�tȗ;��܍�8���GŎ�U�8�|�9��xEَ��}���+���%����B��!9��,�� 4�9���q�`n��sר֑-^���]����[P��<�Q� -�UR�^m*p�U��r�'����l�o)�yE�p^SF�-e�S��,�������r�U��&DA4�����5E �*��紫�ء��byJ��*�VIi���V�z��S�{���{d�R����<%��E���T����z[ׇ:E��Ŀ��Nr���@��`�B ��� -x��e��=*�d���PE�n�W*��^�����]Z���;��.������R|O*�������}]�7���>�]<H�w�? �(<=�a��H� ���2���B�{U�*��.GU�r�J�]]�u�~�jBʨi��AY����U�I�~���4���k^CD���o븝�D�g���|��5���_�^��ݍ4����jSd�ϐ�>G��EZ�(=P%K ��@��0k�A<E��0A�q��1�\EH�n�(5�G���}�E�c��^�`��p�c#�mX���Uia�R�yJ -�(1̃Mx��&�ilB�G0�R���&j�4!��(�0g��a�l���{�n�Z�k �ZPUC @)�.�;ilK�D%��Lm,�6��hb��(��c;f��Ў1lG�G�Q�JB��3���}��Mp�x�?J���p�]�ks�aU�[��v�x���ꡠ=��F�%��$����0q}3�ы*�Ŭ�pS�`>�9C�-¿����㏶���@���;�W��$k��,�>0�`�Ї���=Jc��̣�p�t����6�i�/GP)#(�a��g�@{�}�� ��c���b�p۰H�iU?��`�K~#�`�MR�ij2M`S�t��)�m<�؊��֛^�7y����;�6���0�}%��R�<Zk-1[(m-Ƶ��`*v��T8�6�s̰h�h�yj2Os�����9��,=1C_�0�;P��Q�w��8G��ƭ��+�-7��pۈ9m� 0m -����;M���s��99��q/5�$^����������E�r��{L��-M�7�Q;A+嫧��什2�8~���Λ�qG���pиx�����Q�q���8�~�D?�����9���27�����75f�.�v�5D�*�v��vbNa� |㼋&ߧ������)6E�I��J�Y9����&�S��(������B=�'�k�\��j퇻�,D\�.ST�k�x�����0���f�IS2zH6�Y��9��5��,���\��ųXT�3(���3�����WA���΅;m���)�1���?C�� 4Ĺ!N_�a���{��%�q�%t�������^$�8����4@oGத� -�s�A3�).� �a��M���7E���9c�b|�R�0�,ðJ�!���+�����k}���s���3ҫ�U��伔2ٖM�3f#ޯ�1�]4�SJ)��q.è�^�*�Gü�0���af�cfߥ&o�d�L_������|���/�@p�7�a`8�~T���ð���;4�M��-�M�:�w4�xݫ�&G����-��ۿ�.��6�+�l��X��1��l�1��86WC�R x�BHDhJnʍ&#ɒ�t�(ɚfK�%m��˪VY��[�f�ڭ�&E[�n�TmҺ��2M���D۴z�����z���}��=�<��@�%����'p1�lP�)�� .oy��l�e�����c�� �w4�Q!��$n��~�~�R� �{S�� T�$�us�]�UN� -.�g��?g�g�?��z�4������/a�O+��v��ob� �P`��������py_���-�r���2�<���y�0~ ,�S� *� -� ~ ~�F8*�h�7����W��e��5���� - -�2����%��v|;��j;ON���,��4'<EĖ`Z��+�88����VJ�h%~=y���P�Y ���� G~4�G+1��z��c�1��į�da�뼏�> -�)x.����ϰ�QJ���?��KPG�+tC��lZ���9����49YFc>Em,a�I�X�>N�쎣�l����8����4����K���x��dwN� ���&!��|�[CZ�v5O&�b�8��M�o�eU�����!8�x�A����G;�c��8��=��,:� z���.On���D���ޞ�Z�֖�Z�^��<�(��_�p���LJ7" -OXO?<#��s�n���j��}�ʹ��|·�I����3��$5"�Ѩ��m�bFV�QȩfƧ �j|���O��� �1F^���(?#<��AN [}0�#�=4��-�\�$�i�?���Ѹ�q��Mul?B,ƹ��������:���bp��3�x22]D/F;�G;�����J>Z��-�Ӗd�b��c��*���V$u]b�ⓎS����p�r���ˋ/�F�6���>�9�=F���ʓD���yW�O�H$��I-��*�ui�5`�����p)�VW)�Y�s�>|M�E%0օ�R�(�?���~x��Z"�ù��nj�M�F�5������8#w��߫��|��o��4�3��G,�`��g����l�3̿��O��nX�� ��o�~���O�SN��Į{Sl�Ěv�O֢��+�㬄C -{Mܠ��{�-b���UҜ^ �� fTHcF��3=�34H�!$uYm�aw���� q(�bg!�PŢ��R�+7�(�1e��${�a0�\q�B?�`7hQM���/0�tiTiPLR�S&�\�����P���7���U�k�^8$� �g��t@�L�b4��|�w$��cQ�J����O��Ab�(��f��)"�H�(h��o�<�� �1��T"�ERU���:���ZKi��K��<.&���I���(eOK���d�$��_IF�m�,]�'��}�y�7�1�L�6�_�0+z��@�{��U�iN�Y{�Il�2��T���-��)�KQE����o��Vɶ������N�������l����ܕ/��H�i���_'�-�X�@p�jP lH�r[�����^"ŕV19������&ɭj��W���%ùI�Nn��%�I��#`YȪ�R-�]Y ;��&�A���G� �i'�� ��T����/��b�ɑ��<�H��!�.�d�%���[�<�ƃ����=K�e��=����]9D��� ����(R-�x@u�+�fV�"�d�G'9�U��-�Lo��{m���a=�����h -"����c���>�� Q� ^l��'���?�p���W��@�����"9>����( -6@��D�����.ߤ -�-�)��D�+�l(6�F�qE�P����xZq� .���j>;����Y�s�"�`��B4�0j5ɌpP�F8 ��VDkt0y����q�3�"���fsߗ9���y��n4/p'��] -wa#�H�NC�Iڡ��j�qtP�1�Xh�#F��Q1�U����Љ@�x���C��F����v�B��Ԃ`����#�00 -������M,��\{���(�~5�8�G���4��>�_����� ����!��g�} ��}�5j�x-|����Z���*�ޮ�d -c)5��;���zr���)�8���8/�G�51�p7ֳ1���;3��#�9�W'1�k����r�M�����5���C���xN �q�1���1E�o�ȧ)�i��S�4�P���I��F6؉���d�b� -_3`:�[�Ļ(?>��5�Q�sB[n��eC�3���=�;��]�. -y'����<G���&�3��~ξϢ�w$�]����셻���2H�G>ǻY[�f�eG]8vi˗��j��Nc���Q���j����a�0�}b��� ܁wy���k'h!�~��p3nDՖ�)�w��d�Ֆ�ym�.hßkǚ�Z -���2uz��w��?C�Os�2Μ����'/�]�����lcĻ�s�Z-�,wB��ƻO[��h\���Ey%����U���SuI��/���� -�2�~���Dm���2�m�<��kDZ�8��I|Ď��q�����$�0��$��$%�ҍ���e�Z� - ��+e�zӕv�і�lĊzLk; F���m��*v����qU�Ɛ���ߤm������}��^�=��l4ϟ�A>�;��5�]�Ot3ʿ�+;V|�����!m(׆�}����7�Z����.5�mbrg�vϡ�#8�0:.}��w-��#�}��o)��R�����m���19����H���xО���,M�,�� p���?���D�'J;��V��*������J��ms���E�ʝ���<��~�(�2.�+ԎK���t1�/>�ȿ��d-�=0�J�� �r�M -�?I�k�_��\"1���<O�� ����cJ�=[�v�-�$�{��r� '��l;Y\��������M�qS�K�2�_e2���<��|�{��g)R�a����4��)�s�! �SD���`�}R��˄���_��C^�I��Y�$ح��^��.���9v�Oث~��1��i��l+0���{�æs�-�[���0�t�{��=^'y�}�����"O������ v��� �.���ja}?r�����(��19BL�=��=���l;ߧa�B�Kl�K1�C1y����� y7�ዝ��^ہ��6�<�.*�|�d}X�\�[9g�@;�*��������A8V�1��'����]���G��R� ��!�'ZYR?�M���l���~��O���%:�.�l;���ZΏb]��m��4�l/���c�(s�lA��RT�ᗳ�b��"�6ʧ����,*��,z�"x+_��J�V�`�U��pv�bC���p4��G'K���:�8�O�-83^�J�<*k��ќ�hL�� �׀��(<�S�o�*��TeR+�w���I��ဣ;BpD�h��dG���3���r�4*وg��r���[I�FmCd����k�=��ޒ�ͷ�;�:�ڴ��͜o����' -O����@�K�Y�(j�@)��=��!�����o���:����qU5�M�[k[Z����*rd_�P3F�7<~xSQX��j�~����R����.n߉;PJ�lE�)��%U�\�K��T�-h���37"LJ~L����5m|�����OL��F�x� -����%p�I;y�ʭZ�u�-�� ��H팡��#� -�dc���ժ���(�O��f����h�.�H� �,p��r��/�]��×���>� ÷�l� u�7K��Q�&�����������6�h#�6�0��N^�1 -0��e��X��� -�������/g�KsQ -qZ���D j�u7�BN�S>7rZ��@Wn�o��p� �"�lV��o�}#kc�N�z��tfiЕ���lJXW'��F �[�_�-5��1���p�87KU�Cb7�r�^����S�H^���xv'�k�b�c��d�H�� ��(�-滺��3�$l\ !c�M.�|�+ -��(&�┸K��Q2(��6�z��~M,�ۤ����,��9)4���Ob(��ӂ6����A��n֕V+36-?�A��� X���� ~K��X��Y��"���B�`��&�^�%V۠,���پN��b�o�}���$6������*]~!Wf��46��=�_'�y���ԃ0��]�]/.�Q�v�TUTHE�[l��V��ª%R�h�bG���#bpN��I�]HN"����Levp�*�PuQСb>��8�z��(��ʖ� ��>�.��������].�C��5R\�'&����,e^2�K��2��0{��TC`5w�f�r_�o������/����S�Iq�����b��3�.���\c�"�"1�+����9>P�_��A�� U.H���A��*����gpY�C�w�q�3��p,�#ʚ�n����V�Ss@/�Z��0 �a��E����0���c���/Fuu�]9�Lf� ��hp�8�`c/�p4���>�.P �5 R�*Y�IA [����#b��X�dNjS�Y�T�8(NG��c�=w����k� &1a�wÑ��5<�^A��9�4,bb�,�"�8bnDL |��A���a�I%�d��)�N��%��I&��Wq6Cy?4]�;��-؞���D������C�����R�M�H)� ImAPm�Q�qh�@�y0�#;�Fit��+u��t�,�31�q����l�žx�Pjy��3�mȬ�5����w�xZ�H��!&�Ĥae0*�/2\8�N�a�ʌ���հ���cY�}�E���.\y9|ix��o��:�ռ��6���ۭ�Y�jl˹��X�=ƈ�*�=��c8l����Q��S�S�0���Oy���������� �{/�W�bl.l��U���k��=��%��o��Lp�)s^��8x���r�Z��n8�&��!@�5���gx���hQ���]� �=���ZCU���G4֪aK<X#e#����l����Y��m�0�l�N�E���n����S��e��/ ��w�&톻�0f�����]��M�w��I��)�m��iҦ�I�[ۆ>��R --0 -��U���!���18��,v��T��v���p8��6��4�6�ݩ�v��<���~{}��1o�?�M���}?����Bk�����WCG��-J���(����$�6H�_�����m(�-(����-oK�NЎ�k�n��cL�;���-���).LB�[B#]-Q�����>��ur�����`̮��<M���C~��ao�j%p���;{�����Ne�m�o��p}(!�{�I�r2���x�!�ur��~��8�QQ��GslR��$��mpW�0i��N��-,�"��S���R��ie�Q� ˘Zʬ%����)HW����)r�$19���c�1|q����>yD�����h�y7h�-ڿJ��֜Wm�s!���Qp<����/�߃7�g衯�d��P�U�^��9����u�k>|n۬�����v~G�v��p���7�%�;���DVA2�����ɇ$�9?�/�g~��U+W�ƿ�%���V4P�����6h��9\A�_B����=OE���f�w���H�7I�?�W؞^�i��촯�/��N9�YS�W�?4.����$O�ղ!^#����=���Ѡ^�p^�I<O��&��#�Iќ����gd�0$\O�����!�����������}>��i0Q�/b'���R̯+ੇ� [���8�=���I>�^�;��hROp���ǣl�&�b�y���9i��ePJ�f����0C���&��� ��~6���RGi���m��E�s��<D{�&6G�OA��=��{/,�I�]<���� a�J�Vp+�����\~PpX%p��j�]w?1�e��es��=f��^�c7�k��k4�ibs9r7��]��N�"e};;�4|��F��;���ڜ���lL 2��>�[̽���4������?�U�����9���Ӱ�о�i�1��q8>K�o��f��v��í���b�D��Ym�~B����,Uy�6�֨����ɗ�"���v-pt��MFfh����RV�1�&2v+�m�Of���e_����T�yP���4�jͪES�f�^Ϟy1��ܘ"G���:�q-���n'�D�p�q� -��a9�X��p�0�"���Ed�"���u��[��M�W�l5��蠅s�� -��@�2x�ᩃ�YF��0���A�-��l?�ū�dN��$��&TV�@���Ē(PlA�5��t�V�3������jx��i!3:��g�L��Z�h#|+���H��(��a�K���[D�/J�0VM�dT��ML�ȍ. �UW��UW+\�pe8{�Oƹ�zI�$,5TU�WE��$��W���ɛ��W�%�LNKB* ���5������r��m�f�au���.eި����o�q˛��6�������o�+�0�@��J�)��d3���F��?�P/6��p��*���bl)��J��6�/��R�Sb�#J�F��09��"9$:����w��~iI#%��4]�4�E��-��x_cG�!�+mvls���_���W��$�Mp������ ��ȍ�������ҁ�t��>�CV.2�e��pv��;#���% �����y<;KbN��8��ϗb{@"�b 9J%訒"G�����s�I�sD��Hn�q���ӵS��s��|Rl��X�ΰ0�3�A�� lA������ob}K��&@D��\��srgK0�#����!�cR�on��y�ĝ�+��aq���z�y7�xE�`y�ϯDr�fO����16���I\�� � ��6 �w�ϛ-���z���_$�Qq&��K���,v�����Y��T^�!�g��C!��-��sfMN��/b-X�O324źTJA�A��}YR�s�|_�x�.� x���`X����B,�!v�0KB�� -��"T\���� È��)H,�[��~$4��?ú�W�$@ �� �lR�K~�!�"��B�∸Ŧ� t|I��$ƨ�X?����8Yc���=b4���y#���`�8/��xZ9�����5��?�̒|� e�+bG��a@),�� tk͘H��h� -:Lݮ��W�rRA��L�2�P�Z�_� �`��� g�%@ �>a�����u�I�2('�*s������D<��#��O����8):P�i�D� ̫�Uܡ�L������(���USZb�6�gE���{��=A�+�jI�QK�6��F|���Mģ�x4��5��W�0��� �6��Xn�C��6�\����R��A��s�y�����J�k@��89�p�6U��47|��iJ�4�&��K��i��&d�!��g��-Wd�!~���� �}1�^�a��5)+�yq3�"��wr�nr���dH����>C<�@�@?T�=���s�fH��,c���m���J^˰5�gA��b��m ��� �0��f�{�K��Q�;�G��e�`,c�� ���ӧ�X�Y�]�ax�y�S���˸K��B�j2�y��P�v�+���k��$�>I�O�е�����5���t�U���ЧV�����l����ؙ䵊�Q^iu�jW~��*���_�QQ_i���$6t�%��dUP���و�ŀ%*�DPD�"u�6T�p�R��C��4��;x���?�g?�d>f8�9�9����}�}/lq��&uٞ �u�!��Ї>ч�|@����U��>����|���П���.�� o��`������ڲ����[]u��u��Fd�^��Ĉ��`{���*l�+�����Kz0,�mmS�1�b��N��j*�5��bf�8���j�A�������!��אp(�+a�Q@���}���8�y�����`=lἼQ�v@��0,m��ˠ��>*�&U�u��P���eE�"�#U� �c/"s���|X�PX�;�B0�}�C�H��"�^ �@m Mj]����H�����D���l ����l7S��LP�J.��`���H�p �/�>��C�����/u��mB�%�U�<��C� ����,���ʠj�h� -@e�\M� �Є�����9�-�y�0m4�`O�>C嶠8��U7�ʗH�#�l��T�6�y8V���y�v�A� C�{��Ih�qh�a��{�"����'��Ξ3��y�|���E��*,[�⫕��|�v������E�mݦ��c��w�����{���^��8tX����~�:~�S��uh�g��70426�hjv����k�o����t��t���͝�����e��a����r�#�Q�x1�� ���SR�3�3e��>~��_PXT�����yeՋ��5����4�mmk�������x79���/q�?y~�Y$��,��I�/!��/$�j��$�L�ٷ��*$�? ��$�>�p�8Bh��¯��Y��O8\ . �� -X������ <� L��&�%�P�,I�K��d4��)�)!t*�W����jB-Q'a����q�i�=��g�-YwI)��$>����?9/$!1k���0�DFFV�S�0GNn.��'/?h|��X��,Y -L� -P,��23h�l>�3��<DZ :��)mݳz��.��[XZ��;8���=�����,v'څtK�}9��3!IJ� 9!($�����Ҷw@ޮnH��������!������d�����~/Ώ�6"�SE8����ǥqm���FkFJ�U*�5z_:v۴����V7T�FԔF�WŦ�Rr�r�E����"���%�si���F�������:�Y�`���f���ͨ��gUU݉*- K,,��x������U<��G8UKz�1M~��pu�eծz�cmM7��[]��^�똬g/�"��q���b2���E�1��t-�Im�p��K*mM5��7v8�U6���>�y��s*X1�RNRjQtFb��G�ꃇx�%5�O�7Pi����%��K��N;㚮۶�Z<�y ށ�5�a|X��g�d�ӄQ��m�����X!���j���o�v6vXj���=�}����-����A�+�UPxB%��<"�[�,�f<Ă��)9\D��Yg��M��r]���U���}NV����fO������@6�eH�2���'�0<��D8q?��Ҕ(�ɵ6,o�4Sz�w�hو�~��-��.�[�-��&_&�u@�����.�]�U�����w�����uf76�+���*�Y)����3|�Jz��}b;�%�Ň�}���idׅ��k��� -ǀ�x��#�,��BG���piE��R����)k]�����W��n���Dž����n� -n -e4D�*�쀾TC��ᵶtE��B����yS�fa��)����#������ǎ�������vc���E�[�p�2��������f�n�eOFM�ec3�tl��u���Fact3ְ�EР����oo��w�^T�į�VBÂݨ�^ =k��<4XƟ2�& _�����C�˯���������F>�&^c!��Q�eQ��ij)ۆJ�B�^�,Boi6��Mvp���C�� �r�{h�`�^�y���=0K���U8m�CeM�H���j�&b݅���*6���ה��]>�y�'��KǞ�ܱ�~Wp��;�0uQ5�PUU� A��QG���k˳�٥�p�����v���i�vSr�e'���Sq�A�9`�.Q�"_ -�/A�UQ>�"�����Ĵt����-w��+�Ú���Z{���7�b�6�o� �YT�P8�*#�w�1I��-����_R|'��;&p�eD�a2��Ϧ7;ĭ�a$�m*/�1)1�..�W��MxU�\V��G%� � � � � ��<��x��1�!]{�9�0�g�8Q`�w����P��i��}w�ޚ�ď�ԧ�߫NJK���M+�.�7�u���p�����ݭ�ݵu�v�3�ֵ�:��n�g�VEw��E�⾑�@G8B�}�;!�H ��H90\�}�%��k;}I^��/�����g�����M����e��_x�9�T^}o�&d�|��}��8�i[��� �`�V��܊G��Q ]z6�C#:r�]\���5T�9V���a�[T�ャ ���roߴ=�Ą������tF�W�c!�ۍ�CǕZ+%�u*]��^��[4���@� L� � ���u�[fl��M8bN�>N�:Ԟ1ؙ���i����T���-1i��z�B��� -�Y�i���A�����?�j��o�q����nD��;7�I{�ف�Z,��Ē� |�Z#�)T5�ܬ���2�a����9K�G�w?��#�w�')��/#����*��ZK��FR����M\��P��T��Y�i���� ҳ^�z�wg�����������K�L�j��δ?Cc�:J('��i���r��N����u��f���k���^K�uS���?t����y��;!��PZ�Í|T��/�wbH��x��A�� -�ET�n�hX ����1a6��z-�\Y7��'ww��`ı�����#i��!dzuoZ��/k+���$����� -�U��X��~Ћ�+kGZ������{(�h�X��x��HV��?/W�S�u�H�6"��sh�\>�I&&4VI<핁ʏ�;�h���;���M=���:F:�b/���Տe&)ܹHQoa��c>)#R۩TR+��wJ8�����y�����cв�44f��W�� ��>m�xp�6s��"%T5�'�I�����jC�"��>a�Jq���i� 8�AtZП��;.��� ��c8l�u��a�B�y�bJH�"�7���.H���Y�|Ni)�b�p1�nq�����߉�Д�$��yj��y,lW�ܽꖣ�� �;��6s&'�<YW6�M�S1ä���t�]���< p��s8�-����ۏAN�y�<��z����w|d ��$�����P7���;����3�S���IZl�'5�O�4���'>��kBmOA�%�ꉠ���7�% ��Df��I��@]*�P�p��^Cr��7�ٷ��PO�O �����!��a�z��j�|��B�� n�������<�-�'�A��\P��sـ��,@=� X�w�Ӏ�ch̴��n����r�����[�L7�����E aH?�H$��2~���=eG2��h:`O從���ٛ������C|pv\X��@Ї� l+�z#��]��; d�5�}��K��%��$@�<�xZ�q5d�^���C\p��C8pn \^��遼���A��D��%�~�9 �Ƃ�Ob@�_b@�h��J=����Ͳ��l{��/�����ww�T�~��J�zN��:+Ä�HIq�"v�(_�=�Q�*������l't�����;h�E������������6�2�@� 4� ��(a����Em̡ym�7����+��Mʉ��Rf�P�?ȕc�Y�2�H����V� ��4c;�+� 6�`��ڦ�;;чf )���Y����Q5�ᰒ���a�RBO���Ūfw���&B����1���V��h8�"l`��ؠ��i��s��3]�|zy}�5��eh�h�����<E��]%t0͒J��@t�+ ��y���<㟆�����ݵ��;f�|��Ό�#����bjK�t��N��pٍ ��U��&���(�P[j�����0�aCl�~�` -�1c����&���2����XW}1�_Ң�Rl26�A,�� -q5�ZVű˕�f����_i�H��s -��.��l�8��c��3֘�7�H�w���������L�W2�F)_�UH��Lŷ)*����"\i�66����5KՁg-��'#������O��jA&t�Ѩ��l��D1���*y�@-�H�"�\&2+D<�D�m��7\ؠ��f��a�v�{�%��P{������mY N:�j�bkkd���V�8|�\$�VT���zO�$� -�E+�6�w�k�k7L6��>���/������~�EEy��T�I��'i"D�Il��cpk�iҘ�=�-��m�4.��� -"�00�l̾�+3��ð̀�0A�M@@@}��i���^�������{7>��;�jS)�YO��;�|k�@f.�i���<]�9_m�6+LMF��9o�A d��u���Ö���þ��:�w=�͝SבI�l��JjY��j��h��u��<���$�TH -��bssހ��Y���A��1�����Aܽ��w �����3���ю�i6�Z���ky�C�RV�t2��(.��J��k�o�1��w�~�Y�Ϣɛ�ˆ�W�����8�6P{?)��CL�t�M�\���+R�Z�F�4�y�r��9�SѢ�p���"d0�Þ��,k�_��\� -��1�]�ȵV %FX� ��=Yٺ�4���-��DRA�R�m6jXM�:Fc�6��M��2H�E�a/6Syt�P����@�Α�����w�_=]6�p�<B��d�+zid���-�;dB�-����(��r��w�[��a����?���9��v��խ�gW4��l������Q�o�dBp�CB�r$+Q<@M��1�X=|J�[�K��f�t�sH]v.�����?D�꯱��]�p�A���ףq謗s"dS�l�_�G7�&�Q��$Y1�QZ<k��D�Q���~e&�ϐM�g%������ و��/�G�۱;����~�Ŏ��^��,�;ϢP�'�����I!�i��$#�<���Œ�� �����+�@�>�/7�s�l؊5��Ŝ#�Kʦ����>5@ȟ��G �8�v"H���)9���OD��Yy(aF�:m��2]|o ]�f�:l�b-��^�9����&�^+��i�?�C���� 7v� �;2d���#�8F�� <�r���%C��$(�(Vb���n�.��[�҉�X��K����Vs��ZDl�@�$H������K*�w��{w2�$�t_"��'��� (�h=���Ř��o1��W�q�����B8�� ?�K���$��Q:ĮM�$�d lL���7��Y<з�������?ł��X�ᮣh���,�k����09��x�Et8�Z�-'@�ۉ�Q�� ~u,$p kb �w1��Q4P~�G��!ĸ��X�Y]�0��'�>�ذ��>��<��K�����<�~Qo]���/A�;� �0 �+.B֊P {��+rqd4�(h>P�����M��b��/��� /������9��5��c%7pF.������q���ć���1N��nF���I��ݚџ��W�W�W��S��@C�i�>��[�s���½�Lq�<1�����Mk��)�� -}܄�8i\d'<��dsZ�Y�)n��֒�C�'����Ұ�`�[泫�/{?)��v� �Д)�lj<��q���J�4"���������FV���F���$�Y��:���4d�#wz�!��Բ��A+�Z/���^�>eI:�(3`��~e����ʉ} -=�GR�tl�nn����!����S�L<�����#���ԛ�JW>.[���c�,��h 1h�B��/�$����� -���oY�M�*Y�I�`�+�]Bk����v9:2�"�`/�g��|����f��OVF��L�����_F��[�$t��j9�Q�ܔ�%5"��ZX��s]*�Slf��� h�0�A�E�K��ϼ7]u�{����Ʊ�|oyvl�%7�5�G��I�5*��Jn�VHJ6�SU"lS�yR#������n/ڃ�{�A���'�g�9C��k#w�]?�_�z�N%)�UJOo0s�N��]�Q -mJ��D^��H��E�6�Q��L����!� F����[}�x\�5u3�{�!b�`S����NGfl���^[̦T��l�^.�jt�"�YY��kL�f�N�.:Exzi��� A���J|ߘuxNԇ|��9b�}W�qwcR��6#���B�.aRlf>�j� -u�I��4�m��A���+��N1����t7�C��N�>S���xs����}w�&]h�'^��&�6�Z�c��&�J���Z]�F����U.�T�!���� Cӡs��__aQ_i��d�1$1�Dͣ�}�S�]5�hL���KD�D��4i�3�{/���� 2��W�K "�t�H�|��lw/��������8�;߹�j��u�h�S�W]�ag~�U[O -/�O'�r��b��y�A�J����d�&e<�%ꛓR[- ���k�B`8a5Wra�D����<��v�0u�:6��<�7Ņ����ƽ� �@!O�S��9i�Ĭ�����Tyfs�4�5ђ���A����!��)?�b��e]���}~�[zB��vDz�?"��7Rc 5l��R�O.�I��Tʄ�Z�����SDy�ZaVk�%�j`�ؿ�r�d���-.k����o2����r���{����Gd|z#��\��HŊ�D��<%QXfP��ʴ�� ��UiI���9�� �|�14V�����i�sK��ͽ�C�'K�#�rzb��>!E�<f�U�<���D �WJu) -nm��US�b�<R1*��|��+��=ؽ�G���#s�YkS���&�ז�1�=�cA�a�K�9�/�/.B�M#�:94Q���3)���)bF�AJk-�QZ����v�%��3�?���g�y<�&*��ǧPS��5�^�7>/xx4�i�%�X�W�)H>D��b�fI�OIa�&ө����|v\g-���nQ |;�A�-�@�:{��u��){�i�?���0�m�t��z*�E1I�O2�y�x��q �>����h����q������77�Y�7aga;�4Y���v���3�l��5���͙�^;���VχS�ǜ� ]ᾠ��^p=���>��������)} a2�~�4$f�:Ԓ@�{'��_����;Tۻ����2N9�d,�mJ��*��\a�!��N2��H�92�.�@q�jg�/i��Ŭ�1�ע��- �b;�-/_��6��{�7�h}w��-������K����fA�~*��z(X� 8��xH<)ѐy�����, T���T_���lE9�Q��)k5�[%�+k�pu����-�|B��x�� �Ϣ����F�l�mP폀�/q`�*�_[Ңn5�/}��,CY3�"�#����9pa \����x��͑m�?��A8��쭡����!a�-�~z�j�F���f֣�)`�!���WH�<.����6���f0�ܟ!j} ľ��w����7�{�M�o�ɦ�xϒ�J24!���H"�Bd8��p݆֡�,����k+}�g��� !k<��Db��\��;�^w��׀��- -�YM��${���#<e W��}h�mA��뢊��B����̜\}��4�mVR�9+���4���MAS���q�9�L�u� G<&����� ��fP^X�k���4��I��J��s -��syҕY�]���{F\�7-���Or���Xf��u�vG�НG�ш�d�@�l�ca{��5$a�T�u��i�����d�����0��_�R&\�T��x&� -z*)=��)z�ۅ`��P��~"� u�����@�� ;a ��+Au���vs�!��G�J%�~��8?M�z�)5A#�Cc̠��8��#�sM�f���ì��+)Ä�l��~�۫'� ��J�p}k>��n63x�Խ;�&�I�c:��p��Ϝ���+t����%,`<�UqL�&~3�CPC��V��h��A�R{i�a>�W+�@wn�/���r���sn�Ȏ�瘁�4���>���Q+�:4�m�tf�(��$(4��u�'� -f�����42(K ����o�W0���V,�8��5z�M��7F|;�K8?�E���s�ڵ�����P�c�I�x��"�}a��J�*)洋�8ݜlv?m��4�1�� ��;� �i�|��ڙBOۉ��]�E���B�Ş�O{�6���je2�J��/�E��Ji�����7ɲx]�{�^�R��@�� ��cؿ�i���ߜ*�/�5T�]_I̥c���Y,\�^� YN�HR�K�zA�<G�/+��I��Y�6E��S��0���� �ۯ1C��e�|c��c�h��N����ˣ.��n4�0«3��:)�X���t�\�A��(��e�����遪����V�9��u�hG��S8utGQY¢�[HBH$!!!��)ʾAMdR�"��"�M�<sO�o�M������}���>߯�T�(-wȊr���~��@:��L��Y�A��Nh���Ŭ��� ��W�����٭MpR���f�>(ͤh�´�B9��@)(�/���H�r��H�!S��������;�q���� �+w1����`��糗�����#Ot�������@]]zl}�Rs+;��Hʽ]X P�P��URU�}�2�M��w ��%�� �7��J��{6����~���q��oM�nzmr����1u�rEYV��1��(_�R�n�K�y��U.��Is{y����d�p�qD���fS<�_�x��������--DW]#9��=FtU�|���Z\&���f�U¼�R��H#�5���N����o*�;��;���{�O�X�l6����#�y������+�دmq��7��:zTY����1 -���r9_^�Jnݒ�Jje9%z�@�)�z���%�y? �u�h�,ӻ[ ty}��O���'��Y�9Mk�_�>%�D���j���l��V�WdgW�"ʪ���*��e�"nI�T���f��F�C�8������h��Ͳ�����!����G�c�Vw$��n����5-A�0#E� +]xO��+�x��Ν*��C1��KȬ�� -��;d�����Z�<������y���U�Ä��!����Δu_�.zF )hce-l�������,nc>���������t�m��~��k*��;/;�v#(���ᦟ�Ӟs˚�|V�F�jF���<U<H�\�G -�uӢs����5�]�`��3S���O�8)�r�[{������;dn�w����h����q5�.���[+ǃ��e,���(�U>B�Ӯ�X��<b��8�1�O��S��+i�~-����H|6H7��;{>;���P�=z��#Ҏ��iWl+��m)�ܯ� -?.�$�M���/�8۟i��ġ4�"�jPGQU1$Ã���Ą�C�<;|��� M4lF�Ϸ �� �1�YT��U��|����χ��s�e��]ҍ,W��w�fyR���c�/�X�dl $[���� -d}���9z[�)j{d��/�BuGT>sޢ���U -�^/��9�;�g�H���t�B��@� �w:'� Pw.����f*���5�I�ҵ[�Z�>T:�n.�X䁻�.������-bw�!i7���8�%AƁD��!ć�!�n�e�b��x4�0�ڢ��eH�h�j�?E%s� %8 9�6ˆsV�X��kR h"7&A���l��"?���2_E���(ȳ�ծ(�w��J� -!�f�n1C��MH��� 8-c��Z��M<�������!��HH�����p��������,27�@��k ��*|�M��X�P�n*�5Cy� e�=b����~^.���n^V!@x�*�XC�M �}��+ @YE��*H]�,[?�����C�|d*uwR?B��[�,�`��D��( -�$N�`pYN�w3?�4�?�+��BVxB��e���q�]���<�d�T���r�_L���J�;+ �d�/�8��ϭ��� ��~��͐C�_��{��s�-_ḐYrj�[�2�yp~��t��zvi�9��&m�{2u��Uʴ�yڿ�4�c -:����%*�zl���YbO�ہ$zˢ���]N����a>+��O�l̬r��4x�d�=gX�W�����i������ɠ>�TPy�����@�tܳ����Z��� ��-�Fn]�%흓��f:�dK����S�R�I�߉m�8�媁�:�>��1ٓ�:�)e"�5y:����8Ðq�M���{vP\����BA�6c~��9�贔sjR$t�Vx��R�2����4D�d�G�ψCiC�{h#���W��)�Q�)�a��d��(K�_da��9(=�ߩ���Ta��(㿙RP�sY�cR��Q��D� -y�+�������I}鏓������^�4�FI:�x�}�T�)�eɁ���!�A��9�.X/��ͨC�O�}�?��4� + ���mNǺ�uڣc]�0�Z[7�����"u�*"[Q@��%!��}O�a !H��E��� e_���S�7�������}�{������rҚ˹<����(2C�sc�%�H���>~S�c^'����`�2[Y�F�4���"��9P?8�qƑ�B���x����7�~^������Kq+`]��+3���U��2-�,1�:Eu������i� s8c�j��\��!�P?8P��g�I��-r_1g���eYȮ��X�qC�됎�a����d��̬�y>�]���J*Y-�n��̫��#�2�$���&��;�ii8㈰C&v��g�o��V�Z��0S�k�2�i��t�_O�����s�1�*[jJS��Q/+��Jx���Rl� -������"�T�-��@�b�����^u~�õ�T��~�.h�X]��@�L���i.��k�1��rRC��Z��0��%���~��MhL{"�K:�C+����6�w`��%=�3��~{~�\�۲�>�&�w7E:Yj gz*(��bV��BatC^:�:[I���c�*��%w*��ѽ�G�BـH�6��K����;�T���i�!����n������>x?�P_c��]�)mF���"~tuA�<WA5d�Y��B�NY&�*�E�$O��8'c��N����~p`�';�s� �Vv����K�Z�������i�w}XK��\θ][.�I 9�(W��f�4*�0�n�8'�$�V�K�*�9Yw�Sm�;��"�p�5�/���3�e�%�m^�����>�;��w��!ɽ�*�Ve)'ʠ'fP��,f^^>O�S,T�k$Y�Re�E��;̽����{����A~�����{4�p�n���G=~[z�!�Mc]�[H�j�h���H�QH��e�\]&3[���� �Uby~�4#�4=g�'�c�б;�p�U`����rW4�����n�U����͏���uƜ�oKt�h��ײ"��Bn�4EU�`d��\�=�@�+��H$�O$��!�8o�a@�佃`%�����U��l��p����^��g��A_7?�r�6�ZS�tM��:^��ZBά��3JUl�Q�J�F1��#��x|������A�%������sF����~7{��_Z-A_�=�<QfN���#�7�Aj����h%��eP%5w��j �We�s��*��U>�e���L��ځ{�?pM|����j9��{]�y�}i�ȍO���8^�w��'�'���0��d�B��MF��Ϣ�[���6���Ck���FY�� �-�k#� ��{���Ш���r��x�5M�9T��v4���=��$^W<���zYa�A�,#���ɩ]�j����YO'w��ɦ���V+���8��؍��t�#z����FM�K��o:�vN�:��b\2G��ӆS|���[�^(�gi�?3�l�#$YJ-uIDKW��X��J��l���s������h��=� -5N�E�3�����_���y�M�L�1�s��h�r�;�rgZީV�M�5+�l�����Dk](���09?2m�s��q3��h�q��_�&�ʙs�d��^��{���Ϳf��]6}@4G<ʝ�:3g������ٴK�Y啤�|���w�l�G����Y�wܯS>���u�&7�Q�Rdz`�����<����"�۫KU����n����&��td����)��8���Ar�wNA�L�ݩx(?ͮ���L����A3�%�T�5t-F���D�߾E����p�Nೖ ���� -��ɐ����D`A��x���7���?�DC��Hhs���ö�Vi�: 5�,D���H��ߐ -#98# �[ą+�t�XE��I�����!qS,�|����� ��y8dn��P(�廂�i�-�55� d�X�4��}7J�}�G\P -\X�nK��kY�����?�A�G!@X�mM0�w�m��2��P:�C�?(�h�F���#��B -X�D� 1�K|�!D��(�I\\n�����/~nb��B²���RVx}��Wz�`�'���;6QQ�PAB��"ć���~�5y��l�{t[f��v��q��ص;:v��k�ZoA9Pn����xs'�"!!�$���pA.��� �C�g�q�~�~������ͼ��y�?� 6�o�`�N$��p ���� A pU ����ܮ�w?�~Ļ���e�[���/�jo�����+@�'�p��};����$��iԧ@���7�p+�v��{��-�y�G����8�'�OO-�μA_�{��x�0����cd�=B^������C�{"�M&t^r��� 5�Ȱ_�)�+ �o+R|���߿$,8�ȷ_�5��綞�g?�|�rx�f�]�F�}���W��+Ô�˃p��w9t�B -�M�M�Ew�X��<@Z��y�R*g�T�o^�q��|rNXz�5��{�w����4�?�� G����7!C��������?W���IЁ[�[O��T�kAz�NJ���%�o�i��sr�Y��Č��9�R��T\��5Mpۮ�����2�n ����>�\�c�bH�m�+�9$�]�[O[O~� �_]t�K�� �Z�7�5�w3ʔ�Ny�� i�ϸ8/`Lh�>ʫ �4G��c������^�T�C���t!�]q�C<tH�L� ����r:�5o 7<� 1_���Ng��Mj�?�R�z�L��KL7��E<[��Ә���H�a���/���NLs6���p��Z -u�/�Yl�z�`gx��+-�\��ˉ�r:;iۄ��T�==���*T�}d_Jn�3��_�����������l'��=�������,�v ����H�q���R��������G������G~9���>j&|a`�{�M �U�Þ�3���� �bKR[J%�������&7�(�� r-o:��7g�-D���d�����8���9d9��s�k�8�oo�$lQ@<��fx������N�&�UnHl��'��e�FQ=�>��V+�W �h6��`�&��ĸ�*�,!�,��g��<�fJ�~7Q~k�HY���pOݫ��lϔ���+c��LL�܌����$մ -q�&~�ZS����H4�-�Ż� :�ag��;>�+�����K���?� -��Pe��~+��"�g���l�7���-�Ji$��k��^"����.f�d�U ��I�pf�l�+��:�a�i#+٧�9˥Փ����yM�gI��P<[��w���Y��jl�&�P�ʣ�Y�zf���mNu�s�ctS�oL����`_�� -[O���e�Id����x���_�o~����a5�`[�����_�+�0Ic�z%֒�#��Ps�ŌU ˤl���8�(�O�$��XW��{t����A8'��n��W5�|c]�=�u���s %�U/�j�Z�i�|��`֙�&m��^��k�9:�3�N3�Ԫ'IZ�4��(���;���k�d��8��Z5r�o��5��["��5&�W�֔ӯ�sC��E1�f9&Ǥ!�����|4#��N�5q4�qպ�,�v��J�Jv��w���U����=��c�x�y�E��}������uKS���u�3�4�+�f�E�S �r�]���n�EU�2V����0tq�a�\?N�g:�x�@��m�\�;´��d��2�����;��흷6�[��V�M>e��^)�d�0� ���D]����Q��rrs)Kj��H̝q�[b�H�N�+����qn�gb'���2U~n:��?�r�z����U}{�[s�I�]���:4�Pŋ���w�VNQ�%�,&��Ȃ -�jY��v6�p�-��r�xW�t���U[`��@v���h�'��̪�^�O�O�6Uu�|[ڎ=��B�hl�_�5pn�kEq�j9VR�NH�)��":��e��1Ye�]:AfO%�С:H��ߊ̕����y����,r���qm_�Ʋ�ѻ -&�~@�ҵ����١�{�(�]vGШI�6��B2���J�k���4z�$�^9�s`��o�&8��|fo��a3�c�ٿ�7�� ��6psC�#jGN_¡̧�gՏ���.V��� ��!�f���mYZk!��Z�'�����H�SI$�l�+� -x�{��6"#�됮�ϐ{C{��o�b����7?7Gn���U�~Lu�/��Q?^?��+ ���"(�f�L�����|�hG��0�Z��DZڱ�E������M�Ȓ@B�%�JB� ��,���您Xh����� �'�&y��������>緼�}^Q�_]�%�����z��`=�'�ꢦċ����h��|������|o���{��A�9����ל��j���H�����\%���5�ۓ��S��S�����x\w��5Lfv���.³�M�~�Zo���-Dt�o-�<�.i=%�#��~Z��#���E�S�⿓�8[$Z��@�p�i�?������E. ڊ��ںCq�FO���W���a��贀|�,ѓjj�g��Y�j�P٘#:7q��7�3��T��t}��BO]!�3���|�p#G�b�lH�>�prː�-�pa;�Pkkh�clv�;�>�����E B��P}���-F%ztָ��� �D%��±y_ą�%ɐ���,����&��2 w]��C�\�&�l�B�T谙��3=�F��B�~1C���i؈4`�T��࠙��l�͂P+&P�2 v>�Ѐ�($��5җP �����4J��C����� x�b:�/"t���&� -�ʁe( ��g���%�^ĂCf �GC � -䙑@�1seI��q| -��C ��`P���� ����t^�Tϟ���U�C����1�� �FI�Y0a��#��>��8�B ����A،������(��@"��$Y�@��7�fzA�Ԭ騽��9��^ �b4C���r���ڠ؊���3�W���0���#8�A(:a� (�h�b ��E�Ʉ� 8��l:���"��a�ގƽ��{��A��?�� ���5��_�(g5�ׁ���}k�߱5��k��<���2���:��ڍ�'��a�H��&��)�r��@]�C8v������Q� �#�<�R�_������ 9��(.�`U��W7��7O�m��=�����c��n���.-{�a� ����ԟ���a�)>:P�C�M&n^��u'��Td+P2@*g�Q![�O�\?%˷��\�<!��6.�a?&ht�8}���G��v������c��a_7v����LQ���H������Wv�U�D����BcZ��)�d�Dj��1��ͣ�s�?H+t��Z#����6�A^�{w��gԳ�������\���gS@4v��T�K&����U���!!+�ʨ�Z4���b<C����V��m7"/t��9J.��n��x��z��:}�r������ur�H<�|���|p1��N�h�7�q�Jp�\h��!'�j*��x,;~�N��0�VlL���O�s~�R��NZ��#��}+�����9� �=�����k�HHo����H-��I���@�;mn=.n^n������X��E,֝f~5���؟%�֫��ӭ�u�z�����&� ��"�*h -k��{�7�"�i����Q^���'&�,>n=n���x��1Q�o�+[<T�XǶ�ɕ�wi��O�:ܮ��o��?�U��և?ߏ�Y���$�#�` �x<��|[G$@2�`:v��B�zJ<#N�E�<�c�~s�Kȋ���V�%ھ9#plϑ��Ԩ}�d�hQ�6+��ʯP��nG5J�ݑt�o�{c���2=�N�R��b�Cn= n�t<#r��d�!3݅#��C��E�zs��������̭%'�Ԥ�|�q����?*�����������u���k)Z��dx��kh -`X�鐄[O��ψ�{�x�;a��w����]�ԯ:��6=?�ul)��=:�$�����9C��.��O�����sEy�y9�5�Z�&�*u�Q���T( �J6�b�6n�<#4vȘ������J�̞+A�;k#��Vk��"��Q���^���pJT��{�ZV岺�V�Q[��w)��U��H(S�Ǖ��E_Lן��f5�G�X��ŭ��3��6�/pB�R�W�M�8��Z��'5��˓K�� Er��������O���S*�J��*f��>�B�Ä���2zY�3G����b��l -`~r�a%�9[�d���?��ׄ�����W�ZŁRP�j�߽�Z)n- -��7����@��L ���FdJ@� -�U��Ѣ�@�p������|?<������s�Ԝ��E��?4x��UMߢ,��mQ�m(����'8Vd�z�I�� -I�P\��O������S��_�rR��N�xg��{j��8���9��f0V`�X«+�Ӗ���{��YG��v�k�X*:RSa]��P*OvW�ҩ�rZn����^͕IZ���^�4�A��C*VS�b��6���ad�L�F -w�P�6��h��u��]_y-�������"�peI�uYa�CQ~�{�\L�gg�dY�,i�Un���/���e�H2?2�c����6�8�ȼnD2A�!��w����(��<�����o�z��ꪠC�W��(Jb��.��ҨYyY�yS�s�����O��M�${&H���J���%g�}��l��&��I&6�{.7������.�s�f�R鲴��wC�u�Ί:�eqU�UAE���,�5�8�'�H��k#���s9�����%��{"H�W�/��_�US��Ł����[bc��o���[�y�����{��h�v^Ҡ�1�l �W�u�ů��S��q.��d���t��Ry`|I +����P��xъw���Q��B5U�!3���0�܉�u0Rd��L`� ܺ���VӚn;~S����\I�Q��9��,<��~V��\��~�ZB��� ��T0"�ְ.^���U�2�������>�@.q�$� {![C�a5�}�c��wwA�罹�߳_P��i�� ؖw��/KtD�v:�5�BB�e���b���l��kE!ת��������!��NM�h���|�����+`"O��Y�]Ӡ��rh� ONR*��-y�*���iVs���"�'�D|w�Mt�%���4�P��C�Q�-訦�;n��:���C���qO~�g7m�`!�'Ho-Y#�_C_���͏ ��9TZ���.x�L���X���#�o_�C�eԃȓ�'X������y� -gN_�+�O���{�μ3�ƺ=����� ^@�G��s�����h� -5��p��~(~c������7.�į}�$��o�{�5�|)4�#�p0��`0�w0ˊ5Xh�xQu6p����C[ڳ![�S�M��Ikm`�|�������V���P�x��� -���ꔮt�vF�Ӽ��^�bU4��$T%�"TEl����S��f�2�1U��U��V���2@����혥�ФV0Z���e��M�U=(~��>n�����d��NҘʹ8���H���0�߷"5kE�:x5Osq-Gc��$n`h�M��-4M�V����;���?�AgF�|Ҋ& �V>@K @u@Q?rFVA��VH���x�Њ����,z�a#}>�*E���4����ɥ�E�J*�x��^�h�7���'k=�V�f���4(~���h@jo�H� �x��8pњ�@�)4t��ԙ�H�E�=������93��v�������K:a�BGT~�=���QP[P�$���'�Ⱥ�C�́�?�/��o���@;]7t����W��?�S�;���.`���;��f����g1w� ��Yc�V�+J[d=��~� �%�@C�CS�B3pý��`����[�����6� -g�N���/H�9�,ݓ(�=�!��0�r(�1�����F��N��^��G���?�O�g\Kj��zf��8���c�l��/p��g�c���:�t���a��#� v�va�V�IО���IO�J�Y_2�2Nr���c*1�P:���'Y��ܥ�-[��ڕ�i6@N�d�N��~���z��5�~��6a��'&d��������3qp����.2{$= q���b��T2�0X4�b� �[����_� -yի'�� '�ʵ�;�?siدL�X�FXc?�p�&n&�g��6�y�#q�'}��N2k�D����t1�e��faH�<E/Fa��d��`"������q~���c��׳i�;�y����O��[?�G��gO����ۇظ�/�j]��Ӻ��]�aM_i���3���ֶV[�q�V;�Պ[�������}M�@�B! �BH�B!�%@�!�.{�RAQE������|�}������� (S��ly� {�=1&0��2�V�Һ�t�Ʒ4��7����)��w/��{�c f�H�^����������y��w�SQ�b2~����4��Lf���� ;�.&��c -�!k�'�z�dlz���m���W��=/�5fs��C��z��m��I�GgbƏO�OLF�<�0��� "��-�b�ac@,���re��è�G9���M ���}r��%n�����v�1�{^0r<���g�K�>����&7�<&u���=7A�:?���q��( ��!��#q������P�<����L[Թ�L`���RZ���Ԉo�R�>�&�{���?͔XL��Q5�R�����:.��_���y����7Ң�����S�Tx��4��K@�>y����F��]\�6�mռ�vVx}Ì���4/a�dR��D��z��s�M�0J-q��R�2Li��[ܠk��[7�{y��3:���6:�1��4`��0�yI� ȟ\� �X�p]5'�_;# �8%&�0!�������&��I��2���z��mZ�G'��W[��-q�������{�&thH�g�n@��2u8�q��7x'�[�=x��Y�,�g͔��qB�㞈rpD����l{�b�.��r;K���,�hf�x7��|�C~uԇ�5�Y��;�j&tҳ�yc|�pi�@}3����[��u�x!w^1��Z=��o�'��9,%�'Xu ��m�鎷��.MI -�z�ƫ�U�W�4�zp�q����c��ұ!�����4D��IC��"o���l[0��|���c��oF��w��H:%t��l���X����I�y�j�rN��W��X� *bM�}�\���Kƀ��i@]/��� �A�6�W0'? ���=к����_}��<����FI�m�(�^ Ĕ�<Ky����Zl�*�0�3i8XŞVs��)�S����A��������`Ib�+l���+s5* ���0l{�2ʬ)�r�6�i[��uԉ��t��&-ׯ ���U)S���~B.w"$/�%N���y|�1F}���^,ڕ�h.��b�9�QY��;0Z�Yկ���C�}sA�Y]>��^ΰ-��8j%|L�X����j\^Z^�o ���d�����������1>hE�О⠹�^��那`���r^�]鷮�����(b_�:�W��~F+g;d�0*��S���'+q���4�6D���!�H�����ID����4\ۂ�#�\4?�ټ�`R{�Qu�:�����P�Ϛ�{+41��j��B�A%Ov��I��eI��$ -\ff^�Q"����G3��̷�L�nH@+u^*�˔��dx���#uGAO���uޟ7T��u�{J�����S*5�B^>�%'/�C*������ ��iY��Tik(Oz'�'}B�g-aӲ��1`���I4���`){'xR������������YMm�f����"]���$�d^�>��sY�⻋�ž���{3(%����� -�ȇC9��n�kl�z����w� �?[��|xT��4l���AK���[k*�n*��u�*l�SF����DE<״B�7�fv@�Z�V�Y*��IPNX�E,[ =�Ð"�1l���`V� �W~���mm�@c�ݲ�v�O�Z��Bv��G�Ր�$UT[aˑ�K�$��{pJ���b%��- �i�Tm0E�O+Z`h�'��c�kH�-��txR�70P� -t���]f���躯|R��^��]�rc��@�6RlR�繵\�����*�'M��Kї�+p���WI�p���r�p�0�Kt'О� ��`�J�ٰ4u}��~��t6U�����m�t_�-�>��g���8�jI���J��,�� -�C�W���'�i�'�q�'�a�3�b�o�C;��CJ�Y�45/��O@������5P�:�����L����҇�v���3g�l��[��8��z��H=��=�K�ݥ��M��CW�o?�r�k��?�����9�L��Q)��5ˀ�}�}9����w��S��q�J��6K�2���4�L�D��DȾD�Bq��8�qp�[I)i��q�j��Ji�h5�wZm�tB8���u_�������|��������G(ou���*�m!�����oe��m�e4�W�6K�H�r�l4u�7�;s+��o��_���u���*\�o��[`�&(�T�ɯ�w.\�p�A ~�0�>��c��pD�]�H�Q ��ʖ��˸�)�D�$�t�H�m.����J,9�SkY��VLY�5Cְ>N�f�)������s&�_���gj���@�58!���XBQ���CN��z�|�f�<z�X��%T$� -�|E�1WQ`�(6e�\7p���K����g��Ҙ��e1�o�t��A9�������~��M(��B��*���i�[A2�=A4�����^M6r�2Q4=S�c1k�^�7;�Dc��n<����z�|e�_�"�K) 47�\���}������g@.�T\���\�,�U���`�Z4�U�B��](���p����)�x�<1uVL��ӂ���@|��/�r����ߥ�S�^HQ�j��U`�%�E[��MI9��B1B0�1J%cU�3�T}Q��j^�����=�;�V߆W&~��K��o{�O=�����d`�>D�)D�9�� ����|p+x�'x�/�S7W��gd�f���S��a#��ǔr�:��z������N7@�B8Πs�/.��c��:먆�p������c�>`��`�!�#` F�%�Y�\�@!��<�}��E��9V�5`8�E��wA�����WG�TYS�U2 Y���yN�U3�yc2�g#��d��AF���Kw�1�7���f��P�?���PW�����XA����}�����REo"rR'#'�;d���_��}v&�/�FV�d�� �O]d��c��0��1��r&�1а��s }e�?e���X�V�;Tߍv+@^�*Ƴ'b�D�9ӐHy'g!�B��ƹ5�c�;��dž��WFÜV�A��yr�|~k��+Mz�nw��H�O�lr_�p�=������&����9 ��)(��BAѬq�q�����G�� G�W��y7����^,�sd{9���9����q���?s��N.�WP_�@~����aG��J�}h�ߩ�Iq�$������uFDG� O&�5\^����P_����Ԭ���C�s�n߲N��'..���v.n㣩20�v(?�ew�[��L�7�^�P�E{n�*J�h�� -���HgJ��I������OT��WX��kBͲ�����Wt��[t�?����Z�q�~j�EK<.o���4SQ����t6t��T�0=Xu,3j�P&OK����'�1��rд[r̬K\n.K<����S�k? -XuĿZ��o�m�uox��~+���p�_"\�R����`#��Is�ZE�G�g2����0��:�9Y�����-����4�����Sʑ;$e�m� -�VQ�m��ƾ)���}�3�7��M/�e�� �O��6 �h�X�����B��v�@r�(� �J�z7����`��OU^6�g?c�,_h�1G��-3oyK��5�)�ֿM.�{-���eR����[�O\D�[ -;]���ԋ��$h{7���e`e&��C3Os�Ls�ic�.�8�3��8T��3���$���Ͳx�U����'ie�S+6?L��r?��[��{]�=n��=o�{=���n����R��M�u��P����M�^i.�4���0\����Ud����K�����Fo��f��eX4��{�}��^�q���_]n��s�)��z˫6���&��犤˧:ux[u:W��}U&�*���oX��9�}� -�Aq� ����Ǔ�[N��zS�2zzXd��H�����uw�l��[��6���Zf��Ռ^���}/����(}P��P�6�U����Y��27*w�^A~�%ߔ�\f[�X���w�>�p������9��I������)�neY�)(v��;�v9���b��>�YW��g� <��"�Lz{��~�3��~&���CGe`e�E�)>�G*���t�iX�W�_��vܭ�֎��K���nh�*Q@$ʾFPAAdd �@" ��C@`Dק�hmQc;Ӫ@�����������������9<3����K�;�l��;��*�?핝\�%�]��;g�,���c��Kj2NU�Ε�z7UZ�gy�Wiʰ�2uĽ��Ω4���2�F<b"�Λ��h�*�����m�p�5�U:^W�N����-?�V��Y�hQ����F�m[%�w��:�)痹�f�z�dh��>r�Cߢ�g��̷�Y��&[}w돬OE�L�c���cb3��X��V�]�h8����S~d^�2��y�y�4��F��M��ߩ�J�����U�]�q>��[ƿ�[ȿ�W����4���L@�2!���A�O�f�Ha�,Ë�Ux�\�{��^�`�w�kR���ܶ��KJ��j��7���?� -2v*�9�< �H�p� -՞9M>b�%�|�77�?��9�\�"�+�%;}��!���3��f���/���e�U�wj7`��=O����4U�.֔E��*�ݨ�'mU��m��$�S(.r�<ss|D��~9�;\��)W����(��r�d��g��1�>�X=��U�"��0ŃjS֭C_�nt6�Ol�̮�YT]yr�����2�J�H��e�Kdy�<��ETP�)�h|�$�~|�MnF��˗���. ������C�e�^|�'e��S�<�{~�5�mBc���M���'V��O���,�e)?J�|��b�S���%�H�q�ƛ'��M� rS����dy�Ѿ�"ڣ -`"�̇`>Fds�k�$6���3thw�Y�2^�ʝ�n -ZPV�\Q�VZ�E�N�έ��%(�q�����*���K�>I%W}��&�z$�оT%�����+��h� q�z<.�����Uh�������/�N-�~V�r|��锹�>v�H��Cv �&��>�J씬V�$VV�'T�z�Ux�V{�U��%T'����g�71�����MNĭR#�k���> -��Q�c��K{ǩz��w�+�<�X�~r��-f]Vk����i�S��l����N��j瘺V��+nQ�a�h͈s����ѐ�>(x��t�L0$5�@�!� ��eM�b��-P>�`P|��D�0K������2~�is^w�˳���q�v�t�E��#�[�N�p"��8m�{#���T���&��d�T�=l�lmj{ ������ �e�-���7�Ssn�˼f�z-jE��x���g��7G_���_dѯ� �o� 뿶���'����ن�Ҏ�K�]tjާ��-� ����F(�3��� �o ����e��k�� pF����g�G.��w����έ:u/sM�P��M�CU[� i-C��[��� -�3fr����"}P�!d�r���A=��T�8���,���Zd=�F�� ):O�D]��x]��]�'Q���N���xK��ˎ�_�T�=m^}�i�y��a��ǣ恏�,p�V�c,��@t���&��8?`��]#�_���ܟ�p��b��F���"�$b�����S�BG�g����9:�57hT�i�h���j�88��π����z����"��Pw,�@�Pw��ˀ�&�{6 I�K��[���"_���kB�x�9`����a~5���I�R&������^�����|�~�7u��[3��7Ƙ^��,�U@u+����)w���3��'h5Bi��v����.q�K��t�Ћ"�yP��;%�R���w���х �H;�C��^�,�5�@ �a�����i�8L��Ok��x�5�h\h/����Nt��t{(�;��.:[�ؐ�`��H���Ԧ��:��� �Hd�C���!�?�O2e�+�a>ɂ�le6L�;�kڏm䎭�K -� -��aEc��z��;b-�E�z�����8}8���<Ȑ�s*;�|�i��V0-3X�:|O���y���e=�d߱�r��� �|��b�+ -��ǔ��B������;�L�g#�y;�7�ټ�����7܀ď#���.�x��5�+-BQڥr�S�G頢�Iu��)��0�e,c�0f�0Ìa�:�%IZ���Ꞻ�-��E"�s����N��~>��<�o>��@�L�b*Pʧ�F( ӁҤ���3m�th������SFu�(�� @�R@�R8a�s@ {1ӻc�9���Ig�}��? xMj�P�S��1 �rM�*��J��� �ޝԧ3ƨ�g�P�u�S��RGt�RA�����@�������W8�8dI�����ĬI�����˙ -4�&�r��V��s3�iu3�h�t�h�z�>���}�3{ȧk�7�~�~��/>0�7��z��������5�Ǟw�����l~'1��Mz��'j=Mk�?{Ƙ���ވ��a�+���n��{8w������|�,��^�C���|a~�/�����Qþ ��q�8c��Ӂ���� �X>� 8l�8�=}���1���=(_0�l���| hZ�O�o�Go3��߹�����ߗ~��������-��p�<�a��܌����g�a��m0�c��ӆ��ڃa\��Ќ9�!9��� �0T�����]7� ���S�_+�ޮ��{�������U ��җX����7��b�>`�:� �� -{�o#�W2}Zc(:Ps�5�/2I�7"ma3��c�¨;�l凐�5��;�6� j5�|��=�㦶��g�q����q�h �G��Lp�3��5�v,���`�^���X�!2�>��������)�=�����3R��mx��f�ɫP����:��!��m!-?��n���e����sK��{a��.�5��*d�:�5x`��Ď叻j��Ζ@�#Jp'}�=���������t�q�����L�E)̞D�[���X>�b��l�v/�_�͡�ַC�X�d�X6E�Yc$�ވcd�:� v�#x�g�c�ND�N��BFv��\W��?6��:� ���Ή2|����1���a�x���\���E[�Y�m���lnF6no�x��z�+�+�=v�#��4D��KѰ�R�"&���6�p'��{�;��=��&�$�'�Atxb����"-@�)?��7~��$�?�f��Y��q�ۮ�*m��T�}���yw=��{.Du9Բ�ٝ���q`Yf�&u�!�p�ŷ`�^Ě��$�Ϸ!�"�&����iO2�f��6bV��%�\�̯&��6$�X׳�v��W�������mr��m�w.��>UL��*~|�� �T `���a�:�}��o�{�^O�yf�WhE:%��=� ����Z��Yf/n�V]�5iH�_䊬j9Y�5I�vU e�ϝ�q�`_s*g�8+�_����:��ݥL[elQr�Rp5xb���d��k�7�)��aA:�m�s�y ?>�Y�=�Q���8rU�(��Bj�y ?ժ�'�Uqs��9�ʤ�}%� .ʼnw\^�)>�$ ;&ÎB.l+䁕:�j8�=/���o&g5�O3&��K�VҪp¼{l�M���UY�A�4|e�8֤*=�\�Ʒ*�oW�e�%��Bn��"�����[nr��9I]r�C{�<���5�:���o��-#���s�j�VfL�),HK�^r�����3����E���U�� *I�yY:ϪD$�^�&�W��;��˜sSj]e)��yOܥ���R�9e`�,lei`��5xc�e��%d���|��Wy�Ik��S�@�J�L�\tzf��oa�<d�J�Z_���\��lU N��O����2a��4��-3���8���t�;�����d�`;b�8��i���G�J3 ]Y�H[�Br�t#�U��\+;�Q���9_��\A�Oey���9qfٜ�yR�mNf�}�8�13��9#��M$��.�x� -�x����3�1];������y�mķ�, �y�]�,�di.7!7*�0w{L���)�J%m^y1è�0b]A~�&�<q�,'�F*ً��E��L��@���"���wx�$}���qG��R�`�:��A�Ѩ�7�|�gySH�riR�'�+w��*��Օ�kUTx�--\ZP�\+/��(+<k���Zg� �r�� W�����5YV�Α�9��� -��Ƒ����7v�N���)}�G��Cڤ�Q�i��"W��56���u���qMe��~�9���"tuv9�T�d[��$o�/��e�����4`@Tdp�8�����8�d�DT\ �vKKh��`K0�� bF҈ T�EE=���,�@��:g�g�?���|�V���}Ҏ��l>�k�C�R����;X8?����k>_��@�k��nǤ,�K�"[M��� #�?�@}���{�yup���#q��'�]�[�%:\���@A�ٞ��NG�M?�`�5?e��[����i���9{mn���.q9�\c����d7�&�lr�F�g��hȻ ����Z�����s�8S2�MFN�#��\�4`����ۊT������ yR������^sz�}��Yѧ -U�*�W�z�u��Iu�kf�I��Xk��b��o�f�{��p�Ȼd��W&"����t2ʥ�;���]��RԈ��Ƭ��� m�=�t���ҽ�W�d�D����(��Q�|fxQ�CDQ�Md!Y1���0!���Ӏ��@YPP��e�}�Y97g��"��D���Oύ7��'W��q퍘�7G�\�8Nu-}b䵽�"*���VX�T\� -�xi|��*�ʷi�W�f�&h�N�v������Rο��w[��#�z -�<pĦ���)��%>��Ax����T��D��`Q�mTh���������OPVWZ�S[�m���=AYE�q��5��T���$8U�dTj!�Z� O#��oH|i�����W/ՉQ��T�`�Hu�Q�:�_�:�4H��G�z�E���~�g�}k�G��z1ҧ�y���.s�4�{�$7�\�Μ�y{��UZHz*D�sľ��?�"�"��V'��W���D�z���!�ЯaC/߆t����ލy��7���5U�Ț��6��~$��qgp�p�g��3�W���=�b��4�-���_�@Ѳ�����i]y�Bۻ-TG֮ҕ�'�vlJ:��yu{v����*3p�ü1X��j���S#W�����=��V $����a�}�9���cM�r��2r�Z�O�I��|�O -�E`!�`>%i�R���ӞK�t������D�u��V#Ź������@�u>�?�u��*d4�y�5 ��4.4��G��l�e&�bϵ�r-6kJ���O�K:Δ25r�,p���H�D���Y;�Eb,"��yc0�&&Î�8�ӹ�?i.�r_�A�I$�o䍉���_�/�P�P*F�&S#Gyv�=L� ���������U��hș�|��\�ϘB�r�gM���8��;��z~�z̹��<�!���fS�� ţ?%h���m���Y��:L�eǚ�r[��#g a��e�k��vm��d�u��$S�K�e�5�.y=c�������6/�0_=I����f��М?@�38� �x�.g f��-IѦ%ۘ�:$9�K�|�@@�L��$U�oK����vH����6�ץ$�,!�' ���@d��yK����ƙ{�6���p��J�=A�TZ$M�&i�I3tIzH@�<!Iϊ�IKEݲ�z]��z�����e��-�F�Y��YF�&)���Lj��r V캳�s��ܭ8w�.`��_��&y�ɷH�K�-? ���u�O���E�m��m����_�z|�7�h�����SϏr2� '��r��MbM�{� ����"��$s���+;���}���������������~��ï~�=�����W�������~��}�}�|�������f��/�+_����5~�Cd�5���Ǟ���X>�smA�����֔ku���[�R�_��?f�lRf5*�7(���(��1�����;ŋ^)>������Pt�>��O��c%�y�$#M���l�w��\����T�^;�{-Bg�T�5"P�s�J�)<Ѱ>ls;���4y|�7A'� -*TxihM�a/�O�?U��H�i�������h��`2�w0���~��Y���wƛ��;�;1�Q�U%�j^�ԩV�߯���&r}�ڕ�Lk�3���,���ǡg��^��Aȍ��C�� -~=�VPӘʐ��7Biص0\N���T4�kpeǓ��o��wb�Tt���ウx|����3�����<�?l�yP�g�DE�tPE�)"Z��R@���뺮c�jQ)"�� ����&��d��ꪈ�U��v�uܢk�}g������}��<�o���W!�x��"�(7l�Njv���)� �. �":�'��?�2r��k�p) -*C��$0P�6�}��l�wH��}���H�Ej:������c���E�w�d�t��,M�e�J8E���h� _#wE��_�&]�$݉�\�$���:�R9 -�@�{*�VG%"�}����}/�q�<~���k�Nx�Ɇǂ�0/<�����,�����?����%A�4�'CT}�@�%����W��[9�訸��M~��I�9���}tG -렢��4� -���0v�S��g�}Tn����������@�nI��;%��SB��8��?�e���~�<��.�Qm��5�wQ��:��)mU�v�V=H�Q^�ڨol�(�FG�6�� -�g��U��(�D �7�O� -�!M���������j��Uc�b�!Q���N���/r��]leTK�ʴ&�0�Rl��������껻��g���_��ke�+���a�����`�-���?�_x�`As�8���S�CKƕy+�Ek�d$��F������V�"�ƭ�o�4$7�m�,,G��9����>��x���|�jb�E��(����}�,�G�G�~�Ú@����k�l��;�6��]٧*\�P��u�Tڤ�`�X�,�E7 -���1��הj�v�6p�3u��,-�qV=�u����t|��W�8�8v���*?x��?�W��l]8Lj3aDw��_�͊���՝*��������$�Z����be�Q�K��iZA{F`��?���=�Q��TKQ�P�F��\����w�������:xR�S��im(�vÀq?��zt����K|mj�Fe�6��j���2E�VZ�R'1��ŭJq_�B4�#���F��= -��] -)J���hW�C8;�{/����z�;X����v��) .��=t7|���x�C����F-i���dR�B�JAT}mM�Z�IQ�Mi�{f��7K*��#���K��-��7U*G�29�u:��V"����?���M�2��{�hC0��w�ú:,Yf7��jj(Zg6�m2꫶�̐�:~�J#�S�U�5Jc�Dy!CT��*���s{y�Wق�wiB%J�P�+��_�����x�]�pj�a���@p4%CG���|xisS�fk�Z���Q�P�1�w( ��N+��&���������;���ak���/��m�%��Q�+�1O�&V��� -x���� �tn�{�Bos�t�㡥�Shn9�n��2^(X�m>�i$j-��3;L� ���<�v'�`Mg:3��Ѭj��l��y6]���0�$�Ź�u�Gx�X�D�3j�1v�&��֍`o����l0w����ql��=�[�Z�h!m�٨A��~� ��T�l�O�[ͻ���t�e8��r3���,�b^L���D�ź�X��M���NpE0`��;}�z1`���=_/W9Nz�/�Y+�.�vR�F0��Jo�E�Z5q���I����-C���������H�Ŕr;�'�Q�+P�|�s|�kJ�a�Av���>���п�/����P;x�]6p|���io��s�8�}��%�6J�4��GA�i�)�i�?�J,q�L*��<��{1�\7�&v�W�b�g4܁`w����`���X�яA>��҉� �rW�-�L�{2'������U���+�8[H��@�*�d�!��H[�������ۑ��/#�ޅ�D���@���9�S�����]-������O�_�i?�τcL��w@��ʍ6���2[��<[�!�I�!:Y닝"�"�rS��D(p��;G��w���yt��ۭ��(Cp�B�,�`D��;�c�4a�S��݀��'T�� -�w�|/Hs���Q7��|���b��y���j��y�w�����a]ރ��7��p�/w��_����{hC�]��{\� �.Y�:����Q��U��Y7(�����@(~E?f��/�ۅC���8��[��OK��V,=��t�yX���W��5�ʩ8MR����1&)j+[�Miv��2�.2eH7���!��Gǹ]WDG%��|��>����k��]��yV� �۔%M�U�4������w��V��W[��Smu#}��]@I.���T1p������ٟ@P��o6��i��΅��Ő|t�W�;<��B�����w�`U�\��s���+[qE�)���+B�ODΟ�E���"�N���Os�������@D5�WH�����1<?[���+���}�p�s��W78~��=�`)�Î�aK XL���`�|*b�`�0o��i��%�`)��@���X�X�� u����4���Y��h!k,ů�Z������, -�L����1��a*`�0�.3er��[���� -���5�],��*a i�F��X̥I�e k� V�e:-��,�$r�Dr�8Ì�S�bL�0���}:: ǯ{����Aw`=w��9`�|V�.0�T�lڬg�z&� sL`/f4���`B�YˆuB��0���"wh�Z� P�;R�����,y�L�m5s:h�h� h��֍7�7��c2@Kw2Aˎ1g�"�hי�L���%�:4�}'z����@��FDz���ao�YSAs��"�yvKX˕5��w��1�u��d��r�3�� -�b�&s��F����4)�8w(t���Å��L����@V�8�)�9���sv?��b�X�����3Q��r�c��>��r�c -��^Q�s+|q�'�u{$���\���$�pk|t�"��F��ٕ����-@6�wm9�٬��u�@n�y���VE�ܓ�- -_=v+�y�z=N(~��W�_T�$.v�� -��U�Nq��M�F�"n�����$|�4z�b�w ks������Y�Z��|���z��W(��$(���(��T�vJrE�3�vI�6�5�V�m��J��3�w�F�FI��k�g�z )���R���������,�`����\�yxp/�~��^zs'�%�� D�4F�!MSl�f�Z�{�Z�����'U�Iϫ6I�~+�1�ѧB���fh��k�:�V�Z���O}hȿ�4���T����x�h�g`���\���p'|��s'|]���� ��/Sl�O5�oV~�!K�a�A���������|�[4�ηD��=�g���5��:��[t�~Ҫ�#�J?R�Gj��h3�?4�=���\n���<��O�Y���� r@{�;ZB�)� 6��+� -I\�]�6h߰�A��jO�< ,�8�ꏿ��}P�_�B���;�{]zwixY i��a��H�Q��p�xs�2���>���A3�j���%x/[��(o�� -�FF����C�#�{��]v`Deh�nE�����BJ��*���Vp�Iip�qI�W�롤{-�����Ch�ߑ��j��΄����S���c�u�^�{�&�_�8N����D������#�F��+��ޒ�u#"ߤ4�iI��1���] {iv9�٥�^��4�HFz"i3�?�h$����=����1荚��8K4$����x�⁇)�E�a��%�j�%ȵo�o��g�.��1٣������7�rT��Ũ��E��'F>�X {gq>�Y~4�����s1�w.�t�C���y����[����+�MI�P'��'i�Q���>�;i��7S�5KR�t��3��$f����Ǥ(�И¸�����c�Z���3�l�S�����x*�ƞ�'�� d���=���\�Y�pцhK4�k�X�'c*�6-@�fW�m��n -P��!Ӽ���s15U�P�Ũ e����l���GƟN<m��pq�Ʉ�SO�?�~,�/����)��4�h�9�L������oM?��|��Es��2�x��o�e�.(�\#,A�Җp��bu -6��K�8�L�vӼԽ�'�9���������G�K,'U��I��:��:='�ob���rRi4c�rdm/ �E�6z��6M�ݤ��� ����;�pm����*����F�ݒ���)����m��2�̏��x8�����i�R�g���̖�ZX�O�2y:��� Sft�Y��ț7H��A��C�*]�[5P��7����n{\�-f�S>�3�d�{X���w��"�v��D�]7�j:[��9ź$$bS�.���4M�45�e��nTB!��VO-������X�X>���9�����}�mT��ϸ*;aJ�$Ŭ,S<�T,�.�(�W�k� -Eu����9����yi��=u��w�K';EY*�4s0Z��[z�!(v4~O6��}\�� ��8�/D�r ��������U��=$�2.�řhr��J��-��uaV��2��I�Y-������<rķ����Rq�KN&͒e��LB���?���Q|'G�m�����,=t�@��'�pL��j�Z���U;ǔ���\��5)����Y�$֊�<{���I�}H �4�I$�=�$7��߄�������A"%kf5����#�N�I�nq����bZUf8^䊆Ⅸ+�GUq�AyQ��F�{BQ��Ʌ�DSe~�y�"�J��k/���$����-C��!���r~�do�䐑K6r����������}&p�;G{����DC��4>�ҮD�v��F�}tQI�g���&����R���-%2�Le�cz~����9)�6���kɊG�d�kAJ>92��|����I/�w�I���q�<]�NF]�Ux��r9J+��+BF��)ʢ�e��)��d�����R��"嬔"���aA���-A��{���=A�J��&�$5�2��(H���d��z��{��{G�稬����ŵˠ�]����2\^�sLNU�ɡ����$SQ��<�<�2�\a�XV���s�՞��v�Fkz]�k^��hhV��lb�d5m��.�C�8�ZJ��J����GfB]�e���m��n�Y1.�H�q����ԩ�uY�yV15%v�55{kN:E�t9�~༧�cT���"+f9��Ǔh��$Z����J�Qp�y'�i�Av� -d�l�Mo��f�|"j\҉8���Mb��M��ɧG5͌l���h:i��e��h�ݮ�W��d�@ d>�?'|Z��B�D��k��p�����3���6ig�"�} �o�Kl�1<�l�蘳1F���Ֆ1qw�lJx��4�L���m-�;ں,B�<�m}mz���8ES��`}{�i��<��A��J�W5��S����H�<qW|se�wl���ݨ�]���{GDt&��H3��!ڡ2��Q51��e���n���_M���1 �L��/����C��g����5�T�{���-���Ӹ�t�"� "��#�g.v��!�?���;nnG���m�b���J6��5r�m��M�+�l�w���;�F���|ex��n��z��}:�'����^ ��8��t���l�q:�r��`�/Kt/��mD����p7�?�ź�T�ozs��<,���a��G�������}�������4�`�K|Oj��9?������@�m ��xl���;a�O<�Ū�˰����@,�#�|�˞Eci�A,y.�?���E�������^`Qaѳ�Z���:���B��������������/�c�+3,m�eo\����bQ�R��[��ޭ���M��>^��D?d�� -�T7:�z0���!���ԭ��<�p�S�������g�R2�b2�̀7�c 0�<�A���Ep�ep��p�up�-��oaCѰ�T�$9+�jb`A�C��w�4��s@�e�����@���{��{`�Ó����L6p$G��XlHK�����4Z���_�ZL�`��/���g��q����e�ChO��@2�Gv}����Ŝ��;8��ɀ�7��6��c��dS��^�I�cr�x^C^���}�:<���v1�A�&���cZ��f9�� W/��b�pbal?Ka� O%+eU�������K�*��=ć�}x7�o�^�W���ϡȆ�`����$��kz-�[��v�},�e�(X1�yf�Y+��������{/����ڻ/� /�s�7`�Pd�cp`�x<��B�W> ��l=��bIL�䠅jV�j�q�Q���2�j����M�7e'�#��i�α�3��3T�@�� �D�5�B !���]��;rl�*���bUP���N��t><���/��_�|@?�§���1��>���/a:p^�B&��?:f�y�o՟0c%���@�!ۀ�ۍ� 9�b����4�1�>��lp'������]�0� ���2d -^����O���&�G�YN��� �w���Y���; ���0�`n>l.�����nK9�ݢ�7��0�Ny�Cyz�26D��N}v��8l��0�=�~����r�%��.�K_�'���q���؉�؉�a><>����mlx���# -�e��2QE}Ѡ��Ѧ��ѭ��ѯ?�8k�q��>c�p����6��O����?� zא�����_����3{ �nF�="W��� ��ov?��s&��$JD����=dV�g��3��1�a5��4�ɼlz�9f6�|fv����bԼ�p1>��Lbp� +=��F� Z�s�>"�6̯��W��]���][�y4&X �`���=|�;�"Û�2��*�QV��Ht����C.G_x)z��B� ˡ� ���Ӗ��sN���I1`�,b����:�&�]�# -����|�b�O�B�A|$�M������'�^M_���]�/���.�k�:�L\��`�i�S�Wlb���L���~�>G,�đ��ļ'���B�����������<a.nL'|O������p���.�%��e ��MϦH-����5��'��K��K<�x4�So�-�ᄧ����]IĦ3�,:�Lv$�����a��U�E�+�$y����p?}����f��N��I���0>��g~<C���]��h�ʦ7�ڮ'���P�Zw����3.S����<r۟�s[*qlM#���dqK:�҅���.�������D'x���p��\��;���grw���X��9i�}\�yo���p�Ժ�Sjי�v�Ȭ��hq���vke���/{�e��jJ�є1��1�8 ���X�١�L�E�b�K���;�r��:�V�`~0���g���'�q,:�xV�"�9��m\�SKv��&���Ƭ~�ΰw�Om椷6듛6�д\b���B�b�Vt�]��[0�nO9fp+� -.�����pB��D۠G�[�� ѸC��8�ϱj�/��Ǔ�7�)h�����z����^ڜ#���s�Uܛ~�s?M�OM.q��'�^�����L�$���~�����^pR��$����U��.�3n���f[5��4 -$�|9��_�Z�_�Q�kY���Q���y����'tof�*������8�B�1;�7�fS�a�C��<=�$0�A�;�K�=E�)�v�z�E1F{�)�M��zI�u�Xd_-��*�e���j�����*A��Rp��\[^X^"x�SZ@�K��q҅l����}�D��§��N�@o�W�%�'��l�֒Խ�=���Ifu�˚�\�*i���PJ��(\�ĕ -q�w�蠟\t�.]�/>�/������LB\�.d�G�����;G��7��g -��'�Aw�ЮX �� �Wɠ6(� k���%셕r��b��J&�)e%��Ej�bi�w���OZx�.)��/.|���'�OIqE��B�P���E���m蝅��~= -�P�Ck�w�\ �)���U�&����ʬ�e -�]i��I^"w���{�����~��>zA�A�=~�e9�*�7d�.d+�ұ�{S�~���N��9J���m�M��5���u�����(}�&�X�N�PTp�J��le�BG�J�,.S��ڥe�o����<�<W1�^�����S7�Y���v�>F���q�?���_c���PW� TkAS -�;�ʴ{�Jk�̊�3,��r�%Ua�t�@�t��T{���p�=~���Y�1:��_VżW���q��Y��S<���%��itϣ��Q Ь5��z�jX�k��q(�")���E� &������E�z�m~��1��ԙ[[鞥m����ak���k����a��{�k�kF�ٮ��?�ijB��~�U�kB�j$5�4�SjM�0�&g��:Z�?�Z���U"�iru����C�MH%���xﻇ����c�5����9��yS���%�gp�G��?y"�s�{���(!ǘ"�%��J�7���ȱ����X�~x�j�-�C�~<��x��Bc�� �9l�.&z���)CV��|�xXp�����[�������o{m{��<��O��}������@�I��i`|/�I0���)؞$�OI�ؚ�lN^���RC�z�Ф�n��}�6qo�� ��N����l��m�N(�>[m��o�O}�Ȥ��@���_���A�=���1@d�^���s�O��'",m:6����||�Y*X� �լ��l���j��m�v�[U�o=��%�Z��2 �hM.֘����@=�q{�k�<�d� �s��x�`2�+K �j_������X���,/��Z���e�^����]��I�,gg'eο�,�9f�8'��N��"Gk�Ȯ5RdR7Eu�ˠ�������E��<�'�G�{Ӏ-��uY�w7 ����w�! �ʼ�X����~�/XEA0� -C��0\gaѿ���ɋ�E�w3D>w��} -�E>�"�|�����|Wx R�xr����_�_B���[��B!�7���P��o<�=����,�}2^ŋ�]m0ܟ���g���,�ϣ0珋p��.�+���\���Z��ٶ��y���"ҁm׀�s���m�������/����z-�s�f�͂�/8�Y��JL+�CE(���`�6voc0�*S�݇mU5l+�������3|�������C����|oΟ�p�� -�����~�uV����z�|p��&}�����נ�e�:�mǘO0�s4F7i`�T�qs�M;H����������s���|�\0��lZ{��`��8��M�h����a�0�<0���7S -F_ -Cڃ��aB��^����r�������=N\����Fp��cɀ�zq�)� � 3K?��6�I0�t�t���盏��C��! ^t�Oq����M��2���{p�Η�cy��g -���\�1��7�A�ѝ���s�9tɊ�S¤LƼ�ߗ�0~ ����%Ȍ��!cAC�@#l@bG��'��15hT�����ɢYKa�|���˞�U\�&�4�[����Y�WP?��1�=4��2f2�b�ҝ-d+��l������,�%�� ���&Z���l��-+�`Y�:���"ԲVm��q���<�Q��<� ֠I�@�s����*��mg��!~�Z�c�b}����$��ϒ��$)@����7���ᝤo%�JV�ʿ�s�+��bh"�e���=���%��A,����n7��"���(���`�Qo�A�4�y��>A�����(sh�+B������u���h�H��!\� �-_O�=�hr��xO8���Q�&�uhtڄN;Q��gF��㨞y -U� x뜊J�k(����^�^�DV�g�&������W�9�?~�Ȇ���|-㵙-�L�>�Ms�h���9�P�U�?��m/��~F�{$^�G�{^x�G��<���g|�{�������BOB>��A�m��wd��K�ו���� 7^O^/;|����k.���Q��^{��t�����s�������|c}�u���u�ɯ��w������jݛ�O��r��fY�$��%�G�~[�u�z����@��ɨ�?o�nx�`!J��v�<Z��~������("u�Ѻ��x�ۊ���k��<���S����UE��e�+HW� a�:���������|������7 -��q�TLE����>x�d)��+���lѹ�K��r�^��(KyL?Cy��5eR�+�t������i��]S����9EI��J%)I�=��9��=��Cn�=�)�A���ʉx�l�.�B�J�[�@���d��t����]Q�ѿ��蜮�O�4� �TU\��F�T�ST��$�(�&aEM��?�.Ө��3�?�a[�A@���L�Q멵3*�Z�q�����BXC $!!!�!��%��eqg��ә:.Emg��{��Lo_O�P��;�~{��y������gw:�֕θ.�E0�8��{��w�sb�����e�<����܀������٩��ɱ�.r:�-u9��r�һf�xp�����ǹ��=�1N7���-���̯�۹/}ڸ�X�n�b\�D{�X�;�6^����<�����a:�]��ۆ��X��%���0�+p�/a���ݎ�{����r�>���~�=�m9#��r.Ys���<nΝ�o�e|�6�A�/���FYtG��t�d�p7������< �[0R�Å (�p�-�w�Y]�{'_����{+0�Z��[x��M���F�ލ����03�_A��SW�x��6�f�}���'�Luģ;��v��|7\,Äp5FDb�x7����S�fo+�q� XmB��UX�n)��4�-�¶`��?�N0n\]R#x��Z�]hu!`,b| ��0[H��{|�F}���u�=f��8+��ɒ��lD�d'��q蔦طI����V�X��X"c[��>�ŕ�:Q]�I�R-:f�-�^��~��#\/�O����>��B���K��$����M���ڵ��a��W�m@�|;:��qL�d�"�tj��,�E�f��]+U��Ht�j�)� n�w���'#*��QZ��K�Q.�1�\�����B�m���O���Թ�h�?K�oD����ѥX�v�6X�1hV�kPu�W�Ԗ �j�%�Fy�O�\˩�U�d�!���pM�Hu�d���o#��?�(eL��G�.����U��^.yP\S�*�}_���Ѯ�Z5�G�f/,��vfM��I��R��TŞ�*��N��h��@���R��+ʆ#�e����/--�&J�x*S0���[�~`�ө���b���<(�}[��O�Z���ͨ��F�� jt)� -�se�U�zj�K�5�*?��2P�1���m���H��|�X}wi��oK��Wab $�0��0{��4�J�H+ߘ�<PS���U��V�\�L��0�ʐd���p�V�4���J��B����uR]m�Xg +��G�tg���[QE%^� +� ���,��^P?�9\!���G4�qp����H�լ@�)U���7Š`�1�;�L9.������-�.�Wk���5ABcKh��7�o<Q`��3<�,0|Z``���Z�1�Js�,N����?�'�@S�=j�Ba�������l�ڲʆC(kHq�Y��R�Uly�˼���}fc@��)8���g��1_ϩ{Ḟ��1����b^g�!��8G3���V�u@m=uϦ�h��Cղe�� k������D�k��Ț�\�Z��o�z�Z�^y-ߜ������̖����ِ��g�M�M'���%|��p?��H�k�ҷ�v#Ց��^m�w,��sJl�ٶ��k -����J��ue;�w�s�Ĭ�.�;�V�>j�x��l���QN��'�����9������3^{!s�=̾�@E(�o%��6��z���¾P��o`-�6!wp��#k0ܡd��:� ɝS�t��!�[�P�G��)��CWه����=����>�}1_ҽ8Mw�e`�t� �]�����! {������ѕH]�Ա�Hۃ��8$�'�� |�����8<mr<p��y����3���N<u���9f�q�}���w"U�?�ݤ�jjz� :A��@�{��C�#��j��F��G��ڃ?Lľ�d���B�,�'�+�k�;g��v\���13g�c��vۧ��o2I9<E�#��H��h��R�R�qS�'�:cǵ|<�ۮ��G76`덭�rs76ߌæ[���v6>�#��;*�����F���p�!��|����o7���*�!寓�H_Og����O�H��?i�a��XX� �}��>\���a�������X�8�y���O����jN��s�X�+����g��Γ� �h4����]�q1�{�?M35M�E(�"��Ua�X�X�Rm�a�ٌm�4�鶧�%r�R�H(�B��r'�K{��Z�"aKE���~w��Q���~��L�����}~�<�o1�\�k���J`�%ί�������4cr�&���]�dLh��VW�ku�U�7ƾZ�1��: -��a�&f�0���0�M7F���St�<�T�O�ګ��+/��[��N�����.!�w��g8ƾ��Żq�k�ީ0y?��3��0$�� -�P�i=$�-:1�ew��~p~:�'r~�����qp�/0�!�q=`�X`N0!}'#�3�<�5g����BH3�4�y�el5���Ï����J�������c9_�k/���`����:�� `����>�s��d�u���F1K6�Ma����� �C��?)�ҵ �3#�5�M�Kr�9|!���7%�c�|;Kg9� +b'�)v��������O�֨ ]F=xÃo7&��˫��ǜ�=��52�4ڙ�����<d�d˟ lK��w�\v���rv�ͪ�ά�f��aւצ�h5#��^���d̹������X�&��g07���2.�*��(���7�d��^˽xg���VG�cU�n�3贺�����G��K4Yu��5�kdO��G#�@c���A����4q�d���KA��,��qxk�ݶI�KC�]6�����0^�G��)��_�K�[h����Mh�؉���G�n���A�>�9��ƍM�8�pr`�&=�?�9�@�C:b��a�7��1͎hr܋N�x�tϜ����<��D��:<���M����?%���[���Q �<�a���3�~�Lf|?������Y��:+M�*<w�G��F4�$�K��Ư���pv�k%~r�F��=���v�j.�*��.��;g�5��ʹ\��cA�m�#:绢u�B�6/O��Q? ��bq�m~^�w��?�g���}��^��%����u����y�=q�����3��ة�c�'��s6�G�p�;��������9�nx��_�P�hn/V�W<~�ڀ�%�puI*�x��%�<\�)�y�2�J�jg}o *|�'}[e��j���{�g%�����|�@sx�\��&�Yb�o<]2�]Q�7�X�*��q�?bp>�[Tn���j���9�������a����Tz]tB�@T,m���HI�PJ�Á��!���k���ڜo���!h��F?��������.���e�8�e�,_���8)����A�l��w����,Q�,WtLV�qT����eq���!�s�|Y��A��d$d�2�E�\��"����%x��f� ��f��ɜp�+7� ����Q&W��\�y�Z�|��Qy��H�]T(ߥyH�#.�h��OH��i��o�/o�W�.ɑ�x��4��$b¾h���{�� Z�"�-�Η:� -��+����(��q�R��HH -C"�B�!�EC�h�)RĹ�,�~E��^�1���يj�݊G���6������3�] -�E3��g��ý�y�_���7��p1��&�t��C�HX���`�*��a���X����{��9�۵�Cӵw����-��-��zM?-���������R�H�Fb��9s�'��,�7�{ �k_ ��j���Eq�L*�Q�DȐ��/"\='"J����Lg*�Jҕ�Ҕ{��襄�$+/ڡ�g��<(I�v�6%i3-&�fs�b�˟{N�5j8��<�WP>%��A�j:�#ݐ�}Q˰'j%vG� -2�T���o4vE��Fn����u�UY��U����ڪ�0x����M��GoS$�0Ib$i�Esx�����poP�����<ͯ��!(��F~�rc�!'�ٱRdƮ@z�*AZ�R�36F#9���1%I1�u��d�m��5�]l�]9dC�C�� M���O���L{}I��s��ی���r ��x�Ͻg� -8=ߌE"'n��!#>�i����8��w��3*�3�e�#8�����XVc[L����DD�f���F���EAcA�1ƨ�d7��� J�F�Q�ΛG� ��w����9���w�ܛ�i��+�NL��R�Xg(�e:e�]�r�<E��!Yy�I��wV'�sV+��j��%6Dڑ��k�6�POέ��K�{U(eؙ�m� ��"�l�5��@��K�lM�Y�z�$]�`�S'[��3eZ��N��&W�*�TǝUW������^��DF�yW���Cû<Ѓ3��&�$r�WYa{�ڑ(J������K�٩ˑ�l����L��N��o�M�XkR�e*m�m�v�\����=���sޠ��<�ߠvDFl:�������I�]U*z�Ἥ����uÑ��rҧ#3c�3�!-#)a�ڌ(sM�U��*1=�&!=�6Ng��+w��U;��]tZ���<��� ;"�N6Y� ?p�����~-=H�耍�ݑ�크�qH���9s��Y��\�sCL�r#̕91]�s-�rR��99��ٛ��e%_�}�!2��cDv�cDV�}D��%2"�Ȼz�ŞP�p?L��tz�q��s@F���z"�p*ԅ�����R� �`��ט���c� �z�UTa�4�pS��v���a���Coه�v-]C� �v��}�z({=8N�P�<�Z���F)��]�T<���y:b �cX�h�J�+ 5�*Yki�37h,V2�B �!�R�*�A�`�9� C�m��]��� ��$�:ro�gX�G��^���b�u���@�vD���^�,���R�)]��e�+GH�z�/˒̃�t���B����V�e��W�}+�+�)�+}j��CX�m�Ģ#�V�'Ѓ����E��P�6�~;�{�k��#�� a�G!d�D���)���CP�bT�ceE�+�bEe<�*SL���3[V�U�d�>�⽵]|+��V�t��#$���dQ�0�ȿ�A-���_��l��ܽ��&���?�Ip���ߡ����,��ƒ�X\�����p|���GTX�/[P��+L�V�&7L�~b2�J��9Dv�{�q�`?���V�v�vɕ@w��C��j��O����C1��X�95���Ƭ�0�f)�k1�&��l��gR�Im!>�݅ig�����:�^5Fx��:ՙ栊�����`�zƝF�����#��1���������b�EL�8�.M��K��Q�<L�[����w%c�db��-]��W�y���_��N`���T��2�;�_��3���@�Q`�I`�`�w�.0��#�m�������1��H2C��! ������70�1�0�V������pk� ��6��po��~s@���@cO��ǁ���g�3��\��~ x������>�w \��ƢO��j�F��B�p����Q���I9�-��������}(:�{����̻� �������}�U��G`� ������s+�ls���7_�C�bd/��W`�j�_ρɛe��0��g��F -��Fc�ӉR�멟B��}P-��3ꀉ?������n��W�\�2a K� �����?_w2�x�Id�C�&1��O�tAAq�E��Q?�u������W� �}cwg�=y]9������ǔX��ĉ�&n����nH�C8��p�F(�DQ'D�aփ��b�%wM��|��g�!$��$�hI� -�,!���|M���fߑzM�ڤ�&�� -#ZM���-������m ��{�� �Q�w�g�܇!���K��DH��^L��]������ڜ�k�e��������b���F<��MF� -4w댐S��B�ѝ��5��������y�����h�HRI6� -=�(J�ZQ�W��x���j�)j�LqO����=<pnůݍ��C�NO�_z ��'���r�n_�~|�?��e�y1���l �k�.�x���.��f��5�\7��u+Z\w�k%�V��I4�~����h���no��.p���5�?!zSߥ'��Cx�}8s�'�� �y�8��<����;����}��o��Y7O����;��/�"�"("�rQ$"A �Kx��@L��rA@Ao(���Zh��˙]��VWG��͞y��纝������}���y��O'����<�\�1���#����Mه;S1:e��\�WS���iOpi�+\�#��a��oA>�@�y�O��}������S��嬥x�'3163��dp?�w� ��֬jܜU��fm�_;q-�W>��O����K ���A�qn�K��Cp�r�r�-���@&�C�4d�oALƋx:��E��ܵ�����Ը1?W���r\\hǗ�j0��Ë[q~q����\�1�Y2�SK����1 y��z(�oX�s�h�i�پ �~MGz^�N�Ӑ�x�d6���K�q%4��cd��a -+����^���p7N�7�؊vYы��D�p�u���G���H���=��� S}@�t�IA��~,�g�����v�t\_�/#"q>r�F���,_�Ñ�E\m�@� ����S��1۱?��b�/�z�.�'���bW,A'����s�1}H�d�^�����w19W�&a8z�Ɔ�x\�$���t��38o��x{ו�w�{\�IhBwB�K���)?���m�����W�9���� 3�?���<�����*!n����_`x�D�^7G�!8�~5$ʱ7Q�� j�lТ;�EW�;���TT�����VA�b�p�b@ؒ���9�kQc������� -¯W^�����������+k��b�������W,���H�l�GwJ2v��Бʠ=Հ������Z�v�6�[Ьl6*w�����3�:�u/��ĥ���FI[��_�$<OH��xA3ߝX���f zv�E8�<S��O9�i�ع)��Ж�����ئҡI��UF~��&�WU�T b��]�R��ڢ:!�θ��T=�vd�(�g�]E�U�� ��ә}L{@m.�7�8���Ci�O5�A��\���h�d�є��ƬLlU�N]��Us|��BX�v���u�*u��C�GjW�ޜ}�Ǧ�VfU��˚M�5R��W��}ڃ���T�5��L�^?�W�V�فh� AsNrס>7��*�rsP��ϫf��U�Y�`6��[RɴHm�no3(�`Fd��]_3�OoC$��!BOH0�����i�yg��c4w�S�d��N�#���D�f1�j"Q�Y�M��i�Ҫ��jyvm!�R[&�im"�v��B�$5kw����eF�羥�Q?N�NG�8-�h��"�?$xFm�-�j�q�گ?Q�����x�M�!��g�>!��pl��©O�]��J}&lz�g��NXn��M�*��� -5t�p�~�b�g~E�[~�ቌ5���"a�D� Ƣi�:VQ�ڇ4�Z�ul3����ip�Cual4*�Xٍ��*���0��|#[,,c�E디�[�El���-(:�g(��g���k��%���x���B߁�����ڿ.�����h<\���* ��PXK�P����)`�6���F)��pE�b�$b9������>��>_w�O�}���~�i�WR G$�"���%�y�:<���},�A�p�U:βߣ��1fS$��5(5m@�I�bS�L^��PP`.�͕��-՚[���}2�|�7�|�7��H�cz)�1I���=yN��-:#T��u4s���i�l5�e4s� [�G(�|�e!8K8�-1`�rZ7�`UAoex�V_k�y6����Hr�-R���'�vR�i�.˰~�a}�a!�ȓ��ƛw�=,�Z��w���i����zn�7L�?�g��"�\8�B��# �&�9�����r����B��g8��ҝ=�M���iUW��ο{+/$J;SD�<�{�2��)ڃC�"�����>`�6��)Dі�0�L��&W0� -�b��^�,w -2ݙ�p�AU��6՚�i�Najm�(��[�\{T����D�~(Q����j��"�dTA���Q�������z��9��8����Y�>�)S�䖶��.BW�-4g�2g��1�q;]U[�t�tu��;�p���e���|�3kM��������<��y~��yHn�6���ub�.9{�ֱ$ʖEQ�,��ߢ���`A����b^L$�����ٱk��I�i�n�qF���|�������mxL�ix�2��LZ�I�b�rȕ�O�$����-� J� ����x3"�1/�/s�\����L����a���}s���s��[ʔ}_�����; M6l���5A�՚ C3�I����.�<8*ǟC����x��'� V%�"9&���S� O�aJ��&����Ep�?A�!�O' }31!c�k����x|��u��Gx���;M��Z�J�2�R�~��$����Q� EΟ�r�˄�l �G,�˵�7� �<=����W~��g�Y0��%�*\�{�F� -�Q����}a%��&�y -}nk�� S��(�qR�v�^wV���Y0�0�w��AO��?e��ӽp)�ϰ��x2�ďA��8��``��ʾd��5�?�dz��={\�ñ�%�� -ǒֲ�@��'d=8�${�d!ٳ� �|���ir�M�}�vW����'�\s����>����1��7#�vk]o�����X�9L�;��|�˛o���ZI��7H��}S|� ���9�zL�OI~>}/��u�5X�o��-�U�b^ُ?W ��#O�?��㩘U/�M� -�51�I���M����(ՊQ�c$�Ծ:���ܣ�|J��8_�k`}:U@�о���Ѿ茦�4:g�&dc�(��Fy7�M�(�1������V�e�m���G�>,����������rp� -v����`~�*��x#�j��0]��p�I}�~c"�3%�%a%_ɍ�d���VvJ����L�8��I�>R����)}Wj��U�]-��t^��6�#!mQvb���Ͳ� ���PɃ@m{[Q�Ei��UΙ���(1^Lb��J��6�[� �HY�#O��4�M��%�4P�s��^~Q�³�����N^m媷�Gλ7y�#B�l�P,ߊ���SD��b��tI��G��2ɾ*�%�O��_��Ո����ߡ'�}T;iG��r�*�c�@��(m�X*V��x��7o��x���fm,��~Ӧ�R{�m�M�yfz�'&�y���J���6�m��)~�����qw�e)���t����4A>���E��|�u�i�}�K�zt�y��A�.�:�^juF����U�K��$��/���*�Z��f~ꬸn��*���-�Βߥ�d[�l�Q�el�Q�F�z���:�&��4t����K��Z��5T[���V[����C�X%q�� և�c}����ޣ�K6O���g�e�T����P�%��/([sT�.�~�P�e<����Nj�����>�*�Eܷ_F��7�uX�m�M�t�ε>���'�K}��;frޱ����(�w��5�pj☓�h��@�@��l�6(�(g�!V��;S��F�@�;�pw�Ln8ƕAK�8�\���C�sf�fJ��xX��%r�� �]r8:�$E�/��Z�}���C"�w-(�v(� 3�p��h�����U.�����U���9�A��/8��%'�Wrl��Fm��c�����g29�3�]D��y2��6���1o90F�_��-(-�I� -WS���<:R?ʂʑV���E��y�rjL(��Τp\$y���r�|C��wd�n$�w�~{H�K�������e�)F��1�?%��5 ����ō����U ��W �^Z�F�q�CDžq�)����cȟ@��i�AƤ��,%5�+RVs �{���I�ɾ�X���$g�r���kĆT�Ğ`Y�ďP$_/dy:*��u�τr�v��Г�@gr�<� -�'-4���Y�I���������읲��)ۈ��?��=��������j7=���UkoX�c+��~W��! $!wr%W $�- Qn� -��)�-�:��n�^V�j�3OO=�ں����v�S����/O���~����MԍV�1���Y<�$ŷ���s��+�p����W�s3�SL�fb��g�����pL���F��݅�{%�(�!1�]bE[���%ؗ[�fiM�$��N�Av����a��ل�C^\�9�s��q����;��a� ��w�(��ȝ�Cy��'ųpT���KqP�]�h��К��}y4癐�w�1� �A�)�P��g��l\��Ɣ�yժ��Q��J�^X� Vr�P<�A�a �=o$ݷ$�{i̍P�;.�����V,A�rZ�[�O�*�*)T�+0�����7��R��T�LT��Vjzx�0�B;!('i?�5�a�)�p�ν���:��`��Λrʽ -`�r�k��`�|t�_@�f���Р�F�V��1��:3�:'*�~D��LXg+ͼ���4�������ç��k����>����í�n���P���(��VS��W��N�\��Ӑ4d�θ5�,Č"T�PiT#l,DE�����L�T͖���Rs�o>&�DŽŖ�S���B���|��c�&��η�����H+�Y�<Cz�q@/@k�#H��A�9q�ZTY�#bٍ -�!� -A��eV7J�e��e}�^��C����B���)N�߅ۿ��Y9f:�z�݅k����u���j@��4Yf�ֶ1�K���Bؾ!{v J� -�i�/2��p��(��a��:�ӹ_�p�&,r�M���Rl�OR�λ|������1���ޝ̦�@���(���u�F����}�F̹�(w�@�������(v������6��q�����`�� ���Ʒz����;�b��-��{�_��x�n�5�9�>w��'.�(���=�:�FZ�c�!�}���QZ� �o=���������+@������֒c.��L%-|cI��P:"ԗ^�J>&����O��X��c�s���ɇ -�`%�@O1��~ A߈?��G�/{�k�lEQ ��ր����A7���cU��P3O���O -�/T�[U�K�*ȱ���L��]���8����0iw(��(��� ���X�����+a o�)�cDCD}D ]�m���RD��*�d�n^~�$?/z�/���/���'�p�,�1ӹI>����Y�`���vGh�B��ҷ��!X��0�<�e���B�u<�x.�5J(jȯ�#��Y"i���.FR;Ȋk/��ć��柌(�A��}��M��;0����!�l���C�;a�K��~ -���q �WB��,H�{ Iʱ7��8i��ɍ�M!�ڔ@Ns;���aW���t;��#��CVÃLR FԋT����l�������'���Zg"�m>��Ӱgv��FN�fd��®���ٮ��=�wa[G)�vVcsg6u���ql�|��� �)5�qX?��tG��R;TK�H��-@y�jt@n��n!�������ڛ�ͽ˰�w 6�m���l�����>�����߃W��Xٟįz�|�2���;X��ay��i���@?����j@�Q��u�C����g?��r��c3���/���B,LG�௰lp52��`�P^��a ����p^8�@��X|ji����ßa� ��'�t��$�?H�n��Ws���t���&ݽG�m�IW�����IEڹ�xn� <3����/�S�_�����X6���ĸ�w��<v���8��o\���O1o��;����8�w��;���=����<B'����5������ ��K<̾������ʓ�ٕt<|u��?�܉OJ��)#�?�����q���. e�#�N�Ō���~����s{H�@�R��:�%��N#���9W��׀o��3��3̻�i�>K�L�.-��� ���ܠf��H�~��E��r]��5�i��t�q�g8�طjlEkQ�Z2" ��$�^�In6$Bd!�� B,�ڥ�L�T���fDZ��Z[Q��w�i{���9�{���>��}}~$���D��{�b>gDH�_~�K{���K����K�^�o���/ �VOPc5��p�AM�7���@�� -�m�qGE�^�� -���B���V)�9��,ߣ�!��J_?u<]�ݦ -�����t�FW�7�O▝4�p�)�WLjP~4�����~���d�����\����J��&߃��qF(d���Y~��v�K����Z�(Ba���x"[~m��6׀��pҌ�BM�P2��^~c����Qt�D1X��"P�D�H�b�X*V�<����'�)d�q�vZ&^��Y����w�mq�h�m��]��2@|$�?a�D�H�]"���\-�b~a�4?�>��<�]NJ�4�e�/��U����+&�Z���vt}�p�"�E��3��,ӥ�-�<i��5�� ��x��19$�:2gttn���WD�qIO� ��¨��^S��5��{+���{������>O~#@>�)���Q��8O~.�f�|̗��RZ'�6qU���~��|\��s����}�y���:��T���^b�C� �*�*͔���Gc��<i��C���[�U?���8�͉\7�q͜�w�%\6/碹��� �}cg�(���s����lt���8������538*���Ѹ����܌�V�h����[�旖C��ܙ���|�̟+��\���}<g�'S�b>�[dq�e�-�w���h��/[��x��kSɑvW8���:�FyG��� ��°�'�t����&F�f<whǃN���c?��w�b�q����鷃9���W�L�K��wH�X�t�v��H���Zȁ���ﶝ������y�<;{ܢ��S��4�*���he'�eC]{(=�T�i�]�s�sw��u0���P�Ã�{M��{S9�;��}b��'��s)뻐=�9�r\��~k(��������1��b��j6|L�@��b]-����,z��*;}^��^���n�y���|���w�7e����~(���t�L�Md��T��d�\6~��O>(�dx)�����k�����!�N�(�@�x���{�Q�n�W��k�����bp; �E���vr�t��>�c�G6}d㓑�)9��Qɬ���Q�X3zE�װj�V -��R��˝/���3K� rŒZ�^R��i����^��rX�hA��n|:z[�G��n���f��@ֺX)r�b�K,��X9n�,w�e��*��6��^F��,�E�w�vF���;n/1�r �_=Ϋ�W:k��G�(s~�.lv�G����](�I�x? -&��|B8�&Đ�O�G���,��!g� -M,!�k'�^�Șt��~b��SҼ�&�ھB�9x�\��Y�v[�&}���ԭ�&t�س7E�R�5�^�ɟ4��I�,�de�w$��c��N kr*��X���u��� ��0�~U��I���} �|�JMn��1Z;�;S����Cs����ImY=�>���I��+�}����B����pN�F��x�OI!�!��y�("%pɁI -:mJ�6%>fv���Z����p8�'���4zlW��0�.E~-Y�ߍ�X0�쀱dz� З��A�ZI �bnP)��$����ĐU̱lf�e�i��S�x�u�L�#�BbkQc� ��*�����γS��f]���FA�=K�;��Hf�,qf�e<�o�Z�I�L%�b#q��LM ��4f[o]��Ѝ���rSl�I���즇>d�� ���_�T��U�;_{j眢s�]â�h��}�� 9�M��7��!�ZG�bu%������F|X3�f>���,f�V0ݶ�4-��.&�+�舫vQ��H�AD�_���j��p�[y�l ��{V�~qhC2��3?�'sm�H�}���m��|��"6�ʌ�(�G�dZT -1Q�DG/#*����."���s�.<��),� ��ȗ��Y8�<Th>0Uy�ޫ��H�h��3#̤E�&%�;�������<,�|��7Ke8��Z�N�3�0��$���dK(KHv�{)Q�H�A�XJ9h��d�P�`.s9c,cɱΈ��|�qɜ?>�o���������=�=�_��&�7�Y����뿔e�A, �fI@"��fQ`�iA�>���r3���|���x�ſ���u.�O���f��;"Y�W���<���@B��L@�H��Ʊ,ȝ��3X�����, - `AH$�C�� ]Ǽ�mx��1� ;a�v�4;� ^!o�:��:�T�ǔ���?7P�Fl�zlFxh�ú��o��Y>��cY1�̏��O��#��Μ��̎J�+j3���}�4=�����xF��3���nhNςR�}o��A��H�CąC�Z�(+|�;�dyO����1��3 -���̉���ؙx��0+v3V�0}E,�q�x�mbj\.SV��� -�+19�W�n9���:�ʕ�"?� �gGC� -բ���MŒ8毲c^�s�{���̄a�H�g� �LK����E�+y��q[����iLX���I��K��k�C\j��k|��4��W�wiٶR됨�+Is�jX&��L�Zӌ���H��Ե}q_;��#��2��)�O�ɸ\S��25�1��N]�˺F�+fĺ�H�f��Za0<��*�DkP���ҙ�d] -ħ�ʊ�BG/��-��������SƦ�gt�\�\�6�i��>�a��̐�8g�✑���C8e��)�>Ni�p�hഡ���Z���g&�L�o�FØ4�A�r��h�&�Fl�_lkːLg��9s����5�Y���I��y�����ۣ�l�zgo�W�>�O��.��/q�f���P�G�?G���U�r��/t,�(6#K�g6ט8`'|�ۊ>�����<G>���'�C�?�����c�Lz,ġ �n��t-L�sa� -+�Tp���5t��d�[�~��.�3{��I�Fm�͟�ɻ`T8@��� t>ܘ�Ev�/�}�'�+�K��Aؕ�`[�F�/ZYJ�#�4/MŦt�G�aUz��4+�#o���F��VX+w�� ��[�)�����9, t�Z�&� ��h�E�=�4��M�J'�W���)7L���9�B�R����U�Pշ��x���x���*���J���a����(f�b�K�/�Т>: �Ρ�������z�)���蒚��#��!xE���zhs]S������ߓ� ���\+�>���jPn���1�~ڞ�3`���qM\��� �ԐjH��1�QcvW �=5B��~���@�Z�U���օ�xO���/�{�R�DqO�W�T��r���k؞��������A�(~���Ba�!<��Gm��r�L �/j�_�1�Q>^�>^�>j��Z[[�����r���gi����Aʹ���"w����o�}�-� --+��x*�����Omsx���Рf�P�j�10�D��42�c����^t�w�'���b�X D��I�!��l�W�J-Y�b'/)��C���p��Z�g����X�~�_�K<���w�!>C����b����-�/^�5r��+M�o�/��+�>�pD�r�.�@vOw�~u[W�%n��>�7wK�������(\���{����E(�r%ʕ,�-C�l�o�|��w��[����2V�|_�]钸(�����F4�߭MO���� ������2�"W�\+�j�R�Z�x6�T��ۥ�ݧU(��繪;�Ϸ��*]�RT�Fk�zzm��H���z4p�0���c���^�-T�� -�k�b[%W�\��g�|[8Gg��*��4�Snv��KY�57(��������*eê!Fs+��6�x٤'O-���|���s�܃+s�h��kΙ�b8�8�S��P��+ʛfp�*��ֹ�Y��I�4����8Ъ�}� �����F3mk�ք�N�hgM�]��ʝf�\����.�k�FU�T��p�n)��Q�6���b9�.��d���s��f����{��� -;�#��mvwy�ή:�E������[��gt6Q۹!��[q����8۾/���xW�v�JI7/w_�A_���/�e�y��O�I�f��N�6�4�Q/�Tr .�"�����.���.�,�r- �r�ʱB�V���$�N��66�LӦM���M�Lڤim�lg0|������������rz��S!>���d� 'BG9��G�/0���7^cn�{�l����A&��0q���ꯑ�F�@x����֊oqm� -^ ��8�mJ�t��Ű"N��q"�Ʊ��n�pdK3��v0����m#�l09������Q�0�WD��dD�C���B��ї��<���d�m�*�Þ���PNGFq2j'ǣ���)�g��>kf&���X�� �Z����؎}���@��N1��2É?c0�O�M�E��pw�Kp��A�o��/��DZ�q%{���~�E?�b�j��o�pB���L'�$kO62�\�����t�Oib$��Pj��~��f`�}�K�*~Bw��J��N���w��%xg�ȳ�[ѿ!�몌��s��m��\�\�f�v�2�He4M���F����L��7����z�2[���'k�=Y���љ�>��+?�U�-� �����C0TF����xɜ2�_��/�R������O1���Ѭ(��I)3اT1��җc�'�Bw����n:w{����=w/m�1ZUGhQ�ś�:My�Ӡ�;�������R���ޯfJ֑�sF��l��z�ɜ�9�{ê��S�ӯVУ�a����<=�<�y6���7Мߎ���&�4s���S�#괿���\�2�4A��pk����o������d����1�{?Տ2�����pz5�tiR��d�^������B#�B�Z Z�E�x�z�+���¥;�S������k��/�����[�X�$��,�:�VERɠ㲖a��hW�]����hڊ�i)���˥Q��^�ǣ3S����UҌӰ��0��i�籗�BU�;T�~��x�!�%>��^�>x]jpItO�$N�w��e��t��+YO�!�!���A�ې��P��XFM���R'��&�e�T��4Ob3/`-�LE��X���t�)��l���X�t���;/H�[;|D,`@�C�����>AK�ZM[��p�Rp���1�q����F��+�,��fi�j�QQ��u��&ۋ��ޢ��gJ+����7�>ܐ�{UjpN��B��C�Y����4�?F�e�0�1T[��[3���b�j��J���c��1Wz0U�QV�O�}���EJ7��?BW�o!Hq��}�.������d�c6ɽUrB����*���j�VRm߈ݾ�=�C�š�ܑ�ɡ���Diu%�7����>t��)r]@��)��?�q�BSDS��.��}����R�S��Z2�SzQ�O�h���&v�2l�,���k�0զPZ�����ĭE�6�s[)�s���R��F��O��0y��Q�_G幉��_�։Su/����I���/���X�1ɠ��V��*뾆��qL �16�Q���1���4�M96�i*!��B�����\o��G�i�E�|���7�ߓ��'�MA2�xS��e��g�r�7�$��Erg+4y��f�}��<Bq�r�m�Ѵo#�=���T�Y䶫��������"��!��Gz� ��vu�&��)�#��9)��ۖ�V?�3_��âh�s����K�@��B�P����ﱻ{5ʞ�d�D�ٛ@z���^%��R{KH�`g��$�Db����v�/��ؾ����=Ab���"��F�f��!���9H� �9fA+do����5����u$��8C�p���č�;�%f���Q#MD�{��c������1�CA"��$��AzQ����?�Ò�F���D"�P��7��1��%jb9�!l��`K ��@2�6�l�Աq����nB';X7�g��<��.�j�|ʚ� kƗ8+1�������D{P�|cR�q(����dNA�DJd��܃��k�<͚#�X5�3�1|>��,?���G�<u������L�zL�8|�" s���N�}ͱ'$RIE�br�c'K��J$"�DRY��K1��23�0�e0�cc��O�3��u_-���~��]��͘E��jfn�&��52�`��7j����/vE)�S�+�]���k�x]���.��%Zo�t'�f�R#NJ깶T�mD��6X�u�J�3��<�,o�y����B����`��M�����=�,�5�9��ɔ?U�NV������[`�V�A�n����M������P�)&G�hS�!>�@�G+��S��*��*~�Z�Zhǖs쐸��=o���6k�S��r�U���w�����=[�<��m(o�#P�̿]�T����碦 5��Ԍ���Ԅ]�8.i�]Ҧ/R��h�^\|\\G��6(��5Ę�#�s�bvW̎����@M�k%y˝���(��w�5��$\S����ÛjNo�)�YM��w���j���d��-Ľ��<]+!I�/T�#�8�=Xn�k�s==^�T<'�%�*��ⶸ#���k x��<��<SQ�]���� -��B���}��^��X�,R�Oȅ!�y?}䨏Z�]�4T;����{�O��/�+��������-|h����PQ24/���1_hZ�Fz ����%tߥ��(z��"H�!b���@<�I��y�:ސ�k��[%/9�NhHE<S���D_�5TCC.��������ύE;�$��@1\����)�<^�P��V�;��m�7���!W�<z�Y��:��xW��Yo�-n��OTV����E�U���+��#_�\�䚭��y�"�dX��ʕ*W�\���\��$״���������K�8_�REa�)�y�|��Y^/9)��r����%1G����Z�h��TS���rm�+_�e�"���7��ך�czs�((C��DT�4����PO�k��) -_9�(����5Uq͠Xk��qA�8�2ΰ�S��J��q��:�Q���2~@Q�SF����"���G���Y9���5L�4��4��3?⩌(��5FqMR\ar��I�&8j��r�9Tn L7��t{���gv�����m������AV�R2+�b��_A��5�4�*�<�X���u�fڊ�f��Їo,p�r0��q�����/��g5�=U�W5��j d[�b��vV�b{�=d�8I��U�|��͵ޓjk�Ql(��#Ψ*lt\���:�*[pբ>o�I�v�8q�K������!�؍`w�q���:�l�If�yd�[Dz�el������Hm��Ɔ�oX��FHn��UM���2��W�y�c�n����a;��Xr�Ύ��Z�����F.�h�MV��2� %��HҚ���dR[LcC�٬oMJ�xֶJ"��FV��NR����?O��]��}ɒ��bq��������\�{R����� 9-���u{2�Hkۗ�����`ֵ�kۏ&��Vw%�C+;F���B;-ci�:g�e/q]N����ωq0���`�H}Rn�]��wR��HG�ٺ��nkEf���u�gc���8�!�������pB��p�9�%��$�tcq�H�G���b{�&�)�螹��y���י������D�Gz�b�V�i�2$�njt���C~G��],��hǺ-X�ԉ�={��ܗg�{ "��P�Il�`bzO!�%��.Q�����>+���,��D�2�� -���p7�o�Q���y��zlW�{E�~��dNJ��$�4!�O;��v'�͕X7/b�2�=�y�È�;�9}'2�#�H�Y�췀�~�LW8�k�^G���-�������^�>�,���-���ڟw�9U�w�-*u�\MHr�f�G��'�Ӂh�^���`�����5��#��fz�P���$l@4S$��d�,B|1ɯ��~��F����*���i�~H�O���@�z��W�_��q�u�Вy:1�ljH7f�z3�ןp� �|��7�)~!L8���s���Ād�dx�q��c�`t��7j~�8���ޥ{Wz�r0P��~��$�ϖ��͈�oO�7����Ih�/�0�I���8��� <�q����*��3:h#��3"�Ã^1�+�ae���P�VSq��`�9�`��'h,���t�@E}]q��7�%&m�RS��Ո\ )�� �0�0����� QKY\ʦ1*�ƨ ��b5Mj�&6Q�6���-ڨ�|*=��g��������ϻ�hvOe�м�J�9����NVRX�laAJ�P|�Eq�6Y#RkX�C��#Ke6n�������2�+2�;p�`h��X� ����̜x�p�Zod挤\���Sfxw�G��\�0� nJ����H�EN�52P��0�M����9j�L�LE���h.Q��J�.EX�(����jv(�����Yzp?9�����y��>�Rb�"�tcW���)���(ř=k�V��_�L�,!��e��*2�.C�|E��*,v�B�� -���l�)[�*(�>8��%��I�}/��ſ!Vz'�^�%̢�%%�I��^����XY��d�N��:Y�q�e�Vx�Aa�1 -�ORHB���(ȶ\��u��X��'ದ��iz���V.���zrފ���y`$(�lF����A1 =d�����a2$�)"i�|�4U�����d�f�m�iO��"�kZʻ�:�V�s���K�+?�C~ɭ\�R����:F��T�^��;���Ș�"��S�\g�N��Tw�zjf��f��P@Z���EijZ�����/=[��m�j�dl�w��$��;�J��5�6s�3���>$�^bx'CZ��^`D˘�� Q|2����B3P��ᚖ�)����əS�(��Mʊ�w�]�35qa�&,,�����}H�ٟ�=�6<�{��ǨA���@���*JŌf9��4>�oK�0��v�_��|��ϒ����E^9c�3Qo��jB�t��"�\����467C��r�W�Qy�䚷_#�.jd�42�F.q<��=W��y��W)#Y���|z�O3�L��7zȣ���јBW�.t�E^U�/עYr)�Ј"����oHfh� -9Wk�=T������'\�x�nz`��&��xV1{.�<@�rf/��Ҙ?H.%Nz�������в!R��e�T歁�S4�<X�*7�y�^��T��B��X�^��J��*��^e�ջԡ�%��H��גw�B<٥ԠL��KA�[!y0�����e]菉�{�O�/ի�Y�T�T�Jw���V��i�V��bյ:M?����իԥz�:W�Q�r�|$��9�kf+���=5XI/��]�;��Ԋ/ߔu҄JɵZr���Yzy��|M'u�馎5}աf��P��qjS3��S��6h�Z����Z��ך��{p4� �Zr/#��w�8m8 8x��=���!��oǿSr�%���F�����"�w ��}�/! ����ak��H����i>��;��j@�E���v�Q��Ͼ�j$7�C������:70��#p�8��q.�'�� �.�&���q��_c�p���9�kd�s�8��lb �^Q�{�\ܖ�ҬZi����G���CRǣxO�h�?��\FP>�)]�b�)����H_Ǘ�]��9���}��^eO����ϛ(��9��tܱ���ه�G����a���R����?ç��q�W���`X�������[\�nǿC�;��4�= -���p���|j�V���/y�!Lgjݛ�zVjs��?n�^�+@)u�J�8����G �~pex�O\���8�����D��^n� ��[0B���Y�D?(_O�LOT��Z��0�>�F=T��c���ú�f�ї�v���?y�&|��7-8���-�����B ���J�5W6�\=P��^�k�nk=��p5��)�C�u}M��e"r�}�.������!0�\���cy�p��Z@f����)���m)�5��-�qm�SO�a���>W�O�'�s��g�Lu�����e?~M�\��'G?�A�"qŒS29���ij�<��7��q|�r\kqmµ����i'���p�l���Zh���K��<ѫ��P�C-�qP�}&�.�'"���q�8��i�8�B\+q��U�k�z���4t=���܁�������~B�=yb�����xV�Lw�$�p\fS<U���u�8�c��j�����bOvҟu���6���Nmf77a��ơ m[y�o��C�N�ԛ|��;7^K�8��Ҿ6�nkQ}�}�ޮ���U�\�j;,���r�����DZ�hc'Fc|DP��"(� oX`��]vaaY`Y�E`y�"�|���D��M���$�D���d�i�I��1m��M:I�4M�='��������{��~�\xl�sӇ8;� �gb���y���8��ƞ�;G��qx��Q��#Ɵ.Wy���ֿޛ:�����ͩܞ��3�:+�˳5\�c��\3瞶r��Z���bb����t2>���9�`c�8��,���qp�],��>�/���ch�$����{Bښ��I��Ηg`�cܜ��+�W��h�-N�_����/)���2��8��p`#���\�ad����w�w�!��8��ʗ��:;�>`����_��+����O��I��/���R�y�n�xM���r>0���)[����j� )f����6���1�f3�״�3��a�l_�����x�/�q���_ѽ�3�"��������'���%�������Oe� -gV���B�[�h�zD$�w]&C�vE�#���� -���G;�[�B�z�6x��0LW�Q<ϱE�G�;�)��[� - --��u��������Z���yv_�wf�h��i�������Y��qƦ1�P�U�ӫ0�-���8+]q<�l�o�cSm �hM�%�4�I�iJ�Gc�p%MC���B��Ga�$J�[��^N��$�r��|Y��ع�0�Fb�IIt'eҕ��3YOG����rZS�q�8iNuӔ���i���GhH?�3�*uʟ�P>�V�j2|�?�3������ļ�&9P�����Y���Y��ћ�ִh:�7ё�F[� -wF>�F6+�4*���2����.�G�>jT��U/Q���M}�*�WX�� -���`�"7^M�:���ϙ#9�})O�@�z��d��#+���X���٬�ĥ��T�W��S�S���&�{Nչ}�4{���2��7(�~�%�K��|1k&�X��/$?���,�ϊ=�#�w����ixT�Ж����06�n�!7�zM:��M>v�[^UZV����6* -z(/�L7�Y�R���?Ĥ��:�$�ܗsxKj� -.I��E����'p���K3���94i����>?�ڂ8��� -��h��R�+�\o�RX���M����.Lƣ]�X�*��0������=��Z|Sd�u���T�����a�=}2oyܤ��S�GavCU�X* I��X�9��:J�L�U`*�����"1��N -K�/=���6��_�9ڒ��5���7�"���J������s��-ki)�,�AM�l��T�°�6`.I��$S����|�J�0�-����6�+�$�2��r���h�o�S�k��X�������9�ΕZ�Xg$�� -�V�[�C=h�5�iX��`��`�DQT��<���,�y�*�W��V�ȳ�ȵv�S5��j���d�n�i�%ʪ���~+�PVNrOj�������G�ao���]��S�c�����W�a� -BoGg�H~u"��4�9��u��%��V�k�ɬiCY�%�v?i� R7Hq�Kr�I����#�>���뒃���bF����`�HrK-jg�s,@[�M](9�Ѩ��ɮO%ә�ҩ%�YD����� n�\=$����u���k(�F��E�7(���xMrpE�}V��)w�Ij�E<�X��f�˼D�Z�k*��9d5��lZEzs8��1�4'�ܒAbK. -�ķ��sۉu7��ʆ�!�[O�z���{D�?!��_����I^�:�$9���G%�v����X#9V�O%���$�c>���%Eg$�� -b:S���"ڣ%�S�:��Ok�:Yӵ�Ю1��^fu�[�Y��5��>��LrMl�y��s��Q�=� ^��B]���6�B���Bt�D�-"�o9�}��y�Y�'ԛF�W��~=A�e��w���������d�"~w����?���;�e9�Ӳ�c����k@�a�Ţ�d4 -*!Q��;!dX��)�=K��r�װd8��x���y�T�wX����pH0�X*#"[��!�PDP�BP�����8�D�((nEM4Q%�eZ���8[�51�q�� ��qr}�{�g��<�0D&�[Q��%�S�2u(.�Eq�̊O�OE7e^��,V�c'���_����#��p5��:6�SpY# *���I=�7S�R3u(�.sS_�����I�L#���֦�ji���853���i�d�����$�70�¿)��U\G����' -�]��s��n��l��qE�Y!Y�/��ڂ���������<�M��M�:�!��X̓���p��b��y=侔��q.�c��bʤ��Fo�<p:ഩ¿M2�!��� �㴏ax����u���F�:0��Я奯��k)v-�]K�n���p������絸s�sa9}�3�ح�W��X�'�=��~��A��Q�>f(>fd�BO0|�Ax�|�b>ŋ�ͦ��#�u<��3����@���S�������H��|��X�M~w��P����җ\��Q:c�p�px�����ax���*5��Ȯ�_�v��B�^�{>��<�<{�g'>fsX�F�m��|.w��+8_�e� -$��p��t��g/=`�ߥ1��K��>�'�ɢΩ�=���}q;�kb��GZ�=�R��� .�w���<����O��Wj�!�9�a�!l����:�ol���;���tk���s �`��+Sϔ��Z�'Z�ߴ�{b9��j�L��H��@��'I�X�>�-���K2�v�:Bo���` -�(\q����,���g9�z�}�\&\�xv�9��n�g�O����Γ��e��4��n��z�-8��w�0��+O�<�!�R,�m�Q�B��Z�]e�j+�=��#����/_�!%}Jf�ʠ�_`�iýWM�m}=��/����N|�8��L4�x�O����oX��qNYD�G��Z���vڸ�L|«t���+�Eb8�9X��D�lF��Ԯ?�JG��/�'���i�œ�'E�Xǧ����1��U���-�}��[�}|c��Ew��P�i�K��D�pw�~V�l0��_|cq��4<Qx��I$��]�k �,\��J�M<z��Ա��t�Cm��6AY#/����W��O�gK>��σ��k"]?���i.�D\)��JK�E9� -U�{R���X��zlhzQ��WIޢ��V�2T��~N2�`���D�+=R'ܽ�u�����t�v4Ҷ��U�j����������&V�mU�6Uۥ��}�6��WתĬR������ -;\T~����L+�ʅ�F���}�U��Nl��َ9���n�ږ]�������kkwUt�Ѧ������v���]#���,�t����I*ꑦ�Z�3G+-��gY���j���Qe�:�e��jI�'���������l�}8z8�����(���tM�-�dz}����TI� Qa�P�����V3�g=[����闬�~�i��eo�k��a[���?�E�Ok����f��3� -)���Qs�Y�v���~n�ة��R��i�M?���WA7�|s�r킔=`��S�p-���1Z<(A�R�hp�Y��!%J��T��%;|���7������s|�#r͚�oG1G�~������)eXc�A�z+o��e�S����:�i�c�ҝ&h��$-p��4�h�:�*eh���-��aYJ^��rŻ�U��Iź]���f(����F�s�^ @�ݸsr���Ό킟��;�W���2��*c���]ܴ��[�*�5Dv�|�)Jr����sOT�[i��LsG(ƣL�=wi�� Ey]�L���e�3ó���g���[��S3�xs���-`-�.��ĭ��߲ւ���1\�J��U���{�W��D����9^ъ�Yީ��^��Q���)��E��t����H��5��h��� [F��^_ƾ��KX��y�e�G3-��ԑ���m�Do'ŏrW�����j�O�f��*�w�f�F*�o�f�'+����Px�zM ڦ�Ai������),�PX`������>�nF�-���P��٬%�����+��5��U���b�]4+�SQ~�����8Iӂ">:FSG'iʘtM -���� -[�А#z'�+M�V�>C����0�·�g�xf췥e�%-PJ -l����R]&PU�iMS�̤���X�`� -�"�e���"�����,���^A ��T�tLM5s\ʥ�qfڜ�4��2͎w~�=e��;�?��=��~�����Ee�QZ��R����� ����8C�b �� IWth��Bg)"l���^�0�>���U��aO�0�k(=��k�6�7p%_�:DJ%<��:8��Z!�J �%sh%�Q|�hŅy+&�O��E�G(�/�1U�SY��ȹ2D�Tp�FE�U@������U��O���;�G/�{3�5\�W��g��u��"���&;*1��L��(6r��#G*2�Sƨq -� -RX�Q��q2�LTpL��b�W.���`��x��4�tZ��+�V��>�$�p�4�{�k��3��߅��-�#���I��]�$c����)��.��G�� -�U@|����K�����T*���k� /�n�f> ��3�gy&Z������^z`;ZuhWs�]�N/�ы�l��f"BtB�':*4�����hvU@�(MH��_���M4�wb�|&&�;%C^)��:G��5&�F��viT� ��}#��{rK��Ƒ@z�؊���Z��l2L�sf&��9���)0���Ӻj|z_�f��'c��3���+��@yd�e��T�e�h�b �T�a��4$�A����\'�$�,�\3�p�4R�z��Dw�h�4i6��Ji<�N��&K>�m�5��<�����~=�U����-�K#r�4<'DCsc4$7Y�s'kP^�\�h@^�����cr��Zιw�/�����Sx?���e��B���<G̔&��g�iČ�5l�K��G���j�P *��c5� P�"�\�����T8S}��P�"�zmS��&u/�B= -�g7��'�Kl!����:��x6�P*@��ijƾ.����[�y��^��M}K��T2P-�ޥ�Y:N=J �V���Rե,O��J�X�B���/?��e��}�-9�X�0� ;Y��_G�YQ&U�#{�gOB -�#���`��-l�:�sE79V8� -9TW�JOu���?Wվ2Im+'�Me��]�H�*keW�W�T��]�5�\�@�Z�5�s�X�z�w�t.1�����ൄ��Tr^&�X.���ji�g-�ji�*;��ZXx�[F�7��-,����YD��TC�uWWށ��H�_7�?/b?0��Rɫ$�Y�Z����UR�5��:�]��b��v���;p �%[��үk~�r��%��\�b�1x]=��.��x��L� -���+Gk:�MC+j-}H,��� �ס���b+�зa{Kh/�`;����<FO#9���o��Y�Fl� ��Z4�w������|4�����E4�tGs���tD��6�8��=걏�>{i?��\��.����s���S�&Ě6si�G��$�tMUx��k1���M�D�o�ڎ��a��zO��� ��18Q���d'�4��O��c�y�^�%�)��?ʥ�(�E�����.]��K��<BۼE -�/W/�����n��m�Gmz'�rfс�\jH�J_v�.�6����2�k�?p�_g^_@����(� �q��ў�v -�F�������$���]�S�g�M�_�Es0�������J7��]�}g�!��_��<��c���V��oEغ����C;p���c��!�` -� ��ү�܇��_T�d��d�{���H�wI��i��,� �t�����u�oW�;��~�7��V`�CW�#�zD�|�dt2��Ac��/o�so�sT%Yq9ZU���z4�A� ����"�ޢtVJhէp����sx���<�N��WW<z�돦��I���K>�/fJљ��"}����u��{A�t��s�Z�B��;NŎ1�Q8M6~�o�%��T�'>�� o>� -�Sn�F�e�%��e1:����ߙ�G��C�Ec���J��V:�}�����Tvû���� �ۢ��/�!x������ -��l<�]��)F�T�5�����Щa��'{��Y��̷2�Vu3J������J��+jq�Z|�l�����^��&����tt���Eg&�g�U�����S��o�ӠZ��o�%�]��jUO��mk}g��L=��8�:���/]������Xm3���V.Z3�*�ʩ@Z+ѪAc;fO�����;Z��b�lsh����i>z>T����uŧ3�C�o�YeZ��2�ˌVZ٪j�����܃�,�0�K4 �hE� ��� �;�MXXXXX�� �{�5rYr#Y�1F�5�2�o�V�ڪ��u��Sg��۱c����1n������v�;�=���>���^>ɩ�X�ы���8��1��|������'x�|��:3b�G�)��?lo~�|7k�yi�f� -�^���B�tcW���j���rf���뜜���ş5��o�7�p�Q�������x6�af�%���P��L��1�w|�h���9(~�����~�m�5m�/����]�Qxi���}�m瞍�}��l��(�XP G�*��T�gS3wv0����63<�D�a�BN1z��[41a�1�![��o��:�?��*� P�M�|o<��:����M˸'h=�C������ҙٚ��6��9��d<����-�l��@D?��F��u���ؿ� -=;IW�qGAg����/�G������u̽���y�O����Ic��;��Nl]Ǒ훘��`**�ɝ�v����@tC1e�Tӿ�E߮v����;LW�]�㏳/�, OЖ�2���ђ�9�I�є�������h��$h�=���(Ivo���r��^�TL��a���0��`���D3����$��Nr�N��3���d7�)�J����I"�1�Q\��7�K������Ԉ�T��j��P��F���@�-zT�L%.g4�6���H��75����i&��Y�H+�-��VC�M{:h��G�q�z���wS�q�����|����pf�O�pf,���F����W$GΑ�^]{�˸����u�� �{o8�{w�nL�5�Hs����|\v2��e�Q��J������Tf�R��H��!�r��4�m9�$��ļ�����T�g�E}��9#�u=�\��q3�pg�N�)��N��qe��2Q�m�:��*s��*r�)����2L�e��</v�E���P����O�Y��%�|�����<- ���΅b�A�<?�\��m^A�y=M9�4�FR�Gun*��t�3�y��Pj�߈=���A�lwa������EO�W�&���`)���a�-�f�%���=b���%���CǔK��j�.���O]�&��é(��� �҂=�ز�۬STX����������'��̓[r��*َߐ��D|EV�o ��7����'�a�>+�=��Pt�#��,V[����@ʋ�p�w`�'PdO�V�IA��|G!y�r,�u䖶a.�%�|���cd:�'��s���?ct�c��'^��=W�Y��/*�I�yY��:͢����U.�Q�G��˂��G�_�ՙ��i$�����RL�5dV��QՃ�z���G0Ԝ%�� -�5�&��#}���j)��%^�%zZ=x\뿠ا%;�5k��/)ܡguʫ�j9U��U�[� sM4Y���j dԚH��b�����C}��Hm8H�k�D�\��o|����s}I\���G|���5W��G���{�]~G�dB��.�����yQ�|�k5&W��!�"��KZS*���Im�!����' - ķv�:LL�,�m����Qm������"��Q-�뼠~_�� T����=���^͂����� ��.��v����DҾ�$��I|gq�vu��q[��v��]KdW;ۻ�bk�"[�!��%���@H���� [:��ԃ�U��ŝ�5��8x@�0$�%[��W � M����ݻ�}���'�?���d�� �a�@!�4s��~�' <I��Clz� C�a�l���~�u��痴�s�qJq���wMh_m��U�`��$�D���G�s4�Mc۹cl�c��e�q����Rn��q77M��f��'.�j�V��N|���7����s�T�{�n�!͂,��,�.];�U�)�@��:L�,�v-�<�������'���4Vx���S�2��Ts�9vN/�;�&��z���c��f�k���zZ�?�G�aC�]T����}�q����J�!�� �t:|����,h�[P��u&ł~��I����ɯ%��jVi.*Ƒ�������<.�:���ES�W40`U�`�AAPT�PL%��ϼ��H��KL�#�t�h�ծ�m��決��i�{pr����������}���g�D��,�<�K.+$'�q\��Ue`�mU{�I�f��E!���˥�X�P`5����,�Ugj��ڇ��E-��"80&��Y������_K �s����߅��g��f��섈�ⱅxl��r���tj�\jy����m8RK>j�!&������R�3aN��[͝/�ߑz���i���6��B���C^�2��k+չ10}D<�t���s��S�Ģ�C�G�!���u��p�`��q:~�n���s�Z��v��Nա� :��?�����qrs�#���i����A��s���,�?Gr�s�9��<v|~I`��.���I���`�^p]ᾴ��1�����:�N�S���L�b�y���e�A�J7�v�J�����9���N�o�8s�U���;vo?�z_7���_q?��l�/�E�b�w�mI�/k�Y�r��w�R����4���̖�����ǝ�Y͑�]/#W��H�������랰���r=V���>dc�/��}6�����������Zo`�u>�� Wѷ��o�ˆ]��/�^�=�'�C��3V�10�ً�uW������v�Ď�l�����j=�m�B��uI�a�֟��<'�Cg�t�;�3�bцov�O���(x��0�H}��즅0�a���r��$-s�,"E��Ts�&Je�;L��8�:�����X�g��������W|��~��XX����ʦ��G!�b�(�<��Tg1��p�QƵ��X���DdwC��Q����ʆ莚�m��]a�?�H�V' ���#��!�8�Ӊ���دiڇ{4�8k8٩��b3��D7����E���3���Pލ�j��y�X�.+^4�D8�p299�X8p&�q��`�{z�kt>�e�_�����_�%+��5����ev�̧�� ���G�)��%�]��71���ÉI���O����?�D=�X%��a��*jc-Ţ%���,-�_���zB֬ �m�ᶝ�_�uj���z���Ix&XXI�R!d�W6>��O��"�5�'��\��9X8�̿I4g���o�&�U�`�4���w�v�8��k��� �`;S7"��/�ky���آ�M���!U~��y�ٚ㘫�M4��$U6���fiz�%*oQ��-�ה�����tK�Z?Qqkk�&"[.���+� �o��Lk|�9۱�V;8ky��Z��K[�V!������T�z�f�.U��d��M��9�S�s�Jۖ�����n��_Y� ����C� -;�S�� �w}�<W�s]��}�(���ku;�v�JO��_��R��t��v��l�[U�1TS]̚�����욦InY*v� ���ګ�Tع\���*��J��I���kL��4���r<)���\W^�����6�ˋ���|ۓ]���{f�8j�[[���Ye��T��O�wVq7�&���5��*�HV��0�y��8ϱ��}��t/Ө���c�F2��ګ,�S���V�(���\��� ��^Ā�����ץ}�o��³��ܝ4��U��=Uԣ� -zh��Q�"5�ˢ1�I坪�^���5Z#| -4�w�2�T*�� �Ni����� ���R��#��m��N��>���|�8� -- -`�Ὡ>��^-Tԫ��}�j\o/�����A����}���,�de� S�_���������_��EJ�Ѡ�J -�D _�� ���;��w���f�Y��3V`KIfߦ��u֘~��ӯ�F��,e��(�?BCb���!�iJ1�`�X -����74p�%�T).d�,ƣ�5~��=dUL�3�f����&v�*ƍ�h�O9�`� -l�Q�NapQ��]�A�J�Ӑ� %�ip�Y�$(1$E��J0�Q\h�,a�>O��kd6mS��"".+�tY�)ܪ���C��sc��7��|�-E�?��i�t�+J3vQJh �����%��(>,Rq�YL�kJWt�(�#Y���92�W)̼U�чe�����h�Ye���89����fj�1xU"{�L �R�]��g���RL��I������%��b"�.�9FQ�$ED�)&[�1� -�-��2K,+�EAq+0�K�ݖ!�k��P{{`l���W0~�O�x���y�2[R���Z�1.����X/E��*"� �%T�qf�Dv�@U]�q��(�h�"���"�&�)�Ō�Ȏ �p�d���WɅ� -*S9%�B�d'�r�j��s;9��f:ΝG��ө��.��{��{�� ��Ҵ�DM��P�� -��Д�ZM��E���}Pџ�k -��_DY��ރ���-������dj�ߋ�;[R���eVd_����Q^ -��U�M��'�LUH�S -��Д�xM�MS`l�&ŕib�J=_/��6�O8�� g4.�tW��,����Y'�7�0��ї2ؓ�iFƓl�%c�l�g��8M����Qz"a�&'((!X���8K�b4!)E�ss4nn��$��/y�|SZ��S>�蔯5:��|�Z����K-�z���x�8�Z�b�ʔ�x�W�i)������+`���� ����q��4&-\~iQ�M�+��,yg52c�Fdl����_���������Q��y�Y�7�Z��F�Ɛu���#*�sF���pl -Ʀ ����"���z�0R>�1�6LԨ�`�Ȟ��fG�3'^9�[�!�K�k֠��r��+���50��\sn�-�"�l�vĸ ?�©g�[$ՔJ�fy|NYH��0l -,`�*�ш�>�*p�G���zkh�8 . -�{�T��˵(Z.���\��~�J�5����Y�ݲ7�}�E9ߔc�EN��J�_�����PN-,��H%(���P�����]�А��r)D��n�W�G+|ԧ�_NS�P����HR�J�zT2�T2[U6 ��wl�D�ʹ��[�A��k��˩��B*�wj�U-M��$䋆�r��-[S6�;�7�pMv�D�1��L����xѴ�!�ƚ��X�-tO���^�*��� �X -��4y�+�s\+٘�l��+�\~�C�����L�7�"�9 d3�yɼ1��1��]� |��7��|e�z�j} ��4���1�9ɩ�._������Ԉ�����)�x�t��E�n"�M����;�R�G�j~��������Y߸���#M'�ymT��Έڧ�6�"z�'qm%�ģu�;ڸ��qh''�4�v�N�����?n�m�� -�N��� �JX����`��J ��n���y��5<zU�/�t�u�v��:����>ޠ>��}\��B4�.ꢋXt�慮c��캪�<Z�&[XI0�a�����+s7z��şu�[��:hQ#��w]��<��a�1j�8�� �$���S,p��Ru��n\کf�S�p}��?:Y{��y�w�"GD��N��C�;�Sg�.i��+��쓋���R� �~���壪�$�N�F�~�>��>D��C�/�G����C� �:��a�."J^Wѵ��u.�7�����D�6��Grr�b��n'sVOdc�;��`�&��(%�t��y���]����mU�&�[�T7��g������)���7$�k�/�Ǭ�u�U.���,:�>����Pv��D�(f8�hX��d�L��0�0�t ;��p�VW��2��%6�&�s��)�ϙ\?��#�y�9�>�:���ÿ������3���E�O<�T`a����+v|�a�_�p�g=�:�]:B,C}���&Z!@o�7Q�/l��n��o���&�f�~��G�Di0:�ǰ�v��*���S�N=�f�o�>�!�թ�����^����b��?��.�s��ׅz�� 2 -��p�)�$i00�_�pJ�T�Y��:8[X�:��Ʒ[����B,�C܆�Z�� �|�A�;��`��������p��C�ͅ���Yp���]%Tb���6� -��o�<m������=Gf7Q��n��b����3D׀� ���1x��IXO�S�X8I���?�D=V!�XU�j`��S��|�L]�ś�d�YvI-�� -dB��<uEKCo[�`��� �F��J��5�j{�ʏ$�1��)V&�\XE�J�/!�����u��n-�R��*�Q �ª�X���Vw��g��öܡ���!�� s<���.V8�H����+V&>� �����U��S�m��lw���>-�u\F�*���"� -��bm��9n�8�^C�|n�瘵��Wg��Ug�U�����즫�w���Gj�C�;&��1UNY*�@%}K��2�[����U�ܪ|��p\9.�=��eY�~�����cw���E�k��&��u�W���j�ziY??-vP�`����t���5R]cU�6W�n�*���A��u�R���2٤̡-�?�S�Î(u�9��,J��sZ���~ts��ч�ߗ{�h�U����L�yt����ņTE ����2��I2Y&�d��6�w�Hd�5�����(�T�5��J�(�X<�z��SEE��A�R4*=�_H���3���|Ͻ�{��އx��9NnߩZ;s���kV���NRg@��f�X�� �ڂZ\����j��LM!���A�����{P5a/�*������*"<?�?����Q��q�����^������UA^Z��9!r�F�}�Qms��8,C-aV5���^���j�E4�f^���T��*���<j�J�O�$�U�\Sq�T�%ƣ���o�}�����bۚ�ع��qy�D9#|�6/@�����ƨD�G�U����|UE��PEl��b�T�B��*6l�"�ٌ�U�;�ǿ���O��(�s�+��2y������~��N�ar�JFeg�8-��_M����UM\���X`R�!]e��m��T_�E ��%v� i��6)w�n嘎);��,�o�-�#��s�K������Α��Q�7�َ��� -�¶D�|��j�}U����&�ʞ����Tْ,*\����v噪���,k���N]��ԍ�4�RF�Q���,s�����i߃�.���/����<le��g��'�{ �t2��>զI*7��$y����ʖ���rSL��f('5W��"e�9dIoTFz��3Vɜ��R-}J�<#S֯���g���B��.獼��{ͧ?wa�1cۘq����F�3m����-�!�ϑ5}��3�d�HRf�Y��J��d�*WjV�R��ʔ�R��hݡ�ܧe�=+C��Or���V�α�h-��d��+�g0���]IJ��ꉥ4G*��5�WYف��WzN��9 J��(9�"Sn��*1�V �Kd,蒡�1��*�6�h�K��]����=w� u��\�����:��=ƾ�|.�z 1��]1뒝?I��d.����P� -#�ThP�ͤ[���reXT���j��*�x���nͷoWD�!����ܒ��o -��p�G���e ���������Q� �c=�P#�r� -����3��e*�V��O�� J"��$V����)5+�Ŋ,[�y�E�7+��S���ت`�AV)��7��Z����/��Z����q1�؊��4c�Ĕ�M1�>�d�T�(�r�"�B5�*R�U�U�Z����ה)��A�Nͪ[���6kf�~��jz�eM���u��[���Gy����H[:�ǝ�F���6` J�)��L��8^a � -i�Upc������M �o2kf�U~�v�h��O�R=غJ�nҏ[�hJ�I���\�W����y7{t������6ja�i��h� ���;`{�} �xb� ����t�f�����`Mk����q���$���:l���R�������=� ���81PN��i��k\�G���C��;�}%g��{�b-���w���}��5 �9��]�5�k�&��5���H�ESuQ�.����H�Eb.���^>1s.��3����u�;z��ۏn�����|L�}�5�^�z������7���nwČ6x7��Ѹ)27�&1���vcນ��������0�{�y�[~Bo�!�M�ƍR��ز$�\O��$�W�8`��yf}�~ς�æ���C�顰{X�Ѓ��Ĕ� ���}�3�A�}������)��C�6z�@4q�g�4�g��q�Gq�~?ͷ���O��g-�)�~�����ZwR;?�k/����uKų;���H�8Ji�.i:)Lރ�>`I4G94�d���}9H�!�jc�C�x�(�y�a?��G�vS*�hl�M��ӂN�^���R��њt-J -�02��T�8<?� �^4]�c�}9Ł|��o��o��8�Z�&�����y�CW��[�^z�3�[<�sќ�����N� ��^��cF�R�<2��+����cT����E�%��%^�?PP���\�����u��D�ͼg�A�,�\g�߄�A�Ԙ�����1A�W�L����m�k��W���xO�S���~@a}��t���xM�j��z�o2�F��/�N<3��ʘ�1#����x���cf�C�9b���[����Ǘ���ٓ�y�aO���o�r/�A ��t�=>�[|̰V�ѺqL��Kmó��3�ѧ8�[��z �ryM�s}�S��{������r�@0D�d�yP��ǿ4h@���Z�]�sY`Y�a�DAQP�h� 5�;�Qc4Z� M�8�c&�1�1i�i;M;m�Mm�I�&�M�����?��.���y��}���7s�#f�/��>W�.�0V����&� �i��=p��sT��w�/�������3Q��i?���A����� }#O��i�i�C��é�Ѧ������!>>�����|��������,�w(��,�9ݦ���� ��A�&����������G�3�� �$xY,�N%e�H�XB����V�XM<c��4{o2�N0뾦�0N� -��%���&W�eV����̴���=�*������ b��g&�<8�0��� �F7��0�e�(���������<��N��Q��I/�ţT����!tp��W��h���%����b����X�p�`��h�����Y}p8��u��qLO�� c�@�O.��"��^Hϒ�ݐ���d��6��ޛ�~���k��2���x)���X�����i����X���XC�Fam���sd�0Ur�O8��mT�Vv�؛����v��ߞ>�&��z�8}�c(9��g����N �FvB+1u����|<E�n���(7�n#�7��uT�(��Zv����>����q���Q|n�jp��A���"J �4��K\V�W5�FX���I�}�����8�.���뱒�u� -`w/wy�e3�S���=���\7W������#��׳+�zc �hxFX��`YaٵΥZ�3��V�<֥a�~���J+f�����Z��^��S���q��%����u��9�9ǩ_Ҋ܁=�ǕȜ��q������M�1s���X��3��~�A�n����a�< yX��Ӯ�95x�I˼Z�ݣ�y�ꙿV�����Yu��c�+j]��Z�~�f���58���ԏ�Zn��J��r�?-��HzK>���f���u����k�O�|S�la��.,P�_�z�+��_�utʱ�_����8h���w�1���CΩ.��jB����9U��]_���\+g�-��>G�����h�35�?_�����%AFu���#G�Em�6-�RsX���j�U}�J�F�Wu�NUE�WE�K���@e��e3|,[�#��:Ujp� ց��b�t��0m�8-�N��M�Y��A��橮p�G��-2N��������<�G[UcWML���Uۭ�A���d�J� �T�xZE��e1�Waҟ�?UhtN��Z�>�$��A��=��Z���2���I��apS��Gͱ�j��V]\�j�M���REB��JU�X%��I%I�*NZ��䵲�lSA��M��k���=e����BNe������T�#��mh!�%��u?���A=|��誆Do�T���X�%'�49]%)���XU�Z.��^f��Ӗ*/mD9�[���W��'��yUiYwe���̙_(-�9� �Ip��?f��-�v�ͼ_���t��FrRe�P�i�l���UdNT�٤��l�[��Q���Zeg�*3�W��J��,s���WJ�%彋�����J�u�M���\�$�#�}�����y�����; -�:rR�����d*?+R�Yq��NQVv�2s�[���j��Zd�_���UJ.�(�e�,�_tY�E�ߣ�gqNi"�s�x_d<;TI�]Ù@;��Z�K/^Z�R��d�wU~��r�������� -�2�)�0W)���*e�6)�ڥ���+� C�.E�Q��UE��(��;E��]��NE��K�yr��:걑z�_�� ->/�S�U�%^2��J+���8H)%QJ*I��Ԥ��l��,���e(kP��C��E��SX�� -�8��ʋ -���U�VA�!��˝�BΒ���o���ц��Q-R7�k�O9�����ϒ�|�*W&C�A1�I���PdU�«l -��UHM��j���vT��;�Ww@�u�ȧ�����O�[��%b{������z쒶Ђ�e4Z�I=v0w������'��E1u���Ux}�B�ܐ���5�(��X~�U�mZ�ͽ��<"��'5�e�<Z�ɽ�f����O���/y6;u��^$�ð�{��~���ԿTj��T��t<��'�m�ڼ���/߶P�8b4ߑ$oG��: -��^.��F�utkVǐ\;�hFǸ\:��;��;�:�"��/4�ݩs�����2��XŞfO0���Y��Rޒ���^)�y��ɣw�f����2�����#���"� ��� �,��*�@D -�� -LFѨ�jwTƦ��j4F�����َm��&U�4�&'m�xrlK�����<g�>�s�}��}<�R�/�0�1�d��L$���0\���5�Q[��+Eט(���#�L?!6;��o{5����@ -�k��$�%�S,>����s����e̋�\3:���bf��6f���B6�%=��[�!��R�(�dk���S���.�pw�:�T�<��{�ji��:�/0��8 ����.C�B.,4{m6� -����}�Y��XsGZ����~l6½���mb.ԓ��6ҏ$�z�'�� Ћ�Y�����˿���Eo��r�X�|�4��b�R@V��JP֝�Q��"��l���q���RU� �@)�i�����R�������@�6�h�~����%k��q��<6ra��l$چ�����vj"���m�Ik@��@"n��]����¹���Ks��4s.v��E����3|�{ �v���u�EM4�M{��%��~(��2�d^�l��O/;\��r��H)�w~��1r� -uz�Ű��8A���2lc�1p��� ���m�G��E�_��z�V�UWv35�߸Ò/�����+�K���%�N���5@�!JX��9y��~\�R��es�!p�&��A��@Mt�Z[��Y��p��i�M?���q���|�!@�2�:�����һ�䆻����X�~��������E�݆�F?��:x+�]o�q������=�����Z��Iz�p�B�Cp|�%^��/��������03Ыi�G�c�q��5o��a�D� d���E�M��[��c��#����}��7���h*L_�l��`�qX�����>Z�K\��G�&x\����1x��g誧h��J�'�<,a���دF=��Z� �u?�H�ޤy~����Zѐ����o^'�.������LJg�"��3���Ђ�p��[�ܛ�'���/�~%�T�Q�ZJ��Q5P;Ѯu�����AќC�![�a9^]���?>|��<�� �1�-��p%q�3�#�9���~�.��yU����jt?���iT�ڭ��X�h�V<8��Gu��ӏa�h?t��O� F�)U�>Z�m�"�@"�$�x���NkdG�OEp,��������1��J��g�)-���!<?�I�'�{a�v;����&%[=��!g�s�s|�X02�(�X҉%�8r��ǩ/���r*���Q��i�6�c�.�mx��(��H[���Pݛa���8���POk_p���䢕�8*b2�5��N�b -�O�̇g!<���epU�U�zx��^���o�����5t��.Ԃ�:Z����5�Z�Ǣ3�9������H����O -<��d�<bY��"�Lp��U ����Z�[��dr9UTE����JrQ ��1j.9�`?�J]�C=�[��z" -!�(��k~��k�?V�?j��JΥ�U|3�Q����-�R�p&*�����E�/p��s.�dV�#�z{_v��ىX��5?�B��z<\��J!�4����Q��,�r������(�Ba�}Z�tD���ja�w����r�>��};��5j/q����?�u�2h�it�6�c�vr&NW�� g�|a�t�ֲ� Z�'IK���䜮��Y*r�Q�K� -����߬�,��I9��h��#�;䜲\�S��ϕ��tv�-�~���ݛp�5��\3 ^�8�c�~��E.�Z��K�U<РŃ�Z��IR�k���\���q[�y�E�;�\� �Q���ܥY-J�8�������4��)���U�����6��5��u��_���m���T�0-����9^�F&*�c��5C��2��9_�G(ݫLi�+4�{�R�4*��eM�=�)��R�ߧJ��gg7������YQ��}\�����d e�ėR䀧�r=�5�he{(�;L��4gL�f�LR�O�f���t��J��S��IS�4%p�&�ݡ�A��tR �����C�N�;u�'��(�=�s` l��}1���Y ��ٱ��4�w�2�Fj�����4# B��1J ��ic�hj�L%ejrp�&�+1�V���m�54+��.c�UEG|��n�I���}�C�;'Q��n�T�oe��d�������<L��^J Д�P%�FiRh�&�M҄�T%�(>"G��Y!c�j��UQ�}Bc�wn|�s�ѝ:���܂$8�:���UV�����K�z9�ap�4����� A��بDŌ�&ct����e,Pd�2��*<v�B�ת��+ -�������ٍ�x= �~V�Hk�<�_��8�����c�41�_l� P����>�(�AP �H�,*�*�E\�R��B�+ -����b$����EH���g:�I'MRcb���5��4�x��jcJ�����d�,�����^����� �m�x�`�c���4bt�b�di���%�hhb����)*�N�����"R~��ԋ�r������."����p�x�~dݪ�s�,i������/c�dHP\B�F$F*6q�b�FhX�h MNUt�IQ�y�:U�c�a�Rx�:�Oۡ���K?���|�Th�� -M��Ar�>��hdߌT��̞ͧ����'�g����{��b�>�aƾbW�1JO� ��4�"ӓ5p�x��3���"�N��~���Z�djW��z�>��M��.C|{�ww�������T��s.�ːIS�'R�e$�DgxkPF�"L� -7Ej�i��2������t���V�S� -̞�>�O�WN��r�c�+o�Qy���+�|r��7�]�\�a���~��l*�LLg6�Y�*^�D����'��p|��uShnO���U�� -������3q��'��//S>�,�T&O�|�[V��R/K�dnF�� -߿U�I߫����N��B둆+��\s�9�=�U܌OF��1���/d�/�Rς>�-�'��Hy�G�r+LR�� �+"�� -��)�P��b6�"�����ک��o�%�y}�|��Bi�������OI��?����*�Z��j�Cr�Y�V���je�X9dV��Ⱅ`fT�:�F�Y�WV�t���7)�}��j��I��Fw�����P�$��oi�c��h� E���y�y\�64���o�r�q���6��Ʊ�L6�a�V����4�"���V���po@�Ԭ�L�9+�RhB�&�O��A���,��c)�*���V5p�T1������$W�|F+�V�lHK�ؖ�=.����ĸ�z^�������e?�r���k8`σ��,Q$��I.jK�?�r��&5Ԥ��W��&�4s5����z3h�ְ��>��V}�fbm���r��m��썜� �E~�Q ��$C�� �^�?��a�������;��N=�k=��U�h��&��]�����ƭ�"���z�H�C���l��4��N4= -w�B�D]��˶����3�B_���-�C��l=�tM��ly��`{(f�L�İT�����h�����Ax��ub�i�O���hMC����$�$�#��s���O-<���$M����s_^�mur:x��K������v�!x(�pC��#� -cQ�b>�99μ8��8A�'��:y -|�:8��W��ȇs�#g�p���^�����uر��cNA�E�� a�i�;N��>�9���ef|L��J����pJ+���[L�&x �F�����=��������w��@q,��s��|�0C�R�k, ߘ���M�-�6�w^�֛?���00��rPE����ur�ps�t�qe��������UC}I��h�uw>D�}��X��g���-p̩y�> Dߡ��)�ݖ �<]E�~�2��ƾ�0�~�?���QC�p`Ϣ -O�O�G����������{���8�|��μ� 8#��+h����O�s���b�a6�m�d!�p��Lմ�z�ce�O��D=&�������8*� ���;p\�_�'�a�7�,����x����d�c����#(���q?^Ï��q@upl������������z��������>�p���E>m�ſ&oDC,��[*E3�'c��cZʱ�����}�ͽh�6�x�a�<uCmLͼ�K۩Գ�A�t�e���!�����g˟D��?���- -������ 38 -�z �k<��̇g <?���v���N}������/yr�릫5���}�<�o:���䢝\�i |Cኃ'A;�?��3�O<�`�j��h~��.k� -��Tf�V���X\A�,�{�v��.�z�+Σ��?� ��Gc���U� �b�z���I�z1eS���5.+\�da��ö�'�x�+���@G5�l�8��O�x�K���s�:Ʊc<3�ɩ��@|ap>�p�D����2`�&�<����<� ��ZO�i�{�xrH��et})'��i����cޕ�������Z�&�Ɖ��`�@lQ��b=�$�2W6\y�T��-�Kg��p-��Z2��n�{��i:��t�J>�]t ����@o�� �F�؇�Q_�ճR����DZ:�h�~Bl�ĖDlFbˀ/>|E*r���G���V��V�<�z�z<'��e{��,�K����L��Ko�~�Z -�����V��zV���^�/��5��_�\Bdu�P��Ms�b�Q*�LR�g��x�d�6+�g�r}Kd���잋��k�L��hB�=p@i�e������2������H$!��Nvsl�ͱ�6�I.`�"d�#,��D@ �4X �A�!�@����rHeZ��2� -�Ȕ�0���(*�"�RM���a�o������������| ��ٌ�e��#&�2��@\����H4E0g*r`�����'�a�P5M0�i�,j���:��N.��)����ʀY*lQiP�J�* -�QA�>��B���UN�5����q�@/�w����y���:�����r����2��=�GS�T���XU�Ui�PEp��B�TR�����PA�\�G,Un�eGu++j�2�_R��5���b�B�`Xg�/�?�ݛ���b�vr�>N>��ev,�"J��Ru�dU���,ܨ���"RU�%[�MyQeʍ�U��QVӣʌY���U��mTj�.%�Qb���`�>S�����)��Q�����T>�6�y�����i����Ԥ�4J��Td���<S�rLI�Ƥ++6W�q�J���%~�R��$,TR�J%$�\��.�%?����2�\�)�&���<���E$�~xw>����*��2�i�*��}y�S[�x����jU�9Zs��R��hUrR���*��<U�Y�Ou*6m�L���Nߩ��Ê�xUaTx�'��K��:�ܿ���)bM��' ���J�\r�A~u�RJ.٩c��<Qi)%�D()5V ��2�e(Β���2�2�d̘���Ed�+�ڥP�vg((��s���d���q��s�ds �V �U��x �` 5*&�,�H%gz+!3@�a��2ʔ� �բ(k�"�K�S���&��Ε!�U���䟿M~���h;� ��}��?y���a���������4�ׅ��Gȧ�w�Ԧ����%.�KƼI��VD~����jKU��*CA���_h�_�lM,^� ŝ�)٪�%��UzBcK�klɇW��K�<��܃�fgM��������o�t��|��'�ڤ���t�BK|d( PPi�JM�\�(������Py��+�j\�Cc+kt���ڢU�*N��s�Ӻ�FT��j����j���dM8�[ؑ6��f�M#�2�[�%�o#k��)^�2IM1Ȼ&J�j�5�&Mcjr5��T#j���?��k�?�6������z@=»�RwW���O�^�z����I��"�����s�Zr*���xX��>!|�k�1v��s�����,r;�؎������I���8�S4a��^�9f����&���!���v�6c ���.c]�����aT�O�$�c$������pu�l�G4`�;��6@C;ܓ9����u���4_����s��&�m�m��u��� -��r>i�TآB��>� ���8��x|>w�pR'�p���4��&s2�N8�Ṋ�:�҂!j��}�~�]�y+�W�&����|�jj�4����wf�j�#�9�po8��9x�܇j�h�m4q���oE��⭖����7�m�]ƾ���������S\����O2yE?0���#��\;ɣ�9�dN�2'k���l�k鋵Ԣ����|t�x�@_�9Fӡ\V_g���]��i�j����;o#��%���I���3D��?"��ztS�n����f�fS��沉���=�$�i'1�ˍ(� �/�K��6�j+i��X����=Ă@~��⒄�������?z'D��<\�.ֈ��p�Խ�E@����K��b� ;�R��5 �6<^.����}�Y��#~��R����CA���d��`�Mh�u:Ho2уd>P�h�@;Kw��m���>��=|���<Rb|8�����q�cP�#RO�^����q��<��>M-^����W�m�k%\����g����ƹ9�#>7����nj��BaB��Hg[��!��"��Y+� ޥ?/��/����������p��I/�����u��{������Mp^n�J<�a��؎���G��ߒ��Cl����ܤ�?c����y�� ā}���Fպ�� -��%J���s�M��������#�u��_i�piט��:��pQ�xz�(��<x� -����X�G��ʨ�����1o�wVG�F"5�}�øJWp;�q>���"y���:�z�&�=M�;��u����,��y�x����?����|9 �p}(3|����J�7��Q5���%LO+i�Z��ʹ�vx�h���h��h��2g5l/�p�9���|%�8N���랣o(�b�QM��7���sh��p,fL��x��g=<[h�^x�� o�#�g�>|f�{�����5�����6���n� .<yD*%~5�]�5�G�ha<�`[� -����{�]���l�i�� -S��#�a���w��p����2�i�����.%0�as���9�a'�0!!@)�) �@��@Y�6k)�W�nZ�4�����b{1m]5e/�b�)U�ڵ�Tu��i������i���������}<�=}a���)g"|fnȣ4��K��F�&��p�8�Ѷ�!xF(����C�DC�@l��U�F��1��N����Q�}8�߯�i����m��_,�F���*���7M�ӆ��.2�Y4�k\p��5�%���Tw4�e�WD���7Ү>�������R6x3^�i�YЩ���8�� O#<��ӡ��1���1E|��~~��[cܲ�F��n؆��!<0� x�/k�%�2�/��Ѣ��3��tR&�r��eG/'z�� p��� �.��K�����h��]$j���.�c��;N~��T����3����7ϖ�Z ]�V}Ra�3�ے�̌ s�݊^v��p��j���:�C�����]�E'VG�� -z��U���?Ռ��� �D������(�;m�8�LS/���V#�̀/+���͉n.��k�o|�� �7�x�=/ �mAc7����.hU?����Ŗ����2O��e�O��D�E����S�/��V���#�'����&5��)�[ !��:&w���Ψf�u9�o�2�=�G<�#�9���j6#�]8_O'h�^Z�YƏ㌆S�3�,C��]���hu'�����,5��i]��˵%�F�-��lQmT���w�j�^U�L�{Z���T�����SQ�C>_�jXջp|�[���1p��v��#���3�,��q'kDkt���bT������Vml��cKU��R�q.��7�a��Ы҄�T�8���yl����ו��}Y�)���r���.#�[�/3�_/&����)b�E�ad��.�kJ���C����H0ˑ�+{�UeIv�n�Vqr��F� -�=�OVn�Y�N*+ݫLӫ2��U����?���D&Ӫ�q�m������s��\`=8d��3�h���Җ�|����arcTfLTIJ��)Y*J�WAj���*���V��E����TF�LYǕ�����[J�<P��}�6Y����O�|�_��*�ιjbL2�E�~d���f��ʑ�A*6G���<s�r�fY2r��iUfV�2��d�ުt����˘;�Myǔ�Y �ˊ/���?*�����W��~ �o��x�GV�c� �Gj�>�ڎ|[ˤZ�d6��Y'�%V��$�s�d��VZn�R�l2�W+��QI�J,쓡hTqV*L�E�/^RT ����O�*�0C`�[��"c�b=u����g]ܽ�X�y�4"K5v)E�5X��(�d,2*�(CI�<%��P\���-�-�hCY��m{i�Q������T�㮂?Q��� -��M���mt\F�p^n&'�X��D+� ��3�(ې�y���B��J��+��Q�$���k�h�êhG�"�� -�hUXe�B*w+���ά �N:����� ��9x���` �k-�&����Ԧ��C���.�#S=�U g~2���C����j�"�S��:K�5� -��+��V��P�N�b�u���X��,n�e�ӂ�3�<����W;��s�e3ϰ�!F��y:9�<e�b�j#��mTX}����PO1�'��),�8���n�P#�4�k6M��&��&��F�t#�w#�S�c���o���\�t�W����A�w��l��d�R�,f�& OD{�<�vL������d��C�yح<>#ri�8�����Ò�a hczj{�e��&/G�Ѭ������x�ylȒ�,�����`�[w4��v�b�E�u�h]sA���{�apim��bY��2��g�H/s�58.�wz��d��:@�=��̊����R:����P �hxC�[b�A -� � ��z����v��s�騟em���#���9D,�s����O��irr��y)���'%"S8��|����ύ��� �c�Qn�d�Уy��FOQ�ȍ�,�{X�F>���R7pՕ)��9�8K��=� �>��yrg�Cd�Q�����1�334�|2M����M���4��F�i.>Ą6��8��2�G���ƽ���,"� ��3<��F�y�f -G1 ���("�!���,`��X��-��rd��[ ?��� -�_���1�4}�z�;/�����8O��@,�9#�ڐGx3��0z��F�^����|^�ˋS��eBL\��d�X^�=ϝG�(���2Aw^��ן�?^�,} |� -q���6�P�W��l���W���j��V���C��Z@\�7�������MZ�X���5�o����}�� �����=j�=��=��lq�����L���B�L��h��?\�{P���,o�A\vY��cX^���� -�*D^*J0jL4�D���Z�5j�X�q�h�N���d&Z�i�N�N;�iR�X�c'3�c�������ξ~�=������9�{9��H�a��?���8�3"o�G:f�.�D�e�q���� �,]C\�?�3���7v,z��ɱ�Y`�у�R,-x̗�)��ۇ���GLɇ#3i5��� ��5.#�3b��%"�&�-��+��m��$~�����"�D*���x�(�at��(��������k�͠� -��7<���st�g4�h�Opl�'��Q�������UV�B�K�'����X��7��[%�2q��`U�Q�/lf����֟������ԟ��`��$k�o٥�i��(�l�/�[�x�ǔ�F� �}�ÿ�{���M��_����*]S�� �V��`�y�ԅ��3���{p���8�g8H�5H��ɇ��_i����6�� ��c�>�-�S���$�=�(���<�ɡQo���x�s -�uOu��<~���8�dO�^���a�����]�����R^���/w&7.p�����]��da�R�%�\ -Y���հ~#y4sTg���m���g���G`lft��6�� -��a��A��R^�#�#�Ck�{^��Sx����g��a�����d0j�h�� ����N'���@^��\&�AX�//�wWS��ع�d�:����M���w�7 -^��`0c�K+�����E>��SM>��28md� VX��z�U`l��nX��U{���_���'�;����;�Qp�7GF�.��6�Ȁ��K��8��T�S�L�r��� -V'X�ZV?k���0;@�V7h������n���X��o���߃�ka;1@-�+�h0X1 ,'�ɮ�@)� V-X�8�-��i>Hs���'�a�+��U��8;w��>����Q��h\���A_�#��&����� ?3����VXN�\`���&�:�7�v�zء%`��k6h*��@w�3)j��)L������g�r��p�q��F�m��ØDA��k0Y�gU&QG�!���9�.P�ȭ����ko6xݰ�k���?f� ���y��J9%�f���E�N!�� =���A�����w|� ��~� -�V�%?+xv�1�|�\���WM^���ZL�J�b����ۮB�C*0�S���C� �Z��u��O�}��~��B��}I�>�ȑ.��4��/�j�6�z&�gW�_�*�4�P�2C�JjUؤIAm�ܣ��e���r¶)+�2��)=��lO�q�x�s�<���܍�ڒA��$����egp�Շ�*(\A�*�SIH��B�5)4G�a�*+�3�Z�S�=n���)#j��V+���R�e�~K��ה`R�iXI��t)|�װH;s�9�x^W ��g.��R�i�R |K�jҸM|Ҥ���OUNT��&8�0)#�R�����<J5w)9f��,���-��= K���������(&ޫ��zٷ� ������Oç.3�K����rY��g -U�i�2����d��+-&G��B%ǖ�W����O�Plb�b�Vʔ4 �u���o*2���%��=E%?�I�� X����y�M{�X�=X�w�p�A]��9�g�DY ���G(%�$k|��R���P|R��%�X�eN�.S�S{�֯q�M -��U���B�(��g^����N�� -�G��+u�Ib)� {���.�p)�K\2l����*>e�bS,��ZeNM�)-WF[���nE�OUDF��3(Ա\��� -��#C���z��T�Ywdp<�q�� 8?A�n����M!��~~%�mS�X -�|����$S�83̊�LPdf�"� -�*Thv��s�;K���ǔq�ݜh'���dv"������:ƺ�y�w�{�r�5Z�%�[K?�g:|j�S�s�pI�?1�=���<�B�q -v�(0ߡ���@zb�v,��(��'a�\�X��u��x���:B�����������F,"v`n��N5�s���D�限�Eɯ�[Lss����%�X�å,T��&b(�V�Be�2n�2�o)����Ys/�;��8���ʹl�\��=��4�sI:�ɇ���,,��3����͐qӴn���V�@IU�5���|5&��^� U��w��=����S;���A���nR��A?©.Y-xO��M�T.�F�T#���Hč�����j�� �ȟ{ �m#&�eЀQ��Ʈ�Ł��w��V����\.���/}D'֬ KR�o�a�bx6�\�D��9����`�P���~����P\{���LTJa���<�}��I�`��z����D����级�\,���ȹ��k4�p��E���Ԣ�Zt0�:0�4ZIv�D��s6�1�sPKO]%��� k��`�6�����d�ݼoZF�%��J�O����]��Q�g�l�ɱ96��>6!wH KWJ!�`9�$���$I ��r����N�#STEm=��:�6ֱ���R�vL?�����a����>������}��^,!�%��b<�b.��i���\4P��Z=5Q����������?����-��#b���]�c ��%4�X�W1Vr����6�W���8Z���g3L3u�L.�(�&Σ�d6A�b�L.gF,��A����V�]�iC'w�ҍR5�<�x��.9�j����h�\��Gq�1���6r�J�r�̪Vjbu���= -����f>�d[�I"n*�+��G ĺ[��"���:��>z��^.�^.�^ꢗ"���{�D�������nfiF�ZR��`�� X��-�����n���������`�!|��O�}� �`a�ҁ�t\W�n���٠ -�dF{H?��2���N��q"O�F��c�愒��!��g��g���xv�b��<ˢgρkjg_M���쭌���K��s8�2<n�� 9g����6e����$9��E� �S0�D��L�^�r���� -C�U -�*�x�ͼvQ+�[���Y��C��co' ��ː��W�6�W-/��eZ�X\����||B�|Z,����G��A0x -u��� ���G�̿T���ר�۸�/q_�&>�]|���3�S�� �G4·���q���'��1��s�FR��w��a�2�����~�w�o�η�����o�ۯ����I����-q�Eo��8��8�W��e��E<ͯq�/P�)�_qh����;0���g��?��6�Os�>�r�ళ��P�QLe���?��G��FG�Fgq���ȧi��1 -�(Qf����=`�ÿ�W��\J��Ӡ:ُE�����Q��cX"�W��TJe��G�ֳ���W<k�h�C�i՝d��Ya�ۦ���?��!���fH��y��o������țYg��ʀ#��Ju��8@>��3��^.��8�]\b���ı�8����تn��D�T�F}Btw9�!փ[�]���,�Wz��a�E�By�bG�p����V��N{�O�8�q��p���������J���uD���V��N�I� -�����N���;i����Q�g8|�p����j+`?�p�9�q�T�S�'�k���ı�si�_V�n5��RV\Lf�i������Jr��Ɨ�q�A���Ș�̼� �3���Õʞl�\�u9�r�T�^&�U��p��Υƅ|;�_�%S�����pٞ��U�o9�!^5��c�5����)�5�o�Ѭh�+�LgÓ�H���d_����0�ny��J#k��������Tһ��M�#Yr��yc��3��5F�V� ���0:�_"\ipeÕW \���*������bxZ��������X:���b���'�r�v�(>��0��@��-���q� � -$�f8c`Hd��tg��G-��f���2��'��8�K�c8�rVEE�R�%tw�/�? -�����'�����H�39�.G,'��d�Y��t���S"|��e�W��H���M���:����ȷ���n�ळ9�,z4S��o���:�1x�R���r\��|� J,yz�W?x��I>c�K�/Cḛ�쬘� -�&�s9�2g�X�L�5J�zR)�=J2�����L�W������|��u/ҫ�ӅMY�����K,u䧆�������Oe!�1R�q��LV�g�Fx�*�T�,o�2|&*�w�R�f+��V+.p�,��tZA/)<�C��o*"�;��x��ȝ-H�Nd�:^W`��g�=L^��D�[�\?���*�/J~�J�OQJ�MɁJ*SB�8�=P���Y��Wd�J�û�K�ͧd������R��{��g6��G-�Kkx]�]D<���3�뿜|"Wma&�P��0%�XbUlh�b�F(:�DQ� -EDLVxd�B�jXt��,���7渼c.���_�'殎���p= �f$F;r|5hĮ-�RK^��(��'�bI���%b��""'sT�£m -�)$Ʈ��*�M��|�&����]���R""0�nL�v��7�����Ǿw�ۍ=kM="�+C�!E눥�X�K��K:y��z����0 ��(8>Q� ����/q�|�&ʔ<U�)tU*��nJ�L�4�b:��zGGs����OV0+�`)�E�N���R��gJ����Tb��)��O�oJ�|RdJM�g���|&>/�����a�l�t����`C�e}��wt�u�����X$�ljs=1��E��R��SI<�<�C��MT�>2�h^/��ʦ�sr��`�\�c.>/-������<Dpޛ|7��AB���[���>Q��~��룼��-�LL��T�s�S,��w����-�y�K���"�x��%3A=�8�`�J�J�!����PtG�y��8;�җNk�%j�s�{�M~�� �\�K"7����dg���eFӴ�i��V@�����9�r�/g!�B��c��܊���N`���8�?�������h)�-�E�(" G��N��*��"�����ȡL����I��u3�tnn:�\tfed�˶�}��e!˖�I�����s���`�� �eЧ -�T���}�7�[=ٌ�b6H����ʊ ���[|:��ztD��� -�H���5�� -C�B>*p��WJn�/V�`v�J�l�%�� 5�20��b;��0��\��Z���sؔ�zb:�\(�����5ú�ZJp�C>�Ί+UbQU��JW X�US�f����PKؕT��D�> νW����R���ࠕf�=�&�=��Z���,\��ק2;���D,&�Ť��|LT<��D�8�� ��xJa�1� �����w�螀�B��J�|6��+Q���p�O�(���x�7���09�%'��l��LuQ-�L�|T^�g�2;�t3b�8��|[���=�lW-KP��Eǥ�������Y$l�9v��B�X��"`���7��~�o�|�p��a5� �:ל��] ���g�5RUG<��}��6�����Qkg�˓���s�s�N}��Ku.��zd�&^��)�e���2uY�7��SK��{V�ǵzB�.�:3|KH��|��2�nMR`z�i�������Xon�w֫�z��W���7��)�u?R����Z+EK��9l19̳>���L�˻E��e������/k0;�����oP� K��lX�8�ͼlx'b�2�c��d�F�~����[�"���e�It�J.� �gmo�\�3M��7C�����}P,H�A�Q�^�>.dS�v �� ��Ȳ�~$��8Wk7. !�{:i q�,s4����/���z�(�C���apL�S������b1�jK����P�;#�=��3��x4-_ �g�g�MK��hZ��:�!���k�uo���{��>������/ �����ǩ���Mqķ�����d#���)��h�O�쏩�_���v����m�m�[ -� [��U�;.0/{r4�D=�O��3�K08�_�[v*2�4�����v�)����1��U\���Ο���0n�������e´�6��}�vI����@=��!Z�@�Q��c_������4�O���혯P��l/��/P�?��9�xֆ������)��]� <$�mj�4�^żۧ]��/v��v����ls��_ؗ�d�^kLq&��;�36��l*O�&��}��k�-o�����Æ�Cx��m7�b���[�g�H�͛���!���ױ5����s�7�GK=�Om�,�/���|'�;�2�s�H-{%_��xH�Ùn����|8��Y��i�����"f��i;�l)�,���d+=�l�D�?-UU�.pz�(�K9_��P�0��cጋ�jd%��K],�Sc��lr��y~�7b^�!�¾9i�$�$������8��՚'a�)�O��3�?�C��3*��������2�������q�,'V�n�ڜ��nP�S�'쵤�����L���hN��5v��^>�npJ��)�s)�!|�3F��U>N�L�b�����;UM"c{U���n\߈��8�8b�gc�F�z�����3n�<x�au�W)���Տ_�5�_�U�(�26&���X����4�W� ��Vob��o����O�$��h����$ �Es�90�;�3�bX���`��5�o�ፀ7�5�&����,P�u�����â��o|b�fg���D �[ɡ�lu3gn8~����p�����P�uE)�2����xC�U» -�xx�<��b9����D�/����n�Gc�s��&l&�ֱ֖%$�<|�r�v��ϱ�dL���^!�bx����+��@~ ����ӋTC��@Ş��z��=������G��k< �k~x�k�N��[䪻 �iև�u�2]��x�ZGx���� ������[�'U�4 �,X�t�j�E&S�/��OE���tj�j����sY~�-" �=�)�\��(q�˥j��;�o�������3A��t)�aQ�lL�������vKtH[�難m���N���*���J�]�l�M����G2c�=v x��5V���L�e,.�y�v�<�Y\��<z�fG�f�Q��E�E�%�$:��EAz���|Htȸ"r[\m3gDN����22[m��V{"=�H��)�~��N�>�s'�-$�z�� -+����3��U��pkå�uYԫuJ�̈��ѩE^t�,�����m�;�f���V�Ev��h����h75�sk"5�h���iO��� � �<{V�����{�I�b]?=i]]\Nv������\��9��}p)��0/5�ڴ�����n�!��u��ܒh��'��iFDJG:��E(�~S@��<�p��T�:�g埌=V�����m�zd7�<ճ�p��P��\��KO9�,Gm�ӣE��h�1/�:FJ~�hV�����¡]TrW:�H���nDr7Ⰸ�+2��>����(?�}�5$�r��6g�z�L=Z]GY.��%� O=�.���"��]��8�n�Y����1��BVb�А%c !�����_�.�n�s�;F��V��l)>e8������Ŧ���r7��F)ռ���T���~y�Uu%q� �(�@�FV v+�'6�%BTP��M|� %b7ꂚh����X�qu�&��k[b� -�;�=��/n��|��;�7�w��;�?3sfjRP� �6����0;� ���2u�Q��.ݸ�Z�3x�ab�}K,&��?�oN�.�P�h�j0a��̇5I��!87����8jOfU�֝�i@�s@��;�����2�z�z�(y2�{rC{����B�� t -�}�;������`_|����j�?�����x)B��9>x���d�qM9��2y!��6�H/^��2�{��yq+6cAh����;4[�#�dRg���?���9�����˚�O�y�~V�����?��wa���hK#��lu�GrBQ:���r��c�Ա��X��pK�fhu��@f��&��8�m1_8ړ�;�Л�>�U�J^�!��.��Eg�����4�Ç��,=N��K=��3ea�Ĥԑ���i!�g�wI�$�3�o��?E�.����Hgo�7 -#����Ϣ��5`ҷ�Hn�b����Sp��?���_8:�G��$vgIz�qd�Y�>�o�n<�u�0�K4����9���^9�<�����E�K��Gg�Lc!/z�Eo�^�i!C�Nz1-3=�d��)f�)|���鑴�~�]JIG*�cs���0�oI�5k�����������(.�(���>"9�H�9�Ȃٟ>�ɡ_�qm2ߝ@��!m���q�1��:���^�P5Ҭc�:ڬ��j��#�c���(�"�b�#�� 4��N^�6��+�^Im�(۷�h0Cd(�w����S���ZH���@�$��N�������@�J�N /(nCo�����G�g����F�L M�x��2L��Y]|���l����]9p�~ M��)�IJ=S#L���>��R�ۀf�ɼs4�ŵ��C��*hQ��@��'nZ�q�_�O�˧K����h�ӸO�ȍt�$�ƒil:/O�O��!�y��6b�pE�������`�4�[�S.r�8YM���ך��uʴ0l��i35�������J�ɀ�����z1�o�_W�������c����g����/��R�b���ڟ؍r%�´L�V��?�ҿ��7����.�II��88,T��+Ŋ�(Y�̫�.e˕�X�r����V� �Z��ԭ��ݣa��M�y5o٪u����:t��ӹK�7����|�gp�ޡa}��G�8(&v��a��F�=6~��w&&&N�<uZ���ɳRޝ�:o�������l�_�V�\���5�����6lܴy�m�}�}��_������ǡ�G����ߜ:}滳�/\���?^�z��O7oe߹{��_<|�듧9�/+Z��� -Ȉ�Ȑ�����5�� �~]�]��k�����=������^G��e�z�&��A�$��da�4DK�H"F��I��p� ɘ"٘�%s%!�`d��$]r��|$YY-�H^>��쀙=�����L�9*�9!��~�I��`貤���6$ݗ,=�4=��5�S���s��u2(��ėaV�)Q����'�_��e*�)��/��P���cA#E�}�;9���R�K�!{�]`�l9��PF��� �����ȋ��d2�-�ޔ�D%����1 *:fȰ�QcƽcH�<mFrʜ�-Y�<mŪ(Y-�l&Bc�V)a����� ��@��P/_!�k� ����}��b�w����Ƀ�=zL�O�>}�,G�_M4�DM4�DM�H���o�.�� �/W�]�h��&�<��Į���[ٳ��|����@�C��s�ްs~���qu��������~�b�&��yr�;��#�R��2�_���={YHS���U�6u�g�c]|@�J��Ϩά�ڱ��k�N^�6?a�u;m���}'�4���)Ws�Y�o�5r���iA GT���ۖν?R��,�v±ӂ�*���� ���;*��u�]4O��8>���\�hFX%��ר�s���ĭ�n�* ���%���:�@�yd�&�}r��=�Mɱ�����7���*�ڡf�~DMJ��L��=��,^�h��{���E�n@�]�lU�w2�U�܌Jf@�mʀ"ͯh����0#�s� - �Ƞ&%�߯�=���GX��8����CjLj��D�nn��صd\L�����c�UE�)OU��S�_��v\��l����t�ǂ���P�A93��de��1J���)���V$��E��J��/��.*���)�I{��mTf�+}5��ު{Ў%�����>ߪ�7/_R�QM~�d�X��L���͘�ӣ�:�b?6�s�������ʢ�AyH�����³��'�y���T,-�h���|^T��j��vQJ�I��$[���v1�P�0�ƾ��.�m��E��������Ͽ�{eyT�Z��#l�M?�n]97)1��ҋ5��F|�}�=U�U��4Z�b>>��q(��b��V3'���q$���Y�`�`j弲(3W���"_e�+��K/"��~�F~��M@�E�9kp��48dm?dY��6˚�%�e�5 �P�Ck�JK���qݰ8���B4�M��ЭJY�j{l�����:u���/�}y������b�vzcʐ� g�X�Vne���o훣��_M����!/i -��=�eu�BT$@g��֪��⍝6�~�S���Җ�5�ڞni�ր�V�8k@_+@�5 � -d �i�<�t�i��]N���i �5�ᱱ#�z��:��{��3M�����i����~�����q�-c��E(T�t�R !ʾdO%)K�%ɖ�*��I!��"K�4������6K�3? 潿����\�\s�\�w���s��������_��&����S�������ݩ��֍� -7����=�`���% O�����g+�10�v5ۃ~�u�`5�,[��������o�:?�P G���$6� `��>����`�j�I� �)�c`wM�a��._�!4<"z�a�3GS�s��r�ע����~�Z���U�m�iB������~��ey���Zŀ]�qlu��-iU� �Z������]�V�lk�lB���)Sd#��"�6��c�`�n�X z�/�F�%���km���R|ׇE8�t��3�o^M����|�ߔ~��ㇳA�:%��%���w��m��#���?�``�{�4ȓ�Ў+�+�������IF@` A���@��C�[Z��7�P$��g_{�D ��D8܃6"H�F� -b)S.�L�E;�^��N��7��T�o��7��ꁵ -��c�ؽ̩���e<_!��ׯޗwi���5�/�A"���v���J�q�t���`���9l<Ђ�x+)��D`e=e�����`�G��C�#H�����S��#8�1D6�;�ijK$����������E����>|&��h�U�ׂ��+��Vh����gR��`�8��#W���6�/��Cp�Hp6�C.�:OAoR� ��I�1`K��r:��i��W�`�x!(/4��� -n2�3J[��LC�� ���l�*S��XD�b�\�7Q�]��~���<�p�)y�]��;���zw!���X��9㉤�}�Ci��_ž���� -N�֘KS)���L�Rp���tyȀt�^��(�b`��P��r -�ҔfQ�X��;v�f"�� =��I|�`^"������%.�?�@��!��!V6��T����r��J����������,�:����ʡ�q/�,�r�|v��|aU+ � �E<9�k��R������i#����# -v2P��T*PPܜ -��r��e�Y�����A0� ?���%�u(0@ �`�d@l�'Ww��r��:<6�B�)��E�_�y��~���ޮ�YC=�rR�"���� ��D��n�=?PT�N�3���9�9�S�#�L怮���c1��X�k2F�Ҍ8��ƵL��\}(( b1-�W�n@,A�@����%҇S��x2S[����56�?~V��t��s53����5���}�?�S~<s�H\���h�3":�tjհ����Ir߮U���cU���vpN -�6��,&`�DqП���H��!�(6��MP�p�\��r(�e���-��o�@� ��D��V�G ��| K�li9�qv�X7/���c��PSZ�8�7"8p��7�q�Çe�����MU&o;D66f)M��G<���;P��p ���x ='�;�p�k88H�I�$i+�mXE���[�fQ��,���[�OkM�l����AA�{i��F �j���kd�w�T��G2X���o=)R -4�_��/r�2��N�u�! � _1{��}�w1iݬA�Je���۶ee��k7UeɁ�H��� -`��j��Q�r�B�?����r`N@�.����Zm -<�oP�UHm�Wu �=̗�rG�C�8����էD[�o���D�!�]��Bw��e��kU�ݔ��c"Ã����.�{+3c������a��_�e�,l]������B�Bn���zLT���@"�T� 8xG�?d��j3p��LK�ӛ�{�v -��!XFA�#' -D������i��������mF��l�M�´���<x�h�/[�^���Á$2�m Z:����~�M��&�M�q���E�ehˑ��?5%)W��e9�͖ ~`� ���,' ��xp� ��h��Y/a�-� �����.A��4V -=�da醝����j��{�{�J.�\%�.�w6����?��O�|!����-�o�#aƞ5^.��V]LZ5��._�u��`�i����:�bIr�����$7�������6 M� �I5@�'J���d�������(ȚVN��mj W�v��zm�'.�z�������n��%!{�~f�o����;4�/V�O�{"B��ϛ�>i�X{��.������E��Z�#�\�/�����j@Fg5@�>QK3@�s>H����1z��DP���<�����y("��sVè��1����՚�����H�[���e���'S��+e�����<{x7-�b��'N'�#�����x��1�Ow3Sc}�ƍ��Ӹa�Rt]ܖ�s �ꎗ��v���X��Rv5f`*N@�t�t�E@�$q�A -^,�%OK5��J�8��e����q��+�FW�ٷ66 ��\G����.e)�%�&����4����#�vF|�1(p� -?���q�O瑶=�L�Z4�]�lI��#p�[W�!!|�"�W��P��y�B@��^a -���Ű�-��Րe�-��������&����&��[�PN@�2(�wõ��@p)�)"�~ʚcO�H���]����%9 -F�����?�}��уO^A�zyp��:��� -�Ӓ�R��'�(8�ko��$%̍�G�(xL(�7�P���Cj�R$���( -~5��J�B�c�=�,�nx�:������eW�țv�5F0δJr��f�l��!��t�²gn���4웶���8$G�(���G���;y���=��8q��wT��S�kJr���}=��<�c -�(X栯"%�ń�Ag�(8���iBQ�]Z�`� �,�m�TǢ������_A���U���yl�ƻ��.(�)g\4m/�.Ľ-���ɛ.�& {���=|�,���0�`����ٶq���K/Y�f�sw��%YIѡ��NvVf&�V��E� -f�H r0"�pa{T��&Ӵ)�P<���K�9-<�ݕ� ���K.b���,`?HP�9�y�%����ks�䚙;�� -�c ]�v]��K�>�#9 -F�H�~}���ӗ/؛�7�X4o��i�g̚�d���gn���������`ca��m�0��/4��zH̦�ӏ�w���?�� ��wəq������:��-���~���m�s:��x�˓W����7�|�#; -F�( |����gO�>�����e���S�O�5w�S��)������Y<��+�Vla�������97�˷OjXt�)NG��߲��K|�xrg�2 -?``0�`�&!; -endstream endobj 9 0 obj <</LastModified(D:20170330061653+10'00')/Private 18 0 R>> endobj 18 0 obj <</AIMetaData 19 0 R/AIPrivateData1 20 0 R/AIPrivateData10 21 0 R/AIPrivateData11 22 0 R/AIPrivateData2 23 0 R/AIPrivateData3 24 0 R/AIPrivateData4 25 0 R/AIPrivateData5 26 0 R/AIPrivateData6 27 0 R/AIPrivateData7 28 0 R/AIPrivateData8 29 0 R/AIPrivateData9 30 0 R/ContainerVersion 11/CreatorVersion 16/NumBlock 11/RoundtripStreamType 1/RoundtripVersion 16>> endobj 19 0 obj <</Length 1018>>stream -%!PS-Adobe-3.0 -%%Creator: Adobe Illustrator(R) 16.0 -%%AI8_CreatorVersion: 16.0.3 -%%For: (Eiji Shinoda) () -%%Title: (名称未設定-1) -%%CreationDate: 3/30/2017 6:16 AM -%%Canvassize: 16383 -%%BoundingBox: 8 -184 248 -72 -%%HiResBoundingBox: 8 -184 248 -72 -%%DocumentProcessColors: Black -%AI5_FileFormat 12.0 -%AI12_BuildNumber: 691 -%AI3_ColorUsage: Color -%AI7_ImageSettings: 0 -%%CMYKProcessColor: 1 1 1 1 ([レジストレーション]) -%AI3_Cropmarks: 0 -256 256 0 -%AI3_TemplateBox: 128.5 -128.5 128.5 -128.5 -%AI3_TileBox: -161.4399 -540.5596 416.96 284.5601 -%AI3_DocumentPreview: None -%AI5_ArtSize: 14400 14400 -%AI5_RulerUnits: 6 -%AI9_ColorModel: 2 -%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 -%AI5_TargetResolution: 800 -%AI5_NumLayers: 1 -%AI9_OpenToView: -544 393 1 1345 924 18 0 0 315 117 0 1 0 1 1 0 1 1 0 1 -%AI5_OpenViewLayers: 7 -%%PageOrigin:-178 -524 -%AI7_GridSettings: 64 8 64 8 0 0 0.415686 0.415686 0.415686 0.707843 0.707843 0.707843 -%AI9_Flatten: 1 -%AI12_CMSettings: 00.MS -%%EndComments - -endstream endobj 20 0 obj <</Length 9777>>stream -%%BoundingBox: 8 -184 248 -72 -%%HiResBoundingBox: 8 -184 248 -72 -%AI7_Thumbnail: 128 60 8 -%%BeginData: 9644 Hex Bytes -%0000330000660000990000CC0033000033330033660033990033CC0033FF -%0066000066330066660066990066CC0066FF009900009933009966009999 -%0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 -%00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 -%3333663333993333CC3333FF3366003366333366663366993366CC3366FF -%3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 -%33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 -%6600666600996600CC6600FF6633006633336633666633996633CC6633FF -%6666006666336666666666996666CC6666FF669900669933669966669999 -%6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 -%66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF -%9933009933339933669933999933CC9933FF996600996633996666996699 -%9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 -%99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF -%CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 -%CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 -%CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF -%CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC -%FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 -%FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 -%FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 -%000011111111220000002200000022222222440000004400000044444444 -%550000005500000055555555770000007700000077777777880000008800 -%000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB -%DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF -%00FF0000FFFFFF0000FF00FFFFFF00FFFFFF -%524C45FD21FFA87D52272027204B52A8FD2AFF847D2727202727527DFD42 -%FF5227F826F827F826F827F8277DFD26FF7D2027F826F827F826F827F87D -%A8FD3DFFA82627F8272027F8272027F827202727FD23FFA8512027F82720 -%27F8272027F82720277DFD3BFF7DF827F826F827F826F827F826F827F826 -%F8A8FD20FF7D26F827F826F827F826F827F826F827F8267DFD39FFA82027 -%2027202720272027202720272027202720A8FD1EFFA82720272027202720 -%27202720272027202720277DFD37FFA9F826F827F826F827F826F827F826 -%F827F826F82727FD1DFFA827F827F827F826F827F826F827F826F827F827 -%F827A8FD36FF27272027F8272027F8272027F8272027F8272027F82752FD -%1CFF7DF8272027F8272027F8272027F8272027F8272027F852FD35FF7D26 -%F827F826F827F826F827F826F827F826F827F826F827A8FD1BFFF826F827 -%F826F827F826F827F826F827F826F827F826F8A8FD34FF52202720272027 -%20272027202720272027202720272027207DFD1AFF7D2720272027202720 -%27202720272027202720272027202752FD34FF2026F827F826F827F826F8 -%27F826F827F826F827F826F82727FD1AFF52F826F827F826F827F826F827 -%F826F827F826F827F826F827A8FD32FFA8272027F8272027F8272027F827 -%2027F8272027F8272027F827FD1AFF20272027F8272027F8272027F82720 -%27F8272027F8272027F8A8FD32FF84F827F826F827F826F827F826F827F8 -%26F827F826F827F826F8A8FD18FFA826F827F826F827F826F827F826F827 -%F826F827F826F827F8267DFD32FF7D272027202720272027202720272027 -%20272027202720272027A8FD18FFA8202720272027202720272027202720 -%27202720272027202720A8FD32FF7DF827F826F827F826F827F826F827F8 -%26F827F826F827F826F8A8FD18FFA826F827F826F827F826F827F826F827 -%F826F827F826F827F8267DFD32FFA827F8272027F8272027F8272027F827 -%2027F8272027F8272027A8FD19FF2027F8272027F8272027F8272027F827 -%2027F8272027F82720A8FD32FFA8F826F827F826F827F826F827F826F827 -%F826F827F826F82726FD19FFA827F826F827F826F827F826F827F826F827 -%F826F827F826F827A8FD33FF522027202720272027202720272027202720 -%27202720272052FD1AFF5227202720272027202720272027202720272027 -%2027202727FD34FF5226F827F826F827F826F827F826F827F826F827F826 -%F8277DFD1AFFA8F826F827F826F827F826F827F826F827F826F827F826F8 -%7DFD35FF2027F8272027F8272027F8272027F8272027F827202727FD1CFF -%522027F8272027F8272027F8272027F8272027F8272027A8FD35FF7DF826 -%F827F826F827F826F827F826F827F826F827F8A8FD1CFF7D27F826F827F8 -%26F827F826F827F826F827F826F82752FD36FF5227202720272027202720 -%27202720272027202720277DFD1CFFA82027202720272027202720272027 -%202720272027207DFD35FF7D27F826F827F826F827F826F827F826F827F8 -%26F827F826A8FD1AFFA8F827F826F827F826F827F826F827F826F827F826 -%F827F87DFD33FFA827F8272027F8272027F8272027F8272027F8272027F8 -%272027A8FD19FF2727F8272027F8272027F8272027F8272027F8272027F8 -%2720A8FD31FFA827F826F827F826F827F826F827F826F827F826F827F826 -%F827F852FD18FF2727F826F827F826F827F826F827F826F827F826F827F8 -%26F82727FD31FF7D20272027202720272027202720272027202720272027 -%20272027207DFD16FF7D2720272027202720272027202720272027202720 -%2720272027202752FD2FFF7DF827F826F827F826F8277DA87D7D527D7DA8 -%7D26F827F826F827F826F8A8FD14FFA826F827F826F827F826F8A8A87D52 -%7D527D84A8F826F827F826F827F8267DFD2EFF2027F8272027F82720277D -%FD09FF5227F8272027F827202727FD13FFA8272027F8272027F827207DFD -%09FFA82027F8272027F8272027A8FD2CFF2727F826F827F826F82752FD0B -%FF2726F827F826F827F82627FD12FF52F827F826F827F826F84BFD0BFF52 -%F826F827F826F827F827A8FD2AFF5227202720272027202727FD0DFF2027 -%20272027202720277DFD10FF7D20272027202720272027A8FD0CFF4B2027 -%202720272027207DFD29FF7D26F827F826F827F826F8A8FD0DFFA8F827F8 -%26F827F826F827A8FD0EFFA8F826F827F826F827F8267DFD0DFFA826F827 -%F826F827F826F87DFD27FFA8272027F8272027F8272084FD0FFF7DF82720 -%27F8272027F827FD0EFF27272027F8272027F82752FD0FFF7D27F8272027 -%F8272027F8FD26FFA827F827F826F827F826F852FD11FF27F827F826F827 -%F826F852FD0CFF5226F827F826F827F82627FD11FF5226F827F826F827F8 -%2627FD25FF7D20272027202720272052FD12FFA827202720272027202720 -%7DFD0AFF7D27202720272027202720FD13FF2727202720272027202752FD -%23FF7DF826F827F826F827F826A8FD13FF7D27F826F827F826F827F8A8FD -%08FFA827F826F827F826F827F8A8FD13FFA8F827F826F827F826F8277DFD -%19FFA87D7D275227527D7DF8272027F8272027F8277DFD15FF52272027F8 -%272027F827277D52522752277D7D27F8272027F8272027F87DFD15FF84F8 -%272027F8272027F8277D7D27522752527DA8FD0EFF7D52F826F827F826F8 -%27F826F827F826F827F82652FD17FF2727F826F827F826F827F826F827F8 -%26F827F826F827F826F827F827A8FD16FF52F827F826F827F826F827F826 -%F827F826F82727A8FD0BFF52272027202720272027202720272027202720 -%2727FD19FF20272027202720272027202720272027202720272027202720 -%27A8FD18FF52202720272027202720272027202720272027207DFD09FF27 -%26F827F826F827F826F827F826F827F826F827F8A8FD19FFA8F826F827F8 -%26F827F826F827F826F827F826F827F826F8277DFD19FFA827F826F827F8 -%26F827F826F827F826F827F826F852AFFD06FF27272027F8272027F82720 -%27F8272027F8272027F8A8FD1BFF7D2027F8272027F8272027F8272027F8 -%272027F827202752FD1BFF7D272027F8272027F8272027F8272027F82720 -%27F852FD05FF5226F827F826F827F826F827F826F827F826F827F852FD1C -%FFA827F826F827F826F827F826F827F826F827F826F82727FD1DFF5227F8 -%26F827F826F827F826F827F826F827F826F852FFFFFFA827202720272027 -%202720272027202720272027202727FD1DFF272720272027202720272027 -%20272027202720272027A8FD1CFF52202720272027202720272027202720 -%272027202720A8FFFF52F826F827F826F827F826F827F826F827F826F827 -%F8267DFD1BFF5227F826F827F826F827F826F827F826F827F826F827F852 -%FD1BFF7DF827F826F827F826F827F826F827F826F827F826F82727FFA8F8 -%272027F8272027F8272027F8272027F8272027F8272052FD1AFFA827F827 -%2027F8272027F8272027F8272027F8272027F82720FD1BFF2727F8272027 -%F8272027F8272027F8272027F8272027F827A85226F827F826F827F826F8 -%27F826F827F826F827F826F827F8A8FD19FFA8F826F827F826F827F826F8 -%27F826F827F826F827F826F8277DFD19FFA827F826F827F826F827F826F8 -%27F826F827F826F827F826F87D5220272027202720272027202720272027 -%20272027202720277DFD19FF762720272027202720272027202720272027 -%202720272027207DFD19FFA8202720272027202720272027202720272027 -%20272027202727F826F827F826F827F826F827F826F827F826F827F826F8 -%27F87DFD19FF52F826F827F826F827F826F827F826F827F826F827F826F8 -%2752FD19FF5227F826F827F826F827F826F827F826F827F826F827F826F8 -%27272027F8272027F8272027F8272027F8272027F8272027F8277DFD19FF -%27272027F8272027F8272027F8272027F8272027F8272027F852FD19FF7D -%F8272027F8272027F8272027F8272027F8272027F8272027F8F827F826F8 -%27F826F827F826F827F826F827F826F827F826F87DFD19FF51F827F826F8 -%27F826F827F826F827F826F827F826F827F82627FD19FF5226F827F826F8 -%27F826F827F826F827F826F827F826F827F8262720272027202720272027 -%20272027202720272027202720277DFD19FF522720272027202720272027 -%2027202720272027202720272052FD19FF7D202720272027202720272027 -%202720272027202720272027202727F826F827F826F827F826F827F826F8 -%27F826F827F826F8A8FD19FF7DF827F826F827F826F827F826F827F826F8 -%27F826F827F82652FD19FF7D26F827F826F827F826F827F826F827F826F8 -%27F826F827F8527DF8272027F8272027F8272027F8272027F8272027F827 -%2027A8FD19FF7D27F8272027F8272027F8272027F8272027F8272027F827 -%20A8FD1AFF2027F8272027F8272027F8272027F8272027F8272027F8277D -%A826F827F826F827F826F827F826F827F826F827F826F82727FD1BFF2026 -%F827F826F827F826F827F826F827F826F827F826F827A8FD1AFF52F826F8 -%27F826F827F826F827F826F827F826F827F826F8A8FF5227202720272027 -%202720272027202720272027202720A8FD1BFF7D20272027202720272027 -%2027202720272027202720277DFD1BFF7D27202720272027202720272027 -%20272027202720272052FFFFA8F827F826F827F826F827F826F827F826F8 -%27F827F852FD1CFFA827F827F826F827F826F827F826F827F826F827F826 -%F8FD1DFF2726F827F826F827F826F827F826F827F826F827F826A8FFFFFF -%7DF8272027F8272027F8272027F8272027F8272027A8FD1DFF7D27F82720 -%27F8272027F8272027F8272027F82720A8FD1EFF2027F8272027F8272027 -%F8272027F8272027F8277DFD05FF52F827F826F827F826F827F826F827F8 -%26F8277DFD1FFF5226F827F826F827F826F827F826F827F826F87DFD1FFF -%A8F826F827F826F827F826F827F826F827F82627FD07FF52202720272027 -%202720272027202720277DFD21FF52272027202720272027202720272027 -%207DFD21FFA82027202720272027202720272027202752FD09FF7DF826F8 -%27F826F827F826F827F827A8FD23FF7D27F826F827F826F827F826F82720 -%A8FD23FFA82727F826F827F826F827F826F8277DFD0CFF5227F8272027F8 -%27202727A8FD26FFA87D2727F8272027F82720277DFD27FF7D522027F827 -%2027F827267DA8FD0EFFA8A8FD05527DA8FD2BFF7D7DFD0552A8A8FD29FF -%A8A87D522752527D7DFD08FFFF -%%EndData - -endstream endobj 21 0 obj <</Length 65536>>stream -�(�'R����5�� y���}�1�GS�}�p�Q|����ٹ������)Q�T`�d|�����&���XI�N�@�+T��t��*3?��>�S�8�m@v-���+��TV��H����ի��ߧ��`i���_w|/���l�-�-r���|EE��XrF�N��������Z���I���z�*9ҷ�C��6�1�3b��DV�D����\��@!�Q����i��E�v��Ƀ6t7s�q��<ܼ�%R��9�Xo�4���l�bO)����lv�p+�QX�34h��zQ�J�`4���:J���~��N�P�P�+?2wkX���0��!���=��qz��1A�|@|+� �������$��"�����*�j�G�ꥼx*m~zFV��)v��H�[�/T4^�������!�nF�3>l{W�\��-�������*R���H��7L��@�,P�#P=�ѻZ�d�6Y�/C�{�_د��By7In-3Oqp4�|��,sh����xM���q ��^�IJV�����RĉQ�����xK��öM�S�mSl緤�J�����Jd�*t'z��1w�����^e )`o�i���$a���N+�w�]`��Ԥ{�A�������0_>ib�1��f���}���x�����Xho���r�g�����M�ƛn�e��?��|ysv -�Xh�K\8��&wV����§y����}�e�̒�Es�4$?Z8h2�=�M�[��C�j}爓��l����E����B���OI�u��Lւ�2�� 2�#���D��oP7\_���iˬDJ�q� -Y����w?]����>M�Zz�U���~'}��=6���^o��ږmM�ێ��nN� ��G��ZI�z�utL��ج0�`��91�փYj����/�?���J=v��i\r*>�穼y$�D�v�!�e�������Z����حK6�M���u���O����`_�b�\'���iq�/��jYdZ+ȸ���M�^"���������#�����(=�Ȯ�Z(c|@K�)T����d�����e_ś�fs0Ho-������J�?�+�b�+0K� -�^q�E�'����yͅ^3����/@uG�Y3ġ��+�);���/4����>�yr����)C��9���=�'[�@�u|�S��~E���tnam���A�y͙�fT{Қ�ܰ;e���$���i<_�cu���9>v�H�6�h���(g��pY1���$����?P��Ŕ�me�Vs��a:z�w5c@l��w�r2-,���k�H���˜���},N���3�hV$���٦���/�`]}�=���֠�/{�UF��^-�պ�h��6��n�U�Q���?��� ��m��w$�RⰮ�&@�� -�\�J�����dL˽�� ��>w|�d�C/��v�� -��8������[���ʎ�#�衃(i�[���'��������G���q -�J��i��Jm�P�H�o���o��:����yPjlP�20(u�(U?����D�sh�J��߰c�D�� �e�興�b�4�K�b�{����ݗzW��f�?@��O8�)I��R{8�Hv��Z�D%)��\M<YF@�-�A �@i&)�-t^�̃��4���aF��z(a�sِnֵ�XAM?\�/l��(�IY��2{�v������x�$M@P�� �R��b�2qRs�,��g�2�.�ҷ����9W�;-z5�������#!}�a�O�l����p���'oB�{c��j��yj�����]�&���4�����x�H��4���#y�C��n�&���� -ʉS�X�߿�y�z6�� �������_�Ap�h��GY.�|��l�a�g=iS���~&ڲ�~"��Uc����;D�?���-���%�) _�[����[AI��<,�|V��A��U)�CDT�"-��#�����]ԓA��6݉��r[w�+�n��7_��m����{�[&���|����ML_�D*��-���T��D��e�}��G�U���> %-���bN����v�K��� -�^�h� �*gà����n��_�,�z��w=ܿwbx��;����K$��s`q����T2�\���(����*��5��0�Z��0�T#]]�i���~:VN�]����x��p���Fz���ͩ{��H7�1-U���B�w�K7��#|�ɻ��|e�I��4����OVo��dzsP7iP�%#�L>��\���v�=����������BA��B�.���Mf��-�(�E��6/�)m�}F7Jk�~�g"�Z�{��u���K�߸��~��\"U|'R�L�w�l=7�������D���gq�ks����ޮ�tZ{vճy�`��3.�ql�D��Q�u[�=�]^�ls!gf���+�S�]��9��hq�3��������k�A�4�;[���Ae�vc���r���r��g7���ZO�6�|T�H��a�5��P\p���ٝ��s���u:gHs-��m4�(�,-o��ɹ���՜\�R���c!��d�I_J���8Ht���(�4}4��U�Ӝ��m�;9��[�g��c�� Mp�Q�s��3g��zzT�Z|����]S��h���퍲P��|-<��~)��u�u�J+���{�Ҹ�^?I��c��Q���xl7�Y&e?�{mgo!��n�m�s���4wܨ���!���sn��?_Zḟj���*�V���##Oy!��+>fmM�d�O��"���[����3���V���!"͖�/#������.�-�m�4�)XWa24��]ޯ/s�=ג&ݟ��R�]Y�IU�Y �rߢTB�N"�4ǂ���V�.� No� -TT�����]��F��g�[f��;��Ƨ�����u�����z��+�l�3������ܦ��M��/�\B�n�Y�Ί��[}�K�֧��I�p�0���qx��(�n���JL���z�JTdyD4�s��-k;�=>���J���q���KwQ��ߩ����C#��,�W�E|�������=�=��.rffm�֊�M��@�+�N�Z���刺>Hn̿�e�k"�c���?ڼ-�O�|d*����a�^��]��:���6[Ͷ�Ri�<o��45�lo<W�O|Ŝ�e .[���/0�f�c���J��*�� v� �j&U�(�F��4����\Z:�|�{� ���A6[�K��jzƲ�jq�b2��I�&�'� 8��o:�i�Ͻ�)b��E��gW�g�1��� E��DM���σŲ=���?��Y�N�-K7Pz�9Piǭ�/�8�v{�.���`������P�p#T=���|� TWW{�5������}9���ǃ��Oc��/S�zOxC�����tez/�w�ng�?�@��D�Ը�̞P&���6�^���qe�y�J�cm/��h^��9^��]�Ee������M/��0g2��f*�諳k\�|���Z0�Uߠ�K���D$[�l��C�H�en�D�pQ���㯳*�r������<���i� ��W�sNG}��<U���˵��.vPi�����]j�q��q,�}(��g ���^��LJp2)�!��c��a�uOq�I1�z�/e��I��V.~~��=��}nn&����u�أU�ְ�g��Y�R��ޱ�� -�#Fs;�~��]4�}HT��^Ȟ>{��8$&� -ګ1��ϥ�z_h+�~�LMb�z?R���/�Ŀ�+�e��$��Y��0żK��B�P�C��j���Rm~g�=H��)O���/L�ǝ��-�I����#j�R��{����k�X�!��ƭ�,�F�o5WH�筹�������j"5�P1�!�c����4EA��ܻvMn|�[���1;�' �S��K -]g�Ґ�!mE����0i"G"���}Ѹ�;�n����s3hm�������%���W���!�5��$i���Q:��m��eͻĵ��i=�FOJ��R��:B|��xe��+ݿ˯�i>i�:���w��zU���>M��m�S ߛ���.c�I�� ���M�a6�ά$m:ݝ�a����J��v��|[�<�J����-}�� �BѬxj��b����=�&�[�<.��rz���g�Q"V�Bg�)|f�����ʋ�:_�xLdm�a5X�<���Qv�|��7T_v]��ʽ������ ���.��o��~|�tny�������rM�k��ǩ }��+���/��Ϯ��a�l&�����(��pX�XGV�U6��y�tT�<�X.3�cY���i��?7v��Z�[f2;K�R1�/L���^���l�t�!uH�q�� �[5�����u���������=�똧��ч+b_YK��1e��b9ݟ��Uu��� ��n��r]�{K�I#K�՝&�~��i��!�������n�qpa�.`��@uH����^�=0��Sr�����,0�+5ql���+}��WKh��; \�� {,��/���?�+4'������G3��mfT�O��Y3^Y 6����͑zv�ѝ�_���)��u���NJt�.1 -�1}1e��V˼�.��3�@-��9FoNԝq���bF��u���~ډCzH�t�z��@�:k{<��h����Ѭ�j�I1?Wm�,FIǣS�Z���v���v�:ש�� ŕ>U�%b��c�z̨Vϙv"؛�F�x+��x�G�#eG9��-�XЏ\�'��m ���62��β��x�o��-jb���{�U���9ڡ� \% -�&������tS���^�f6Y�'��>�ʼnΌf� ��L$�L��͖�{$#B��F�f̎+��N7�=ۧ��n�כ��N��ޜOsV}e��WTl,[�JR�����Г�����j(V/��.y��t He,1z{&H�D���S��ٟ#�fxJ�_x/���(��4����������>��K:$����0�st�3�{��@!��[�0N��_O�O^��.@F] ���&��<@�i F*�, Vb�H��@�H@� @'��4�=F(Ք�!��]6pWE6�H7�&*�b�{�b^��[�D�;��X�2���r��lE!Hh_s�:�A���� �K��U*A2�!�[r��x���p�&P�1|�B��{rȠ�8�j�9���G/��'.v�r���/�H=%Uz~$R_59y�=7��jR#<������)��^.@O#8���2Z�7��Ƹ<O���Jgw��Dq�F�F���^7���2`���GƋ������~���U�6��� ���Ŋ�Wx�l�FE�f�d�*��tg�zߪ��+ �2@��(�8�M��|�Q]�j�3�>B�ak��^I(�v��A ��j� �7��Ǥ����3��^��.V^G�����z��� �y$5�;R����lP:���r�����^�*ѫȍB�߯�v�9�(hAד~�3�/y B��]O�qwF�T"l�;��Vq�!��7��� -��^���A�8���g�.iq�'���?c�Vn5���Y��O��]���E����� �]�k���G�S�TO"˩��e�r��V�����^��K��qЁBە�(���<��W����8���=TY���E�����i �c��4����L�Z�"Q�g���`D�AB}���=��֮6d�0]+���;�4�����;hX�+��:d��'9�=��?..X����ʖ�x�5�:�U(?��Y;�ڃ���`�^ԫ4AI���\Uz_����/��j�^����˧�tL=C�����^Ճ�|#���=�}��g݇�E�&��R7��</4�,^_'c��JFi���R����C�$����@Q ��jW��z���z[�(%�*�� �}Y��7�q�Qx��J�~�E�̭��9�GuN�M�X�[s���Lx>.o��5�V+q9��ӥ����=��3�Ԏgfdҿ(Z�����~���N��%j���hQ�����Ba�|�a�k��������q��,����%z��5�R�k����L|��%_:������]kQ}�V�&��&�ʢ6j �6Zu���/�pfq�+�uդ��l�ulv����k������W�4�8�Q\�o��X��wfX��.}!?y��ܝ��Ƿ��o=CM���s-��if5W�^�Mh��I�4QT4��=k��B���'��~���鯬�b@��wcb9Z�ٻ�B���g�˫�U�{�`,��l��2�8tuf�����&���B�;o�T�͝|��/���җL���LWe%m��5�q��^ӿGR��5y�;��`���F�Q^����yoW�n��mUl�`c�=*���]p�i`��j��\��%|��uN�K���2@��i���� v��V�X;�X'� -�{�-U����%�L�5�3��+̨�S����i���Eh���~��6" -9�N^K�dcU]�+e����Z�'��X����b=;u��gx��k<[j.���xv��y�c�x~2�������H"�6�-�I-&R����������|t7w���9W{��2��Q��P5�=PV�k!���V��5R�K�44$P��g/��fr��t\��8���Huv�Q:�N��t��<�{����@���P�*�o����.�����'�9�-�4���~qo���x=��JtU��OW.��T��ւc���#�})�i`�wN�_C.3�Q�R�c�ko�*���'���7)N��#�K�@��߮��V1�ﭦ�!hö'j둙�Fz���Vϧik -���*����-�nK��v$��В�[�����4���e�c�7g.{ޞl�\�0��aWU�a�j���K����L��D*+��{?�����XYE_,�CoE��1�rvs9��.5�Uw��l� X�����������I�����P����6��A/��6S����y6�j���o�2�l.�1�>�{������:��|_�^�����a������Ǽ�d�Jk��<�1'm>�Y�&ˆ��!���j�i�uO��>��9B�ʁ�A�1%����<(�TT�N���]P��MR/�I���~d��G2B�ɿ2�K�%m��O���m�����5�|���L� -SF���"W�kW�g�4:ȇ�}�4��6�/��Gȝ����P�1��2�nw����Ξ��5�8�D��È#�.��?5"�J@.DT6x��{����bt��"��O��P�X�A�M|S[����Iz`(���k`Ԁ�������j-��K?��AO2 Q�"����ћ��>K"������{$��6�L5�9��U��)E�&-��ϲ�O<�meU�Mu��R�HM�����d�q�S`���T�G�� y�{�B:J�^��>��n�»�+��V5v��?:���<M+���K$ݞ!A��\A��_��~���3����s�O77FE�S �k�*z��B���I�d��.���i��m�.QLH4�>��X9��������.^.���.v��;���m}��&�e��/���ʜ�k�� -Oiqur쯨��n쥖�<�P�^��4�GC~*l�O��7b2ĭ�A��*��f��R�Lwk �q\̳�g���7����6��J��6��ol��5 0�G�@�t���ջ��Sc�� -��H�,�����q�S���8$����cH�� {�HA�x�\��OPڙ'���%q�}q�zۈv��<�k_��kn�(�{�t��I�n}|������?HC����@i8g_�.���r���etYH��h�g;Z�ɓ%�̴r�.�V��M��lݖ��qۣ�Bz����f�� (m[��a�x���l�<&�� �j,3���立^}�<���4�{ܚ����l���O�O�c�T���ϴNcv�]�i���(�/#d榵��=��{���wOy�.G�Ư�ߛ}ּ�-b�e������n��j�ҡsU8���e��,1x�Y�Lg�\1���x������}=CnGr���"����٥��V9�ߗG�(]G��+�:N:�-#Og�>�߬Gt�Ĕ��M5R_�s[A��.��;X����������A�s���^�gg^vh���b����b:�(��������#�H���H���j��e�Ϧ�� �+�XN4�W�,�>ZX�x�(?g�7�9ј�7~�Q"�d@�N���1�o8 �qq2�Q���8>c�X\����ܿ�:�f���LSF�h�n�� �v�w����n=�2�J�z�r]t�E�n��}S�x�N;S���N�_�:grs�=������fC}믇�3{\�� ���ǻ�O����C]�=�-;�+C�ɐ�»mIɯ�b��Zf��G���&��6Ӗ6�eH}��6�]���=��u�>�y��eWy����wm�u��ӷ�N�|;�%�}Z>���-q[�4��:�T��/$����Ј�x�5�}m�;�ǾX������(�'���)su0��ݤo&��)��� ��Rv�I�l -w�%b��a���k,[�~e0���^��Q}H�G���:j���u~P��(��5(��$��@�����oP�s(�(�%�+3�]��|&Ћ�^ ��$��I�I<�eOJp4���I66G4.m�V�{����sluؠW���v�o�S�~�D�s - -m��������z�x7a���]�w��N���B�{�c٨6 -`��I���W/%���R����bo�a!�oW����;!Y?���{{���R��j���B�x��/��i&�j�4�.}�F��R�(a���JR_�"�$�|�3�_�2@��~y��CƦ^���r�'�����nA����7�a�8���p�s����C��]��7j�{�(8����P��_%���n~�l��j�oc���ȕ�Ɯv �f=���Q�ոu\^ܩ�}�h/��6>C;{CR{��;�4� ��^\�L���p��+�t��0rs��{$3sP`�-�V�4�z1���?�*@�� ��]V�^lQA!.�?9PQ+���=݄TbŇMu/�\>l�� -:b6�1���#�r���Н*���� ��s��M���iTzM:Q>�*�q�����|��弓��:�����Z�v�]b�����]!u����FA�ka~�(���䗔/�8뿼Q�� �P7���7tr'B?�6���v�,_Dߚ���u������P]�»�'/J������!�&t;��xڜX�TC� --Ρs������~��jH�qo,wW�t o�X^�� �zC�;����M��" -��I������'�o�~��=o��6����K�hXY$X�?Jz���r��m�z�22D z���}: �e�N�����O����t�ށ�o�oWF�E(��U���#���f�������h,�s�=7�Ncb�B?E��:W�%�x6 ���V#����[�J�x�I��G�C����-8��1��}GK�ׁϟ�V��%���T�I��~�P��5�qi0�6�߸m����G��Zyq��S`&�FR�~ ����PIt�/�F�}� �Z���NY$�����:���r��z�"Y���M�q�Pv+3��$�� -�)�8��~��u��˓�����^?�݁�Ĺ~$܅�[�G|Ӊ ����6KP�>�_} �9�& ��S�N���S�����E��x~�����N�k� -11���ƶ���[^^m�4�*{��gE�37K��B_� ���U%-3U'�l�N��T�r� E��H��l��0���u�����!��x'�[����xs�r �����},goV��"���>8W.�ɤ}f���έ���6|e9UF`]����F�������7��ʫ���143��qe�b�K$R�I�Z_ ��%y���L�<��q��N��r|�=_�K|�h�j����_�b���NE��X���5���dfj&�ʢ���AJ2l�7i[�"�V�}�����\�?��1�g:t�\���\�����6:�&/~���WA��T]������>�Qua]Jiܼ�zqd�q��H�w^��\� ��;O[ -Ĕ� -z +i+{��}Ob��\�����&�<�ovy7̈́&3�My3����H������`g��Aj!������T-���~Ӗ���֦��!��Q_[�OU�\FY�;����5 ��]�A�3����BS`�q��s�N'�.p1�7O�+39�j��)����_"��N��Z��-͟������vUpv�W�!��M��f�Z����('}hT.��Rme���F�Re�����h�q9�Yr���K&}s&q��t�)�{T*���6Gܸe.8Q�ƜhdG?HC������'�Z���*%RI*�쑗���j7�U|�4;o��A��dS8�-m�M�4y�WC�Zz��z̖~�ݻV�� -h�4`3��g�8*f�?Φc��u%f�Zc�+{x����q�^�)F�(V�Dg̱�ݪ?�ݥ��rn���'��?-d��^m�T?v�}��.��*���.U���5����1| -_�L��eN��q朷��~>2䝙�1K�.ҷ쳜�El�Y;�x!')F�(�N@j�����A������)���Ǯ]X|\�Ey�f?h9�B(� ����0�f���'��8�㔏j��ٳq3���4>0���jԓ˻ԡ&���`У��{J��显���~~�=EVI��_@?^"u~�}��C�������j\=�CD���@12!.W��|Ⴥl�oq�?.:g�'��xh2��|H���:x=�|W7 -I�<��+8萴�O���ƹ�k���XD�NI_�$:����'�~ws[T���P2������j�v:�z�Dg�: -�M��D�|p�j38B��a����d^�^q�K���TK�CiDdIOB�M�%X�0w�Dt���v�����n�X��K<H��x���yzf����5&[=s -Ű�y�� ���%�BoX%O����yfk�d�en9��)P�~X!�u�s8u?Bv�/�����x���F~�7����^N�_�#��7�_��Abn�\�F���K�',�'צ����H�)fa��U��̨T�������=7�5�5��Sr/�QW�]��Pl�;'�7���3����f�|�9�h-|��8:����'���:N1�%��WI�p.4UV�y�4���ˏe��H����Dn_��C�s��ij���a���0Lm�Q�w�}�#h �i���Q��[}K|�V��p�n��an�,�* M��Vw�d{�X��_"i�� (6�#�&}����Χ�W�xm ��k��7q��/W,��G���2O�IWH��e�����Ap&Ny�u���]��yy�`��m��[n�o�^�l�\��0�f������M�ˍR��b~������J}�<1;�8�$/��U����V�s�~�1[y��@�.ݞ0u|�{�m��!B��W����o��l����6�R��Ħݕ��#�̰�[�0^��ذ��aê���Pg�����(5��=֣�����q��UJu�'p�8�p��hю���c�� �>���x<����)����qk������M����}d1�W,����p5CD|��Ք��>K���.��=���:������뚛�<���K�#��q�Ӈ��L -ՃL�]b9��=ڷ�]�F�x]��-{���9���:T|=,\L�, ˸���w��*5?�������cg�(��ڢ��d��2�m -��#u -��m��)@Mu�[m���N��Zg��O�0���x�U�6~om�#L�`�(b��*��v2��GK$_��yL,� ���ߢ:�M�43ef�s�����nCoM�o�v��i�Q�$�E�IP�¿ĵ1�kڬ�hI�,�>�O���7z��n����+�cy -�6e�w3�������̉z6?�Zd�r���Wۓ@��'��p:��I���a?�^��H�m���ϡ��?�e�)���h9P�5*�v�_B�n� y7!Z��bԘ�B㇝ū"0�7+� ��,�7���$�O�.�NN��u<�������(g,��4 �����̊��c-�jѺѵq�m~�E����N�����E0���&~~ ����|�uv��A��|�o��^�+(2k�[�I�#e·K��+�}��>zD��t����q����fWr{��Ԗ�n���[�lΪ�4?���C�~e{Q �>ܣ���ҪS\%f��/!��L���0J�n�5uo%G�8��Q�L�?���(�#��ی�j;P�F+>?�ͯ�����2l,�Ĩ�n��5�_�T":�� -XVZ�p]fW`_:m�!�|OBf�P��S�/�4�������B��(��^�|P<���=�){�����{@�p �1��4P��6("H��A�i���]lA��En� �@q��&0�x�yqd�Q.|�~1��B��l@]����R�GgZL~ϯ@a:��Ԯ��@6VPu@�NcIȳ�����W��X���XJ�@�j&��O�e��H�BlLvf�j���9}7�!��gwG�օ�|����Rd�D]5�jW� 70P���ȅ��+D�ؗQP\V{�H��J;wO������B�x.�UE�&��b�"7�fl�q)���64�*�s�Ffz'�#ɡ~�`�Ǜ�yoXAr?H.�s�@�k�@j�� -�C��(�|@Q{�A�t'8n\�E�w�;�{�(���Z�K����\jD�ym� -*�&��Kdo��~u����~���+�5ԝ��f1�V+�K���Kt�7SP`�?���@q�@�g2��Tk�ު�u��w��,%6wHb#�:q�oΠ���հ��;�����x�����}��>��^t0��<�n�d���{��s�\���\�!{[[~��'���Ӊ�Ok�վ�l�0����k����$ȸ<�Ӵ��v�ask���?�3�?�t�8W�z�>۸��\Y:���0�ک:���&WG_�k�����'��ٜ��s>��A����O@ޟ�A�}��@�I�����\-dunD�.�;#Bj�KA����Aq�{��<�*�U��Q��{q[��������/<� #C��Jh���8|���\Rj���v�&��%�K�Ekj�sVN|e~��m�� ys����Q�'���H���[�^b��|�"x�uxq�s��.6���V4�ƇB��`��h�g�v�mr�S�To1�}YJ��: -LH��N�q��KM�W�2�_q���%�ڤ�H1�w9�R����4O#1���<��pr����g�p�֓b�ݬ�|�������ϰI-Ex�i�Ck��S?;���q�ݳ��G�����.�VD�vp-�)]/�[�����R��?7�09&RW�D��aAz�]�r2�ϩ|Q��6|�9��y<�9����"���Od�&y�m�� 6ό�7�/�D���O����ٗ�K�Rg �sk?x��Z�r`��&����"*�ZUZ��_6|��<X���8&�9��ڭ�#t|c $8��? g�F�Wskn�֤-��d���Q:u�ku���1��4���u���k���g� � N���9�2se��itP�ϨR@n�|v��/ -������T�9D?�g2v��p���5*���P~Q�}'��Z|��a��������K��t_�z�Zd4!?EU�]���r����h��{R^MdY�smWB�"+��T*��%�t�����/$��\A�8'3��Z8q�����, -�f��.v��MT��{_�֒d�ja��3�9*z���hVp����mc啂fUdXTR�@2[��T�;qwEx����A�?������%��`��� -���^�W����v�(5�ȏ��4��F����J\|�ϻE�֭��u&�8w�B�_mAU��E���C�f���Ԗ��vaq?oՅ�� w��o\d���{=q��V�x�?�o��ɏ/�_@�!�E$����ܱ�}k+�~���>�������0pj�c��h`7�U���/�\ʚ�n'�5��ލ#���y�� -+|g���0�8�-�J�F��|B���v�2'x[�3d���=�``$���M���-"������x��_�:��ݨ��ϥ=9���x2�2��FY-���$U��XG����_�mH�ө|@�xI�_ -���O�������j���gV8��\����qVA�D2����� X%�B�8����^~n�4��,����]�Fz���D�v�����7K��*�&/8#W罊x;�0�> :3������7�VUY0�E��^�C�N�QE!�����S��<������a���A�u�!@�Ɉǐ�Y�]��,�s��ra�9k����|����K��j��JG����uy�v�7 �z����Xz��.��|��K�0�j�1�x���_�eۆ��ً��+f�j$�&����*g��!W*�Wi�KiWEֱ�̮ژw�iwW�A�+���ӡ� u:JDݼ���`�7Ts��N\}�7�5�5�ҋ�SC�1U�A���U�K�[ڋ�ev�t��;���-Õ�KyW�o���e�тml'��}z�m[�>[o�(��3Sd7 ��H��a�E������N�O��2{�k� 5�����o�ы��Gs�۪��Vc9D����&+*�ê�/]p����Otf���R�zw���N��@v�~ո0�Sl��W~� -f�mg¦.�_�@^�i�HD�G�Wɜ�W�|霯.�9u�V�0=*;�|.��oF*��KJ/��Kj˙�ӻ���,�5|��ѹJ�~��"�c�O*�v�xӿL�R�P��=z_X��D�gu�3#�Qd����H�'��7.�=�������y�h��9�4ǜ���i��mk�m�����(l�3ޚ -�Ir���v�,[3�-_��7Rn2�g���JC�U���I!�Ɂ<9��Z�F�:��Ej7�� -P3P�H?���YyU��W�nF�Y7�s��1��T����fTZ=�I�ʈ��U��2?�.��5���m���)�ɀ���d��'=#F_ -c�e_��d�� �Y�̅�-kY�sE�W��j���������Sw���.�U���U;@^��WV���X��</�"��/����A���ڳ�XΑ�%�a&���s���%7wɳ3����*�ξ����n6�.�`�����f�/��J���E�8}��s�: ��U��S=|+lmo��"���x�v���G;�a���>�*� �F}��3CF��%m�ʪ�e�.�&�@�u�&&ٺ��#8wx�Wac���+��J�K�J���*�{������E��s~�|-�3��U�ڵ��n|�H�S��<[�n��1m���.�μ��` 4�[���$�;��}td���P��Yx�|�Vr9�.�i,QO��1��i�/3�Yf��<���(���_��uԴ�R>u�j�0hU���*}��O�d>�ɝ�2uO8U��{�n�\⡷�Ė*k�~쭻#o��w*m4����7���߰�3�1d�U�Ou�X]be��Eo���Ū����7�_q�B���ɩ��ޜU�䭚�1���\�*�v?�}�L�Eg���ޓx�������Љ,��%��1py�N�� 6S,?�Ź�q� �s�o��dz;Q��6K]���x�5��?k�:�����*�J+���WS^�kU8>�oA���nD4I5X��w��X[f g�=�P�^e���)���=jh������S���՝ky> �%=�G;�NoUX,D���j�f��xo��^�\��%�������/�V#u���v�S=K�A�_yw�[N���p2��*�l��)w_&⇸�j�~�����)Y<5;7�����ZtX�bo`��١��#��>3,��u ���Y������_a��^Ek<�Ui��vۨ�er�J�C�=�����:���є�{h�5fc}Q]�L��ZT��ۜ<�<s�>�˙=v���7���ё�Վ�9�F�5�5�w�M�]��X���4�ΐo"�~l"a����W�R�7y8�X@�lROrS�c���2݆���y-��}vXt�r��q�;X�*G�G��4L��>o}�ʲ9_��x.��;.S�EwWsTK�52�Vm�α�+���DAA)~��%uK�Kh������_)G�]����� ������w�C�fM[XW��d�^.��}��{��F�fЂn|�D\��(��P�����������P�1�\�և��(��X/�,�,�¢����!g�)�(��v�HA<�ⲡ7P�+@�;��,9nh�����@���@.4��:x�g'�a����4�L��I ^G�K�{6Lp���('�[ ��O� -�Q�Oۛ������M�n���mS?������_�� -�6��6�2�'R�O�3�I"�0#�6�6����� ��8,Y?�b�v�j)y�b�j'A<HmL�+E���r�*����/7�^.n��h'���-��5�[�_H��Z�D��6Յ� 'ILY�v�����*��}��S��2�Y�y=f�����L�[=��9���,�W�4�?"�[��T3d���]�}���{IÕ�x]��*�������V�6��kC��`���2&�l?�xhD����>�x��|ۺHN�N��+��|!������!�of��ލ���X^���%9��4V��M��T<Fſ<�����W$RnQO���c�_P&�b�$�y _g@~p��<��<s%c�����mFUsޛ��С���s·��U^R��@c�zk8�'O�-�D�-��w��O/����u�n驺�+��Tw�B26���(��]ލ2�X�(7G���'�ߔȇ�Fj]=m���|���z�^vЧ����<x����^��s"l��lߘ���u���Fjx�>zr�c��Ͱ罹����nBpw��@��r�����5��5;�Б�ˁ�q:�By��:ʃ�l�����0��e �s�5��Y~Z�?����Q��B^�"�]RZn�|���]�D�s��ܼFt�b��Þ����@��E ��O�G��:-�����%Y{��d���S- ڟ4�������C�v)��R��� m��p��ڍ����OΣ�P�l �.~ΐ��x�W��;��̚��cL]vW=���\ln�?<OD�O�C�NufzIq� W�9w�B{�$HY�拜N��̾��>u?�\];\��F��:���^�;o��y�g�_�!_m��W��r.��E��;9�F���TY��Ru��K�ov_�{��s���%��/��ID����tPNID��@�çK}^���y�a���;����O�Z���s�7{��w��'�e��|KZXx�-�{d�}�خ��IJ���^qN�9Y5���6���ӿ_��u{��3�S�ap��_����H㘈��$(X ��-r<�����;��c#Π��Ȕl#��.��<��ք<��H3O)��ҍ���� �Gp��H���=]�����|�=�t���P{h����ΣD������y�'��l����PB�Ii��9/������s>�:n �>nJ� �Ee��Τaլ����>kS�����l%��aL:[7詧�i�_f0D;����G�D+��4|T� -�A�/��r7O�~\eC�)����$i�w~�t|���3���f����.��� -z����]&����E�c�w��17>�2��"��T�𤝟�����K�6��Z+&��D�J3���n�:ȝ����HA��JA�sKq���R�߇��-[���^��͂W���5!�[sg�uoj��Fvד;עc���hS���<\�Ws{�-VTo$���TkG2i���~�r�������>Q]�����hS�E%w{�J5�D�������d.Q��$���NP����5@Y��s���X��h�_��W��}�3f�b]GTw�/R�B��Z��z;e�ik2G�R`��R?�wD�SΉ�Ę�uc.L<�wU��1/B�B<���x�tt��p�@�DT}6�:�I:�����v�������y�g���w`@%r �ϦC@�ם��{���S�%�k���m���%I�l��u��o����G�?�p�_�Ԍ; /�[ �3w2+>wr��t�w���/�\��������/3N�(A�Q�p�4�E״�sr�4mJE7���ѱ��K�ZT�5���zu,������cQ�/K�}#L�k�7��#+�˭�Seg��xG�d�u2�Ӯ�[i�Ċ�������"��BZ -�"M�=|J���%������m=l�\}}�@cE?���~=�B�n���m� Y�e��<,�2)��z��i��H�9rx���E���HY�p�u{Do��9M��u)���_��2��=������[k��H> �=�)�V�b����X�%s�w1�C�~-�-��O���~�=_�d��'Ya*��%�ۓ-w�5�ݵ�=�38�K.��,WA��.���҇��������y���mϟ�m;�����פ%�C�(�� (�����+T{����J6�>ٯ��Ք�����b+���j�"N4�-���/�Ɋ[�k̎}v�}�'�[�y؛�����n�#�-fx��h@P �܍�[�3ŝyo-�/�|�\E8���9b~\���<rb�Fz�r���jn��"�V8��˃�y��S'���p�k�w9������dI��=v�sP7��xb�=֧5���'�z�2n"Fͱ�J�;5��l��� �?R8�������^���u�w�sDaS^�!���^�4�.�¥xٖE��_s���٤C�#�Q%��.�=e�[n��7�t�2ʎ��O �lb�R���C��o�,�Č���2Y8_mrUz�i���ӆt���(��k%ct�ݽ�H�f��k�����+��W鞣��6�31��r��e-h��\u1��^�Y�>#�� �`���iA�"EC��@a���yn3�������嵃s�5��Hk:8_�N��nvO���d�J��n-���A,f3������h�U[�=�(��A�q��we��n34�;y��� 7�{�c��W��s����eJ-�ʚ\#���⢶�Ϸ�u�t -�]�X���=�z-���5x]�4K�x�mfx�Y��؏�ǝ���5�n6-���ZV� �Kw�k���6X�M�g��87?2�����H�B/���$�9 z�,�V�q_E���+����#Ke���X�#�q^���})����A���:?��X�Q>h��%�VzX��l3f����|��m"n��&c�*� -I֣3Y�'�L�ߵ{�J�v]l�<��$�[.��~�D[��3����!����r��ܱ��\[��9��ّ.��^�����r�L�r�J0�=�â!���c�7c<�ґF����7z%��8��p�z���J�r�j4�_K� -��[Čh��抈.���(�q~��/�r�.tg��l;�e-}v �ˌ��T��Q���S��g����5"i����0�(gȧ�ϡ�e�ڂ�!e��8��墈���nć9���s^��A2�쌩|K��{О�Gq:m.����)���Io>��7Ҙ���Hhx��PiS����,۹������W"� ��¯_��v�u_�#evw��|��t�1�ܛz��j��I/�TF��\��X���d��qA��ǫ#�m��l= N.?,05{X�"hp]'�˘�k]�k���s�x�{{~.(BoY��6� !"��v��������n�A��L�W����yo<-��EO#���u7��wy@p��;���__����o?���0�v� ґ�Fg���v���JV�6Z=��hmQn}��2�<=#�����2�y��S^�!���7e����r���'��UǓ }!�̡�g:�^�(�.���Nh(x;>mӓ�6Z�nZ:���f%6��7�.w��C�Vs�SPkd��*� n�j��gx����Ak������U8��W�E�?p��<-�#B�tK�44�`ק�ȥ���4�¶��gZ��+����B�b-���ͪ��ە�q�+��ΰ<���%��Yg�>��L�<�� �6�B��v��`^�ҧP�s��0�`{��dg�DD��$����U�j�X^oA�]��7��D����Y���j�^+=$���r/~�[��c�����|�pQ�\;�.>((���B���`�A�(���8�I5�y�$#2���RY~�P ��X�i�C��}�)'x���w�Wl�N��6u��R�����|�A.J0�%Ъ� -K��*Ɔ�6�'������"�:*��C� -d52�SHDl�^T��F����y���G�k�>�5g�v �Z�a������Ц��2A��@n�%@? �|�w#>Vz�8/�먴��wU�SZ�ļx�L v����jb� -y�/$R�0�N��D�E @[?���r�0�ߴ/ wl T��V����|��[n��x�Op?��?��y��5��e��o^#ZK�ћҰu�9�l����X�էP-�n��S>��!��? Y��z��W9�69'����k���u���LA�D7�c`��ħ%�K�#�dXQ�zw�71��A;�lx��V�8�^�Dv�>��O��t����gF��K�ݫ�@�b'}]���8$rބ�/��(����)����e��o�����Q�Ro�Z�B��W�m�>������3��/� -��D��3V��s�}>t|�ry�.^ͥf����ڣr�)w�<���[G� ����r���H�����r�����o���е����Vң�m_�%����9<�M��̌��?�O"?7�B��T�<���e�~T� �x��t��v���l��^���ۨ�`����_��J2쓭��~ �6�$�|�;��~Ÿ;ξ��/i��Qe��T*>���Q�^>k�z�8=�*�<j��뾙+�_��Nv���֭_�.l��Rv�=�.pa���DW>*�Z/\�*#��/$cs\�9�R�&�rdI��w��>҃��|��>D���}N4���l۽���A�<~�|���9΄[�2ԮJev�?z�'g�u9���e~��ϧF�o]:��t0쩦��q[R�ysl^Y��)�3����:�Q_A2��{6 �/B�I�~?V��Ǿ�!�{�2����*��bt���2 ����ry9Xs�|����j�Ԭ�i}�*ōs�I9<�s��q�*4L��,�Qf�6'�7/�Ƙxs��rg��o���ɜ�*I�V' ��i*Խ�_��r���i��-�:|�]Q�]L�Q=�Sײ����,�թ��#)��������!���Fp�0��l���X�R����7��_�A�#�i�d���������ke���SP(U6�`������?����l\����B X�����3<�-ҞN�e�qL����-�����k#~�[�=E��"�>+!W�N�{��#{���jxi�Vm���䠠�U�8�V�eAq���/���VH~���(�Ik��_jr��Y'�u^��Ξ=|�\�?��f꿎�����]9�_0���رnԘ��[���1N;�HM+�HK�}�S�]��8�aCi����+��^���AN!�m����g��Q������~�ɸ�n�:���{�9�fb}^��tjNG3hl݃�c¢y}N���*���zf��B��T�Q��*�l��A��jj���,�^�cE���JQn�Q-, 1���F��r �����v����n��i�Rh�����ƋYSl �a��ߌ�Z|�O�7�]�ײV&�m���#e���lH�S�v���4�_:ڢL�CX����Ǒ��5!ز��E�V*�HK�/�� `���uPX�������֍}F -�̗)X��99v�&s�-U?����d�O�X�_�� -eۘ��A�_�zr�,hB�\��gő�W�1: ����g��p���3�cF�N��&�v(�����(Q*�����bO��G �]��,sY�Щ����'���f�q�]�N`���F��i��$�����X���B�����#��G��k�����Lgw�ѧ�+��zW6J(�ڎh�r -�/�D9ID�3MPx8��v��P�36���V�)^��1�fҐ��5�rYEj�ޔ����Ƨ$��GS�?/�f�b�'�Քcws�ߕ������[����!w�����l����#�H�~忐���D�g���u�PJZ�Η��>mi4<����܍�FN���hf4��-�R�lb�z�����}j�/�t���+�+r�#b��煲uˢ���h�̏��]&�� �,���x(��@�2�̥v����i���C�٤A�Ȣ�����Ӏ���f֨�`dZ~u���^�,��'f��Xȵ��W� -�/�9�XGjalt�m+h�~�83!+�'��)��Ap[б4�1Ch�4��)���L�~.�KX�?���_}�̎��z/8�9����Q7Py �v����$/(A�p� �v珫]ȝg���:wU��dRq�MԻ����$�3��GZ�/��"�[���k,�i\Ue�r -�������iO�?��V�k�X���0�y馿v��z��_�-��&f�eV��=�<QWn�/_;J�� W�o=I�����=�}u&�vִ� -9ʘ��;�\�2rY"&d�r����q}�������rt���d��az1��\`�C�꽴+S{�|{�j\ceʯ��#�yw�;��ˏ��/D�@�M�}����i���5�E�`�ڒˣ��b�N����\�j���Q^l��D���@�[j������ ���C��֗�6��C��㛀�G+���`���o��olµ���[�/�,ʄ����Y��s��uz}��ʺ�C��V��xk��փ·,%'؉��Մ� u����o��f��T���&#�����P/yP�<�p�Ө�vYmm�؝m��ֆQo+��������MZ�����&�!�=��fd �]_]���+�x�r^�.^J�������������� �Bn���������{������jߛ���=v�:y��0zg&EP���ِZ�$�<�Ύ\u��F�QO��F_ĸ�#��dj+�" ��c�沘v�����lOI,7 ,��y|��_���d�$���� .�Z�r��X�|iԽo�Ԣ��|O�/U@�!���nr�R��{$ޝ2�*�,c%ޡ�2n�K�|�owV��&�Z�y],�_jnU���� ����ҜWk|^l]�y�x��.���R�jZ�6$�����$��}���ȡ��̾���t�C�u\?X[ ���D��,�[��k���u��!/��HC��.i�K��ͮh~��l�8����tS��i�� -O��nz�Nv��t�˝� ��n&�L��K���)�$���Wm9��8���݆x� �w!hmb,c}+��]D��u�\�� -�N�O�@.��\��y���7c����$���\u��cÑ?����#h7#�觗�FPW^��ʙ�O� ���s���6����1���7�jX<��s'&;e��Gh�tV��r�o�>��n`ū��WbP��-�>̭�}@�{��^���4���M�d�Sy���3�y=h���T���!�o��MFsZ#Hj�_����Zw�������G��I~�������L��RK��\�;�[d����2k1��}*��M"=�h�ʹ�T|���ԓ�0Ru����v;�Չ���W|�^���_��Rz�,$J�xۏ�d�Awk5l]�yI�һQ�p�0C$�$�7wl��ׯ����h��k�*�R�d����˷��z�JF���5��3����7U�<_�� ����kQ笸8�+ �tz�h�K���?�$0����p�Vz��'�8jz��ة�;|o:o}3s�iR���[K�^y/�Z�3I���t�����WyXξKh�O�h]��g���H/�Wr�kC<M���6�g�דϨ���A��A�H�/�Nr�jv��6��q� -���c�?S}B�VmiY�6���+�������`}U�m�,~�|�>)�-�! $�s�I!��HYi�Q3h�����@�3h/�M�,'�'�`��L m��_��%��� 2��d�x���� ��& ���Jjd"�2n��@N�r���ni���A�S�$�� Dd�d�?���\����O���lOV�r���Mw)&�儬5�Q+��|%N$3 c��Лv@�� +��פV��,;��e����d�0yOn�'IIp j����~�NBV�l_�h�o�8���{s$��:؎�x��dW/���BҚ�d��/6��D@����@��@�Y�ФqP�u��u�R���;Po�h&nc3w������*{��VljVO*���!�&ޟT�oy�����:�3$ -�S*����������BҚH��!�Y���2 Y� �!g��U%I� ~^� -%0k��"�#[�Ⱥ�4Zm�����������1d��66�W�5�L��~�Փ��&��G��������=�D��^\��t�+���>� ;UQ?�D�ǭr�nrN5 ?��_���(>nT$:sf�}5S�~vаY~b�V�3����_/0�_���xFe��o7��Ꮿ���\�]Z�챾��w�AΣ5�o~7�nɚ����i���� e�$��=�w�8� <���� `}�F�����+(6�a)�f��k�`����s��p:�6��d��b���U<�+a|\"�� Nq�q���5t�r��:��{�Yˋ�ehЛ�qur>�y�_H�\�Ze� �W����!��6��-d�:E���ۛ�¶�K<~�a⍲��mo�G�����1��7 -���U��3%���uNH�V�碟3��y�-�m̬R���H�O��urd6���������$�>j������� -� C��ґmͰݑN��ն�\��t�c�s�wa�a�O��������m����NG�e��qU����\����b��^x�n���7阞,L��G��t=;����x�4��_��(�Ik�����뉈�t[��I��ZE -wyRzj����/;�~������>l�}_�'�O�b�\X�[�z_�nP}r�a��qs0�暖��M1�=�7�Q琦42F��5P[�Y4����d?7N㿐�l"�1���4��MA�����F�;��e���'�����eQX�1��BH��������,wl!x��p�u|p��̈N��MV���^��*��}p�~Q��ډ�B$b�E��*1&�)P����H]����rp���c�@~�%�~c2�`��<<�7�����c���9��A��q�r�&����u�F<{���]���WG���t����GFyJ[�ۢV�2'Ն�/�2B� -���J��,d������>�[2����wb���@�k�m�n96�֞c4xܴJ��6����S�EY����aؚ��7{躌��&7��pR��ƨ��u_%�ˉ���0�#'��]��r�!�nǜ�7����.0b��KQZ;����f���|���"9����V�z2���l�^��迦f�|Q��y���ˤ1P�c!w�Ε�S+-�PI�+�LTV�O[f�h(��rb -5F��"J��&�NFP�-���T@i 2uf.d�,��?H]�i���Q�a��_D�3m�"*�-l��=�c�UE7S(�F�6��z�v��ghZ)0,�keӖC٣Č�~�I(ru�M�i��r�0��F �>�̬�gG-�s�I�;�����2�[�����&@�~2��\�����Zy�_KC�����8�j���W:�v�����]����������|����N,�|+'�{�$h�A[�\c~^Y�ܑ�8nوN�3�y�֕��+��D����ڕ��b�b�/�]&��K$��F�%��6�%���27�����?du���T�*��)e���+5߿��E�VeO�L�P�H��7@X��γ�Cd�El�� X�l �?O�6<T�w�<��?�r�(ܱ���w��R�Wv��lʏz���uix����51�k'c�?�U���t[+-����E' ��*���I��?`{��51�VY�]zu���t�7�9gkBLo7qw�m�w�eǥM��n8h�m����,��_��%�^�_e>?/عz?mV;�5��z����VAe�����H`b\��;�K;��BR���}�X�F;�f!�f1e�^�;��\�l��En:�Xa���a��#K0�F?�$��K,�,��_�r �U���E�Υ��9v��m�;S�\���}>)\!Q�u�>�R��y1����V�8�����:�q���af�} �gm��t�ؘmfM�UK��ۢM}[D�����������X���@08��=�����B{&a��]�C$7/:����l��波�9Ma6Q#�kg�����Z��F�~۲x�y^+�M.�iU���,����ޒ���.�4I�Kbp���2'���&]���88܉Y��2��%�G��9��_4238�<a� �D�R���W�sG��UՐa\��௱yᐽ���ȹoL��3�W��I�.i~J2?��n�G���k��m��~��O�W>�����rчק�5�M�JV��!z/o:r�/j��2E0���qa>�r6����h�m��ٶ�g��p2wLT���Uf4�N�v��t����|z͐K�\$�ڹE$��tMa f]��&�!�!���M|3�N\���M��o����Z��cp��o�CoN͎o�,� �? -/qG]��Tv�M@�kL�dF���X�{���;)R��jR��_�UE� �����NCֵ�QǷ�_����+��N@j�@*��֗�r�����|�9^f)��B���9�)��s����t�|�sW���U�B��Q_A�v��t�d���3)RfE�Rs��+���o��)kC�k&O�qw͆x�&C+�*K����-���:)cc��.��ge�%Vcl�?̱1��QCi��>���mT���?��e�Өa�!�O�U��y�>�;��Ƈi�FaLJ��^���]���`��mU�V/���] ��~��K�R�]�X���]~a��,���G����lřc!T���`����<?�£nq�<�������q&(_��O����Ρ�#��GQU�4ީ��U����>=���s��]~�šա��ZX����O��e��K��e��Έ�V���w�{N�>�MT��=�yP~�Xӽ�c��'�7���5_G��?����RX�{�����y���w#�1�8���ǶV�k����5��jn�`PcZ?�wG,&lgOMZ��C�fy��g��k_�Ї���ߵ��Y ���0��bw���j0R�i��ԥ�?X�R�:�$�e����N?0R�ضŜiZj릲�鄡f�����|=�G�g�e.�wh���p��:��O�j�K{5(��m��]���}���T!���b�w���=l˽�_�{��W{��ئ�>��(����a)��;R� !g��g�w� vh�?���^�����U������,GC�v6�w������<����u��N7��ڡ����p���MKm��b�K+�P�Mݯ�+���1o����Q��!�N�Y�/��S&R]�$�E\�3�-�ZD=�U�/��ͪ�<9�+}lP�h�UEґ֖����5ii�����M���6�[�P?��zy6�ը�zR}��u���� -Wm*���,'�XYhZ�/ryйƥș�K��l��T����GNv�/������m�^7��w�pS�#G@*/����t�ΰ9������� )�לB�\u����j�J����APO7钺�:;�La��[�b��:���%���Kpȱ��kײ%���'Pb�K t�_PzC?#䟟���ZY�/��Z�gk�{�D�4�����c�=ة�b{�Z��+O���a��T���wK*���- �#�m���ۄ��w�J��-�ve�ݦ��/@��`X�ΠDO�=2谞K0.��/GlGB\��˘'�Ym�#��a�ڳ��Ƈ��Qh���V*�Co\�<!Mi���Z����Kԅ��Qvب�3h�$�.T��������JD���'��_�%�7 ��+����~MR��}?ǟk�3�=���G�-� �N�J��8������oE��_(�����@~���'k�b�M#����L�o����:�~�.)-1��C���n�}��jx�����zX���[DV���C����&���qՋ������U�i� 2����B{r������� &�+W!�I�W'��qK�R �\� �Z�3�Sy�#B���]��B��j��[�y���b�R��[L�_Hr��_UM��]d��dL�2�n7�~2��dNk d��=2l������z-���Z��`8��Yt��2:�:-��ݯ�^w*�w)X<ª+~���,>_'������~�]_楎?�1m�#��_���d�Od�YHʀ,?���}��I���@V� K�� �(���\tj_KQA���"�����ҥ����������[繮����+8�9�b I�(AI�9����>�\c�~�����vi��j_�*��swg����X� -U��e+�����32eG������f��BZ��9�|j�qIr�|1���A�7p -��ѡ�����K/.1��]V�WC�s϶��:BUn�o�fpF����� ���kl�٫��(��5/&w�ΎT��V�Z%j6:���R��ee.<ӛ���L�n��y��\��Z鋏$��դЮdo��BR@��w�i�l� -}�~��������5�e.p�������g�<�����:AD�� Q3��#7�à=zo�.��=����y�f���bۭ������"�矩����w��S3)<Ҏ���3�E[�1X��M���5���}np=��y�P�x���A���C{��������q+���Q?�*n<#�.N�>{��Vw���uN�}������dþ��_Hr�ajb�<%��"۰��I�_�%�k�x13��?�A��E}/��GOڇ!���x؈�/�{�o�x�`Y�J -Zu]D��U����7%rǂ�8�S��N�^|��(Y����8��A��l��J��ww��Bzϑ�T�?$�PɧV�ݤ�y���!��O<������0��a���`6`�qW���Yo�h������}s�]G��u�;}�е�o �G� -����Y��k��y�Z#'�-hqs���My{l�Ш�_H�]2J -���GLY����]a�V��u9i��c����tȃ�D���"-R����A�mY���ק���������es��M3W^�0��m��W6���3mS�8�;�>��N�FG��U3CC�6��/��I�IA�]������.r�>���),��p���O�gڞ6�s��I[��kK�І���Shk��e[46� ���b��Y|t��[����p#j�4rѺg#Q�~RU�QWy(͖�������I>äX��&��c$���p$2x?x�� ط�S�$���3'�\̮����f.n6�~eo�r=K�5��Mg�q��n�Ա$���F�P����(&g+ �|���+���J�ޥ��)m��I��$����R��HNt�c�r��x��4��Q�fA ->�:5r�q� -i�.���u����3��Z��O��8<աs�*j9�(����F���q�M�Jf7��W�!:�J)��т�K;����Iai������0yAO��q��� g �^�z�fCޫ����Y�:��U�թѬ1��M��z��[u4�w���9� �X��}�Ei :-)��":h��rn��]gz]W���U�m!�?hf�\���� �I�}���az�C��i�q<��l�mQm0#�Pt�Mm�4��rk��T��<��y^�8%Uޔ�����BL�ʵOR�=E���by���+K -a1@�*Bsu8��!���h������;j��I!�D?*�]=�Q�C�U�b��؞6�4(I����*��z��5m=4��( gm���qpI�k�!���N,��`�����n��Bs?��\���N��RF�u����K�7�~��fh�i�����-u/?y��X��G�znkj�V1� ���ht��W����NIOf�pP��fy��,)�BV+-V]�` -�M��RO|�o�� K�Z|u��h�`eM��1�v����e���H��fheh� ����A���?>z&��1��"ץ���ֈnZ=S��>�F�$�E+����M�n�GRQ-Ŋ����{� -���-id������|WC���C=�ǐ˳cu��u -���v�П�͡��Co��z -������?m�����Ԣ���Դ��C�EK�4�0��R�S���J��䏡~$��"��k?���@�v�L�<U�^��s"�q؇�?�`ctgtt�?��C�:w�Q�͚?K�.SH/_K�t3�24�-�fk�|���[��sgV� zƦΡ�68{���˾�У�X���\w~Ү�ޑ�!�݆����ML���U��O'���4?��T�H��}|_����A��L�V��r���"��v�/2I5+�[�]vH�B,�M|��5ooo���������֜���r��� l�����I���}:ČUYg��1��'�&�������*-���x��v�L¼O4��:*#}�N�ۑ�)����_�iK�:�|g���9v��C�7��͔[m�4%Ɂ����s']��U� -�Lr]f� ��U����@�Ԯ� TYXk�vdM��u��.�:r��o��FY��s&�a�UJqkc������I�<�^�=y5x�;c�⚥6����T�rSYO���6ϱ���2�v�F[S�v������wz��z?�`���)��mW]��`�����b}W�9����� �"h��A+C����/d�N��_�/[��*�[�����פ�є�zA���|�O�.�AU�푬G��`z���d.:�^��ם��E�J��^����b�h�=P]Î��#����QE�1ۻ#�y���,#}� #5�����Ѿzp �9���-�S>���S��X����*��h���m�;|x�����h�q��1���Yc8EҀF��D��~ ;������t��\���"��J��E�)x�f����\4�j}�8 ��_�!��U��t�`���aC�O�&�_Fʦ#����,\�Y���M���jM)Ȓc��}�ʙ���'�E(��=6'�V��#�J�B�w�[kv�����%УthA��!��4ޞи��@�¤�^��/x����i8�[/ߍ�k�)[i����f �%���Jc�u Y�c<�����r�l�\\H!�Wl����iU��$lO���x��G�ɂd�I~x̄.�~5�)W��WKc�w���=��q{u�ʽ4ƙK -$,���{�U��g#J�g5.�Rջ�ܫn)������Ȇ�H�,���'f���bZ �&/����5�z8���c�[`ؑ�e�� �Ez6��=��r_�Kn�k�{�í���=��0���z�l�gO�v�4� 뻓ϛ�_�g�~��& }a��wlK[��� ���>��t��Q�D����ju�ig۱� �A�n6�)�N -&j�χw�T���i���~�"�u��P�j��M�����h�-�8#�w��l��6�~a�m��~M�l�[e�����ණ+�[��sw��|��x�v���x6:Ha\m5�q�U�X�#�|c]P���vu�j/�q�M��j�YFW�H��T���������/W�h^�g�D8�G��A�_0xV~��v�!6����cw�������=?c0<1�8��U��ޜv�6���ilKu��j:��W?�]��B��)������}d��=ȋ��9�V1`��`V�������Y��ڇ0��=`�Ũ�W�~A�Ա�T��7�'� -��X���q�di!�!�R����-�/V��)T>����G�\}��Evi��7�ɦ�r�#���uf��BR��l٩�º�����l<�S<u`�ѿ2����mo�x��T�2���;y -�N��'�; �e���}�V�m��jR,��e�<A��Tr��2��~HQ˧�����)�i� J��������^"Ө��?�t���ڛߦ@�_��OE�l�/*5N��.N��5Nzp.��V3���8��e�/��LJ��r����O�[�?�G0�����q��8ލ���x�#`�B���YU��zx��r�=(���:��������ܘ�gd�q���C������P�?��0N��"N�#'����I�읂8�+�8�N�)�\�}�z5-?�81�x��g� �ok�a߅m�yz����s���������MR\ -����L~!�ngş q��p�/��~$ՙ���D?�4�:�w���o���?O~��^�k�H/����ǹϻ��b�,���~��p^���x��Nxv�u��B���m�)ܮ��.�`���A���������דּ��������9PH��`Z%@��������)�9��;�K��Z���UklsO�ݖ�͎_��C���G��>��'�&m0�6�G��p���|��Ӯ�ʟ�B���S�H��!°�Y��RG��{�eV�)��{�[�JI�Z���R����C�־�~cˢ���?ֳ����n����b�`�Vr#���]�*��j�Ek�O��_�>Mc�=r��"w��MT��á��߂��}0��|��Oz3���������.��������|;N�=���CC� ���p\j�W����������/�"\�U�tF(�~ڿ��T�����JG�h6��腷� -�N��B�g���-K�l����?��+_���{��(�|����R+��ԃd�VBQ���$��I�����={v���<���7p�3:u֧Z�g���Z�>�ڇ��2�;�^/,<��^J|����v��;��w��l��x���� ˉ����V��4�u���w�ok\ɿ�"�R���U'��U#�c|��H.)ȧF��+�g� -�o��18�F0���2��<�B2�ϩ�����qw���mQ�'����0y�w���y�s"E�{���U���}J0��>}�6��Ű����xc3��i;�_Hr�Lr��L -��%)��|R��g�ŷ���ϕ�V:��V��h4s�Z��v�Ǜݵ'E��Eþջ�=Tƾ����\��XY_ -��Wi����o��۠utg����QF�=t�f�z#��^>!�m�m���vR�*פ��n\ ���2g�3^�\�lj{��)Rt�*��ؾ��z��,ښ�y�4tQ�~���5����s� �w_�����*�[zؼNt�4���YZG�TὊվ��ʽ�)ҭ������K`�*zI�?��{aߕu�p��7�� �m��s=�)��q�tP�^Эon�����>�m�,Q�P� ��)�]l�P\8:�Z��Y��!N_�[���5����i����_/�5���Q-���6���I����Q�2 ��H��L4=|�۵o�?�����>�iuY�Zڲ��HthxOi�����-¤�S��ֺ�QQ���T���<�ó���w�%Y/={҇�Ҷ~P�; E���ڍ_�D,�w��~�=!�� +)��ۗ����H�ɭ(�U9�kYg��m��=��p�I~�Z�ZK���D�>F�,�Sg+��X��D�J����N���P���C��k9іW-��vg뽻X������Hf�H�~�Rz����ş���p2O�r� 6���z���q�de酕�E���7�[o�����{-uGC�#H�� "Cr����/-��"�ߑ)"��_�6��Z�;�#��&�:җ���m�s�������;���qPc���q�9����]Qڷ�#k? �3s�B�~���63����W�"V��ד��J��f!:� *"��^��]\����p�@u''�(�b�zUAN�J��B.{3wV�+Y$��1;*|^��(?�IA=��� �����\r���Z�0�6n��tijSoF�LZ�<�u+�r�m��;X��4C�W$��&��9�x��]�w~\NPk���Ss6ހ�K�Q���L0��L��7�t���2<��G�~�+��ޜ�W� ҎI�3,(��[`�<jaU��Z���ʄ�E2� �Ra�.�e��XOO���1u�3���P�2�R�:�I\ߖMV.m<v������yfrX��M;���űh@^R؟�����~�[\�yQǻ�~q��w��-Y�rw�������?���������mN�~���}��ć!w1�7�>0{�+$;.���NufZ��tn.��}i�<�P��C�b�ݒ;�{"w*����Y���E��x�a�M��m��a�����Ah\���y�#��m�]��*�ӄ"�v�&��)}V�����/�� �O��ћ{E�s�CYo�D�͇��w������K#� �cD�@Rl��/�8��U/�֦Y|�g��fO诼� -3^�2�3q�(������#�_�C����3����[H���Q6u`)��V�=_��ʭ{$|��*�����A��W̬��Zw�j=��x�:�_��&ǭ�)-]���~��-@?˛�t$��ٜ�`�:]�=��Wd��p�,�wZ���v��|�ڢ��p@�F��ܿH��5�"���&�a�:�?���u8U�xO�C�m��xZ�)�OL�������[���M�t?[�5_�f�wS���Y+�<&��=�X�j1/3Э���ʼnB+̛tI5OV�N��s���U0���g�X�_u�$��������$2wC��EU�k��[��6�8pHl_��p]����4پ�8l����t�/�t-{ ��x�i^(�&����dY�<q6dm~� �ظ����%�2W[x�6��9N1����2dPm����Cq� x8!�d��.�����9/_�=���;\��g��/����%xn;r�6M��0 �uۨ�NM�o�a_= �v�M�����Z6酯���=�鈍a�j��� -� -�����!s˘�v��ax����jh,�;.+�;Y� �^Iw������N��E}!<��sy�Az�������^ױ>� jCK�}��l���U(�̧z���j���{��A"[n� �O���e-Z��s�s\nY�ZՅ���hA�5p~,tsf�"�m.�g�^c7�o� ���Z�+��&m�^-����ʗ_��~r�Qng"��7X٫ӆ�MI8������ŗ��5�Nl,�,��+=g��:;?�ݬrG�^ -���~�hD�*�W�L'��$`�L���Nf�����ZC){a-�|AX��X��X�W�P�]~�m�gm�wC ���OO�'�#�n;���C����z��Iӗ�)M6[3{tNr�<Z�B�S��>�`����4[⨮m[��� ���K��n�}����X���5)��d1�_��s�ڨ��"K�Jg ���<�nC����@&o�2������cC��u`ю��ӻ���zZ�?4_�ޓ�0=u��=�}v��Q:tg�a�����,�1�c����N���ڎWQ���&j�O��r_*�.�}��VN�҉�~�t�Ј�O�g��r���k�����e �d���4�f�����6l�p��#�vY���- �kD��ktLJY�Zj��������3k�y!�)��6>���ۙ��:{#��T�*�*� �*z>�l��^�(FP�0F��P��G���h: -�Yߡ��!���C������9��v�M�k<�o�SZ����dͳ - ��J�aseG�w�� -�\�����0�&z� -����/��%����~rr7�s�k/J���X�����������m��M��⫟����$�����D{��GjkS��4ø��=v���p��b�=:�C�_��RN�-ۀ>F��T4)����9�a@��-ŵ@�j�K� �8��(�M�)/o~A��rw�I|��YD�_�$�.f��K3{����ځ�S>{�uR��̳䮜s^/>�n�N�t;&��}~��,�`;)��Jp -�IqҁYc���+f#���$�Y������,��@� �0��M[�ْ� -�ϡԼ� �=�����ΣX��Oe�> -��2^��Ӊ�i�N�G���Qz־�M��+�����.��H�k�ʼپ `�Na�~!���O�mL0tz��)&�I����Ma��Z� -)�N��cxI�0�O=����S��W�% T��2�z >��x/��9���;(JϪ2�d���AG��>�X`|��8��l_��u&�s�A�8&��N��Z���+-�f���GS��G%��d���|�b�!w�~W��{>�/��m�JBc��ʽ���3��n��yy�����A6�{���U���_��Ε���� zb�d�H5ڌ.:� �y�I�N��z�8�������ïL�|ύ,�}/� �����##�W鍼��.�կQ}P#�sg�xFy�:���EM��s����&/zg��ݓՁ�� -s9�PD6镖�:���f��-��n�¤�Y;�0�$��g4����������ӯ�g����V#|����~ꁏ{W��������T��'˶�����y�)1'� ���c�Nm������ts��7��E� 6j���b��j)�+7&�{tf@�M�zZ��A�W�Y������S�G[b�{o'�7�"W��ZW�hx�c�ߵq>���y��/����hU�u���@�I7T_ �;oS��^���U�2p�>�q�_��P��_~�'@��%�}�\~�Lr�c)���w�`�=��A�j�;|�^�y�� <1��~�����%� /��*�1�����ןb�����/7�`n��(;����:Lgاuӳ�A������� P�1 �=MFI�:p�V*�ȁP�_�W��7��3|hu#V|ן�ü�`gO���缭�KނI�G����Ҽ����യ2�kG�żs���]F��l<���N��9`�O�0'% �x����j��������Mr����L��O�����.~��e�H�QP�P�����Xͷ�e�;tQ�4ۯ���Ubׄ{���{���q�n�m��m`��b�N������61��v^0��6�N -y�Q~m����t���� -�'��RL�X�I��1v������r��G���dM^����D��_��t���Lٙ%�*j�ij�c���39J�M�Y�m�x�U�a�'ǂQ�{=�'+���U�u�\�$ҡ�}��(W���Ez��\�K=5�ǻ��M=��l{��]�ڼ�n[���i�>lqx�[`�U7����]��xc{��!���?�IWP^�㕬��i���ӘG颵䫞�[�������"���d�;��9_2j���`��#Lf����ݕ!��a���i�����d�7��-���_����%Ý ��}�ji�v�w��"L=�J��Z����"騣� � ��߷<�gU)i��L@Ѽ�-q)B��:�k�d{?Ȅ��$�5+�T�n�vg�k �a�A�f�t��n��ψ7c;2��Yr4�S��C��Ӣb��^z��ڗ��>JC��[�/���!O�5-%b(I�a���I Z{�nR7j���%^�t����M��Pb<>�p�/���,U���Zr��o�����x�����F�dS��ӌ�{�i����J���.ȯ�&O;~[J.��4_� )��â� ������"kkl�?_����:W�v�����-����S�>�ņ�/�ͨd�����)�a r�8Ď}ݗ�y�V�ĸ"i��r�6��wc��5$OU�R-�v�l�E�����x]���Ϯ�@抴P�ϴ �o���K��_��Ӻ�ՙ����6����Ƕ�3ʢ��E&te�(�j>�s�L+��A�[s%�t#�ܬN�M�*δS� ��a���H[�xm����L)�������ZB��</@��3U�;�l���ǚ��&�����a�<�p����:�������1���h9��")���s�_�mv�W��^X�V�[����a����Y_�@��X��Iu� -�~���ķ����m��W���X����s�p�Z8�x���ѣ�4t�ԩ�ee��)���"v\jq>�T^H?�o���m%I/h����3�:bqB�[v o*o�z��@�;mBZؠ�v��-c��q>�=�3��\/r�ԕ���j6��x�1�2?��]� a�YWW����R���A -~�t��Dg�,�.�����!�=|�_F8���[G����#rk'��"�<FG2�gt��OA��Җ�TK'P�3+q�'�?�d8)�I��ц�v�{S��-Ǧ�b�� -\&�ǕC��ϕ�*L�X�ZQ�"i�&�*,R~oE���x{�q'4`����p�,�ۧ6.A���T2�^6�-���\�L��.]Gc���yM�f��o��QMh�m�)�3�R�˪M���ri�)۷�=��]pk��ӊ��U��������8���3�8W�x'h���1�橑��ݕ�p�M���F��pU�ȩT��[uF܅����dfZ�Lz�vB�R��W ���M�@b�r���&A0�>Qo��U�ت5���)���]�9b�N>��/�6 a>Ebc�e�G����_��/nzk��ӎ}�]2���2����gu� �?b��hZ��u�w��Л��Sv�cPEf�a~_}u�HV�qXX���~Z��8_����� v�]�c}�k�/�>�}DG[,�O,ѧ��L/������ -���ۓ�d{g�!�MN[�iR��̂�|�O�:Ѫ�9�Ƚ�Q����Q�C���]�4{a�/ `�����2l��Va��|s��E!�7P�/�wa�(���mw���C���:��c�.��f��ըi�E�WiސM��HFK,�*X�(qD��®��}ڗ��y��o����n�c��n���(K�D'��X!Ff�q�^�:l�Z^�����%�-�e����=�ѹ�����<p��<0��<H;�9՟�q����}���=��B9��vD�Kc̚� c�a��Fx�/�U���H2� dv̉���n��z�.w:Zb9��� J�Ң�՛�p���͇�u�H�33]nf���]|����G�Z��W�_��0���0�sU7���*ڔ%T�9�2_`�5-��Gf9P�F����i/+��|Q�ArN�Hq��}s�}O�k�"� -w�X���ϗ�J��L���t�b�!���vuρ�z0�OD�G?[G���s�b��W�s[{Z�7�*P�����Qf����1ϾK ���w�L}Y9 �3�u�А�u�ʓ�Ob�!'�w^�_���7���ہy9���r#����ju�j��Q��/�W䆍g�f�=`�88�g~pD kp�怉ۿ��U�i����V��cJ�]�y���id���l=F�(Ԙ��20�����+��0��!�]��� -���A�ԗ�k5��������ғ�Ƨ7n��]�w�Tw:����:s�tr�A�3�v�μ�n:��`���!�Ο<���+9�u������-��є�T��4#ϼ?��u>�}����b�6�n���>|c-�i��jV�~l9���rl룸��j�<)�u>Q�z�_���k������V��Q�5T����?غ�������m苤2��X�K$�� l"[�e��{��kYWV�O_\`��B֣&Ո��d�n%�Q�ʦv*�G?K�� -5,[H�,�0Uڭj��wEy� -�4qZÍ��^�)��B?� YhƢ�F$��[��.?*X��Q+����FO#hl��1 -}ѷޝ�u�4��ơ6:Pn^�V��洜HT�� .�b�_�d���5��� ��>����o`RG�I0S�_`R� -)|�/��|:�H��~�i���$�2+pe9� eVꎫ��^/�6J_o�ή�=:�Sp���f�I�����ӟ|l��`��y�&�LD�L�~z����������η`Js`J5%`*��)�u`�l��L��)��>��2��@[��'c_y )8W�r����l����>�6N��ˆf�9��g���X�`r�]`Z�|`��?���/`��U`z����x�d�R�r9���!���4��%���0+����/�������%�8(���φ���{���I��~�>�@��2]�J.�:�n*��Etm`���I�����D�T]h��hI=����$�1���0�`6����G ��)�0���n`���ҋS)�U -�W��8^���%���\ -���>�cx����I���S�M�c�e.��Jy�����[x9�g����WI��O��ϪR�d<���y$ݶ� -og��E�����FR4��Ԏ�.�V��/�(E�I��k)�aj]m�b�^!�?�������"8�^N��J���t�q���o��#5��ZV�ǩл3�����4��1�5w��ڀ�?��=�/��)��8��п��l���Si8��E�L��Rh�� �8��y�ç���'~-��`X�<jپ3Iٽ�7��M�Mn��&ס+�/�սw��>r�Ϋ�ݜ̾���2�F�{$G%�)�B��,N:xj�W����&�dZL�Ѕ��lq��<�j�c��7��{!�)�t;p���#L����m�K�zV��JŊxy�����hv��/7�N�+����xē���xv���\�'���Y_z�d��?:t�Cʼn���Y�?�����ȇ��e�ޅ�[x�G��4���o{�;o���m?�h#='�O�g�9Y_�v�-Geo�=��b;�f|�n���X���c�~�:��R��#���/���n��w���"�PE"���,�V�D�uz��Y/���K�����4;qj��{}|�A`��o��hm����� -���������Z^�b��z�"d�&N�n�f?���[ܭxt��A�컕�uѺL?���O�y'��/ğo�?����WY `~����g�@�n#f]����G�y� �$��Zџ��uou��5ע:#�u�{l:Z�<���b��)�Nkb����v�l_��Sș�4ؾE�B���٢���y�l�)�B��[�OJ����P�������I����'��Fx����V/ǭ��=�~w�(:Onا�ܵ{�<�n%���6���0A`�12ɦZ��D�ţ��6�ҥ����B��״F)���ɿ����^��/�Ӟ�r����:y��+4x��̡��0���u�w���d�����ހ9�ޥ����m��x�Y����Ui�@�ҽ���Wo`�������T#�\U;oc��gyN�[�l�%�pU�Dž�"�v7�/�%��Q��g�>+�����y��6��p�;Qn��ijc8�Ϋ�e�����)y�R�+?r��oi�s��h�j-��P9���]q�*��ZQN����*������������.�Gum/���$wq��y2�����z�{�����]�I�-'k��v�z蛢���>�>j1w�Z�J��h��\�aU�EK~<�(�D�t�DHQ�rn��0ŅFbk�r ncB��T��#�@z��I.���份��+5�%��_��U��V������c�$/k��1�s]O�ܞ��L�`���AmP��ӭ*~�jG\Ձ���C�غ��Y�5r��Z�Q�S -��I�:��Ȟ�̡��a��ӑ���=�Uf��w3� -���x�j4ݵ�\�z��vA>F5�����f7t�y�R|P���_��N�d��֨$���օ��t��<�|S�H.�2��{���vݯ4��=D�]^3�sG�_+@���~`�G���o�u�S�dKȩoBѭj�(��X�Y(R���D?+@�,��ݼ5z�#���7����<]�|�:�p�p��:O��^b��g�E����mf�l]�w�P��= S�j�S�P�e$��,`���p�CKKd��63kb���s jzsީ*�:��=���hI��Ʀ�!��iV -��b���������k;)0�2Pe�K���+�~�]���y���-��N��Ô��Ԛ��hÑp*d�����/�� -�B��J��f�}�� ���A4���_��t\�֭��+X����:}��n�+��#�@�3vWY� �^ Z�ezz5���Q[�]R��C���a��\9��XJ�����1�!*����ky勴��U�4λ�+��I�V'[Pf�c���oGz5��rR�z�-��:�9|��8KF�_6�v��j������Z��o*_$��j$���d�_�`I�~r9�*�VA��_�Z5�.���3�\��G��O>�Z�����u[M������M̖�����M�Y�S�����Y�9:ni - �;��=��!�����O�� ��a�jZ�U�|MWTk�Z5N���v�����;9q���� ����.] �y�LR�)pi����+3.��n�li4��Dyօ��`�?�%��T��Z�´1�Ӕ a�,a�D�ڎp�M@T��i�Q6������Y��cD�q�ہx*c����>us�{�LP��Q�=��hޒ�W�(!��G��kr�ɇ�M��Z�P6X-��%��z��O�ؖ:B0H�\��̤���@�:� -�ϊ,�����@1�L�Us1wp�h��%�eZ,b�m���Iz�� ��γ]Qek����YArN��$EQ0`� �����>k�}�s����nf%T��̒�{����d!h�.R �Ϟ����Ipu�Qv���8���+&vg��� i7��J)Y�.U�3W3 �Q_�uo_{�êXѤ�C��i�cn[���Pv���@��X���LF��Z�,��S��x�NR�-��� �+������ -��v���` -���|�8���0v�Y��_|.?�C�a{�̒0��7L�ۧ�W��ҿ���#3�ݰל�v�m���n��JS�.8N���&]��%�����D�֏���;a���O2,{P�����y�|v�0�\ﵹ��>d�#��j�Te�jZd�3�L�9̆"�_l}��ۺ5!�nw�h<;��Ah�ƅ��u���i�̨�F���u�!]�nE"�)��[�kX�5��'{��s�9����S�ӑ+ -����i��(fo���������k|, G�uF,�:��u�Tl��Y�X=�KwfZ���w������N�ɪw���S�V]>Ę�zfx��lpQ��YX�/_f�\�3���a�nn�4 xF[����v�ηɋrg�"y/�0�-�<I�e�����4�XM�`_O��������3����-�9ck�C��4�t��N�6V���R�|=�w���K�P�m?��Qs�M,`��_( -/�dr�7:$U��y�|�mzS"ΟF��I�gs�f�W�d;M����AU/5A��≖�����6Zz�]�tn��� ��M��7]�����Ժ�e<��>�[�t�Ô��3����M:Q�PX�]8�ba���Σ�)D��ţ�lkh�0�]� �zL���iq�O�]v� ݪ����"��g�"�HW����/L�1��`/�A�6<G�;v��V��R�v�5~Ҵ3"/D���CC�j�-?(���ɠp� 4��t�R�6�zp���E���75)�<��:U��k��F�n�}+n*�]�VYg�be�Hi����"���OJ�od�w�q/�4}�$g���=�-�"7U�l}�H�\�*{ZA�NzR�M�ae�j�*5�=����+���b��bP�I���U�b�zZ?~a��W:������� Hʧ��In�+��pK��kEΏ��`�]�ppQr�>;]M��;��w�������B��#��$K���]�'o9�����~}br���ig�`�����:m��5ݨ;��ᰩ�,����lW�dmL�f�3nw�u�:�L]�%r9����O`\�����s�Z���c�kY�+��'ޣ�N -����3�n��;���}m���2<ɏ&{�uN릚��^8�������f�r�5�|T�~@'�a؉�� ج����]���� ����ajkWM?k�&zۭmȃҝ�!BQ��nz���Z1(�Jn�9��;u������MD���+. z� &�% ��; ��, ��v��d���V怼�Jm�J'�MU��P��=�!�YN�~�gŕ� �H��QzR�ūދ��XPE7!� �Zm���C��,W(~�)9IX���/�绐�H��� � (yWԐ'�;�&�!�%��n�������� ~�*}�S�>��/�FN�����l�����D�c+�A�>��@9^9i�ٌ�a�hC�O`l9��z��诇6.[@�2{@�G@W�$���zm�R��{Q<�[���oJ �}��"����ӯ���>-� �}���@�$Xcq,T���_}����Mb:�D�w�<�w|pk����������]��,��*Ջ�܊�G�ŵ�e�P��^+�X��/M�q̼��S���O~��Ip|$!R�#��C&��M����"���J����s�vx� 7�)�`s]���U���� ���f�|�Z��Z'����7 }m�!6�����?k��p��O�́H�����5��+Q�[{��[����[і�v��k��1.z��nV�{��������.V't~?�R>>����l�v�.7���xgm*Ӯ��J��6�T�I`[<�I�6ܨ�.��c;��<��~��|��I�V���e�ӕ�&�3������&��+��!ָց�r�}��}�V�;�j[%v�J�|^�\�d�un�kKt�(v+\�ci�_��c:iM�J��!j�OI�gc�4�q���dG� w�ݢ�d}������Ϩ���s��/gR���ٸ�|�v9�X�*�]cS`��4�W�"�ny�-��ю�Q5���w���33�F-d�(���u���Z��/>���C�$����� -���-k�+������1�����ܔV�#���}o,��Uf�ޑ���������^��Wvq_dK���$�F�~���L�'��<�Zkc&_��t)2�i�����Q �9� P+����1��/F���_��i��P��W&~���`����_�{1;�u6��>kw�w����NÍ���q�8����y�7x�Z�Iv�2��i弬M���42/2�̲6a��A�7�W/�1ib�7VF��R�C�����j �"#�4X�K�/◦�I�'��� ���,h�)�� w�7�j�Y�6/- -.Wg�7��隿~k�]�� ���)D��G��A�SDn<Xr�ѫ��G����R��'�k��jk8G��0�Z�[�t�H��3L�r�Շ�wZ��/>J�s�_�9�'�ޱs�E����nQ��և�_�D�pN��\Z��DE �tC�F㑹��Ë/�Ç�#����os�b��Y�h�44�� ��cQ�����[H������J�{[p�DA����a�u�~ǡb������o��Z~#&Tr�ޯ���1%>�6��f����ʊ��Y����v�F��l�:W�j�����zɝ�ٷ�|��4�B��lQ�.�Y����N<�٥֣ϻQ�P�kf $+���m�@�e�'�v�o�izW�6���*��Y%@�ˑ�1�2�?�[�Ơ���^�|R��H5��z��ڽ���;�n�+}{:�])�]�d�/ ���GZ��A��l�Kj83�w��ݴOP��Gxc��a���UM�����ҷZ���{̮�p�F3�Q����l��Ƿ1s8f�B4����OpI�]���>���y9X�pi~aO$~c���њ���R��m.'�O����ϪYi`XW�[|����!Mll��rj:�<�� C�N�L� ���k��}�/�օ�d�:ۨ��9���8u��M��a�9����#/(�q[��x����u��9��[����rfiV�̶��WOݺ�=�#��4*����]���h�nwnQ�wHD?���)f�f}J{�u����>��-Yl4ow��9�7���P�r�x�+�[�� e�O��ɼ��t4�x�-�r�~��n���|43���R?�;�m_.��<;��vH�[o?��fzy� ���\e��wQ��9B�6KU�&�����/s�Y^=fJ��z�<;�f*>A�14�Ci�b����[�\����3[-=K�mR� ��1�y��/0>�1� -:ޝ�uȁ�j� -�C�Y��6p���Z��ʚ��bZfw�U�5�>�Z��϶T%Z��Q����W����|�˱�dg$!iM�#i�-�p��y��� �d�&�Z�I���'�ql�Q4�T;wg��P�G�=��6X<-� -�&��N��ԋ*K��Zh\��� j���@h����l��B��[i�f�>;����(�ݐV���s�ڻ�^Q��g��X �_EW�o�T�`� {��t�@�\S�)��P�)�YIok��4�����TYw�j�XN[��k�R�c�l�Oe�s��zAJm+s���ue�j�e�]K'w�HN�.IȾ�Hȼ(��L���͉�)S�'� -K���v���WF۳>r0k���@�8�yP�z{\�ڤQ絬��T�J�%�9�r��M� ��\*��u/�'�C6E%�5��e]:=!FBY[o��D�o��� X$��(<N��/������V��{�E��G��ޣޣ��jG�)h��MgH:�6W���l�B�"��-���!�Qt�����f�2,8�t�N�j��⍩�Dh�X�H!�ٶ��@��ލ;���G�%dz�sө�c�2�o5Rf��]�y��5��=g5�4��v����y�ҋvAj�kZ�ŀ�-���y�*�5�ܾ}���r=�f}ѣ�H������o?������`�`<�*�|�4��<s�j\>op\���\��Jld��/��ң��N���.s;��(n���d�cW����#�����P���lC���FU:�1T��+�3�"����Sd�s�K�|p��|F�ܬЏ9��W��K#l�>������r?0V����0�>/1U��5͡�|�>��4R�?��!�c&+�.ug%���5�DK���>y6�E�������w���'�Pk�PaTn浺�L�=.��Ƭ�n�l���1�-}gj #C�7��9����KٳΆ��r�B�#E!����(��_$7v�3높�o;�o����9� -�7w�u�N�{�#l7�XOV�xan^�T�B?DY5^�lٿK�����&U�6�GC�VH�,E!�Չ�N���F�xD��fhk&A����>��k�%��`!�n��b1+ǝ�uNjc�ܩ�F�~�}j0�Bt&�����+�@�� p�p�.C<b6��N�ߛ wRU�ٗ - -��I�6'C����[�L���wx�u������3)�6�*�ghq��Ѣ�BѢAqhQB�_�7���g�3*-.�oE�^���튝c b{��/f$�|>q�8�����$'�$����p'@��༧U���ib�hNay�*��m����F�Gv�T�A��!�S�1�S�Q�Apsِ��vA���]p�.� ]�����:�;�U��{=���3��9��Ջ�o��V��儰z�2��`I<��]�W��K���,qiB��OE��#�]���y�u��VcXt��ڏ��ZvYg��6�OŚT�:�nU��Ku�X�jY�jY�N�"d���)�zy���iwҏϚ�A����(HL�YNW -��+L)��&\������4���Ms[���[U�G�j�?�*+6U*�Y��\6�+�t�ӣ��(��yr��"���mYh�v~[���B]��"��/�r2�'��'�v��?B�{-�Z[>5h�?B��g��F1�4S[5V1h0h�3)��D/5�}�0`ǽU~M�#��γ ��1� 즘���Y, -tFUU-�����lR��� N��)�)�.�n�ZH�&���i�G_�m}�z���b��j��:���͌�� xz}B��|�Q��Z.���]�|���Ӻ���T�J-'�*�h��u�]� N> 58����Z�e��]D1@�V�+���Z�t��%�&?����u��7��:��CF���3۠�< t>F�\a��ӳjy��g�ɉ��:t��&��?昔�l����`����u�E�\a��U�h�I�/��s�� r�'��b pc�x�.��#G�.1.8Z�Z�$/��NLW���C��1�Ue��"�R��R���2��~�-�S������. �����bZ��6@�YmʀĶ}@ -��b= H���u90V�6S �/G���4�b�3�كv����b"\��`r����`���nW�����A��]�CS �����C�?·���6;�bq�z藐���eZ<����k����Q@� =�yhF�Z���/,�|w$kh)�#�o��A����W���?c���9]�Cl0JC�n��9b����a���T#\J�m~�|��^� -�N���uD��t��f������7�ϰ �`"J��&��_����k'w�m�K�{<[L0C�X�$�,0���M���A?{���=y�[�d�_�oܚ>=��q�ּ�=g�[��ث3�>�^+ë��y����5��)����+!��O��ȧA%�L�H�O<���3�g���-_���+�t;ӹ���'U�ڻEPsxSNdp+:���Z�k�s?��?�����2gx -�'7�bO�b��Jn�+y�ok��f0�sw��_}4�g�;���{�o��/�G2����͵z���h���vͲ�-�/u?��L�}��d����948zE$8�m*����>�F�n����Bl�iH��ݯRU� -^�Z�sg���o�|)S�Ӯ?=��~���-����^���r�����|E�әۖ�7iZ=��'���[�@oV�>�G�>mE�.���͊n��-�2V��[�K?\���n�Ad*��>{9�9O�b6e%�Ç~��Y���pjLO�V��zg�iO�==��;�a7���m�U��y��Fj�^�MzG���--K�[gY[�M3�ϛO�y�=�yzX�g�[N[��n�R��Ie%�B��V����n���8nU�I`Ǒ�����O���r��);^8��+�dΔ�f'_Ʀ�����{K����J�Q�%�}"V�w���>ͤ<}�F����+;YM��I5w@Cî�᪙A�(/ǽ��9�����<>o������V<� �RNס���h�ɤo�v9�V�����z;������ɹ�<$=}6���P>��e1�M:��gF0�����,��ण��!���5�Jc�4F�k��"���h��i� s�<3P����R��.��^e}����W�_��|-��C+�:; 9u(��./��W��Xf��|��ӊ���A���l����?�:�d��h��Ӎ�~o�>�>��^CiX� �J�>h���p�[cg��$���S6�ޱ'�zHv�=�8�D��y��"~N���Hߋv=���ߵx����o钠��<���i�8V�fo���}|7��h��3��-6���n��;6�)l�m -��W������>��ҽ�*=��D�>��]"}��aW�ۯs�ms�ٰ&�!n����U>WP�_į嘈�f�?y�f}�Zlq�{�g�B(����)�س -��]�$�K�6ۑ��Ʌ�7G�A�\>=T?~r���Y��(�wb�9�y��~�`s��Ӌ*kI�e�Q?͖�|vWK�֭V��ni�0�E��S����"a\�֨��,�G���=�������;������B�3P��7����P�% �p��uPbv�V;�w���1X[yev4���,� -��g���|�Y�4vVI3�M�'�MGr�r�@�NO����;8��/���8|�w 4� �)���C*�4nױ?|�}� �Ն��[�yٽ�&����mh�O��Y�5K���<\��z��U9�֪���݅��t/���u{�*u��v���������M]ܢ6�o���?������!i/�� -n�����H�Oʓ�{� (5~���n3���>�B�.�9'U����-�沪q� $J��Xof�4 h:��Nc�y+BG��I��^�"]8�[!n� '���X/UQ�gO��ݭ*ɼ���5?O���zxKi[����q���O��6�~%��ۏ�M�C<,ֻ�-�.5ܜl1sKY�s�����Mc�L����u�,*�����,j���ߦue���q�q�|����M����*^'�V$e?�ʷZ���*�ȕ�r3Y��� ,B��<���=Q�}�1�p%7��hVz�}%Z=��i�w�3�ELܚ˓w놗7`;�3��I�u�hJ���V���q�������J��@ͭ���U���O�R. ��A�*�朒w)�+�g$7�YJ:��d�I�g�9����o��AGsL@&�A���´0l3�w�EgG��kSsՈ�����.��;d�,��& iA�ǵL��TI1�V�2�V�����1FiΣ$�J$�`� ��Sn�Œt��Iɮ�u�b��bi�E�zo�ض�w#���4�a�S��\�$��>���80.��X��mg;�g]��Nt�?�����L��e�LM�+��Z�Ie�U9�:�J�"�r�`�rc��s�I6�$$�?���\q�H�xo��B5({��8���g2�Y;�Gp��穙~���_+ �Ee�vi�͇s]Eg���F�g��j�۸�r5̴T(�Օ����t��P~GK��S�l��IJ��5m D\�B��8����~����_ܻɏ����1��217)u���^`���/>�4�'����NM-�������mU�_k��#�f�nW��;T)�i]Z���YY�ȗ���2�5�>��H��� �C�)�0�����t�eKc~�K���/�� ӜH�`.�"$v>�Y�_���)'`��T�_��JE���#=qo1��Ld���Z�1��mY|V�d\��)�9h-�0Pj��N�n�!!�BF�xExl(H��ο�Ǐ�P��`��&�Éz��rD+b�;�E5�U�]g�[Z������[�{��s�D�+��<�����n����cB*�%F���`��o�Y������Ѫ"����x�\��zg�{��}��hܡP��ʉ�f#A�V����<4�6|0�K~Jo{��nd�'u�Jʲ�<_a�<�j'�5&ҕ�R��/VC��(�Y�;cvT�� -l�z�5��WLC���{ -oZm8H���L)5�y�s�"<���d�6.3��b�� �w�G7�e�:V)��v��"/T���&�m�F�a� �M�Q5';��Y����g�C�Ŕ����4�ǹT����~"7�ɇ�z�&Ym� >�G�FP� -������c��x�To�3�x���q��(�BW��VO��0 �����nZ�+�2���F7����e�Z��(Z���,�?�[dq^dq!��bM4~1_�0)�n�_�λ`C�ni��d��{B� �ҵڈ�,}�����%��{L<�J2.��3���g�s�(��ַ*&�b�*$�4a-��h��A*�k��-d۬7G/���0L,���&��Ľ��r[�N�m��S������筈ᮎK�Gf#��)̘�=�v��Z��9N"-7W��>|�6y� T��c�8�WH��=5��-n�R>�2lQ-�_ ��T����3�lx]ԫ�'}^��ǵ����Z0/p5�� k|sz�*Y�,S�E�x��Qk4Sv$�����+����۹��$�7�'/qՏ����r���l���rx�q��v��H�o�l�z����&��|-s �U���|\+*��������-�(AH-��U��W~KP��-u�Q1��� -_�L�3ܧvz?jǕ�B�j���`�?)�#F|���eC<��1Z��6Ի�jM��\�|��r��Y��b�ɹ-��Ӽx�vE�]ק�*찔�SW$OK5>�n�\9��<f�Ѻ�)r��)b�,3_��Lr�He�= |1*C s��w���O�[���>� ����Q��%t{P���� -Wb�T�����D��u�D6O��L��K��4����O�V�j�� -X��4=,��;���%XZ�9�#����O4�[4��4۩g8�����^����{&���9D2_��C/C�*v�s��>��Ϩ�;���N�2���:���V��m� �@����|". ���Q8�\�(���%@S#��t��hUt��O_A�]f�J_x�����E203��+��5��v{�R�>4�=�ilP�P�@-5S�5h��G���L@�U�{����,�A2jY[^&9�����Bҹ��K�m�6c2��W(�ͯ.��=���|�E���1w�]�o����b�m`� -�,��2�&���Y� �о��Nj����m`�;0��|��N������������ -�uD�Z��d〘a�Debw��?"&J��gy@6.C@B� �0�O�_8� t 'ekmh�H��%�n�V!��J�L�PϞn��9��츛�����W�~s�����d�ǫ�8_vH�~�S��;���4@Y�P��P�kP�P�Ի��T�'������\��u�vK�wd�f�"Bd -9f�'G��s4�*�g�� zT�&:�8��'���R{u�t�t�{��C���$������0�Op����g Ƨ��SO?&�G�~ř�������������[����ر�]y1=?�o%�>�Rz�H�����V9��=���̤')�r+���U{h�.p���� -���?Up?�/}�����IJ�����N�Lt�1=�!��0�$\>�Z�ߧ��r���-j\̛r|��r��� �.[|*#4V���V�z���pB%�蕍�!�#��r�[a������7����:ɯ>�fM�͟:~Z�>�|�z��p��N�V��իv�4/[|�_�� }6�c�t����LZǛ<�4ڑXÑ_���oc3�s�G�o��VyB�Me�UW�V?T�7�ҭ��%�w�����o���n�#&���e<�W���h��P��ϣ�S�g�E_Nn���^Kb���z�����n��v�6+j�\o��7Uv&.����w7���d��[���9_��)[m�r�:me���G���Q~���'�����p�'ß������ib�>��]��9��F{���/��Iy����v�R�ŭ��,������5;qz��ޅٔ��f�q=�$�q�v^o�[u?�[vX� ��Ma�"����s�����(��~��uհ��?��ߚ�n#������/��T^x�� �;��3O���L�ۣiĴ�i1�f�vVZN*s�gE��^��Yf�B����6�q7Tߧ���(�!��������g�>���..�亦�ݴ�ݿ�6�Kt��F�*��=�Ft���5&��B�( j`9�4Q��S�����;f�����Q�r�Qz ��\��>�o���J�O���r������m�g��L�y�/~���G�����v@��md2ÕU�(^wW��۾N�r?v�y%@�6�OYv4�j����"m�k ��dD�C���I鄃"aG�2�^�u����r�_��՞�[P�Y�v\,u[8���v��lFu�?��vM۟�Z�쟫g�H�����������Pp�Z_�0�^�^ZO��Y�ӛ~s����T�^�7W����7�W�w�_��u)�܋P���,�)������!���~������ -�;�ʂ�Ԕ��{w��]�g -�2���"~n��kmy�J2f������v3+E�ɤ���맍���0�S�נW -�ݺR�u��(4���<0��P�ϮZ�c��i��)gXb�Y�Yao*��.�N��q/���T����H߿�O�w�ι�(u�}��q���W�L��+�Bn1f�����w��)o�����������s�(�PTvhnۜ�ŭ���|u7{�(�V͢Q4���t+#zhlY}j�7õ~�O~�ӝ�bu�JJh�n��&��Y�h���p��ҳ�7MpkG�>i ��r��G��9���y�+��/.6���>;}X�D��!��{L����U0��T3Ke�u�4��5�'�V�zS���K���`�{Ծ3���&��Q{v6o���U DsN U�U3���5�ٖ�K���W�ŵ������l,魓�U��;����(�׃�d>�g�����St�����\�o���d��^*�G+�Л��� �}�7��@!e�y����5,-��j8;LU���Z�����Pj: -��������Nް�'KU�\;���B��"��aш�G-?9���xt�އ��q�A���%O�mg�g�bii���Y��֫�[ r�GuTnS���^�ϊ���֔*l���5�Jk�����{�R�E��VJ�^�ɺl��z1�K��+���Z��O�P*��4p}t}�t�s�>Kᕟf�2z��=�����9)�u�2~t��%?�\����p�w�5�RZj��U���'í�SƔ�K+���f��d��\'��t�[�. ���E<sdVt�+TDw�.�L|)x��K ���|�? -_���_� -U�u&J)x사��/�@��;\��B�{�����l�I�o��z�q�������R�״�ʧ�JE��d}���^�%3�Y �:�xn��m>"�"��m���L���j�������x��c.�>��=rAwS�� ��d�xcj����-O�� ��ϋ˅� OG�l�M��:m��V���J���5x���$'Yx2�>'����*WT�JYZ�]pI�®�?��������5��2q�����x�� -ԝ���"+�Dž_�N�Xd3�����8} -��l�*H��<����f�m�[��Oe5u�?�V)� �jZ� �:KM��EwΥ�{Q��٩�O��̆�ك� i\�ǿ;��r�r��d�q��,|_bTV�r�p�מ��׆�l �$� �^����qT4.�L������o��5�~�[��А�Ƿ��d��W}����Ϡ��G<���-q�3������ee�*d1�]��*S��~W�tg5�е�S��{Hu��]9 -b����źW�l�L��h�?E&����JO�����{,������]����J���6�_��OU�'N�6ɘ��Ӭ<z���p(S�4�ސC���.�� -e���.Wҡ���q]M�D_P�D?��sD�v�~�|ew���e��thؙF�~����#RշY/N�q���^��T,�/�fr�8�S4bGz�,m��TR�7���rt=�-S�c�`�H��+���$�j���%����������T��{˼��߹6>'s��y���+?��B,Έ�����^i��ƍ�QB����6�T�K�+W�;�J-f������aB�{�r�yn��-�)��k�j�X��O��)�[X��,���:��;Tb�m�(��wҒ��E�/�1�/>J�s���� -����KEC�3�n��O֬ի�7��ӎ�&��<Vl�}����� o՞�O���� 8�:�XX�g�L��YXAgUB�] G�TDJ���d�^����.��ƹ߀�=V��zs�������|1=l:H�Yj��M��ި3c$��-G�7VtJp@k�}���n� /���ي b�g����b�,6�)�z� R&�s�3��fM�e��fT�!�C�|6�$�F?�9��x�ևy�S_�����ڸ2�j�j8���K��ګ�z}1��.;R�)f`�7rP�^�\�렳Z'5wҊY{�T_ �ڲn��"!ZsN��2�b�t~t!/x������qqp�W�G�]=�j�G�\K�T�*]��㏝����� -��e�BfK�d�)m��Q�"��� -�~�5c���4Gv�Z� ��w�)���D�Ax��Vt����\�&�M|�Xn�V��.Oՙ��J⊫��v�e�v����8`���Uq�)�r�O��j�]��t�����_���E�<���rO��s�)���9�<Y瞥��?�l���^���(7G2��)�ɤ�rx?Z�ޑ>֠)·S���B�A��Le1q���}]�n#,Oy>7.��$���%�y�5�lw�^"�[�앲�Γo�j�m�DH����o}h6Lh������Y�(�m��p� ���tܩRn�Ui�ҟm�R;�lM|FΛ�H�=�J�CdS4��C�ZU�@0%��Bso���Jk�q�;��`Ix_������F��=� R ��_9!���]�R�&�����G��қ���8~�/��E������-�L��J�[R�|�� ���c��V5��ߘ\�|"�j�QͳY�������) Z .&@f� ��4�]�¹�'>�L0�" ^�� �����"��_R��z�=�]����w:�A�S���u��>�qa����6�m� ��E0Jͩ"���R� $*4������@� P)��B�-����� -��� ��z`�%�L�Y-�}�FpԪ|]'���kf��;���ŏ���s�b�WF�.9}bю�Aw>�k�{ӡ�--8z���z=�����{����-`˨�71xq�8��o��h�@P� ���aiM@�H��<�4D 5�����i�>�WF�ywv�.��a�H?�-��?�}��R�g��G���;�����D�2��C�?����]�c�m@1�%�T ��A0@���jj���K_t������菌6�2���'܋7���3�o���68�����G����������������5/��<K�| -���s,,�o��oj/��n!�_����_|t�w�3v��[d`v�c���[�w���}�ݒ����ܲo���A1��j5y�ۇ�.��{�L�n�>oE������V�u颿M�|���_����8��N[��@��i�D K�.2�L/[|�e����2z�VT�3T�5l�V���u����vY�-1�]�x��zr����t� yB#��]9���[����_���s��茳U���7]WY�u�`_b-��T��"���(VX��Б��<s�s����nv"�#_�-�Y��ſ -�Oޫ���O��?��=��K����㽨���ڭbṫza���C�� Q�����&C��-�\�TQ1ev�r��~�mf�/�9�h�1 -�!�*I~�� ���LgE�q������w�ȧ�ܿ�iJ=�e�ߋZ�]d�\v�-<��surr��He5w�l��;X�}Vѽw�{تo�;�m�PLR)WI�U��vj9d��o"~Z5n�!�%��9���,�Xhޣ�Ү̓� -�C�ݘ���_����N����̤�U=��g�)�'|�R��Qܞ4w������Mٖ���.X�u.��&��*��%��Dž�¯Q���Qes��TgF����i?�hZ�����q����~#��l�����<pM�� ��ᣂ�]�E�$��_�[�S��qc{n�+)���U�i���l'O���z4Su6 ��t8�Y<�٫ݔ�/�O�3%k��ГH��Fn�{��R������]�'zr��[������|��b���M9�sz_�w�'W��$ -��̦�h+�S���X��RJ�}�I�̃Ӽ��r�Iě�����R�f�h��C�nmdt��~fc�W��I쇣���M�4F�M�<?�V�߹��,l{�_��x���C��Z���8Oi}����Y^�y��ќH'��8�v���o�H��6گ>�h���sե�r���}����i���V�`r�[`�=��t5E���E"���ӑZqv<�Q8�g�bj��bY�ʿ�����{~��l<�S�L�w���p�2V�C����PI�Qk;���nΕ���m������x_C���SS���A����~t -�C}��O�q���fP��o��}}�X)wn٥2(_V��.Y(�"������P��I��e�u5\���0a�����VV�L�{�t��� ���dMh~C��҆� z�d��n:����N��W�Tx�������5r�9���P������[U�^�G,��E�^��0�Α�* -&��w��8�&��B���\��|8�;���K)oS^��5jW5dž�-1�x1#u�<ct�t��C����<6��%�($JT/o*���j�*��=<%�"nVO�(��q1�M�PR -�UP�n������q\���ڛ��n0:�&������X�V�|Y {jlO����h�5@G�B�?<ժ}|�jh.Hbɴ����J/�l=�O��`cE��K7ꥮ�&�n�\Y���\gO�����ܹ��]�G��v0��q�?�X)>@e�xJ��-"��Oj"Y�e�kV��ow� �-|��;��ݎ������RAA��Z�W�^�����qỼ�����UbA�V��H�H������/i@[�x�hs���ʷ�?�I����?�kɘ��E�_�=+�����ԯ�hD,v�O�VF�A2\zz<�}gW�j�Y��y��ȹ���OE�\w%p�n6!yc�M���\�\9i�/�$���-%���('��ͯR ���J�5?y�|;�=�����Bm������������E�r<|�wc�t��F�5;}b;�թ�{��z�,����J�?Z��^������gN�Œ�hx]��*�&f[��[YW�&�p:>��-����=�q���rWj��p��LJGg���?���[���#W�ۚ����A��i�7k�a.H'�-9��GS�Q;JC�n��M=��})�u#���Q<���ڍ_A�|�*�c 6yoH}-�<���� �D����<;Em�dc�P�nZ\��xx�0�ּ����8.����J��TϯI�̟G� -��E�95Ls����g{�X�wU�i� �3��2��ac%�����#�t�+��qYW]�Ǎ�:�O�5q�#M����o6�l�le��3�D����k�I��݈:eP��]_���9����z���c�������V�ys�i{oUn�Pw*�� -R8�U'+�P��ãon.�����x&}n~r7�I90�t;���;R�ͳ���X�Q� -�&�R=F��N{�|,��T���j�F[��@[{'�1���X��]Hh�/`�����q -^�r�J<����bj{(����SJ��!A������p���r<��:�̍;%8ڱ���*z~0� -2ڌ�����l�{���S��*n��ƛQ�1|��gP&��N��pM����>��JN���5����M�����v)?�'�kX�ɧ��Xc�#���(�'��Jώ6,��y�#m�CVy4}f�`s�_�6��f�����$z�v�R�[�:L�4�H��G�3 �]jJ���茘�#�m"Gݿ)���(m���=�Rt�!^,r�� ��?�9R���<\�a�Z��d6���0��Ѥ;�G���@b��ՠ=�c�^����uE�;jtb��S���@�mjD�|aD�Dt�9gi��&��۴��3�*�=[�#l��l-���X��>7�K>�N9Z�z��>hA�}�oe}��v�V�H -�l�F��K�nd��J�Ez�`B2�ٜƋ������+.�����[ݱ��������U���j�կ����'b�!1?51ϋbn�#b���(�]�`3M�;7��9 ���h�����������_�6'�Ե�I#_�n��xD4��-�Z�Z�Mk�%�����y�;ڬ�@$ݔk�����|�`^fÝ��-ܲ�I�6`54`?����y�bNP��T�Dlb�t��V�^叵�����@p�v���y��M��Rw!���X]=�q��(3b�u�yQ��;�9·Q��֍I�pl�o� OU����Q�'��wTH�^m���!X�Wj�J��z�0������?d��+�C�p "��~wY�C��k-���DZ��0n��Z��A5��i��hUkpP�p�k\}��J]�$hI�#�[�f����jj��V��� -?�R����TP��C�j��ioU�X��_s�[�:�t�Aa�h����o�=�Ӕ2K��3�ڃ�ۧ�M������f��Zs�쇬Hn��U��k�J$�tg�e�9���q��nC�*�`�+>#eY���}ܺ���8��nb`5�J�ą�����6�t�)9� -|��s���~�F�sG������)��3Ah��";�x��˺�p]�����Z�k�ZӒE�Ll�@^F�/�z{�r�v����wMpb@@���=��0˰�H�xg���R�x�)�N�m���4?#h^Jn~�A[N�~K3�\�)�]��j:�<���4#��7�9������PS�d�e`�ѻ_�/��0%���P�g��k@Ѳ��`���P�?e�g����J:{}���y��#� -H@���!��l���~g-��C[�7�U3&h��m�VSjS;��w�#���V�c9�w�(�����^&߽lK���#*9��R�q���v���ab�����\[e8_�l�e�ۃd�� -`2��e�#�v��v�c�[~�^l����h������**��iy\C�Z~>i�lq�jM?�7! �;��|�G����%��.k@�{�-�-���~e:��g�m�&�zY>�#`�m���J��%�Vg��6��i����<QM���+ �^�(\�Լ}�����ї��vs���ռe��1T���k����mcY�r ��l��.����M���8&���@@$� ��@�' { ��m>�ܲ�%q /C���5@A�}��PD�j�a���)���#F��ET�Ȯ�D�zV������tO�Ӱo��G��I��l�����7l�'�����������B�� -�g��*�ڵ�h��u������i��h��|�_>������i��M������%Կlƥ�ֵj����_�~~�Z�O��z������Ym?���қ-ОH/�)`��``�Y���&���a�% 9��X���c�� �Z�_ �g=�i���ؐ�OR�s �/�W�e�a��IOf���.�?� -o=�D=@���Qݝwy��x{랞�?�A ����sę������wG���n�' ���۟|�3���ӯ�����B�\�o�������#,�j�*4�E�ܺu��_8p�F:W� -Wh��^vT[�4������sFg������;���xM�C!������w� �0�#��p���:-��E��̽у���{T}d�r8���������7!���(��n)j��^�����aç��w�����lvO�nv��'�l��g5\��}庢NF5�����;f�Q�o2��W�)3������[e ɗ.}s�)��ONN -�TL/�~}����v�������� ΉU���y��M��ra��&S���X_rw[D�!�ܼU��\+O����Ƃ�G���!z�V~��ɧ��Q}�S�uUxER����pۄ��$�?��)���%f�7���c�_ih �-o!>� �Ym)����̡�u���Z����8h ����������>�������_va>~�ۺo�[E_O��as���Ӯ�<o�Z�խ���d��!�Eу�y½�9�ٙQza�{�!����q�x�0�.�p������3�u�y![[{��<N�B����-�fSZv����J�����ݨq!�?8騾���������7I;�pEm��%w�.���~��Zn}�5����Nnz3��:U�'��L���EO$�`�X2W)��q��1�MF#�q�Ԇ6zUC�:C�m����y5�!3˗����w�#�7N�v1 -�Y�*���։b���v�b�h���>N�^H�I��Nn,�on%���Z���ی� R0RqR�Uw��� �I�Ĉ����f�]`h�gvn||2�Q,�'7(]���ڼ#��?͍����d���lޯ��8�6k�5.y�7מZ�62�0�}�q�m�U6}�/L��:�:6?=/��mH�7o��� -v;�kVP�V����c��i$ ���W�T�y�P|�o��Mo�D��n].�����~>�~Po�:�ӛ�'�ON�x����tn K��G�d�Wa��.;#.|���&CJ�63�m,n�?�n�̢k�J��ռdh�5d�sT7��O�0߷�=M��&�F��H���E��@�7�B���EO�����߫"����_���}��b�:�c�YHB�?m�D�θ�6|a�����K[ǔ��p��r��� -;}߫���v���U��ؓ�k#E�hdU������6�e{����a-K�әR���lLK����k���*4�ȵ��!Ri+q�Ӹ�6i��[.��vv�o���40N�W><������kY3��ґ��Ͷk���9韰 Ү�x����^�*�S���`8}z�ų�̻��R8�Hw�T����z��FO�������K�>�S�N��3��Z�����C�>�{�]B�p_g߶\���й��{䠗F�~R�?d>��b�����0�$h� &Q� ��.�̩�]�9��ޢ�����d�A\L�8'Z�� ��4���&���pD� �ܫp��b�'t}_9�LR��[u.��<��4`�����k�غ4ܡ������VC'�2K�q��/��4�@N��\ϝi7��f��]��c�)b-,'\� -Np�1�ߩl�v_x�{��#n���Mޝ��nu�J����!�t�a����h0��cv~%�)�y�&}��)���B ����'`������i����&kS֒�-�GJE�7� u�^ڄ)�'W�~����/���78��P�����"�=l�]��%+O�'[� -(���[H���M��ͩ<�-+���g���j� �v<� ,y�~ԗg��t�4�xa�����D����%.9Y�z��_�q�s�p��r�����}�]���,��M_dz�l6P��Q{�nm�9oЇ�ݧ�j�AI~�.CT~�����������9߽u�~�n�v,���v��ڝ���`�f;�K6 �k���:U�71p������+�SPW3v��֬|/}7ư�{cփQ���f��6��ں&��a��5B�n/�p#B��z.�.5�NWW?���f2��m�� ������} -�k�ў���,���b�*��V�oH���m�#"�>��6[�e�* *M���q�m�C�L�`�E��^�F�Y�:�K,���>yS��-�$uS��U���<���N|���P=�+pE��-�����=1 -�mDӃԖLoe�[�U?Ӄ�M�%�,R�>�m�3��iv��}��*�Ȩ ��v�C����#ݎ�����<�۴�'�Z�N<����j��J�ו=����'���;<_���%�:LqF����cδ4:Y�J��֚���I�w����w$P�0�=l�w�1C�q �Xu�Q��æ���"2#i�����vd_ @8��Y�\��@��Jn����� V]� �QQ�&�1V �%Vu:��1^ -�-��R_L���J����lw:):���g;�����ً�2� -*�V -������x�>tbʘ�~��}��=\�?x�Pd�uj�>�7���e��0�� ��,,d0���� zh�7�y*�����=o߹A�B �x%��~ɫO�ѣ2���i6����==]**H�Rk u9a� ڃ)S&'�[":r��S��%�[����eh��N�� ���߆K��,=nrb -�!n6���6�rǀ���4�30K�L�f�����~�� -�ա���;Dy�T��?tV�h! &�d�"e2Z�oho�."u�Si��nW�6ZR9\A�b�y.�VW��ƭ��_��[J?�L�����Z� EԺ�-�w.�Z%�4jݻ��b���7�(�A�(دA������Η�����A��-/1�n�G��aH��nyϾ/����a=�t��p��*�<�Ь?V���wK�z�B���H.T"�yM�ěZ��U-a�}�G*���/[�)c3�;U/]�\�t��r3�qي\�@(p���=��ns(�Jus��e}2�Պ9Jds5�J{|@_� �/�.�6oD�vK����?j����U���\��5�2�����n���rKܸ�Q|������{��O�yavɾܢ��r� �rg���\@������dd����{�}�WG���ވE'u��B�W�+u��d�ԢIȄǶ�5�z�B�g-�Eo2�@h���wI9_l�0�< ��7:��MTr=����t=*��0{ ��3\�!8.��Р94�Cmr�4a�5�!=� \�)Ŷ������Z�8�ϱҞє}�ix����@��T�W*�t�.���K����3���|9C��M��d��6� ���4�\� -4-)�i�8�p����z��&�S3�G@����9������M����w-��њ�3X���(6��pގ��>�_���p� �s 6��b�_�!� p<Ð�pR$$Gf��ճ�h6�>�e�O @"o ��@<2{�/��0���r� 5�ߏ��{���U}�Ǩ^=����M��jCΦ��!����]�z`i����rn��>��4Zf���/��W��f�9 ����A�Xa?���Cc��`M�����&� `Lg`X �p��k�.��L�>NG<,êS֍���jO`�dW,v�;�L�u��iplM������,~��f�`��?:t(���+Ъ@�I��Nd���@˖|���m��I�sZ�9��0�\i��Rp(���ׇ�í4��tdxᘭ�PՖe���HP��&����m5�ϴ�=m��F(_��p�ߖ�t�F�1��&��o)��w��|��fY�}���{����@l(@\]��)@��@�@ -�@Rr�R��Ƥ� d�AN�B��i�~b�w ���/��X_4��T�������<�ӡ���䏤�������H���)���Wa��j�?4�*�$U��h-��z��z���zW���v��v�]���ڰ[�+F��!�GJ�x�. -endstream endobj 22 0 obj <</Length 63950>>stream -o��ۙ�����W�~:�� -�����+�;����:�Ŧ0�����E6����f���{�C�-2���[\�����p��O1���<���ߘ^��'��������#��GX)�\R����9�,�̽8 &��f��Ɉ� �쮐]/��?������;�e��#�3�{��/�w�ؿ��_����R{;j�ߜ�_L����H��쭳F��꭛.�[�:����@^v�}�'�2�l~����?��xXG��sӧV��̾�=p����|�T�F֘Vg������B�?ٷV��������&��ׄx������y��]���⪞���~���[�>:Oȼ�7���"4�)���a�Z=�V��/:���=����iD����]���,Mb��'������f��s�KD��R��2�����R�ʯڭ��`�]kϜ��^>~��{�֧�^<����*�xG�U8�k��\�n�|����!������%w�^���Eq�T��<E�Z�ҙ5NS��ѣp���� @���z�\J��W;��y�������W�K�5Tڋ魾�1Z3�� -���,?����������Q���~cϡ�j4��{o�X��Ђ���l��?��S�>��+�}��˶�B��x`��\<L�k���j���2���Vf)��)�KR��vݳ'������,�����B��?Q�m��P$�f���SZ5�F�jO]����u�R]����}��$�gR\������8Q�������M'u -������y�_ ��"����u�?��j�������VT�������ʴ8���,D��:���Ô:����^H���)�)������x�e����o��Ȁ7m'5��cayux5m���S��8��oO�P3T*]ܨ�A�W�?�`n��˫_Z�_x��#a���͌��;z4���q�{!ճ'���X���I��CIo;2��I��AO�}���k��Ԝ��j�zN����̦�c��'a� -�Aq+x��X}7��M��:�o�w7��7���7%�����O?S�Ɛl3�nw�i�t���ДJŒ�\sR)j��!nd@ ű� c�tP�vu����Ojf� -[��oM~6<�2��BcШ��އ��^�lНk��R���0ګ�b���/B�y���1�p����:�_o~�i��s��:�I�N�pDz�>ͨ��V�P�9�Y:O�e���:d���Wj���E�X�s�vh���c�z�"��&^�(1|kÉ�=4H���:>��ޓ@;=fa���� -���H(����E���ˊbL��D �dcf��Rj��3�tp�S�M��ıA�Q��·���1o�G6�z -g@���F�7�⏴sO5��k�V*�<o{��y�\宄;P�4_�.J>��X���ܠ�\����Gj;�~�~���"g��@{�v�B����*�X�y����M�^�32�� ���@�g��n�i���Bi�)/����^.m������(��ueJYuKOk'��,ת�S��y X�%"V��9|�_�ei=ϓ�v�{��C�����>ųv��T��l����t�QgtzW{���cLy����lڈ�j�5h�:�����ە7]^���\�.��e�@�7��h¥��<��pWF�'4=_j<%�q�Y0D�1�v'ĸ�����6j�f��ϊǍNJ��d��-OK9�w3\���H���]̸�;$t+ʜ�+��v l�g����Xl!��E�P��K��뉇�=Q� -Å��7d���j�=��������4_���/u��c#�ʱ�Y�_Y0Jw�j'��Ϣ�\I#��ok�`�C2*z�E��C���x�rk1>*]�j��* �nY���C�bw���n!�19�D4�2�� -�����g�[X�|v>�֤�7ɰ��+6Z�l���1�g�h�lo�{���Y��s���;�<pu4}�h�uT:��mvx�F��_��^�-�FW@ �9�6^K�Z ���YE��-Z�Z\o�4���=4:�v(v�T���l�]�U��3�q9jo�ζ�/�zo����=��u��ڔ�_T�E9 -��T?I�J�<�d������әR?L��^2"/�-���80��6�iE�7�x$���(��sܴـ:�����RLqLu�J�>����Gפ������`�5eϐ#����>TI҇ �L��-M��xw�:l�A������9�3\]<(�q6�3縻����h�O�N���@�0��q����:���.fQ�Yv ����6��z���ԩ���y���֩V�9�7�$���c�8�<�w�������܃�Z���m��q��W;Pj�`�*����e��ᤗ���8�ϐ��:���ve&�n�;���EE��u�����d�X���<%߆����PöO�� �I�cw�oR����1��u7�9�۶�õ%.����z�s��� -�- --�e�w"���6 -ٌ��O�ޭ��SG�uL����?6f]z��n�����.H�o���Pf��5'�(~{R�\-� ލh��o�a�p�g)��ZR�9�b�bJ�K���O��9�]Ԁ�:��T6�V�O�|?��H�����&jb pشtș��n�{b��}���{!��lǯ�>V���/Ks����F�|�tɽ��4��Ծڊ�K�U��IL�K,Vm`_�2�����Y�F��lb�짋�B�&�w_�Q5�6�-J�j���KX֧��<S9V�^�?�����a���v���."���$������n?^�ERu��b�F~{I�YK:a�%G7�G����>�4?3�i�N���t#3o4H��P��m>�ԧ�筞C��� ���W��ˠVV�Z�#59(��ʼn5k�51�l����f~|9�rNnR[PQ��<�������1g �Hl�LbyH-y��r�`�6���� �u~r�>��z���: -<4�z -$��fmi�'�.^]Vבt�B�P��|���}�lN�I��>��(zB˦2�ˇ��}B��F���'�)Q=�MaXl�9�h�^:��Oj������c\$�ݶ�o�D�ٕ�]Q�5�agE����£�M�|5��JU����Ns�+Fa͕��I)[՚U:��i)�]$E���X�Vʇ�M��KI��nFz\��И��ਜ਼�k�Ob���Ԝ�����`R��؟2��6�^usފ�v�M�U(�T&Z���ڼT�g=���zW\�.��D�8��O�eh�`+����c��8n���,��)j�z5} -l���k�J:{]7���/�3;���!E����f�?&n��7�<K7��|��f�� I��E�p�~n���~%�A�����j=�{C���1�Ֆ�5�'�}��a -fxA�������ۢ����.��0�x���*������W �ӹ��w��]4��4wƇe�j�&SEuoڮ��w��zB1dU���l<^| ��Oy����6^�������3�o���K�_�m#Mh�q��C;�;��ip�r����IWt4v`>õ4������v�l��!,m� -��c.�7_���E�W�E�yKp���j�]���������p��=���k ��W3�P��6C��zy��^\�>O+�Ҡ��@���������<.He H5�d��FA{)�;]��� -�W3Ti�TA��pDH�B.�X��gyŞ��� ?d -���<�hS����t��6 -gPq��<����a:P�d���:e��L���@C9�Y�q��!���H���&��Z��tb0�~�]���a�eG -6��qկ/�[ -%$��x�t���`u�0�� '����0�zu��(��������V���N�v@��r@�Q���{@���֘�-���g�t�5�@ˬ��ʇ�"(����8^�lq�Jq�c�;��w�Qq��WZ��?����?�����6�~�(��_�k�����j6��3��{'� �"@ ��� \姪�e1{{y�"��bU��4���e��` sk�Ќ9�c)��7*N���թ��G����C������.�_��w�6@.�K���@��@ݫ@�S����E�І��I=�ڣ�z.L%�ʿ�~E�Vn�5���͇�?~9���v�5zO�w����ג����g��?����@{YE��pS����ٴ�0��f��Lj�2|��~��������h�'d�ߌ���' ��o�AO??���bA=��}p��'�W��{Ѹn1o�!� ��W������ ~����zrJ�x��>�q�{��O�K�ӽ� F?�쿧����ӿ9��^d�d"a��V��.n������k�u+Whԭ_�ؼ4<��u��O�<�y�}�\�x�U�H�u5�!sp������]L�[-�6�Xo%�f-�9��bo9?����V�c�~|���G��w��S몞n�E�=�3:M�'|���8>�~5�B�}@�y�N�&m$��q����J�U*@nz�;����̥g,��*\d��5��E�q�'A�L����K�v�{Q�W��3�#��&��ԯ���/|�i��u�o@$V��� -:^���?� fN�|���%8~���b��F���6��l�`�0��h�b1p0ʝ�F�Ôz����w����������ȧa�}�,�gt�ޥ~i~��X�i�{�&���b& Kp$���צ�J��s-���˳ƺ�=-LUD�lN�6���;��H�gEL�}|a����9��GuR�����f�q�+�Xk��J��hgi��;��?���Q��[���/Kp�]D�W��k��iֈ���?�����Z�)uv�O8M�3�0�{�N{"����%���U�Z-4G;=�JC�F�Y8V�9 ��0�_L�v;ہM��쿬���?arWn��[a�%h��y�*ʬ�d�So��ԩ:��[�kN�W)�D������٭��m�(�������ơ\v,� �N�9��5�v�c=��l��m�����ູH�4��6$��f�$����5<2���t9 ���6�gf��*\S���ȷ����N�zk���yo����(+FNjx�����iy�6��6�o��|�C�X�;����Mp�Q7)��2������Y��q��k4���ـQa�U��T���:���&�[�+.J9��E�� 9�R��{�vc�-����p���Lbl�A���=3�i��5�ʄ3(�ى�@l�'=����le�o\��f��K=�ǒJt���{�\��h�i)��\)���3�Mu�zG���-釟{c�3{)�k}fR�4��=��y}����c��Ѿ9�����CpmP*!߽QF�Ӧum��{j����H���Z��UG��WI��&~>Q���Q�����]�X��e�r�rI빷��� -��w����� ��>l~��O�W��ʍ���nGP��Ź���r���l����O{;��<t���<�h�'L�c��{������+��o)�9w����-�ӹ�\�SI�ԟ��K�Al0!]��2��P[����Xsܽ �9�X�~حF�(��������NLz��"�]��_� -l�*�G��F�wu�0=�<�+!�ƻ�`���'A��BO����Mj��{�Ƣ�n���u�������ĩ���C�ɵ��w�����v��~&�,��l����?��b��i�X/�p��=�0Gm���4�|�;�pBQ��]��E��{�2wR�D��-�hE�L3��ԇTR�:+6_CQ�;C��l=��K��?n�g�w N:�k��ɉs���:[�{��]��s㶊�`[e�v�����y�u�]"^E}���^E�ct���ׂX�(\G��ڎ7��b8�˵��)�I^8Ӛв>(?�U(�!B���R'��^�sav.t��X����\.�.[H��M��VG��+�)Z�'}*mae^�o -��B�J�N�a�N�dnրxf��&�ވ�]�歩2���W �nf����K���;�3g��k���X���`�;�b�<tI�XYf�OD��>���4ې�p�ѝ�F~��R3��,�+��p���;ߓ�=�3�f�;Aw4��9P!h������v6���~��,\�9�:y7���L5��Ψ/���� �)�x�{�w>$���*��_�(�;���amhMS��YQ~3d*u�>���!y>� Ą�C��p�`��>-��8� �����a�l$�3���e5��С���4���1�φ��sb��C�]�S;��v��r^p�ޱj�-��S�w�^��i���P�u� ��J:�&�^�'�-�GLr���E��O�%ξ�Sk�s-o �� -�,�?|�炢�ڵ}� �% ��bFQEQL���Hu�����*Qf�0��8bV�ˉY�{�Ym�J!�Z��?}A�n\���!}N�,��ZCqY����w�Y%:���奔�j��؉��2B/U�T��{����Ţ��l�s���)d�`g.�b�%}�f�/T�C!_1>L-p��m]��&�ID�Ic\����u�M�t��t��hK���a������9�N�ih-^�#V���S���P8��8f.��O� ��+�b�0;;�[W��nK�j8���}���#�gХ��M���$��T��#C|Jd���[ܑ�b1� -�o F�y�ڝ'e��j6#2�:��Z6�Ag�OlǍ�o��K���M�i�HM)��k�����εٍ�.V��A� fݿ�L]C���.�4I�L�3]}/�рK����F��~ry��� -�K�m��ٮ�Ow���UM��ܾ��_+}Ư����3��B�fX�q���nj�m�:�:gdŨ_�飽(�l�����aҝ&�z���a>���y�X��^%��:!�&�\���^����GL�'�_�X4�y $���HpP�h��hRʨ^�U�J����C���gJ��ل�Ms� ��r{dW�S-KTN(��kT�?���M���{�\nI��=w4��7$�&�����%W%�08L��tAlM4�f{ȪT�d����y�x4��m�2r������'O�;l��_���Ji�����f�k0�3_��L�/�}8X��3�-G��~�����K��[�E��� -5c�%;�+<��2�E�|B�Z����N��ޜv$L��b����y���>���tղ+����z��LqVΤ��~&�ٝ3J�E2�T̼ER�@�rz��o�������fٳ%߸m ^R�ʭ<m+A -W��d��&�}���;�̐u��s�gP;̝��1Ǒ�;4�F��xv�e>P�Ȩ&ŧ�MKCE��Z>�n -.�P������4�t��j.IŤM -`r���N�7�Tv8x�v�d��� Z���l̰���;ڲ\ꌻ%�D��;�S����H��:b�e��G~w������.U�T��}W)��p�9��<N�P��%�11�#��[�+X -��8�k��C�T6�W�x6G�cM���9|�tژ�|�|�u���ͯU�ǗjUk�ā�R�*"St�yO�elr���� -�X�M��J��,c�w���k�*��BQD��r���s��b���� ���bD{@ ~�&����Ɠ�됶�l�=4�#�'u�[S���M�r)O����9N�� -s0�1-N�]'���.9W�4�d\@J�a��H���N1�g�K����%[��R1�1�@���Z��R�Ylş"��{E�Nbj��F��K����sW�6�-��vl����c%DhOx����R%��u ��aS9��:��J�T������e��6���A��A��Ӏr�R�N=����Zj�yƘ��r5Pl���������o����J��S�����)����ī�1g��u�9"�t��� ]��Ѕ`��v�h� Vp��J��pR�>~0@����O������@�,R�I���:`��9`��0YK�$�bx^?��nk����(Û{~�g��&��倯^�. -��Ն�{��|]o����d7z���v��|��� -���p����c�����'�(��Y���;lA�~��c�B<N�B�)��)=L�ʧa��po+���#O����~����x ;������O\)/T�T�ȩ_�I<lj���4�������Tr+������.�D���n1���{�+1V�s� �t|���v_��ۃ&Sm��c1IcV���.�|~Ŝ3��>��u5/fE>�s?:�_��?��$�!�|�L���ܿT�D�e�(v ��4���0��@Xb+ �) <�"s���:��neۺ�S��x��8"3=%r@i�KPJ�(�����������ӎ���P�w�D�(�O�C��]��@�9 �MH�`���z�@��e ��.��1��ՍJ�s��ӛT9��7�d�J�ǧZ�^��<��2�1K����op�2�!۳���x7?�����#�O�J�x�j�� />z6��O��j�% -� ���3�N�dy������^�<�7��\����㟩9zBgf�˟��;q~D�9 -�3��Y:|�g��FOc_�<��P��7����0�m<�>M7+�%ɷ���MFH,�����B�.аG�Ћ7c/���I�_��F�tVD��@�4r|q���j_9��DR�-v[lb�7�T�F��%d��}m#�ku�*��"�R��/��~��z>����X�O%�?���!$������P�E�q ����y,3G'�A��A�+G���O)��@]s�Ve%�L3p)�Zz��Z��|w���rőo~��3͟7����ͬ�[/jh'�ٷ^S;�z'x�9j5\�C.Ǟ6���B�U���^ޯ�(����:�:���O��W�v��c���`�ƐO���;䬕�����=f[�6i��\�,O�B��>������g<���g�|m�t��i��!�6�)5�u�z"����$���3������7��J?/�~\���H�փg�R[��K��y]�����m?q���u��}_M����#_�@#=�������x�MC����Q���bG�W�f.�f�.���ml=��u�r�|��hHg�Io��?V��\'v�x7�6�>���R=��<�7��J�[��]��6��S�t�8��-N���,�X���������̩x��ʽ�ݤW��TR=�U���b|CN;�y��h�Xl�ȴ� -�G�Gʬ�(l��nhVK̔-�%Z�hB5$�y$�$i��g��l�U�)��C�[�#_�(�?��xW���y���� v�����]W����۹O;�:�E�h��ݪ �o���ݎͰ��5�S�c�8*߈�n0/�T��g�v�YM�翶���+f O��t���s��ң<i�ymrrkֳc�>|:f�s����f���#�+��TGK������VP������Y�j6M,구V�ݵ��`����y<���g�m�3����,�ٔ�5$���U��w -�*tWz0�u���t�����\ݖcï�ܯ:8��!�O�S�{��.���p�U�3�Em�n-x�~��!��gity���GL�ν�b��VK5�3*���M�����w�����W���"�WT~��/�Y��H�IR����e:�n��W�j�����4�q+�����kk�~L��A�<���3����S�u��ժ2ߧW���y����G;m*);o̚.n��]���B5_x*�e -V� Uz"��Ni��N�l�q��Vo�W�^QQ��U�����G��)��g����rI=���G-���{�����T�k5��T��!�L��{��p��F�`j*��Ψ�̭jTJ@_G����l�Ҝ�L9"��-�r�S����h\x�Y+�V�9T\�2WJ�<n�,�<Y���Z�s�ZG2癵�4�9 q1�_7TWZ�k��|w�&���a�+����֛f��Y�t�:��G]�N�ᓡW�-o]���ٻG��y�5��ɫ�N���3�/ J�]*���^)ikג�ߓ��g"���C�r�_ō�Ή�, -���hyO`�-�P�X�m5�؛�R�^��YOFG�T�n;��k����ڝ�b�Yj`�!&~��⽯"n蕏0�- ��wm����Ey�����'��L����qE�Uv����]�j��"n��!Z�-P{$����9"y*�sn���a���:�X�}nܑ�1������I �9�>�g�u!���F��?��z'hJ�.�R��6��Tio��z����d -֥�.ɋr�� -��j[6#nI6/����C�V�MO,��J�˾,�P6/s� �'7y�~�ê�`�����c�O��G�G��g6��^��J��D���>��ל�A:ٲua��9�\��a����(�k1�������d#n��p��/��~c�.r�懣��qO��p�70�O��c=���q���t��*@�ɘ�g�A���W�Ni���#1�O����ѷ��`P��^Y��mˠ����̛j�O����OX���#�k�o�0!��♑v��g��0*����u3�z�e���a��l|l�E���k!&X]ᩐ�d�'�9���/��#�*�o���+�S�]Y�w��j���r�o�s�A0nwI�������0-�yj�W�^x'#�j"0���ߦ���k�s<�Y�=��L:�,�: -9J���L��SR�� z���kQ{��Pm�"��ҍd����u�(ҟQd�W�X�bĠ�/&���H�.2�������]C�[��� -]��S�z�E���0�̢�I����;���ZgQз� ��Gq�Wzc^?4�yf���Ĩv�Α�h����F\S�M����� -�&e{�3��>�j�O��`l������<��o��}��Z�?=�N(z��s\W�}��\� -eA;�l4�������aXȧ�_��AIfF[].�����q�Ǹ�d;^���g�(zO -X��K�f����"�����}\S�2-����#���!�f���N,#S�I��h{��rD�O/��N��֝��5�����Z,j�M/:�Ճ��R��K��K��`Lr��O��5�8���&���ν�频�a�yG+��C! �c!X��t<���#���;O.�L.2V�\�ㄹ�����i�I0���q�lߏ1W����:l��oId�DK|��)?���o7�XlbF1�]w�|�8}���G���A�H��hp���� ��I��ShC�u�����>�eVlο��_:�R�e�7�{g۹g� g^�N�K�qu�L��ձZ����$���k��O*کs�/J���ʿ0V�]#�l������ˬ��7n��繣� s� A��eG�ɼ��-�dztz�ޗҙ3�HUysV��ԌF -�)�<z�Mm��ƕ>`�_!����}�`h -��츺uѨxnq���P�00��dT���م \��.�F��D����[�ʂ����0�:�T�:L���ijIe���Kڃ�R�Z��:��~9n��R�>n*zH�b��FM ��� �5u�t��qAG��Fg��k��A��tڧs�?B[���Sͳ��@ �����0�|�&B���\��j�}jY��P�����N�6�.c�1&w�MS�U*ƾ0�ԌM63"���\�C�O -�����sX'����[�g�i��z�mw����Z���j�E:�M3�I �<+��>D(~Pz`�*��wo�4���6��ͤ>_w����1?�h�x�����!1���b9F�xPY�ܽ^>���*����� p�%c<���,�}���]�4B۱���h?��6�hW:��:�NE���x�����r�MUxXo��ݍQ�Lc�C@�H��y�D���B���V��ߞր�Y/@� :F|EK�v -�k@�V@p��j�1�����W��[�We�(m����t�2��A��i�Z�CHs2�g?�q��Ǹ_&?����1��1�@A|>�M�x���3�WPyk(8�����f"����i&��[�?�ڴ�������e;o�U��o� �����X�%�����n/e ����}��j���x�/���<������ -v��J�@�16FG��n�ў�ʝb�r�nj�[X?�c�� ��0mR�>ܪ�;��&��e�[zO�ߍH�O�)���������h����<F�_ ���7�3}��Kf/���/���%ko���A�,,AA�� �pP�� -(��1(ț+(�3j�:(���W�=�$�`� ����Q��p@=H��n���z6:N?��m;�����l�?a?2럨�n���X��t�G��>�\</�I[�3��_ -pSO��W��x�e`H�sF!Z�S�����am��0+Au@�ҡ��9����߂o��$[����o�wڭ��#g��?%䊫��|kF>�� ��&�)�f�W3z�=��W��)�o qx�5r%����G��S=.�q�_�L�����{��ڻ�=�$HFXR<�ie��tRx }�6��h d���?2Z��r��%���ٷ�����桮N��G��j^ɛ���}����*^��[�센~!ݬy�Jt������Ζ'Ǜ1 -�Bx�E���6Kj�%ٷ�U�?��T�/%���OU9�ԧ<���5�ʦ>��fp����B��㙙r����Ǜμ�JE.�g�&�C<����B��C�\aJ��f�ޕ0�fj�s��n��r%� -�@�y�O��7�6i]b�M�e�W;^������H]�ުx���z:�6�����F������6Φ��0*ϧ�џ����X���J����K���߿,����E?�J������sl%�z�x]��+�f繟P�)�������K�R��7-{?|j��.�i~px}�_��A��-��Y������#��v�7>sj��[X�ٮ����uo�u��xzn fS�8YMvt�N��ʯ26�R+e���Z}� �]:징����x�5w&q�r������N>XTnO�96?<fd���3S���[��&��#ݧ\��q��(��;P��i����������kx4�v�?hQ�Ȏz�����Jߖh�w��=G��=1-���%�u�n�A��K�{n~e����q{�и�2{bn5��As9&.���y� ���<4 bw��m����E��H�r�9�Y�Rz�-��������'� �u����ɤ��[6�-@_��q�ى�f�bŇ�{Ƌ�ytrk(W�lő/�U'Ц民�X�]}س�V�{��f��o��Y�6l�C读O���@�����玖��۾1���ЃZ&�Ͱ��ؙ�X-~�oD����|���yV.�1��n˞�8�����8]zI+硼����:����g�K�������j��D|�w�B�9F����T:Z&c��j�jCk�n�\|$v�a3l!n;"�8]�/���4�g�X�K�G�6kgkbvE�����gc��ɄF:�A�1�BUg+�ȅ��$:8�{��'Mt�ZH�r��9��{���S��C�\�D�h3�ִ��LY�<O�F�����^��ҼZ���z�Q�-��{f)�u�v��]j]�)�T����n�����J�(� -�:˻�{*�4*S��E�������Y+ܰ���G��.5h��w�[��ǽzmh9^4��to�����܌{�{U>5G�3��=��f �1�4(CClշ�bU'�%=��>;�J�����h��Oz�u�Q+�QF��^��WK��V���j��!3;|�����Ӧ���+-�6�*ֿ�T7��D-�џXT���m�4������P|C��m����쵇n�z�J�ng+x}���܊*S���z���P/�WY����w��"��_���c)��Y�|$iY��$��%��1&x���<4�$��&��V!�@�1�+՜|�ĵ�A�w.P/�#سVU?�z �yK\�vm��N��Yc���z�ٴZ<Jye��qEFli����sU�+�!C�8����C�v���Z���C�n�)F���ؕ��e<�r^�wg�Mn��é9Dn��p�Y6`-|o|W9�^�Ua��)���V��z�Z� �7����n�ty��}┑����ϛ�� ͉��Rv˞)�:�~1� ^�Pn�y�pO�ks�(E69q��r����=�'X�|z�+���ձo ��ڗ����ooT�^�u�\�*����e��^��ȏǰ�����@ -��VB`4ótq��Z##P�1Z쾗t�0}|/~���>*��Qu3a_g|�*��aSY�)���F![��Y���q�Yv���2=��s��h=�u�^�6ogq�{��V�*��K��2�H)mN��{Ͷ��;��xOgB��ck�-^�#��n|}�����q��a�;��X��Tٔ�� -�]u�,�՚1���oH�����D4�0}��C,������5��.Էok���7e��rd�!�Ԭ\�o�;;��Q��\<5E��X�1v��1?X sn�%��AN�[v�Y%���*T�)d�\�1HUb��e��ײj{�.���u'�\ #;�B�8s�d���\��o��:Eם��#�Bௗ��f������U���' s�NU����ʞ>���G�x��M��mN�6�fC�07�Y!�Kfiv�Qf.�� ���؆hL �~���T�d7;�LD/�4��9��N�XƞG��E�y�BxW����0� ����~�6�4�=���Gg��&�-ۯ�E���n��E�^#�U-�,T�f�1<}H�ݥ�{gA�hgM5�"C�Av�B�d�&J�U�%x&]�Om�b�0��{Ā�Ρ3`)h�)~�nT��:Kyp;x����c��� YĘ�S�;蠛Kۺ:�6��b�h*�vg��m�ֳ�r/������ >Ӥv#�OQ�����Z�|+��w��{�9F\��06I�4Zv ��ͬ+.R��;$w72pMN�0���Z)<��� �����|lk1���?���W���̦ݜgz�0��j�|��N��kr^��; -'��s -gt���d�51��h���f����{M@<T�[��f��R5/��R�^�� �f~݆y�W s����N��zo���ǥ}���܁ġ�f���!M��2_t�3mEs~�x�˭$]�rn�h��^����NĿ7��Jz8��g����JS�R��j7�TjCd�L�|g�k渃�Y�of�O��)ù����,S�^�n�qv���]ӟ�H�f�HO'��0#K�-3�Sn��l����$Y]��¼�&E�~�%�^T���xa�¶���#�k�ut�Ux�_W`��Z�f���E�����:v��� -���}�)��y5L�=t���+�|�WKA���i}�=xqV��ܺ��j{�Ǹv�~2��aj۱xcռ���-��<st�-th�;]B�";���Opw!6�$�\�KY�M�3��e��uҚ)���t���0?�D�>�RR��1O�.�Ԫf�� u7ޠ�ǯY\ ��4�$��z����y �&��b.�m�k -��W,�3H���J�i��0�ta �9���_�>���E��$-��pC(ު�-��C�Ƙ�c�O�%+ �(U�7��v����į2 -�° -P����~�t5����V�˽�nZ�6�x�,W5�u��ծnŋ9ѫ&����L���S�?��~� L�H�Mj�C��B7F�(�����bX�'`���X����N�h0�� -�$�r�:@_�@��;����GE�Ŷ�O�9�J�ӵ��v_ٮ"���Rc��Ew>��:�&��Q�"Y���l�N�rs��Ũ�b��'���,���1:�x�N�����B5�o'q�y��s��v�}���]�~VP/ r�v����S�Q���j�+��,����^�r���&�U)%ʳx�O/�^/Qn���ڵ�tmBE��i�I��F�b�Ӏ�� ��#l�B���x��$�/0���sdo�"�}��Ap��Y���vܔ�l��N���{uYʇ>���jY�O���+���������H�j���=r~c2�q����1�$-��xO@vC�cD* {j;�u�~�H�@b\4@Z�����z���&]2�s�wg���7�� ��רּB���0��]t���\:��~َg������p�1���<���Ab<Y@J�Ƿ�]/�t�JRF��4䘀z�3@��7�΄c�J��%��?d��v��ҙ��8\��]%�xfjT�2i|�{����y�h��n^�)����?�b��Sep���`ƨc�������ry��+-���0�<��f� ����-蔔�K���"]:y�C�|;)�����Y���4"���h�?���;g�D��,q�$����5�;v��=�U�^�C�n�#`w�\�l�.�� -�(d��������yzD�!�@���g�xdLt�D -�o��_�jRB����8}��(�#�֝�����E�J�"����@<�,@Ѯ@q&Aq�v@�Y����Ȝb�Z��\_���wl�;�T��^���`��֛�v'���w�pR�-�tH�{k�u-ď��xm3��:��A�/ y� �6���֍���'�����ߠ��=ԕu�����aA�]�����|ԙs$ܹ�i����TNܢY9ުӯk(z֧�Hޝ&����+��kg�S\�TRj?������%���&I�?2o�{��~��#�����+�+ֿMΑ��Ov� �do��i"y�=<�u���v�Cv鵃G�g�M�� 9Ca7�BB2/�n�\~��R}����rn�P�o�l�)���z����/�ϑ@�'��[KT9��Ceg\��M`f�,������V���V�o����^�.�k�-|!X, 7X�&��]�X�3rC�^��S��5'��|��>]2��"$~ّ��o� -C���$W�="y%ow��| -#ռ��� -�M"�TϏܢr�b��>P�P=�'��g��Z��R��2=7�Ɣ���ɭ{hO���v������r�)���� �7���Uk� ��OBR:��/��n�xy=-�M�n��Zzt��"3�7R�lF��A�1��n��?�I<���.w{�?��=��'��ȏ�#h�"�@�x�)h�4�5q}@F�ew�a�<�}�]{N)$��y�Jj�%.��4�x��M��9@7�d��N_X/�N�)�kW�g�j�=���%e�e�u�26`%�R8 �ůUV9&��I������ݔw��t�9�以T��x�~< �j[G�V+h�'-8��� vA[;�7���7�����|��kn���+������%TW�0�Q��e��Y��fT�ѻ �FOx��+�����O�Nz�{m�.��:X�L,�6�Vxlb��n��W�k��m0/��ۊ_��|n\s��o�,��q���%�_��D�v�],5��}Vv���}�dz½�t>����I���sJ�e�E�6Y�� %��u��E^�F#H���u[��:��y5Gk�s1ڛ���nx�T�H�˅�N� =���n��?<�T�Si����ݹ�8�'�fF��pbA��?�7���)�:ZJ��P@_���Y�N:Ո�O�~v"�ν"�v�<����q�[9�cf z��|����iT�&ܬB��͂<���zP��S�UJ��hV/h�컦��}�ڵ�]��YN�mPr41zz^�ѱw�2���sMt��=���Ǵ -�A��}ƍ�w[�m!\לR�7ݲw2<ù��yUu|���~�&=�+a�U܂.�W&_Y�*NJz����9��C+N��K�mp,�-�?�P�5�ٗ��|(���5F���t��n�8��5v��L ��;��t�^�U6��v��W������F-ë��U����nR�Y%���cs�m�-�v�"�z� -�zS�3���8F'Wz��D��m9�;�T9^[��F���j�I<Ț+��O����W��������->��y������B��B��wp�7�wsǎ��H��I�hU�ɩR �z��*� �;��T;�;ʝ�{��/Kz�J��A����J��O�*���jM"b�_/��\�Q�*vh��O�ˎ��Z���N�o��w�;b�����kSq����=2U�M��^��we������Wd��&�5���YVF��^�8QC��=9���Hsq���խ��tN����6L���Ls7V�9'�n����X���<��.�w#��ۍ.㏅M��;�<������LN���T��i���D�1��Ü�[}�t�r!�4y)2�k(�&Z�� vKh�]� -�[d�?��+���;�)@'N�u�F�-C��i��)��Ͼ��@�%mF�'�Q$���/���OHY��#F��lԬ�6��lpk:Ă2��gU�Bt��<\t���I�Xʩ�r��\�;��aȲ�])���ΟgH�� ��9�R|�>�1[j�x��]���}�����o�~���pOC��p�ft�P�Q��p�t�x#z�A��F���譙;ћrOϾŝ�78z��?&����šj�V�{�ƀ�x&~����[����U�=ܝ��쫺�������ۦ�,Ľ��V�1��#>]SKj��G�jy/r������g�g���Z���|��������>������P��ƕ�qt�M��!`����h����7��jٗ[��^�ڐk�27�C%�[��mè�('����/}�c�TՠQV�S�PjQD�= �bJ��~I��̞裥;~�2��r!�DZ c�b7��.=A�T3B�oA �TF*��C|`���ŻO%g���9 -Ia�+��y���'U��($�}�ꭳ�Ҝ��/�t�Io�ថ�1FpԶt)b�*���Z%N.�$8@���F�Bњc���r`r�rF?������Ī�T�| J��[l�o�~6��:J{��|ء��m�R��O������1�n����٬.rT�x�i����TZ -�Mv��HߠCN)��5��3W��y�cOm[��š�~:�!������F�'l��|>���7z�5�����Z'T�x��;!�����*�&[�#Wק�S�r�-UoN7�*�Z\m4d�S6�xi_�R9U�n�rm�B/�eg:Ԥ�X[�EFf�ϝ�[�U�_W�H3pp��aD�������}b�kݤ+tԇ�p�RY���2#��J�V�YZ�{�Kt�fzb����Y���:5��ïlןؤ�~5�ͳ���=ei��#�oU����s,K����.y��TL�����L~�܉yKL�s� d��܄�[u�t97��I(CF�Ƈ��px�S%N�gM,�� �Qm��1��~T��:����5�\��E?-����&o���6�Z?�N0c:�.�V�r���&`0���^G�a��*6�T!�����Ѵ�TӍtj�h��Uo�ʞ"|:p�r�A�&�<��T-�d`J�k��$��0<���d7#�_ �g���F?h�^�X=2�Qm�V����b&�J�����,<�t ��$�gu4�F�/d�D�|n�8!1�K��J�Tb���� �mల��~�"�1�"���&�o�y���}���p�.XG�Q������~=['�[Y�ә�z�T;�uK�����K�o{2��&F����7�\/O{�O�Y�e�b9F�c��vc�W1�����\*ƜȄVb�:���K�x�;Ɠ�`��p�H�~��>^A��Sg[JE-N��t^%���<z?�ߊv첷����T�9�����(GLh����J��#�tw�:�1���-@��=�2P�fc�t���A���c7Ю�i�56~g=w�j�����ytH�vo���KCs��O����v喱/� �u���Z#�4g]���a�SX�ޏ�����D��v���9Ĩcl����`^�co2�D�b0�����Z�)���`�ک{9� �h*�7%����W��Y��^~R�.^�T��FraR\�n(2���{�4J�r���}�q��'�:�b\{����懲�m����r&ƆJt�eA������Ws�Ӛp��sP����B?�Y���Ͷ�Rôq���r�Y�J'�ipK�P������%wՄ�J:��&��1�W���a��4�k H�?��>c��D4� ��2��1�+@�o@f�\3�3�EDO�~�W�x:��}@������v����Z=V��bp���y��8��ni��j��i\Df#�~c$�_���w��VT�^���=��`3��f��>�j�P�(M1��9�"��A������N��g!D�*���u�J�����|�~َ��_9�M|q.1�@�!0��8o%Z'<����'��94Ʈn8o��CH`H��c;֔6et'���O��x��.�;�1?<\x�t���]m/����?���1�G��#���S.� -X��,�]�^1>`�u��Q�����^���a��nRw��ks�<+Cw��*W3Z=rNU��_������B�K�������N�H�d7��K��_�o���r/ �7�{�T�s���k��[t�����W��0ګ́_�W�u�%bf`k����������D�M��/��_)l� -t��z��{r+B�/ ��7��H)#$�(�������|��� -o����#m^�wȺnp�p��ϧ�@�*�9����{��j���;��uVa!z�n��ӌ��PhpkmC�3ڴ��&�����l��$�!ID��%�P��"&~��>>�{���|3�����s���N�w�ݽ�çq��핿���`���f��� 9��a"�I|3S�܆���C��-�xɼL����n뾙)�� ���%�? -�WM�W�� �fl/�9X|N���Z{#7_��Co��v3:��ּ�v���Ly鱠�L^�E�q��@��}����C�>�c��;k����F.��=u�W��g�p3�}�(�?ߜ��s����OB�M�e����U'��y{�cza7�Y��.?��a��ŋo��缑�R�V.����4�H��4>��5��΅��|��p�$�-���g���=�����A�q�|����q��9hТO%���"��a�_����Ă�xy��� ����u���*k���s%HF$���(��(� Ŝ�x��ݫ{�o�?O��, U�F�� ,L��]Ű&��c�V��Ǫ���j8����ǁ��^�e�F@�Wm��o�to����)}���Ѯp.w��n��v���t��k?�K+{R=Z�嶞�{m� ��Ц�?Y�I�i���%�K@�2I��W������>7�Ԫo�����N�t/{�u�~Љ���Sx��V���Q�}�?���V��W|H�pYI�G|��rs���&�H}�7�#ߐF�G.�����)�W����9k|ٟ(��%����Z������!�N�Cs�s����U[��z;��_1��c�e��{�2(�m�[�~�:��殃Κ� �Aw����]�~ԥt�0�ڵP�l�fmҢ�5�xIW�N#�d$���!�~���' -��=f�o�.��ӻ?l55��ud�������L�n��.�d.]�trv�q��[��hl�u j�3�~g�k��lP���YUg�[=찑��lT3�|e7�+V9��+�dU�J�I��9ҧ���;��۸ ���r�k��y������ -k��oӢ���y�������h�<�R�����k��Fk*A5h��T���M�����ڕT���F�*�m�8�*e��\J��CKM}�>��k�����q��ȧI��7:x���{��J�N!���A��Ljb�ǎ�i����xN�ImR}ՠq]V��u���A[w��њF�N��Uؿԣ���ܐ�ez#ʹ�;[�tNyn2%Eɔ����2t9E=/]�p�!$C�*���z6�LmuJb:&|Q��F�{��7��t�.{��֭ W�| -|E^�RS�T��lߖ�v�Ne�[���p<T��p\v�aP���eI���o��Bͮ�Ě=d��%u�������'+�/F*��Z-��-�|%ÜS<�ˎeEw�Zb�j��\;Z�͏�QnPs�\��PdG�fS�ڞ�6m�և)ǭ�R��!��20�I�|�j��M]y.�����-Yet�8z��\ՁN��Dd��`�]~��q���:�.^k&/�.kUT��s����tz�.(�*�i��x�4�����/�3�B�Zܗm�EW����ɭQ�]F�j?��3�K�f�ɵ�-�^^Z�=E"ܩ^h��F~�������5�f��1�L�| ���b�y�c��9�s�V����8�X��ԋ�~f�#F�#�y�����9�5��j�M����d�MK��� �b��(�SM���f��ݲG�u�tQ"e���w6w�R���ʗžX� -��j -2Zm��N�ǗK�17����v��#�7�Zg�q[2�N'�99V�8����[�Yڵ��׀�s�q��!:��3�)w���^�/�f��L�K�έZ�U�S��84���|o�#W���rF ��J��ϕ�Cʪ�i�s�C�o��=�6��y/k��i�� cU�����T(���%�^�Mj���U�o���2�ݾ#r���}Z�^ı1����ɥށr7�My�`F�n̮˧� �;���F�t���fvRʹ� -�OZ��̰F^��EY��x���.�w$�ds]3s�W�߾ ��.�ܺz��ѓ*�1���< .��T��3���cB��W����G�Q |Z��4M��ל��Q-���)�Y�@6��S��9��賝B�u���d pUDó���3���d:Ў����@�X�L�ޢA Z�*R�>9�S�TMo������Oë������*V�<tk�4s�q4s��h�9ѭ^\��e{��YIV��V����c�`kKRHk�<u++\���~�_�bCx���C�]1]2szOP��95�Y�M:"9�Nd2UXk���D�Cu�E�1Ʊ�x��Z�F� -ڪ�Y�a���U��y]����mC�����<��,���B����;�x+�)��O��h1k$Y�+�A�Q�r�mq�ڌ�n�n��Ce)���������3���������p�ߛ4�h��;*�n����C� \�qH�����'={�ɏm351�A -ڼ)���I9h�>��hX����+}q� R�lN�-!zT����h�m}����O����f#����w_�����,6G�����p�� ��r�j���kp�.Y--��:�U��<�#H)ש���`6IK�zw'�T��Z!'1�w�j{��шq��Bc�˧!�S�Yn�ռ������h�By� ���,������ʘ��]��_��!�ԅ���J_1��b��%����WU�@����m܃��W�/���X�����l"��@9�~M��z��[��������S_å�/���0-���.X��6%�j�uͮV��)%�%d'Bұ]��2e[��E��B`z�A��) �Z7�J�T{g -`���ر -0�1T`�n �v�����ذV��n�X������0������g�t?����w��r����]c�ښ�ip��oԑ2#��B�]�t �b�=qH˗�� -�`�=H��s��.`d��'��W�.��f/�n�������"fbl�E\�w.�����Űp�;�8�Z�n�Բ��݄���L�� (�ѻ����@*�G2��K��jjF�T��7�S��v�7��n�T%���01N@�hX�õc\����� x.c�B�Wc\��� h5��4�����h��2�]P֩����nZ��2!� ��j9Ԣ|��I��k����w[���Xq����A���|�)�X@��\�Hګ�ص��I_�2�Mc,��D2�C�(��1m�Sƀđ >���^� @�z+@������Ҫ���slA��Ԭ�}��K�f�{#�A�a9�~�!�5�/�lC�l��Us�1�B��@�q���S�Z7�Џ�E�O1f�PT&cZ��>�,�8m?��@��g���ꦗק���/�J�_IQ�����ܩ3*��� g��C�Q�#As�4�Ծ�`����U���/'+��Ӓ��F����]��uvh�������Gb�y@˹���h���@A�&���M�����Ju����A�;�o�Gڜ�����]�;��@ua��Yf�s>$I���`�As�h����S6�"�_�����L�2�1A��D�u�g� 2=)�����惌��@�Z�/�;*�����Ȕ(1���>7��ְ#�<ǧW(�e��iT���E���W�B{q�������?#&�^�<�e�#���R1.2`^ 0��,�<̧��fA��� �A`vg�ڎ�^z��dr&�-y*�GH�rv���������3���DL�e���tl݁���0���e�N�n���S��>/6@ڀ�=p�.��?���e;�����.<_��gKl��g*�����%[�j�Y{�DL�K|�������s˫3�j �}0�19���Q�Ϲ�i/�����ҩ�x���2R�����[m'B�z53�'a�wK5����8�]��Љ�8����+X"������71�|�whr��r�>rL�r��<��Q�R�9y������JD��d`��G�c��Yûy�Mm���O+��H���ѡ�l���<K���o���z\;j����V��eP �pH�h��9R�)�@������H���jI~ãڃ;�lLok�^w������3�1�)ҩ��C���Y����%�Ä!�}_�va��n�}n����5��7V.���^����0?yߊf�K6�y ��� u�;�vw�Fu���~.�ST�Տ����6k�B���R���a�vo�-4o��e��dY���g�p�銛E9�O�@��AX2R>��3*��Sf��'��[�IB��~e��(���ۿ��_ �F9����~��>zx6;@��>G��3���ϵG>���O�0�<�U���~jacrld�?��.V�?O��p�,|��;'��3�f%~�������G��F ��%�W�W�LD��os, �ea}�/���h�x��l]j���&!_�nF��"[|� ->�?] ��{~5,�s���@�Ԃ���KC���Za����Ds��iQ�-|�N4:XI��a?.ُ��6L����+��I�8ݩ��[-����5|_���I��J�k� �����E]k��Ժ��Yt��c�͋����Νw��T'�h��Ҏ_Z�gv+��2�h�dӷB�,�tb[��6[T䵛;��F�%n�d/�2��+NV�2� �q�3`H�Sˋ�c�Dz�wծ��N�;�;�Ǯ���g&����� �ï�T�aY�֖A���~g��y��qM*����~8�%��ϵ������$����&|��@���ikm��P�G��:P�p�c�g�'m(`��� -�օ��o�e2���9�i�B�<�ئ���v�2̻ ��=�45����P���6i�?_I�g���z��<Pm=�� �`Ì2�ꘅ/�(������/�C�e�Vh�/-�>GM�=�MG������� -��W -躔Z����B����X�5h�V�zfW�C��������<5��ڦ�|�wթ�d���U�9�=-E��䵚��f�S�S1���7 l�xb%�U0Za�굨]44a=k�O?4�Ss]�,�]M��S50�[�+=l�?W �+�FE�o�qŒ�z5�Ou�eWi%Ϙ7����W�_�ǻɪ e�A?�ui�7{�Q�~��D���f����Ǥ!퉲�-|u�$��m6�Dza�jǪA�J��슞�F��i�*`s��c+��m٭��\����vW�k�(4��'m���s�+�-K��0 -���+?h�����+���\�V�9��MsN��$1#Ok!���_"�5>m�ho5if�f���U�����4E3yY�0�^���f) -��ӳ$�ǎ�}��� e�Y�������^Ja�+���S��D��U���q�S���ʋ�V��@��n��U�y�O�x�0�����O-��5Y�%fEy�Lvo/�g#jm��M���}��O�1Ӥ -sW2e������2t����eP��ܠVXz����V���A��s��z�xY.�b��o����~�� U�8U���D�,Z -��%�ݳD� ;�2�L��Hj� ��C?�y�B��'��q;:՟�ּ�(��R-s{T˽��)@�`��3KKx4&�Q��gS-.�h Q����S���m�5����2\9�� 8���8���.��3K|8�l���a�]�aeBw�QDݘ G^ÈqvDž4,B�-���J�eD�I���k���b]%xkX�|�$��r�zT~h@�Vf/�^ӂt�}����p�����1��'H��`D���\��b5��t����K�����Ev�S�I:O>�f��mxB�/~D���@LR�I|J[o$/買��K����]p*+��Z >�2*��A��T��ڎ�ָ�DM7l���a~,�.]El������Jdɝ�0-�2��W�؝�F_RO�������lyDF�铷�� -�4��D#3���3j�:�݁?Jگ�'�B�UYl�I7�A)U���Y���\����\�Z!ۇs��]`�v�Y�t�F����t�z�iq�f)!D�pT�W�*�a����F��;��#4�E��LC����r ������Ν��W�>l�~�uջ��/۵�]Hl� �5ܮD�ɬKc�:,�?]3ߙ)�0�b�Ď�9n�7*ڨ�<��� �p��|z$��S��s�>O����q1�19��f��#���Iw̐�{6$C�O߁��*j�44�]�YְZs���U�|�����^��r�J��$�U��*n�u(����H�N^ֺ��6�)��;������i���<�ձ"�綀6�S��N5�-��0�����p/rV�C�DPq��Rc1�R����j*��̔f>���c���V���͎j�j�&��$��z%G0U�p��{ixV����v�����Ec��x_�n�����7>��)��hs�|#��#l댧���}2���� -�H�L���~�,�s0�g�0�4@�f ,ӊ��&Y����{@�{m@qGۍΪ�̍�i�?d�|�d��6U^?0�K�H�����.�'��u�����?p������y�?�}��.�������J9�ZT�<\`�9��ib@z7�+��̽$ P��vE��[6@�5@��Z�Hjz���u���q�#�m�\�{M3���f,֓¯t ��Rjq�Y�!~�<�!W��F�G�D"xj�� ��!�ҏ��� <��G�b���r�� P��]PAY�t��l��U%ƪ�JgP��T�Vb�������yݷW�\�u�zV�3:��#e�����g���_�$�;9$!��I�CM�S��9�������+J�����Q�b���L1F� -����K���� -�G`xK����'�������:@���� -~gֽmjdz-w�rSL�~�%� ���J��7(祬���`�g�� |Q@m� �2�zlS����M���bl��waV���c�M�R/�$�؞;�`�D>���ر7�R��6&X�M�6�\{�>6à�L�i�:�&�{�&����$]��^�r�wN�$������> 0��v��M�����)|I�1�Y��zb���1Q�a���C�O�u�a��O),Ƭ��Y��~|���:����x����V����r����No|������Q��]� 3u��U��wS<)�v��/������E�T��B�W -Q�1"��gb�@�i9��b��1.. �� ��>��1���;K]s3t�����a$F���ԅg���r����My�l�a�YT�Ao�B&]�gVDX���JߟC�<m�P�I��H�߸HB3~a������\���0�*䌽��?���o��r�i�q@} ����"��`���f�JԊ�[4f�$]"����,���ϳ����E���;z�֪�G��:�)�Aʿ%@PO��a�8��xL�u*mc,�Ze��|�C�s�Q��i� -�Y9(���k�k@U�iǤR��v<������$]"�hW��; -�i���H��k!<�ͻ�Ɖr+��_9ɣ��� C���y��dh�c����ĸ�oA�Ƞ�Я���w|��/��7��U��w�x[�D�==#Ff˝+�[gZ<k��X=n�̣�$���9]�Y�_��|�W��_���oô�#��püĸB��&�`:�`��6��9-����5�I΄m�g���9~��.��f繞���wD@6����`��o�q1�Ƚ�;b⋯���`��X�A�K!b��X:�'1}?63'�X`��1`���[h���7�����BTZ;�y��q��)������lO�������D��k�3%���������}U�ܣ�<B������F��\Z^aa���RNU���/�qW�K^��֫]��)�CZ�I����o��g�I��#X��X{������_%��6F���R��< .���g��ƒ�{��036�����2�{7 `K�K�0��w��Ͼ%���Gb6��^�l���M������r�����r(d�#P�zP��ߙBP��(<� (<4ƻ -�y��d (F <��>&#�Ӿ��K�ڊ֣�� -fg7�-Nb��O��~]���8&����u���}��^�ͮtz���^]�-�7onY8�J(_+�E�q��u0��<�$! ��q�N���FL�Z��T%u� y���${�*X��il�=�i����W�G���\lR[F ��,�V�g]{��X�E�f��A��ߜ��Sx�u��Nw5�2e�{0�0=v9OE��<�7��������M��DCMF�O]^����0c�rQ�;>�����a]8��-lfB r��̔Yj��!��� -c���xl��뇵L_~t��D����E��x���y��s�wA�Os�m�e�}@U����M4ԯu��emQ>#���<�3j��&��d4vi7y�����ޗ#�4�H�m����� (9�2k��P�!v�It�ޮ��z�a�w.b^ܻ��~��V�_�!�B[%��?U�~\�_�<���G����?5�K#�����ӻ����z��2�����S�U�gQ��{��Ox�����b�o�U'�v��sqt|equpxړJ%�j-����,#S.����j�z�y�y����ߗM�� ��l��d�d���P[/T�g�<�:��t]>+v�<!9��G���j�u���ˠ�^km��u��MK��&s�� ��_�h:��R�.�a������,�&��/���~�x� ��u~��!4h=�rɊ������VS�g[G����m#-3� �;��i2�k:9M0�{�B��krC|�ԺWn��3h� _�k��ЭAۡ_ՙm����I�o�f�|e7���,����F�l�eT�iItp(��D!$"ob���Ñe�Ys�.��c��5\ :֣t5����P�ѻ� TS�R �;^�3��C�:_µ�K+��V�w�۳Q���/�;.����5 -����κ��R��w�N�#F%�?^�$�������p�E�ܶ���v=���Ϧ�b�IU���nd{���t֚)��]p�0��M=���ʃ���V��R��% _0�or�}+�Ѕ4�z��I�`����XXO �@�/�DRE�m~v����b�{l��!�����U[ �1��J�5hy�u��54�j�*̣��G�]U�JѼ��$L�(~C��{* -%��%�d�$���P���_�H��A4�U�I�c�9�hy��|��3C����K�Iԃ��H��u�G]����STH��f��6.��a(лTE�bN�.��n���t�$��*��&�j6��X����z����k��ʯI.>���;�m�W!�U��Aʎ(����'��*�Gl0._X]e�, -��,�f;�y>�{T�gCԑӓ{�&mM���A?gW�ه��xȕ��S�a�'����GQ�2��G�@��b�*m������*�k,�B�Y4)�j�}�q��mw��!�#�|�ﬡ�Hf��X�I�3���C���K;N�������FG̙hD�2Y�j�|jmLRG_�o%-�p�$m�dQ�v^��G���I���.-�� *H�K�~���}5��&]����A�_dui�f�Qhd�Ա͘����N�E&[�i�u�(�U��{i[#����*��#�%M(�l����z+�{�)ڞ3�jcҳ1��7ƠD�L� N�r�u%)�D�[(�!�W�[=��"N��k�3{�,��f�C7�l�z� -�� T��J �Wq\�~�͈7@����4^�C[�Yǰ�y���� ]e�4����Cb�зzo�[�ˬ�1�b��T�M4/5Җ��Mx�n��Ε�GN ��zqk�Ze��i�4��>�Ν��ip�h�����NSy5O��}�#��N�(��5|f�<}7&X��ޣ����b���������~�����\k�ա�:5�{�>+uF���n0A�٭�z��ޫ ���tƸT*e�E"ؕ��� -J����9t�BxM��Ր|t�;�T��Xo߉�i����Sp�|����75�q�[Ȯ�i'�]������ٕ������γ�R��&�]4�� -��,K=A�˝�~�oSړK<�u�>���W����Y�ҕ�sH���S�� -C<��k����S��q��.���Xm�]�u�}�&R���ӧK9�i>sV`�'�Н�� ��Y�FH6��L�&�>(Ĉo�g��.�ϵ����ͽ�۪�gٲ$\4=���_!�[��U-�˟Jq]3s�gN�++,�m�G��&�e�:�/���;Cv�� -�r�C�W�W��o�н�D�.2��ےR%xQS;���S�Ƹ�A�` MJ2HSP7�a�p<�I�ɼ��ӵ�TD�5��*[Y�,���������[y�_����kR8�[G|j��#l3�e�LP������*}jS�>��P�6�|N���L�ہ�lH�t�Ɂtu\��i{4i_�b\�WGn��r�'� �� �ͪ�?̥F��6������)���:W[�6t�.Q.1�7�J^��e��K�Z<��ˌC�r|���vp����) -@��[�t��MwoAz�F@z�3 }�����u���}�0`�y�D�%�.D�t��C \�"���Tz���J��d�Yo �Vk�ӟb�Q$8��`X�t;?����/t�˙��V=vS�+�=��h -�����Ԅ���i@_ȇ�(�1�#c�@& �R ��(>�m�� ;+ާ=g�xO�X>N��e�?�"����[�i��d��w�Z��41lf�P�ɍi�V���'D�m�5 -|�,�̼ X�֏�m�������-"���O��gb� -�rk��1��ֳS�v�@�J�B7���7�,-�����:��T���}�;�p�Lt~<��vf$�Z���0{ ���T�_��\c��2i�؞�a��S�c��&vX�����|b��eV����R�1o�L�~�"u�����"������| 0��9d����~w,#��{k�rf��|��^-g�?��PJ�����r��ĥ�mGG� ����&pg���1J������?H -�d|��d��Z8��c�#���6F�����R�4��G������?&���I�k�m� ��"�Dn��.qve���r3:d~�%2p)�Gj�śD^�G��« �`Ӏ�Kh�6i��� j�&�^��� �1�A�k�H]}�G������C��PXwP{�n����s\��E�.Q�NB��-�J!J�(�y���;���.���Xq<H0����_�� M�����0ˀ���Q�q�+�!�C�Y��Ht�$f�_��$� q�r�ؼ{��mNN��9�9�vjI����.a̰���t h}�YnC_)mS����BA,jH�Z��#~.Qn���P����ep��.���P%vc����c�K�ʩ@�5�2kP�P���l��9�V�K�%�����:+��g� ��<�siz -5�؉ڍ!}�5�1�g���`��m��Ƭ?7�Y�҃���e@K�P� h[�?�~z�v�@wZ���9��]�;��w"џt���]%Ćҍ�kB�W`3���ۋ�N˧>�t�V�C'��?���_�&~#�n'N�W4>X�\�O0��0P5�f�F���o��?T���3��H�)t���R�LG\x��<NA�������/��`:�|����Hĉ�/��5�Π��-d=�c��A5�!��qρ���������aƊT�7j;�{xt����wB�ה@�tJCgq����O�D�.�L������������b&�:�gpv�8_�n����ˀ�|���w�m:��H*�� m�*�~���L!L!��~����z��$��_���$����M�M_c��[� ��>D">D�b�'��r��N�s�9�؎��ɤ睝�������z��$��̉��'I���f�k����m�� ��a���Ӡ�o��qE�A�.���̛��N�<&q����fu��x|�|)���K�{G�z>�lo���l�x��fW"�c�\s��v����ңo��i�ދ�� h�0sd\�<<8wf&����!0{�J��%�s�&>��V���� ���7�Äb�]��ˮ�j.�r1�_z�Nz�>s-.&�RY@��:�#m��FP�L��ZY3j>�L-l�M���d|�6�q|�G�HH��4����UX;'9��%���K�?t�{�IQ�)����c�>��22�A(��<ȥvA�u��[ES&?'1�KӇ���CG�=!G��G�Ow������I�/��4С������+��T9�Y�q��e����n"�v��`�Xe�Tٟ�a����W·��O�l-�$�4Z�N7G�uu�g�� ?��x���@��+�g�7����<�M|s�������/��>@.w>c]W|d:� -v -o��(PŴ'��O$�j#{^�Br�Za�uw�$n�[��9$�?��4�ē8*5t8�����༇�q�O��r��AU�;>]�~o��9/��9��s���gd&ϩ=�@Z@붎#+4����w�du�i�g�K���Z�᪙K=Z`bݫյ���*�_6�ʿ:����O`�wm�h�nԽ���ru��ă�v�\Y -9� ���o��@ټ֢��Z���&s-Y��/u���͔��g�N���j�4IT�vO�"��;N���d�~�x��!��z^��o�,���lh�m���8�Lz{mZ��n����E!�p��G���K�I�x��P���*��j**ՠ�֪���� ��h�>��ܢ�|���w�d���*�Q�����c�M��?�_҆���� -�=���mor7o\�|cf<�jX�,r뚊�����}'$��Av� 6��9��LQ�4**�K*��qTʪ|�"�]���<�SS����(<�'�q����@/�"�Z,�p��$��]��������U������yn�=�]�}��F5h��ndS]��ϽʮwV��y�:�!(_&۰��W�R4?�Kv8+~�ɟ��&��.]��B�%�%� -f����f?�E����`�O�-����o��+F���m�k�G;�U�\���o�S7�Xٹ����Z9�Ւ�:uů[M���lYe�n1p�~Q�-_�/AS X��);��n�rǹw���ݯ �KEAb}��]�;�}�;7��[1���%�h���,�<t�r'l��ְɼ�V��)V�A�Q�L.`㽣�R4�gߘ�i�E��$#�/�Ổ�@�-o��Z�d9�X�[g�B������TWܤ�<q�c�fu%Ee�i��%�{�Y�VȘu bh -*fv�������]����I���6�^��t��gP�\P��,U��,��!%�]*����R���D�n�7��� �y������Y�T���j���سFY0�6ght|�X�LJ>���gW*�v�=*�2�oH�f>Pd��|GH���6b�Q��Ww�g�0�i��Z���y���j6~{\�Y�:��E�*x&�����ɩB�f��b���ʜ`��y�1+'!�[܋�,k��u�KFkFJ�ݑ������<�V��� -,L7�L���a�_н#Α��HC��$�4���RW�ݰj(��@�Y�?rq�L����<�\���>'z���2�;�TG��n�T��d���v�'uٍS�km12:�3�TN��ʔ�ɫ����f��;b�/�lJW:��5�]{p�j`>z�A�=�1Pk㞾�f^����a#��W��(��X�֥�z�fi�-�'�*���� -#���G��6��wd� %g�!F`{$@�pçL��þ�aU��E����D6���n���9T��M�\��4�����ގ�cفw=Ly�RZp�O�{�ԣ�ռf13�˾��!s��+�\͋��/lC��RA�v�� ٳg�Q���٬O�b��ç����O���]N�%Z7ds�E���`���P�(k��j�O.=�y����ؔ�d���(���s������/3�^��u�(d�I����P�i�ea]��P^��B���A����3�et���&$ʭ�]��N��|Y�y$}��p��P� -6P�����N�K��4���ڵl�ݱ ���G���ĸ��z-"�����^��C�!ٵ�,��2 �����5#3}iܲ�/ˮ2�q$��׳*כ<Wmu f?h�����¦Ȯ� �x٩g�R��,�ʻG������XX��.�%�p��ج�����z�,�[z �3p;�p��鳥^���b㫮Լ\��+Y#*�T�n/���,��V��_����%�V&� 0̡���๎p4�/�k��L)}��|��l�3�;�&����������~����H�� H��I��M��Zs��k�@�o\�+̵�j��x�4{��O]>[���h�:�����>�_�CWHhp;-f.y�$!0��-TXBEo=�i����'1�e�C3�HK��u��ɂt��ĸ5AzX�A�{��0H��H��6Hw�+�v(H7�7�\��6}~T��zA�W.�Xm�z�)�J�J��v)n1k�����\ݝ*�$Sn����!���>@�`��.n���n1& Ƈ�]��*���$_�cl��]P�1^(@$��b�������+�tө�K�j�|��Z��$]�R0XIk�^I�pA̢˟��+�(�~ԺOH�ok�bT��7�,ob�/�Y���Y�@. &�Vȕ��{�M�a�0(���� ���C'����d���^��mثl�ΟzӔOg#)j��KT$��-��R�*�������9���+x�����~��{��m�z���?-���99��E?ޏ�Y�h�/�5�$�1���V�+�:�u� -�if�>x��u�Ԛ�;3qҷ���k���k�j�m,�E�ʦ!- �7SBWg��^�H@��q��&'� ƢSZ��8_���\�����o����_�b̫��`�7�?Ƙ$�6�7`� �X��y�(,��;� ���h2j��oLQ�X��^�M����m9�\�v�=�AU�0k>������b�D����D����?e�.�*�ۅ|-|�3cL<�O�eb@x�b,�����[��q�����3�0�_��/������^K�,�b���X������{7�~����̃��l���(QA�A�P$%���ԥB!E���ȏY������@{4f�����~�5]�Y������Y5�c�լ."�_4�q;�ʋ�?�KC��h���&�8�UN��1��*o�ݮ�����p�p`2�;�!�����j� ��kN��4 ��;�2�+t�.��!�`"���q.5Nɜ��?%O��^t���ړ&N���fI�([�:C��uMU�H"�bG0s�v=]>��&��m� -0�K���n<E��utbX�GS��S�<�1"QyVl"hϹE�:fFм�A�evaql3��uO\j�P/����ޱ�sx�M�iq윱pl>T�xIh�^fD��y�|�O�� �r�S��"z�=����k�X��6�A�:s�#�3��M��Tl6:�9�dHP�t��J����^�小�Td���~��'��lеf���s�:]��`6e�G�������1�^��!��c^T^U�!��%�����e���:oe�X���h��.8;i�0�sΥ�Bk��gr͚,��E���/<]W���Dl�#�Q��Gń�-���D�-n�cn��5v�g���G -#B�#�=�l�qg�eԥ������Ӎ��3���p}�D��� b�������SALg� 1k�I��"f$y@�2�1c�,b�k&�١�%�ѡ�s�8Z�d�T��\h2�U)�Mό�f�{���{B1�q��3��9�p4�stb���j���� -�&�~Ě��k�"֊��X'��چO]���j��06p26h�^���kj/������Ä��{���$_��O�670�ceB�8 �8��1�H�Ԉ�]��o=i�퇹�"�)�Ti�Ѧ���SYPoo_��+��P���F��1����%��mH����u��,��9���:�����/\^�Hw� -V�Nئ�#+n�%�X"<��R�>�K�T/�7�ù��]�'�UQ]��J��!Pq�ru�Q�����Ŧn&�no[=^ۻ��Q��^r^<��H�ƃ��2 N6ֹǡJ-va��z��Ǎ�a7m#|�FZ��"�=9�z���hX�{<M8��yh�ل��<��l��=��}�p0H"�}�p0P"�}�p��T����&�^-sM.y);O(V�v]eؽ@�P'ja��ﳙu -�&�Wª��P��F�"�����4r�9��$�X�����4$!��XҕK;� �{��Psf�ݭH���rt��p!j9E��æ;<��$��j�v�v�L�ote�JY���_6q;���P�2j-���h���U���q\7�&�F&�46ǚ,,���ο3�^�iV�VUn@�Jz8�%Zc]�Db��Cl��l[8_^�ˢ-?Y-��:����]Oy(d�q��I��QC{ �7�.� K�$��7Z��M-�ȏSe�<��&����Nƛn]k�%�4�k�~y���7术&|r��&��X%�Fɓ\�^�zAv�\{ �,�i��봅t"��v�K��\ޘNV��.ۍ�#�b���s�9��M=�5��I<<č��(�\ ǃb�8U:K����Ϗ�tqi�����U�mu��w���wZB�H���'�i?�������U�E���i��$�n�D�;T�D�:�� �(��`U_��w��K��Ʋ�ה��R��˴iUؚ��PPA2���wzp�zc17�oG���c�]D���+�.�隱c�-^6ݢ��3����K͖u�[��Z��)��ĵ]���I����,zӗ��W�N6���D}9 $�d���42?"��E��m��̎V�i��Y �m��u�]|;�9���߿ZS��_��U^k�� $\�yh�ل�wZ~6!o�Ƥ�g.ULZ>�{%<�� _h��|��B����/4_h��|��B�dž��@}=�hl�E�ڨ�M�/4_h�������D�1zܮ�+rc)h���)m�����x�E��T��I��h��$���w��I^/�h�U����/4_h�����`��6��JA�E�Ŝ��d(7�u���)���M����exg�\H�@��*�f�y4$��%f�.:Ax�UA�h�Q%�q��%������]��T���"enT�A���~,�����O{�Ց�$����뚦���Y���M���GYY�7���}j�<�y)���"��;UXv+���VLUB���j����H�͒���"������;�u��|8/��iV@9�~@&��t4�O��N���O��F�}#�{q���nl�M��_�}��B�ǀƁ����-�7R�������2qΘ`������Ok�%qi4���ESc08ԗ�z�ᑥi���"�z�ͳ=�x n�^+�o���< ���e"Y�{�"�+��oPuw�IF�!�B�t��f)4�;�鶠������$�����Ad���6w}a�l���^D�~Q -ȷ��pd��3��/4�������A�~rt���l;����&|�IU���'" �;�G�V�D�͋�V�znNY�}xl�J��q���M�v�UvM�F-o�l���>T��DY�ٽ��+���yW����}�]9+�:��0mZ��+�F��P�#"V!8��sC��7�)S^9V��".s9cJD#��H -�R�975o?m<����RY�B��ݏE~Ճ'H���X��3/��9��vҙ�T@��⣩��Qs��*���I��+�=�o_��`'v�-(��$�FJ�v �R�oe ��r���f�f��<X��e�ȁWQqy#TqY���>�eߒ��>�m�S��l�p��HT�(�V���sk���"�`����kO��j`�멷رe��MY ����ɂVV���n�d#���_h���q4u��)x؋}���y���N�[�bR� ��^��E�N��+�7�i����U��X~�s�"< -�HD��W��:��3���<�+��с���W3n���NX~�/l��j��:o�oZ_�V��pE��%y���M�>�M]�Ԙ:o�@8f�[I�x�'\b�������T :�Z`��˵y'|DUN'���oBe��:}E��<�Å/J-Ͼ�]:������]\������U �X <�ވ���=��.�WV"���e�2��Y~���+���7}?(�1o�|�"^�NFZmʌ��Ked"-) �)�'���vk_��f��v^�`��v�����C�G�)��Yn4�����d5��k�;+�).^ ��T�x� EI�&3�&Nc �ǾU�3=�tjTx<<W�֦�7�W����N%��(0X�������Ӡ��k�V'�3躾�k�w�Z�ͯy3e ��I����h��)nGn&��<��Ꝫ�������lyf��a�������6��vC[;� �D�o�NQhjJGՐj�O5b-Jv��0 Й8�p�MA�e(����:\gg|�խ�Z[����kRe!n���Z��z"--E"���j'lB�C�&�u�z*O�6�,Xΰ4��~o�N���2$�D��&�2�D�@/�j;�0'Gﰵp�k���l�4���H���|.����rzҿ]iE�[��J��Պ�_�g��}wΏ�? �V� ��9�*@P^<�C���a:�k7��ֈ�fE�ʅ��I��C��oL<,^\��+5�����J5;�U�-K�0�^yPO�iC�i&�� @A%l -����V���:����ĭ$�qj�F�Cr�D;�M�/���j&�i�t2P@�u��$�v�I�C'�������Ԇ���M����ڣӼ�.��c� VY�ы?�B5��w�^p�Wk��h]����[�wr�f -��˄ Y�WAw�I2���+8mJ��z��@�Z�px{HOo��}v~���H��މ�C�'��k�Z> -��eU��|x6#0'��G�lGg�ؕ�Dx+R�؝�K�@�ͱ7bȁ��}���X�=�Y�����=dB5\-�����Ҧ���y�7-,y�|�l&]��W\�vI�u�[�I�v{](d~&*��L]^���@��'�|����z�/��+�?4Y*R0P�̶I�w9��p0RL`����q%���˵��U�����|�� -(���D��r�$a�Y42��d�Qo��M��� � -kW�<t�nC�5��.��)�q�~1�����l�%.'�b�d���dd<�:�.��?�N[[Wp� �/iQ�۠�H�4����T5(��[ܨ�cI��f`�rlJ���%�h$��ev�&���i1�L� ���:5�7��͓[��� ��!2���ˀ��KG�VW�ج�����������h�ӽ�&]ͫ����aV���a�BP�.Y���ܵ��Kh姆Z�47������0������xIr�-]���bBǖ�[�!�C�ޢnpHS�97�Z�))�n�1-�@�����#���18;�E*_�QC�f]w�p5j���0���W�$���L��+eɹ{��F�i 1�K拀o�G+�u��$�Unr�i�+z^���=�>a;T9۶�vE8rZov��ϒC��du�������s��0�Iup��/�V�vq﵉dv�N�����/M��ROm����iM��֡�� ��<L��y� d�ܸP5��1��~�7���a"]������V��H�@����V�kH���W��)�C�:�/̑�h�썯��t�ެ 97e��o���%'v��;�9Y�A�C:S�j0M&J��Sg_�� Z��6�nX� m�i_mA�t�ߤW��o j�Xe�m������U�k3\�.�Q����mx��z��0�ϵ#� ��r'�rq�=תSr�ɋ�l��5���q���adl�ϫ�a�a4�N�Vh��������}�UC���W�2������}���)��q�;�,�, -��A�0n`��Ƥ�P*�� �L��� 7T4AC��nQ\��4�FY� ��B�݆$�#'m�R�&)��2{B��v -��zulҸ;�i�hc�P�1�� -��gQ��v��Z��75d���t߭�|�n��dA�|�L����Z�}!�b`�zӥ��<��OK�W����g��\���!�є7��S��lj��j��W�&:��*^7>�M�rC>m�/�����n���a.�>}&v��65'w���|�H��r:*�#r�,��n�� apQ�ODPš����o7K��YA����o��8c�,�m<���¦:$�����Ai=Τ�5�#eE���:�qѽ0��;�M\JJʭ=���&Х(�MJ��E������Hu�Wɸ�I���B�}��HV#G��|LB\���$_8��=/[�v�)�˂X&,��!��[٭57�O7*�T�AĐ�L�f�7ChvjZ^�"�?Ty������l��Y|0_ �-��;r2�:5=, �˸ T)g� �������I�Z�<�W"肐5�k�P?�����3s��fר)`� ���N�[F;\3��%t�.Ћ�"DO$<��-���q��Ђ�����0��$��غS65����"�Oެ -g��gb���͊ᒒ�Η4d�,�DhB��+:�H -og��ļ��sۯ�*tE��z�p�?MR�?Ng��� `� -�O'^����`a�����QƦ����e��w<0%���<�b'0�%:��5P߿X�[����h�n7��TI�Y�3{�T �1������^ -��x-l��q�z�h��ű���N0]dʏ� -�(y�� -��x@�5G L�~[��U�@@���������B���Z���Cl�X���eZ>��c~�N���i�أV�|l���T(ɐ��-�V��y=�P��x�rk�z��FK e�����u��a^�=k�9ē�%\_hx�a֑y��q���;���.̖P�l��e��3�u\a�i���Km��R����5�j�[%W�2|��d -�r�zV�Aﵧ����)jȫ�0s�YP�R%�{��]:AUi!���G�a�m�Mvi���v�nG��dV^PVgN�i�P�&����W���0��QJCr+'j�ݾ�� �*��预�ǟ����1����˜<�H��2笚p �MO��/Q -���B�lv��Ӱ���S�t�4f硥��hlY�1�sU}16���>�<���r&�� �C�e�`��v�qεCR�$a�O F̥X22�"ڀё�a�o�i>�w�T����}�@������B+(�=��� �4�41&O�s�^� g�8NQ^������G���Qn�v��!�'gخ:5��s���G�ٺ亮��2��%�^ٚi�̕�}�pۗ�?@��B��v�_��:���i��i�*br�.�i�4a�뤋CsO5V��l$H���5�3�͚ao�\�;��]�:�%�DjK��5����DT^{Ό�Iwߺ��/��0"��X�V�/�8P���Eմ5�,�[��r�|kP���"T�^����&�J���0Aˉ�#r�Zi�9� -˄����U����-�������~�{˻i74�=����_h�h��[��d�D��V?�A*�\���nͿ��~E�n��y�n�����e�(UQ�4GnB)�̄XL�����[�`�,G�e��w0���:��UL�Av��j��Ȳ[�~���o�v�z(-��/gn{T��ápmeRGue#7����m0�� n+ʼ3-_���H��J�������?���Zʻ�i�d\R[++�VW����u+v\֯�����ѣY�w -wա4���Z���М_1�2���v�Z���3�6$��_T��`���qd���D�e���YH�1��V�P�a�ZH�˚��-�^���H��=�v��,$9;U�p��#"���͘����$!�xX߶�����&�G�eQ�7�Vɓb�iȢD�����X���pk��lԟ��#�ڼ7����[m�-���ʖ�?:���ē_h��|�q<`yͻ��s5 *��-��5��:�Vर�쮠���2yX��7.��ƌ��)�1�4̣mU�^J��͑ݼP J���ԣ���ZUFw��ׇ!�z'+�{���x��Ӱe�M.��ɿ����c��F�-ҭ�j�t�����nỠF�MT�c:2��U<�(�����#�{�;�@d���e��|���\����Zr��v����h9 ����_2�~��B��j��7�ͫ¼D� ,r��Jh�0,ZY �|��� \n�/��bi/G�q��4U��ƌzy�QiRN{����b�a^�e�o -X~0�l��\x���j��4b�Յ:W+��� ��͓o�,�1�N��%FOn��d�#�cWe�){[v�Ŋ%z��s�L�"��ڽi9,�/�j�s���X�hM���T䖍?�B��sШ�;u��Z�T��z��q -�펐����Vp�[���R=��~�sdEUs���!z��*���y��Vr|�Yn�p��x�^E[$՞�xfK��V�j3;1�� ���:YM�Md�Ȼ�Ⱦ�Xlxn��2�H���`4o����o�r��+fJ���܅T�=���03Zu�B��?q�菮����;e�����|���As���$�2�{�qv�i�j��{�b�}@۳~8���f����3����Y�c�UJVq�)m�V��b�ݏ��wMM1�̊<l�(��F�b̩S6DM�4ZY�U�|D�j��٢n�j��gR��� �;��)��=���@(�$�m���|Pu�#j(FPsR�/�į��./��a(c[�|�X+N~a������<j��o�����Ӻ����y;���Y�]ģ~��m��vW;>3�ޞO'YpK��� /6v09P��pT�Y9q��?̆��p֜Fm�5�@�9V��� �9�z�2�Zo�거��:��q�Sd��[���59��n���j��U�YDR�C��B���� @�Q�c� ��Gu�����q��/4_h��rh��N�)_�x2�ު��#�E_>c��^�t�QnF���װW-��)a29��P��ztks8r�F��Uq�\�p�� jdT����@�]�m����^A�c�&�Io���)=:�h��9���X��TQ�GG��(��:VJ���4�_h��|��Ţ���Q�_����&յ \�X�m��-�m�����P��!o�V��u1��we)�k�*^��)Չ�:�=č�?:>����oe6�mP�\'����6S�����2���/���U5������J���C�?���=�wM(e2��F��d{�^8��{tph�c� �ݘo4���L�Ÿ"~փ���2�ۭ�M��ݟ&�k;1�r�1�'����@[S�4��n8�^�ƿ��q�o���l2����c3�r7��ϋ����n��b;���� ���W�_N?C���7��e}^�ד��<� ����^l��~w7�ʑT�i�NF��xk �Um�[��=������p���ݳ-��#�FY'�������`5��Y��:<^F�x̜s*MV��O�et�አ�k����<e�F�&��"�k��9<K�A�uR�,|�Q�`P�8���&@�ƕnG+q�.;akK��6�� ��<G�v�%�*���Z]h�S<=䅦p���Z���D5a��v�Nл�w��a�F1�%�I�������^��v��:�n����92w�L��Uё){z�: t��κ;�Ku���)$�w.��^�%;��/��k�AG�����̭=�7������G�u5�\t�ZZ>��F���@c��Dm�q����8�V��#�z e˪�6�I���vO�mG� ��y��ͫo�pOSK�8J�Е�t9��O5@9�z�[�{1�$�r>���C3�FR^y=��l'87��]�wDv]�[��桼C��o�`H~�N��m�ۋ[H*x���)�����#�E��*���#Dc�/�� @�e)+���R���X�l�b}��iNn�������G������K����^>l�����[:�.�4�l5��!�h� wtr -X�-�r�D�htj�b��Z��EWA٨��p!:-/�C� O7 -[��IЖ}܊�Lh-lm����ދ,�v�g�Q�⑩�h��;����Y>l2�|V��G��I�+�I�,p��h�68�C����r��*��;8qi��NF�\A���#�$�Xk�����@��-x��x��C�;W�=_L��'�:$��A'8:���D��6|�E��~\3�B���2k]/�&E엒kר����^Z�Ь!~�W ��lʱ1d� ��� -N���汓�3��B���Ξ�Br]c��%bzl���j���4�Jn�e�wE��抯�DSZ�D�1�3��5�e�s!�-�=�eF��><�>40��4��a�>_#t����a֨��ׅ���&���v�ʼ��02� �>��^�q�����s5��� -���R���T������J��ɫF2��%��P���C��*G'f;U�Ti��˵�Kh�������Ҹ7���qo¤���VI7���-4�m���w��W�[�К�c:8)s8��'u��=��¦˺*���+�z�hү}�U�M�K/T�T���Dc���_6��ȭ"��.���[�i���@��W�(�Ѻ -�ô���8�0x�xF��S�Ԇ����M��ϖFe<�e��%�g_=<لa�����!�KM;����s��T�� �ljQóNm϶��4�@��V��$�r�f G��Pڝ�^�m��3�p͢�3�aw�huf�+��xԱ5��e�M����E:��P���ڎ�w�_���1��@M�~P��x8Dv����t�꼢 �H�T"�h�p���[��R�/R�oU@u����#��J� �����r��l0�9���� i{W#�0$E��`�:�)^j�~z��9V��o��&��@4,D����%�̀N��`~i)3f�^"����$d�u�/�h�MP�5镣֍@��� -�U4?w��|a8� �b�Uj�m<��T�55����3��t��k���0�'C��:� -�DW���:�,��`l�:�H�@SζsA�N��h�G�|�"���L� ��i!�}�k�!�˃5���_J�h&sӇ�M�BP�F��r�8���D�6��_���~|�~k�i��¯�c#�b�1��f����Yt��A��-W���u� ���:��U20����Z�wS� +�LWOc���}�#�����'h�4�h� �|����G��H�Ewu7��k�1I�CU��֛F:L�w:����ߛ�F�������b�hnF�\M��k4���0\�uᢲs&l� -LR�m<�����!�Z:��A�j -Bۤ���VB�j& x?:c�D�X#I��u79H@S}m��Yv���|4�RN����o�_�9�� ].�c}a�Ͳ�?�:)%�U�C�u���Q)�7����*� ������2�V���q��z4��� ��*��0��� �nxe�!�7������S�f� �p�{5ܠ�C`�ݎN�m/}�ҵq���)�m�c�M���`��mn^��^��P�\����E}i�ȼS��eX��{�:q�O�j�:y�6j����ʙ���8�Zݹz�����>��#��Iр���d�z �&�z�9O�$��!�'�'^P:�b=!� ���t2��bV����r��[�\����ñZ��aw�Q�{��`5t- v��j��H�@RMu}~�Ab~R�ob̀&I�!C��!�*�I21�{�G\�i=�b��K{7�,N{t�o�+v��@Jp�hp�X̯bl1%"�5571�ɉ�ĵ��9�i���\�2/��x�®,��?(�G#I�� �~�� ��E.Xu��=�&$���cxm(�3����n��_��N��U�A�gH��jnK���}@���\�f����`Z6��n�@C���`�ܾ�\�ɦ�%6�u���X<P�L������W�º��(�y�����͢��8���0�O(Gxe������T;��H�ht����}gE�(�{rU3:$�� -댦5Z���̋�r��%�L�9V+XVK���O����}/� ��� 5�ef�����¶�A^�<�Nx)� �-�S�B]�Cţͤ�opD;���Ԇz:��+_�k����2��YM<��vE,�/3�x�-Б6S�l�w��NM����T6^���ڒ��I��HD^"���A���i��P�<���+р�h�D���3�<��f�v_���F�Y��@�&�+G��rY�6���|h�O�X��+/���F{Wnj4���X&� ��f95L�&xԻ.PCC���%:u�����b�Ե<�+�-�m�v��27+������O��U�>���K\�P�r�JD�����xʐZ��oR��:�x$8+%>S*�2�B�MB�e�VI ������岠�>�h����G�a��Z1_�� -s�c%#vL(��7��mP�#�Z��m�,@k�N06Z�Tk�ʅ�oO_���Xt�,W4�<*vY|�b���-�CȜ o�zȘ��8�t�WrO�t�$�h�{:E�a<��*�U���Cx���XLC2tQV�U+��4@�Y����������tC�ԩ�O(�H&x�w����h�XGgZUo�#��$ Z�"M����\�f�jM��`w��d�kTk�np=qMj\��+����ۅ��%��5:H����h�������2�7�-���^�^Nʫu~��4�� k��Uk,d�g�Pe�x����v�� -�"%�g���mP� Zz�C�r.&�Y�BS��@��S�U�̝C-`m���ԏf�b�dr�ظ����*��{��˃E9V�M� v��y�t1���[8?I�lC�9/���Q�h��Z0�?\�c;h �IvK4�kbc�0HP_�����P���T~��d�_���Jɭ�z��F0#�����z�g��06Ľj��M�N��*��{r<T�e�Q,_-�՝h�Ո@���d����c<�,%���!Y�ζg�Q��s(�ِ ->f∛��м4���ڑ�^DSlF�� ���A�:����l�eq��~���\}r��Z��&%����M�r�����M⽵�ԩ}t4f��3q:��I~�k��Z�Tݜ<�j^��Q����&L�z 9��m� �s�8Hq�{��ɑP��E���uQ#��[�`���*S��g��˗�X���Ϯ;A�F��|����06�GQE568�N��_�qM&���> -�Z���V��N�@�U�ר��&J��g`zj��_�[[�.X!�s5�~�j���yՍ� -��[�8n'a�j<f�b��Xj@4�R#�*�8��C��s�ǣ -��r���X�_���J�������z]��� ��] R�]b2pc/W[D���� #�Yh9*��Ƌ]^��:OlT��Q�Bzҕ~��^Bp�jY�ZVO�r� -*�ʫ;1�ݱ��P'#�.��%�� -����VV -��z'�IwD�JR����.���zv'·���8.2ȗ ��X��Y"w�C�3:�: })�pS&4�Ed z5b�J����2�*���M����C�ot��A�� -��gP�-���R�T�1`��G�|& w�fM 2U��n�:��K�I;�qAn�]s�pc�YQl��cc�Ǻ����6%����6U�'�Ğ�4a�@�B�{���&R��j��L�V@���,���42��V�\��[`5 -_��c*q@�QWNG���Ze�<�o2#��J]d��'���礪��:��N��F� p����~*f�ܱ{���,*F���:n�o��2T�F��]���l�pL�Ls�����m���:8�;�i��j�7�jn���FC��o�k��F����gu��-�X���@��'��U�� ɑ�9����sT�v���!� ���5�@�T�J���ر���F<znM��+M���0��N̜-�L��`Mo�.F�C*��ew.MF��x2�үک��r:������������������g�}G��X�.`.�5��88�_��yr2�v����M˹��|����^�r�_���d{������z1�-������>]��L������t����A����`;���8?��An�ݍ���f����4��ד��[_�0ć@�('���7�b���&2���5�=�&����!@Ev���ލ�f0�����L$}�8~+���f�r�.^���N��z��>�A���q2#3�~�����;��n5y��F��n�����]4�,����tN�n�`��br�0 -ij;|���&Ƴ,>ᰫU�k��"|b�$,4n�,�c�[�wci2X���NO�s|:��tw\e'��>,-f���w��9;�-��wa�����X�)����J ]����o���?�������������{�돟�����?��?��?�_?~�k?~��?��?���W���w��������������]�����ۿ�;�����_���?����ӟ��������P��O�-p�-h�O~����?~��a�~�?���?���?~�;?~�'?~����������?���������W��o�9����叟������H�g������O�/������ �]G�Oac���Q�����w?���_�O����7~��~�������AJ�I0��q����D����9{(u�������P��ӝ����,78��@�V[a&�%UR�@��Mϒ!ӄ�q���u�S��IhM�b�]I \��ɰ�����>�w��n#A��σ�xp|�d`Ѵ��_�ſ�����?����?����u������;?����&����]�U�[R����Ҩ�e��x�ba�����E��>KP��Ӥ����@�O�TӉFB� ���0W~��QB�R-_��wv�s D -ۓ����i?8N�7��hJ -��U��'4by ��W����p�3ahd��;iDN�0�Ie~������y�cI��&@l>c�<�+�瘕��̊>�v�0:��}p�����x4����=79Ϳ�Wk�Ft��S�e����_̬��%��Q��C P��3rs`�Χ�ve{��McA� OT��Ŷ�k��Ӊ��L�0P�� -����oB��'�$�o�;�@mhčhN��g�e�z�n��E���x�*�pt�J���o�w����f�W��-�5!Y� � �|iG�F��zs D۟��.#X�E�xΌ�e �Nzc҅�1�'6$�͓ ��V�9���4���J5�Y��[��bD����| hK�F����hƸї5D���'DnZq�����)ZOV�����It�w%K�5����dokNHG5���(���xr&�y���X�cU3z���T;���r��.�Rٰ� �̴hh��=v"����u^PQYO�S�պ+��BX��No^�� -G�L����n���X��VAkR�[�X�H�$�5�oD�!s�E���|XQ����j��k�+@s<Z� <��r�D���v���}�Ê�+���V�\Y(�BV�i��Z�}<!oU��&�Aujhh�R�u9�SXj�+@CL��s^� ĔdЧ�y��/���P������u�7N`%��1�ն���BXǮJY�ٷj����H́�O���o�CYw���W�9��#��oRj5�v���pQ���j����������o����j�RZ_�E>�x|�HbU��� �(�[�N�#�KW���2��س��ԉޕ�M����vX�K$���*�R�֖Ϧ\�U�a���ఆV>�xv3�bu�b�!���߰v��:_�ǚQ�2��ȋ���8XXI4�l� -`m[�J���z9�����k ?���[���BXSx����cͺ�5 ��chh����Jk=��ȅ�.����c}I� -�%2�ix���r�XW�:-��5�w�!��7����8���:��y��- � VY��3�c�"��+��O�DeN�`u�z�b��(�+�����sl�V��k�IJF����w�ĊqE�bT���<��{���s�e)E�b�+zCq��N3uE�Ūq���ՋJD=�8��k��V�>��Nb �20ŀ�XVIWH]a T�Y:V�y��#�Ċs�Z��.Q�`M#,���c�u�-lX �+5~k��oQ�o��/k| -����x�;Ʈo�� L��ok;�̖*=V��q$�6��g�o���Y����R�m��r�����4�qTJ�����3f��r�&WWV�����j�ȅՀ����[��F�-$Z=9� -�Qᷝ�&/����6hD�i0�N?K��/��Z�Ĉf��U���.�U��߲#�)p{�U�]O�-�B#���ޑ�V��hD�!bƣ�����=�G��t�GU�[�^��E��RZhP -���~:ps��Jte����zT>I���\q���J�Ƴ:o���w����y\a�q�j�D�b��(1�QfV� -z�&��m�~�EM8����~P�{�9�V ����)�ɦ^��FG��$����������a�ܨ�;bV`��-�X�$��A3���XӇ�E�t�ֲ���Y��iX�*��Fa�ܰZ�XI7�6����:\&V��#�U��]<-��Bߠ/�����P���^�(�=�.�P��a-�P�%Sc�i��BC�9� -����SV��g��r���%4��:wHB;��� ��T'W����!�+ш��<ica���X�="z]��� ʢ(�?��HkӢ����������������O�//�+�:.�iV'�QA���*��*�� �����W(k��&��1�g�@6�)��a�|U�F ]9_�Ў� -u�FC:�����z`9����[�ӽ[<}���� ��6=*� B��hb�G�'�F��{�Т�d��RDc>x��1�j��9��Z�F�(��%�X�����9���,'�g�% ��Vu>P��=��w�X甐�_w溉'��<���/F������>>�ڂ�'-1z��P�կcj�[���2��xF�ؑA�[��ZhR0�Hc�W<c��N���wQU��n����3w.�K�?O2��38���]��L���w1'Nn�ЪY"��&i���������b���F�R�D04wʮ�����&�vW�GIp��Iּ�������������'#@���-WZ� (8��d �)�q (.}w��/� -�M0�A�Db���7���5�7�1�JrVil�� �(� A|r����C�V�o9��0����Zg��s�t�6$cZ��3iZ�\�� �.���Tb�$�#W�3�2Eh�g't�{yʨ�ozq&9���\z��k�p�&2��3x��[�쓈��/QZ�����|�]�>����R�[O�a�T���|�3�y���dmS���#��S�f�T�Y>H4j��� �z��$>VbOY�����o`Í���ٺ�:�@�4���/��$s���x��'�ibg�g6Í3ل�?O �T߄nu� �O���x��F�3>� S%��zr7��;c�� ��ku��,D��n��@vN�YI"����b�̬��&z�ގ���9���`� ���2��.$(��A��'�)��-�Oϫ�h�ă�Z�*�5�'��#�j��� ��L���E�����LNcH[��3B|�>4�V������D�,!IM���s���s®e ;]�M��]ү��E��_;�cQ�4S҅�)bA�Q�v�7��LI��{����@�&M�T��<8�FB ."��M��R?����n细�:X���&�#� -�$HD�7�6S���D�D � ���c�R`�$�f��kf�3L3�F�����i������Ljr=���Y���;[��g�F08�'��>�\���DY�� c�Ng8!wH�hDâ��n��;U�:�E����6vA�ܴ�k�0s3���Ѻ��M�����I!E�oB(.}��T��G�"����^�� -@sK���&7bQZ�w���n�?a�Μ���)����lJ�k���(RP��y�����BH��� �17�ZVo�״k�c�a���oq �$���q)��x���t|�⃝BAj����y5L)-���ڭ��j7�g�17���{��ڍ��`����n�ZV�s�t��=(��|܀jT?C���cj��j� �&� �}�v�P>c����n���"�Cп��珄1'M��f�@r �h{����(��o�>���^o�{>�)� N)[G2��O)]Mxߕ�qﻸBJ@{�XJ�8d��;<#&��8 S���1�G�� ���y�� - -���������0,-�A@�X���5�]���g�sX�$3~�S:�!��_u����'GCj�'r� ?,Fu����e ��30�e z��φǏ/k��̶�����(�8ҕM�#�#�⨿/�t4��:]��;�G��IO͒`4`d/)�1:5)�l��6"��P��|`�54�6� �R�`�j�x~ӼL�I��ﻸ�.S�(I�Ŷa�h�PDb�3��}�VC?�3�7<q���o��C�T�Ip��K�{�DOj~Zj/�ų�C���� 4S&�I���IRZ`W�����������c%�qK�G�"�$�-hB�\�I!��NF���x0�����m�k���Ǝ]_�U��|N9�x-@�9�|�|d��'������|J9*Z�G�7/����F�.~B9+���j~��O���y����x��j�n&�G���k�Ȝ�O(�c�q\��Q��)����i��X �n�=��y/\���'��I,@�w��}/\�ub{��N���}z2@e[����S0�ySٞ&ՎS�Óʖ�I+J+o�[X;�"�dy=�ݸ�P�'y��@��H&��Bz���b>t��vH4O��IT;��ԑ����2.��%4- T���#����5%�NЮg��<*�{o"��c�4߇ķ���F=*�{>��Gu�;*Oy)���Ih�/w�g���߄ -1%鯸TϘ0�8ǃ8_��SE�w =4$TTJ�z17��bzDE(�ƌ5Z������20�����:{�Dj��cO�>�i�\S�z\9X#�F��R)��Q0x�r�����I�Õ�Q�Դ�;��S�/9�5N<���u'��A Ou촪��<���|�h����&����0�v��X������ 7��Ge��ܳ���2]o`���� !�2 �����x<��p�wr�N��?��� -�d���0��`"�P��O����w���g�I���i���,���S��� q��6���=�z'd��! ��r�Re9`"&m��a�����'S��SOłX�À��P@�eY1��;�sVJ(�#����I�\,H � rZ猪?�T�3s���{J��7T��q뤉º��7"}��Z�&B* ћo"\����S�F�8��X����IN���(ܲ�86i�� �l$�2�wFo�q08��ÃcEoX)#���$��?8*��X�)�}_�����7��|H��<�J�N�}�@�א�g�! ������Ri`���G�MJ�d�ù���>���mp���R�t�<�z�."��!��ь?���\���]�dXX��0'��l��q���)$�CCKL�p��FY��w5n$=}�2�����y�]��x�S*�DҮ?�2��ɰ�*�$�~Be�7ꢀ���he�7�CN?�2�/����X����,�Ϯ̣�xH\��S�ǚz��gV� yk�Bց��[�����=���)%��*!�R���>\�����~�� ��!?�YLz_�N#�}�� -KE2��X�:?��Y���R��{�<��=�� -�-4$(��D���m����&x��N�ǚ�T�G�:����.�'꽀���#�⒀>�.�(%��<B@b6�����q�:�m5�yǰ�,���t�Y�&�z���0��5�CCh��Z%ɚd֏�6�"فl"~얤�O*��V�l�S�d�U$[�~�5=wP���{�5#��%*�G�s�5, -���h����kD��ӊ��eMJ�Ї�� �v��EydJ�J�x~�(���{����/Ey��O-���I��8'$s��7��E���{�=��6<}ґp(u���p�,�c��!�&bx��g~����ᒇ'�RH���]�+���')l� -��,w; ^�����<�����C�Z7��C�.�O��� ����4����#��#C���|�kG]�۬�X��~1�Ў'��v� 1FٝUfv���2�XٝAOgv��5ϊt��( -�W��P�]K� -��b��KY�ʔ��+¾��˴������]C������X�W�5Ŋ���}��~x܅�`�����+\��-�;Y��&�~M+�5w.rO�AkV-��x��>�߈b?� v���N\W��/j2���x#�]���22����n���6���ZLa��k�e��ݨ��{:c�"|[���^�q˳�Ԏ>��@jqQA��|�4���.�n1 �g*.��\�G��1bQ��w�>��^�=a���L�y6�8��SƓ;(v�@��H��7� �P`��P�I#||���XσV*�KZ��.�{Nn<��M�e��T����Z�������>>/�����>��>� ���>�(���|���>�w��N�O+����s�x�9?흅}�?���>�L�O-����a��~Ba_U�����B2�8g�}���/J �|ba��CRa_U_����'Ju~fa�`�������7ڥA�T��7�©V�.����� ����[��V��4��W�'D�O.��ў(��s���+�>^��W���V��i�}|��食V�Ƿ�y���>��>>���}|el܍���U��7+��#'����>��>�����y_IJ&`��n�>�#�;���Z��nb��3���4�",7��5}�b�or|�-~B&�'�����H*�3Q-]nؤzlmH䃹�{q�����c��ڍ��S��>�RF`�YRIŖ�G�F�TY�T�I_֘.���v�`iף�n���������9���<���s��=P6���U��AA�����9���^�� ��>���×�IB~��?�e#t���z)���h�F���g��2��»��F����� �ҟ2�9.��#ypZ \Jx�j�D2�q�>C�|����>Q�{O�yT�(�*�Qy\F4��$5X�� �1���ƈ/��TsJ�=^��&|��І�U� -��l�Z6��j@}b�Ua#5�J�:d���?W��eW?������D@����&�A@ا���?��^}�Ҟ�&%������j_-w;�Q��� ����LO"�HTl��L��c��(mn�Φ�e���6�( -t��>��]�I��Ec�zg|��I����V����jα��/����A@M}�gc@��� �W(D����Si�0A�g�X͓��#�4|*x��{�������)��{�������T�|νߨ��/i���~��?���{��Ź����Z��ܽ�$܋��{���K�J!zX�qWz�o]��!CC��}��W� -b~B��I�g����FI�T�K �A��S=��!�[{HdS䚲C��Rj��-�Ҫ��D� [{G�<��} FW6��-x�4�PH�֘Ė5�Z*��4 ����z�|���^l��%ŷ%R�5�O�=�l֤�:~�=�XCt�l1^��[oѓd1�Í��n1�а3>��� O|:���d�mS�e��3��*�V�~���*e%]�n�ٻ5�l�R/äm��\ ���0�K��r�O��}:��[���n��\��!e�R)D��jA�V��7� �J��:�Lφ+��v+:Ѯ�(f����2��� ��3�t�2*�Nj�nO_�W�i���J��?�>��f�v��x؞�j�+�O����TI`W�1��&�� +�P~�_^iQv��He���߰Ȭ�C=�*��T6�u��n/t!^S�2o�b��d������2ֆBXb���n�o�����YZ.�y�۫B��ƚR����B�] ~]� ��%���'|�H��h*�� ��ފ~z5��J0�Ӱ"0b��|�g���,�6Ǝ+��0CJ�v�=ۀz��$\��3G+�������{e�BЎ~N��o"+O�VB��.T@���oB�Ԓs���q�|y*ާkέ4��`�!;� ����>���H/���v�C�uK8���܈����y�,��Ķl�u�M�M�wzML�b[z�6�9'ߑD��>�"���� ��m��;��e9�Ml��JX(����)��J��H�f����#�n��W� �I���Jy���ť��>V6YC� �g=����Я�a�7+����A]qу��1�;�D"0(7#a�xx2�H!�N�:;��s -�HNcF8?pS�����S��ԕ�D���o�6(-��=4ՙ}b�@��..I - �H�Kܾ��AN�PBB��t��^M_o>���EB�8%�bК��?D�G�DOt�Ǿ�C�])��&�ڂS��4�D{�$Q�����=�%�R���ɇJ��#^��w�$��!o="+�ْD����p�;J��ڮl+��I��xP�����dFA�PP�e���}���E���_���os^�s�J -(Kʽ�^XcdTddd��z��k=�9�;ji�D����Y���.bۺ����D��>����D��7G҄��ﲒ�P�A�G����JV�~��d�d�����P�i6Y����� -w�v?���`��O&+�}'���� -�������Wcv�X��-ۡ��v�2�vhz��W ���������f;�wj�F�ý�@}?���d����Kxh!ٌv��N�C�����X�����XShK ��#=еw�C��~(��l���f�������#����$?�%<4�$ԭ�D�ÝU�����Lxh܂i�i��Hxh��P�u��и�S�QV҅_۳��o�$<4�y6?��pה,��Ъ�� �����)��*�.W���7�vh�m@�w�C�ż��d���p���n���3����e4ۡM�3��j�ב�lB'� w�yx]�b�C�)���� ��2u�ǒ���X�: �\�Ixh� -�@�C�l��f!���Z^r1��v��R��F�íK.4��������*rk5ᡮ=�� ������ƾ>j7?���8v�uZ�> ��������.�1ۗy -�����)�����#n��|3���[������zh�?������kJ���p?��%ᡱ�C`;� ��h~$�q�C���W ͯŵ��p����|���=�1�kK�CK�a�C=��w�C��jW�C�۴�Nxh���i�O�ڎ@��p��e;�Jh�^f;4�u�Kx���s����p-ۡ�)�4ᡱZ�)#�{���M�,$<4^����Ox�m���ѽ��!"��<��n�fwc�� U���x�.-�r�C㾂u��h�e}w�^�w/8�-xs�nG,�~�:"�`�1�}eQgӰX4ȫ�|[3�ЕS�,c3M��1�h*t1�D�99 ��+G�kU���9����Tw��ϓJ6�h�<�4t��R��]_:��u�?~�g�Tr��g�n�p����BF*��e��[���T�d����P�y~��������x<I���t[�?�ۏ�#�.=�K���Lj/�M�[�R�{�z�7������/f��|_>,?��A1�@5�"�Mdz�T���5�N%��G���7���\,��`��ݻ�GnN�S�p�.�0�.Jt�G�!Ru��j�p�����a4�Y�]����.y�^�b�-x�� ��r->��������%�j��m��}+<�ץtJ*������b���=����r�>) -(U�^!Y(�¯�/g���ΡǭEkF��3� ����ؘΦ$@1o_n����s*��=K�v�X�t -�h��3Zf��2N�g�����]�������\����c��=��v���q͈���7�B�x*{*��#^Y<ZL`Ҡ�����O}�4���S����n�p���c }��ͽ�EYSAO��� �gê/��g���9���7��P�!�7:�����.�����X�5� � ���W�&{s�1y���17�G�$�Q���M�|䡼8��<�w%��@��XI�EF#�E��V��G�����H-���B���R��G�"$�_����J���b�Kp�kד�x���S��Cx�%��%�U�R{��s�^ȋ��+>���D!���i�ԼdQ�mP^��4gLWGԶ�;���;u��t���e,Z*fܣ�|剬�_ؔ���(�(�[����3��U9�A1���A]���!G��!�158jJ�Z��у��vq�QR��jn�У� i7>Rӓ+DR@�8m���ɃD�:�� �'VMv&O�F���ګ}:�����Q�F��Q���{�ٷ�a�1��� u����AОZ�M�Z�#�Py�I.�8����I��� -|��5��Nhy[�r��ȭpP+�j����""���n��0i�M�q֝ ͋�j+�ƶ2�Ug7���%�\�R����r,��H�<���q�AM����H�N�l�a�E.�-��'i��C&?1�.���GQ��$��>��e�7�Z���&���3���CEQ^U�PE�m6ڂ�9�����'�'N-^�?�#:OH�����,ʾqR��(�cQ?r -�,3�J��0���n��/l�������X�������d+�y��f��0��\�2��/���9����Q�'�LFH��Ms� -�m|��$=9�y� ]���|������� �u������@_`��9�4V��OƳr�+����9�-��j �7ֶ�@�N�B#�}WhV��T4טm2!\����n�{�F$�>�ȥ�X��p�Z�Ӌ�!�Mx8�]�Ӛ���"Pr�^\�����#�g��3��y�ok�e�BNX���J��ښ��������jk= -���c&�6 -�l�8��� ܗha�8��k��� -m倥:�4���������n -L 8�B�lk�r8��CP����sX�����(�VuUM�3r�C s��!���5!�D�T���U��rH˧Ҍ�ܙ*�r���0V:� -�o���H}B䠬dV�zm�2��� ��N�TM3��v5�_#�X�J�N>L0�j��k�i�Ͷ,|��L��_�x�1Rmdl�v�i05��0+M�&Mh:w~H:>�R^]���I�_0A��:�M>4����끺Y�t�9v�n� �MV��V{SӴG��4���EMCp�&�`,j+�}5��9C�WU��#ӹSQX�ʴ�/��CӬ����M�<k]F4���6���훭N�M���̫g�o#�B�[�{}�V����f�Cv13L�3�I;���KBCY�6[�7T��6�WS�=� ����i'�n�i:����;����C��ؽJ�%W���2~����3uF��u$b���(�g�p�wQ����e�����x��O�Ã�d+�\ -���*چ4���Rp�bF����O����O�L��u��I?F3�P�C�^G0G�#�p�'���#�]�{2G�� ��_���UУ-��&?KӞ -Bz�!s�9`AH{�!?�/���$-0Ue -dq�R�;�/e��0�e�O�{E ��\��Z�R����G��RCA���]�x�?��#����@G�+/�Ջ�=EJ��������<mb��xLq})ܑ�#uت ��ש@��7�qA�Eø�������W�F�ct�F7�H�s' �q/�9��������4�����a���M@b���T}l�Q���#���<����!A\{�Q�>P��IKi�tDjz0a�ߓ��"�aԍn�X���ݓ��j����H�{� m��`0�����0g(���#�h���$X�\>��K�}'^|Tx<����r@׀��;��Q�������\�d�fÑ�P;ħJ�ؒ\/Nj�4�H {ܣ����4-P�ذ�N9R7<} -�/Ѕ�!�'@@P-[�!?��@A(��)Pj��y�|��¦����n%t��Y�8Ϗ)�:�{��>Q�,�@�:��Eo�yЦ��#6�K@Ü�Z�j��*">e�6��Φ��S��w�2��K OT�qXI.��f=����QF̟&f2g8���y4��������l��R8`G?���m4��_�/S;�L�m�ē�,7�O��|4���2?��Gӥ=nO�s�J4� gO#;��<ԋ��,�@L ;{a �<��T6�;V������o��҇@�7���+���� ��}p����S^��iC6 ��������{�yvU�kջ�Y�q�M�FPM�wSJ��]vt��X>�?��o�3�R�a�FX�z���pX�Ya�rp��Cݷp�b��:��Q���2x���_��/<�7��$���՚e���pq�(����9 �n��0���7x�� ۜ4aӄߠ��T��n�ߐJ�� ƀ�rkz�k��"2ނ� ' ��㜰�zׂ��++AN4Ic3���ANj��sփ�ߍo�b_��3� �Ѣ�.� �8���?ڄ_�F7�\h�m7�?��h��?�tx�E��Ch�=��B�5�����UmF`\�N�I9���#4��o�@�u[��A(/쇻K5��ƻg��� �h�t�(�[qgmT��t��>4�En���\�����(�lZ�t�z�F���T��h�b"s����&MAgeQxX쭀���'��$��^U䶜�MN�v:/8tN��?�� 7vLmc��m���=vX����Q�RÇ�z�6�9��m�1���J�b3��-���wn�yu6q�U�1WO�X�����Q� d�r�Y��m����G�1�%�( ��[ -�;�v�I����K}����[�x�����l��D�3FL*;b��_��bt�T ��B��V�N�K�?T9tX! X��`G��o��©{�RU�u�T��ST9�R����ZL!�D����2��X*vnw��G� g)8��W��`R)7h��Y&`��J#��Y���_К�z���4�ⷢ�[��/��ZQm8��u��� -�d�0��~�\#xx�������I[?Ĥ4e�"#��R9˫�=@1އy��y����ȣ�WY{�<��62�[�4�E�5M���ݚfH���,k���cMӤ)�K�LӨ�/��%��B�i���J�@D]�F�j�Y������xf�M��Lt�ۥ �����_�p=�l�FL <�#n�F��k,M��}HY(~��l0����6RN_)_01O�2�I��K#��t�ֺ���絹�F|0�!�V-� -��EKj1�~M�����kbi�s[�N&���a��fO��{z�8Ac I�x��q�U@�`>�w��U�T͵Oc�6�,���8/h�<�h���D`>�V�;kS O�BD``���@t�Fkn#�F�q9���nz$�9.d��h<���><8�6�hB2}�(�C|I75���b�?�n�7f ��{�i��=gc������l�@5� -�`̅f e$<`�6�s���i� ��p/�p(~��\|�]��w)"����/ ]�F�n�2�ݹ�DF� ���]ː��!����=����`|�,��q�/��*\��0Ar=ۯ$/�I-�Z^�J�����zu�R�����YSc���d���T�3��u��Ue�0��/s�$݉���6�����C����GAL�p����t"���ڿW�;�K��sJO�q���{#?9�(�kH ��U�X⎢��v�,�bh�����3� -\�K��Y4K�Y�Bmf�@��ա\��u�!�*dt����8��`w���bﶕ��Q�X,�n�>}I�A<���*�qO���Y4qO(�L91�&� %�~}M�J�|M�JD��wB����r�0}b���\�-WhC%�͎�/�j���nd;�'��7��JԮ�B�$�V�_�����������?��٫pIW<�/�/���lڟ�m��k�Z�Q��������c�p(u�[� E]����d?�?Ϳ K'�0_x���� �N��+���]��5�C[�?���+@ ��9,���q$�WB�iA��@���99�rG -�o�3@>���a�p�M{+&��|N+ �A\�oQ��s�m�Ba�[tH� ���PsP4V��L�G��*�([��8`��j��9*��A��)���>��)�#ME �,*�H�erG�ط�&��i��[�%L�ʔ���S���JD{�4�9�:t%l���T# -ʎ���}����H�^����i� )x���,�TΊd�e¿��p��8G"�o�0C�Q?�>Q�<�R��\�8�T�$Q��iYr�b� ��?9�`�;Hgշ�&��B�w ;�0�R+�#��1�f5KL�X�@ �gw^�ǣ�y�� Z���_#{:�-����e�G��l>�/�g�+�%�`��`@�����r -endstream endobj 23 0 obj <</Length 65536>>stream -%AI12_CompressedDatax��K��ɕ�o��Cj1�)�sGІ���)�C��f0#7j��d�tW5��9���7������lxc@K�a-,�)����y�s"���2+�����]]y2�kD��9�o������O_��ӏ��v����[W/�>y���w�H>��g�}�嫗 }�G�>s�|è��֏m��?}��Ͽ�ߝ�����ٟ>;��ϟ=��o�}����O�����������������������������G����r��'�dP������\9��q�������/�|�峿�7���|���O�=��勿���z�����G�G�����GO�|Ø��|����~���'O�����g/^~����Ϟ|�g���憐o�}�T���'�Μ�g����_~��O����䩼tn���1��{_>��<(�Mz������O_��g��k^}����������������տz�����Ŀ��7��������_��o�{�|���O^��w��O��m�۟<������͝��Iޞ�����Kr�G.��Z;�(��<��ϢL����x����t~���x��Ͽs��/�?�ov��Տu�b�6��~���>{����?{%O�Ik�Ѿ��ӧ�}���K�~��ߊ��ۈ�<y����d�_|��+.�:�"�'�|��tv�|���O^�>U�-���CLg��3Wy������{���vm\��7(������峟={���\���|������>�3��,C���:�.�O��l��p�{!�<�^=}��P����w+l;����d7�?�z�9f�Kn����>{�3������K|���* ������������~��Ͼ�_���_}���?}���R���O?� ������O�a��3����E���#������{��؇(#���O�}��7�J��˧g�[9�?����?���?��=/�T<��������]{�4���/�����몟%y��������ٓ矞��'/�x���'ϟ�<�/������_�\n8>����g���<��|�����B�'q����3`����ɫ��}���/���j�hƋ���/��{��������gϾ�|����o�_��p�1��������?x����/~��/>[n�������7�j�����������~�1�����������g�=���'_���'w=�߲��q�~�W���m��>hk}�ev�i]�=�C�{o�L�� |ۃg7�>�w��w̏��ɫO~~��g����gO����~�����?��٫��&�"|UY4�'_��������cQ�J�Gݯ.�����~��|��3QH�"���ϟ|.��όt��U��xv���ч��~pUxdɎ�#��<�M��[7<�y\�q����s���v�G��<<�c����������<��Л�ƣ�Qxd�G���+z�dž���q���{ �E�?�G�C���+�o��]p���5.xV��[h��z[o�u���s!����\S�5T_]��m�)�2=�U�Z���\R�%_\��m���2u��M�ݒsN9�}vyK��&]˔^�Gh���rJ)&��'��xo�L��|�k,1G��c�>����p�e!\ʇkb0��C -1������7�Z��<�|��/>{��}� -O�u7�Z�ϥ|lyiW�4J.����m��n�k,+L�&��Ƀor�?��t�"�U��w��'�p��B�r|��G>y���+��ɣ�<.N�䊗�qu�>yܜ:䊷8�U�ۺ�����2�K�W���e���at�:��ɬ��qsu}�?���M���N\A����5�TӀے��n%泴��z\�����W.�%/�?ϡ����kC�?o������ve����Эd0���K�_�!���յ�U�Q��:t�]� q���)d��x���N��Pz�~><��QN��!�H���#�8���O�G�8��`���]��U2Q�������q��8�\�����8Ņ�����<�'?��}��d�!� � �T�]P8B�A��D�LS���Z�l�m�nr� �RrN%�碁�����S�wI�ɧ��O���.,HA�A��WT= !U"B&R*��U.R2B6�t���"�(2RR�$$��JHK�KJL���T�.�r4����*?!AE�~���FI����*�%U�f2U�*�JV�ɂt�|U +2VV����b�6پ��b3�{c����6֜�)]3���N�+�['��������E�BԦ*���CǛp�屶������+Q/D��2��:]G�8����=��h�� �s�� ���d���3BWܨ^S�؋���`�������|�Z��`��ja��]�[-L�>ha��8��� -��W|'��wzE��N�[D�\�������8�c�g�h}�=~�߿�E^َ<n츶��5q/���a�Q��v$=��a�;��q�G����Cv��F]oR�Ju����~�߹}� ��>_2r�[���r'�7�V$��o�&��E�r��})_�Z���2+If��,5��K��k��[8Nf3\ -���2�U��B��j��j��eu�d�ta��d�Y���l�p�o�<�χؐwZ�r��q�/>�`�iS�j(l���?��B�������*E@n;�zj��"���|x���ΐ��PE�p��E���l�ǜeoX�H9<����]ˮ���ct� �G���ͤx�L�ߖ{�ͤm� -\"��F��F���8�]��37m�6���y˭�E�G/LD{_E�_�ov%����re:Q -�(�gŜu��(PH�E��M֫��D͐+���E�j��-�uD45QL���J_�T�#�e�ͅ*t�� j���9�T�es���ȇ��B�Ɯ2�@Y���Tx���@^=MTy4`a�Пi$�L�?�6���?�7�p���q3y��U-�#+vqB������:{�3G�88�ܤ:�Q��yq7{�I�/�f�����w�,�؋��8v�p������ˑ�a+x=��QO��!W���x�qҡ8����sq'"�,���w: Q�P�佮��|�qڕ�;�os���0����1��L��F~�͒_��Za\^��(B&{�b�_��~��J(=�'�P*�C���� r��Edƫ$©\իvuquyuuu}u#��v-O|�����$��\��v}q}y}u}}}s}+�����pE$f9���\�\�\�\�uK�=����]r�ܦ�6�M�mҺ I}�R����*�e��JdOY�(�+%�%e� e.�m��͔����KJ�J���؛���!o�8��ows���{��������������G��+��s���\���gGv"���!�|���4��K�+�k��=U�s+c��af 9k�9���p�����ȑ�ɕpܒ7m�Or���ɧyU�����s:���Xm�`㡍qshct�bp���;��p��l�;��N�;Ӳ��td=-�v�fW��$ol����}�rZ�������-�9B{;��߫i,w����`���Һ;~ض����H����Oһ���5av��ct�� ������v�#N��D����Pr��Rng ����g}c#?�6�3b�a5�,�,��B��,J/2�b��ex#K����\x���n����B��"�%����daa���â�mQ��b\����.�M|O�}B�z�:u�P�����T ��+Vi0�Fnw�L�kfݚ��e$�ևP�J�G��U�u/�x]2����� �{���f�TĠ�E�cI} ��T�*���\V���y�k�����f�w+�����Hv�qt�B�m��rrE(���M������q��=���"����~�8<���x�ӿ��+�*"@/e��UD`^�r"ӎO�P4r�B��(�P8��8Ta�"^�`�]N����XL15��#��������c��E�w�Ժ68�UW���Mi۫kSQ�)i��������v���*�4�wE��_�����x���Ko9�ю48�rG=����1��^�y98�����س��y$�9�;� ��z��#�q$=��đt�S9�����C�������1�\�Q��c���@��L��J ��D����A�U��@C|�hh�Z ��\8�S]^�c�y)��C�7�6�im��)�)�)�)�)�)�]p坸���>��>��>���}�k�hoRz�q�����+~����/rЏ��c ��@��̀X�Ͱ�Ũ�����a������v �mCa��UfZЬ�͒�pZ�Lx����K(�rԇ^/!���"�Mύknxpf�-��Դ��r7�FMk�yw������x1W���==u��Y�~�5�K�鮻���,�����ƫC'�g�Y{7�mw�:�9��WMӽ�>J���j���U�t[pZ����J�Ī��M��ٴU⬖a<�۰��e��Ec>a6�I�&�1ɤ֠_Ӯ^-�7��3�j������� -9�Y:/�Y�� ��Ϗ���N����������C�!���������iM�2��q�~R�c����<�������G���]V��˪�e�w.�z�W8���Z1����]�]�"�O�=X�����#�<�ɣ�8d����q��ʼn�r=�0t�U���!�\ ;�����%�V���fup-N���r{ߒ��X4ve�.�Q>s��Վ�^�ǎ���I�ױ�k���Q�m(�O��Q�<o�J�V�(��(�M����r{I��(u�Ŏ��q<��Q���,{�>�K���q�?Rĕ]q�Zyg��ZI1�/�|�v�2ȣ61{Ϧ�_?�ҝr.O�"���V�X���]a�r*���ߔ���]e�;L��7<�81Q"�$�s+l,��E��X"|I}��P��X��{��B �{�^��@q�B�kq^����%�̈́�A�&������7�w4��Q ��a�m�ln�c��fio#�lqs�䦷�)�HJK��]ÛihYO�]㛥����T�E�v�^~�q�G������c�a��;���2�X���ç���뇞�;ukhp����Ï�:rŇhD�qy�q�q=����d9�7(��t9�qA�Z��/�_��ga~/͟���<��]���,bʫ?`�qmQLn�6n����ɯKn��"¤�H�8�D��MľQDoQw�"2M��.��Nd��a���4�riQw�#2��8ZHj���Ŗ��}���T{i�s9h�b־A�A#� W�fw����Ļڨ�箽�Y�i�-�>�0ei��h��@'�v�G���}��)��tmeSc�X�@n��D��Ӂ4s����G��Wב�%��8��EO'XG����d�G{��ށ�w!�J.Hӑ$��>���$��cI�N$X��p��I3�`�� -�%�y�d� ˩����'���t��k�O�]�8�ƻ��N�x�r>m���-_k�w���h�vh!G6� �}��z䊏�q�yӱs�?�8լm�n�ʛq����a���Ѱ��. 3����p:�yg���֎X)�����6�ؖK�Ku}��'͇saE�w.��� ނ+p� �I�C���o�Mm��N�\B�����y|C�!�����C�=�O�[:[�|�������9�_��%�_���ЖK�1�~��M⡷r{X������T"1OW�uh�!uT��S����v��V��A���5�;NO�{��iiw7�1��������������ei�t�f��H{�d�{Ӽ��3�끩]��\� ѽ \�IҲD���S��I�;K��?��J��_=8�u =v�7f��݀B~wL�d9*32��BODs�m��3}�At}�ֻ 4� >��^�2�f{T}q�,�����>ܖK��r���#KX�������X������i��H8.�R�Z�����?����W�추�`)b%�:=�B�Y�x���,`=�N�\�r9�>�����h��8O�[v������Z�i�?m�����6�A[�jsSmozm�W4Ū�6D�b!h�f�nD5���iozZG�9��?����V��Ż�},���ߒ�1gYk�qF�.CPUQ췴?�A�cβ�l����h�I�����|��ǜ��#��/�<ks;!�բ�!ÿ)�:m3��UD靖�W��"z_Etp��"�5�Ŀ�W|_E����!��*�_/G{_E����}��*��UD靖�J��R>/��Dž`�I�����RrK��N��8nt��ہ��v#����;���5.G��S��9^Y�,��ݱI72��*�eNv��P��Le����ls�8�,:�2��7�Ԋ1���)��,s��:h{F���q���B��Y�I���G���5u�_;L*�8�:᮲uzw����:�AH���2�����u��"?VQ��� -�Uu���O�������왘�c��섯�c ],k�f/v������E�*k���9���+�n�p��\ݚk�؉z1�����9*wg���29�\���͢�����5����q��UŢ(�}5��x�^��M��D���Mn ε�7�7};��ݮׇe��p~9B��#> A|rX�xP���enw���{.��aw��8�)k�5(D1�[D�#DGE�֔Ýy3�;Ѡ��q�z�a���9��7�D�C�����|cu�t���ߤN=J}:V�v耧ԦC�i���T��j�toT���[c��7u+s#��^�����~Qg��F?�`(L{�� A�Mu�U�º#��[�T�0���E�ݭ8��;Xcceݭ2���uI�8�] -ӝ���bOd���N�ܧ���#YY{o������r]I�.ki]I���N����VjՑu�>�߬<�����e������t�Ҵ[!s}ԝ��¢T�ޠ0�s7_9,e����W����UN����M�+�|U~��t�:x+��q�{b�T��c���>w�{w��\��� 'vE�w��8���0��~���`�hY���c�*ˣ7�F�c�T��Kl�;Ӓy��+�~�G�W�o�^���\�\�5���y@��w�{��W|�_��RO�졣6:�Qs6CE���D��`���`$`Z�M�C� �� ��zk����z��9�12�5����51��f��̞��4�S_�i�DM*G'�5o����t�ÌA��~0*��u���<��!W���9��îQ�s�=��!W|и��i_���~�$�c��x�����-P�=Ӥ��t��C�١n�il����Z#=3|w��i1����K���={�Lv��9�9�3��z�z,�bB�Lx�=4��� y�g��Y!x�wN����uf�Ϯ��@�M�7�AZ.N��W���!���+^?2�� �\�g��&]�pl��s��Z)$� "# ����A-bz�{!yq�2Or�l�S�<�W�''�Mv���B�3����C�P�¢C���<�VN]�pȯ'����g8��=l��+�;(��EucQ�?��-�$�����y�N7�����W����qO��{�<���?}��?{���|���������^���������������C?�}�oQH���u�oJߥ�?$9�Ս�> -NOA��u���;�R�T�7�X98���O���������4ه���GO��yߌ_�?ވ�'W<��询n�M�Hw�#�M�h��`M?���tR�Q�q�4�ƽڸ�ڸ�F2Ys<��Qw��E{���Dgջ��^�����Ԅ?��_�s����%�a�������YF'��=.�9�H�=Q�}�.��'w�[=0���w��\�~c�~3���z�8�����^1�$NĆ4<Q����yk����>r=���n9�H�������lج��JW��{��zv�J�:*/5鵞���|��~eV�'�+;��ߖ*�|^Sh˥2~T�[����V=�D����� ��^�߬�g���d}��G�b�ZԪ�_�w��W������_������_���_��W�����������_�r����l�����t����v��sg��z�o���j��%ok�P�q�!;a}'�tg��Y���ݧ��CK'���3�S��?|�����,"j�nN8q��� u�r��zy4��F���w\Y�j};�u�ج[�U��A�}:�eܱ�� ��_��x=����Ȣ���'�}�N�;�0'��^'�9�~����ݏg�~k�����Z���֙_�Ao��:��忳+�,���?,��ّ�z>����հ��������_���_��������`������'=�Zs�Sf5c������%����q�Ƙ�}KN�� _@(���?|����g���G�/��͇���]����ɫWO_>'��_`a���}2��]���̻z�r5k8wYdS� �z�F��������Jp6��K��oɿ�T�.���g�w��O?�Y?q�~��A>�����x��-���^�9��틗���}��ً�O^���;�[���/>;���w��}|��W/^~|��?����O�}���=��շ�������x��n��:�Tx�?h�?9n���Y��8��j~3� D !ff $�q��O��v^��g�]����YtM�4�9�Λ0��(��mA>M筺hg}D>d��[�rPbS��N)B"E�N��%�EQ�xa�Fɵ�"�^�X���ܣ�I���Nѡ�I��<^����}���e��sv��"z.-o|J¢�(���:)��!D$�D=3T9����b�]�l:N��D��.��&_1U�VޞdXJ��@���*���@K֓R���g&_�̌�gR���$�Z���T��NL�0��(���2Y�Q�LϙH�����RԵ�ɻ�*�@$ι�L�(2TLx;q����<V��,��D�A���G�s�n�d�� ��gǣ��B���ɕKz����Q�"���x%�a��"�_V -�858�H��80�-*��()`��"��eݠ3T -rF��Q6h��e��"�V����N�XX����AD(���,+�s���Wܰ`�@� g��&_v�y�T��^KZ���o�jOp�G��]�o۰�)䝢슊��e����/�����iH�����dA��紃Z�d|�w~ƞ[�ĒH ��"�*�T_1x:��_���X��kI�@,+�p��U�`��dQVdRd���M�_n�=�~����.��s�h�gsS�y�OU\���U���"�@9�ؗ)mXv�K Kr-�"�%eg w�C�6�8��A���AJŷ�)O���'�O��j6v��ETXR�>�3c�ߡ1F#˥@~���&�6F�U�r%Ȅ7ۯ�t�+F�k$�M6�,��,B$P�U<�\j]85{APEw�:.*�[���g�'r.d�O����ې�[V[������ɕ����|�*��G�|<�Wc�E��[���y'R�y��"�Q�1!���I����?�9L��J�:j�?H��p�,��B4�e��`�_��̻�\�M�\A����4���2� ƥ�/rW��٪2q\.AUȝ5���DTaI c&ǀ���%�t�(�*_TN��Bh�����M�i?/iS�&'�C���̐yM�@� ��D_غڱ�3� -"-6�h< ^Yi�2a�gS�JǠ�������MR�@�t��O>���6�9�r!2�Iب�iF��u��-W�s�e��B S�gS��t�t��H:,�Y�<<��Ȧy^����!�U� �V啽����'�c��9�"gd��]�2&A�s7wF�k��>Uβ,`�jt0��4?�I���s@�̢_'A�H��:U��&|V� ww�j�߉�!V�HF�y!g;��H(��Y ���w -��f����yQ^B���pB�� -����� ��!>�<�R'�a�1˒����Z-d�5}����E~�h3:,oA�bB�-���0N����4Z��vP0�(W�pzO�7eK�~��Xl,*��s�)��UMA�Mxc�����I�F�X��\��,��B%g�<�Tꀠb�&*��pC?�D�E$�����yy�K*)<�o�/xe -`�ha�5�z�e -��X�����a�٫ա��������FGy�H2M��²�_�J��XG$xN��Pٞ$�s�5}ܝ�qWn����Q��:y���ε���yN+�{M��g�� ��z�x9E>�M�R�=TS�� �9��B dR�L�TU�7�n� -; *��Z�S��r�`��M�V�J-{��:�&:N'����3��s`*.���o�-�(C�*�� 7d��� �а��R[=O� բ1L^M�d0��#$�~�שM./�K���9幝0�i��6� ���B�t�})(��R�`i�"�\А�J@�Bp��M�|�s(�^E���5�5��Hp�.�M�NJH��ڂ*!���*�܁\x`#�y-Vv��H+9d�� �vZ�B�d|���Feѓe��ߧ�<1�v�<6(���r�p�����?��0��o�ً�p�녱 K�1�ԑ;���L�N�?A��[��jT�;Δ\�����$'sK���*qG���H�)6���!$�p���*� B!C���5�_��q^�h��`HV$�!P�� `%��ab�%xgo�a��'PNŝzj'�~���gk:��U,�A�jt�Tx& �|kg��+��� �vZ��*������3~hS,�;�o�Z0��>�T��U)o6UÌ�:u4�'i��A|&�5���<��5XX����ِhIW��o��@q�6��2'!�(/@���r$�x���C�o��Z4(W�У�c�\5�t��$.]��=M+��� -g���'Qc��}>�b97�(vj��V�>\>�lZf0��м��Z"� ��#��p��`�����:~�z���v�)O����_�6��o�c$A�^(Lh��H -'�Ψ]��X�J�ǁ�P����d{���9�Y��仐��$�r �R��<�՚0p�F|y25�t^��Ї=�����py�\-Po���d�{��,�n��*���@������� -��&�@@(�ۊ��"���X�����Z�J�X�X9kJ�AL�T4"�dFK�M�E����m������(�� snoY���:��d|L3��pc(G�ztËգ[�u�>�ݸ%�#Ta8��T,�& �8]�Y@i�$�G]X�����N���B,~.� -G�85�kWtG�(��V*F�K�Pa�؉��#�W"<���_ -H����F����ug3��$5�8�V�?��M�q���� r����Ǥ��u ' -�_��qxe� -#^+��ԙ�0��cܩM�A�2���§��m�?�����=eL��z�G��rm;G�nSu�V [�ň�[U���}�e�2pS������\��{*٩nEY�8�_����Q��|� -*�t|�H��R���H�{��y�l�`�� �R2!�H��T�zx�%%��4�D�lв�[pƝ�0Y��Z�+�� ����A�R�� �x��{S�e<}�|ҽ��#�@<5�c���,���r�.[Z���]2�%M��&<�ǰ�y*�c 3D�)T��!��+# WNțׁ��JPU -��8b�`��T����xj����kL`�xZ��Dem��T��T���w��TU�%���#7�ƈ�r'D�j`߃�Ig��na7P&GZy�R��������$樑D(CoƋ���d���?\{�Y��0�*M�>s��g��FCxљ��\�`$QP*�� -y -}��T�@�TqP�du���U�:>���T�j�L��^Ő������N������l�L9��(����1�N[� � 3� L}O?|R���E�ς�!�UDC;�q��e�UD)aaؙ�����zd��"�΄Xf��3�v�L�ld`��Q�Fw��NF��\��z1� �FG���a4o��؇P -�c����m���a���/�קp��ȴ���;�b���h�� -RI��Ny��xF�v���o���F�A$Q�H�0���W7���@�Jh��ݒ-,���5tJ����A�^�"���������c�`P�z(��JrE���{�M����{�t�2$���9V�Z��?L���YLzt�,aF��Ω�3fF)�� � �X�I�`.��ԗG�,�DL>��T�B�t�s�6�CU�/HNe��a�����C��m�u���T2���˱�I1Sq���Q������A���F��Tp+8�v��A>�U��&ԡ��v��+�2W��>�9����������r��^��hW"���J#�7d-$��1�mP�H -�ȕ2���� -���c6�I��zZ������|8�AF�淺��~C�!|_�x�K*&�(�Tt�3�~B�aȋ����ݓqO8��̠?������ͽo.s�EB_pȮ"G�S/�_�(-8ZF�^���"΄���Dό,� �jp�M���\(j�k���C� 7X ��긊}%X� hNA�y2�țQ�Agc� �]���4RFЄ�༼S��7SQ}���k�&u7�YD�T�Mq�04"xr�,��KC��{�$T'0~�_�4)#1"�)����3%˧jH8�#|M�"v�BU����p�U���\܊B��= ��)�8~E���?��&Г̄*R���E#\�)���q*�>��!UJk���S��a�+�M*�Q��$�.���j!��k$�C��7��E���Z�Ζ��]� -Bq�e�����t�Q���x�8SX'c}�Sƾ�z���/d�{ |�5�O���|�_��MsV@��ƾ�R&% ���!<���n����.@�?Zq�d=s:�W������4��2Jp��43�힖���K'xꗀ�f��2�B�O[�SL�ɪԒ�z��<�ϓ��J���G�F,b�~������?;�����'�|���^�z�������1 -a.��@����� -�~�B|)���F3E B�#��4�Dh� �M�2�8�A�J�v�J���l ���Ӌ�8�<R��f�gF��?���C������3w�8=�J��!��b�8?s�z����K�@%�����12�)I��.� -m��bth�禷������/F�� 1�l���bU E��r L��;_$��)7*S�(������jL������.�P�1�L���{�I - -��X�� -i�Ш5�����Ag��3��k0告��=�o�) ���R�0�.�. k�������DD���Y���V��zk-��{lzZ� ����.3f��n/ -c��8�9���sSŏ#�g�҈.f� <+�(0�a�D�N2����'��O�Fv�����7+�}����p� �=d�1��� -݀�`)ʱ�MA�����h�R��v�|:G6 - -xxdVgR������MU ^u�QK��b���ݙ[1%�r���g�6*��T�!߲3�\�;��i�����O���<\N�9Ma���K�7����qַ':�c�,`RB���8 ��j�#>��pm��}��K�_����u0��0<�LLN�� 횉��;�^�=���0E�V\�!8�����L����#6㑛��MfH�_�!��#�lȰw���� ��C~��rN�ʃ�^`���AAh�0.�j��<A�_�����4R<����d2��u�U���t�#���x��k��B4�5c���r|y1�D��|� 9�� ӹ��&5z,(�ۀ���Qs@`9�k�n^���o�s����s?�YP��H�y���Ѝ!��� QX�>��I�EX��̷D�&��ݩy��Aa$Ā�o��j��l�j,<���Tz���$ԉi�W� -�߆��i�08J�.nC�M�D�`�`o�<��Cf�{���+߯!��!_d�e��.��Q�O��'(2�~�W�'��KHD,3v��Ѱ1��1�ό�l!U�(�]E�pn�� ]n�tE��C�8=�+�ӱK�@�y���ϩ8�j�CڳL���K�_��j�r'�c�-Q��Q&1��F�7*n��"��AB*�'��S�<�ȸ{���p�� �68U�ae�.o8sa$����X�'D<;O���cVb `I�C�� �-*mzq����2�M���Ae�)��Z��Y|"GM����R��� -,W��R�A�B�s�D��b�Co��'��e�0zy��LDN�ۻBqu8�����^HD����d.]�;�d��c�F��F/����D�W:���� k=zh;��DQT�ASj�(a���?t��L���3�,D�WZ��jڨ�虾ɲ����h�k���ȱ�5aNѯ��&xW�]n���C@M�$���i��).U'���~p�@T�Њņ��g=�qr�Գ��ŋƧ3z!���x��v�]6� � ��GA� ��TRo9�� -iO��F��\| xƘ�_����<ϓ��H��BR�Y3T��9"��ypY!� Q9��HZ@�EYZ.�F���F|�^\��5ʯ�P9��d;&��f�XB��}��ݮ����]���T���+}L�!B���ߗ��X�FxL�L~@���^�Ņp�f� �s3��2�K��v[T���B�Ӵࡂ��i�rG�T�נ�`��2`���`���O�Z3�1�g�i��+��wA��{������uk�8X~66�VZa�!!:2�a���Z�M撌=���)HV�9dJ�? �7�<cr�؝��� FIA�y��$�tW"�JI���Ǣ.-���ؼ����$�E>4o:�13tv�+!�y���'�5�a�d�*�tJ4R{* ���\6�]0ʍ|>���.�/A+m�gӠD��r�X��x�V=�u�B�����#�е��]B �v�ܲttci�E���"�qjH<�o$kN�i��Ԩfh�����ѾA�������101�զj<�,X`�h�YoIJ�q�E<��cev"IJ����!)z�@ᏙŁ�q$�!�VDAe����1��z��t�8��ة�p`��a��f$�d˫��VZ��z�2���!h~I:Sp1�x���h�:�I�e�܋ej�����S�IM��Cx��yM�F��A��r����-��4 �U��}a�6iʥC ��;;s���R��Ǟ 3L��6u��s,�9[��`�I�fU��-��R�j ��S�[��-���UŴ{f� 1Ͼ@P�W���h��H���S���!%*Exf�3�6�R��� ��%�=H+%2J��|9ư!%��+ۃ�{+ϕ�C@��L�b5&�/J ->rP�X�Q�nn8�P���r�NVh�U�iDY�Ӧ�o0�P*j�\���+�x�A�]���eid�X/����4�8rHf�/��=�#u��!X �V��U�YX@��dt��D��Ӏ���F}6��F%��GI�6r�Qo��P��!��ؙ�MaA�&քfX����9a��bZM����g�+����<d�Y�Yf�E���z�Q���ʔDfc<�Lld��,6�@a'�Q������E�6��T0���5!ƙ����(#ߒq��G|N[�M3�f��� -�,�M(��Ey8Pk��p;�@���1l$g�022 -��J_�4�m�&�5����,I���u���t�VTlX�i�B�)-.��ٰMa�B�"lC�p�������B��}̵-��J�]S����u�?��4YH�('���y�t1���M=!#��C288�ST��F��:ڀ�q��1������c���=��8V�#h4��(|�r.w�>��f�"�ᵂ*妪��[Z���;:�� -C�0M�#e�&\Z߆MO�L�!8-��Lۡ�8�<� Ǒ� ��UM�����)��h��°9��dd�� �z��E� ��#�0��81�NV.���C��y*I#td�0���s�$cuO�l��X�Wu�'�C֤@�v�HZ����+j&���JQ���sxa�u���vk��h�b�[ta@�7!��`��m��DmC��L��K��-f�0sI�LB��<�üE �#��N� -OjZغr��Cf�Y�� -(�ʊ�n�?���=rYY�\�N�d��\�X�בƌ���Zk-xwp8��v�!_Ț�cΜj���7�̡��U�¡8!����7#DE�����SQe�|T�����]ݚ�����t-�����g���̉�ֱW��y'�n�d�X�P�T�R�8�R�!Ͻ�\D!~R��B!2�3�(؇?A�3���3�.�%GT���^ �&+��!�Գ�-xB��B�� -���BNM�x������5��ޏJא�n������+���F�2�Y)U)��h�Y.��H(�ʉ���z�0U�7:�|!��"T8� (�!�E�t�1y�� �^�M/�9!i��5���9yj`�5^�9~� ���b���a�"*_V��!�i�u�3�%��/1X-K�R�?�Y��B��PP�)A�����n�,O~V�@`���-{(�X�|D�)����,Z� ���e�N��0�j�d�D�-���8~�� W� -5��Զj����"�Z{oEz^�R�N�T�<#��a��AE�������=�q0�X=x�i��{M3A�N:7Ո5۩YC�"R�C���j���B,��&� -K���8���|���YbK)����,/�%BA{i�(ح~�l���B�A��ՠ�X� -�i���m�#�I����,fyy䔐T�,�M2CD�(6���M��%m�B�f�"���-z�r��T���$]�/"i�Y"#�1��coN�ל��ϲi�*���i��#<� �4R+�&X������J�:f��@a�DK�5$�T� �N<XE({������T���##Ep���zL0l�fl ��D������B*4�Ƣ?�L!X�OJ�()jDܓv��$��i����]��e��ֱl�=t� Ħ9�jWș.������U�;;���iJ���휵A���$Zk/P���_�ct8T��UK����2�A1Y��12Ȑ�L�E�Ӕ`L0Ą�"���D�^-Y��5h����X>`��v3T��i�tI��2���$%oJ��s�l�U{J)<�b-4��F�1r���Nq�D$�1�O�=�&����&�����Y�,z=��1�n�z����#,�D !iӭ]"� �� -~r�����'��K�RXxi�A�������l����>edޗ�Q� 賒�� -�u���{�M^��bA��������B��tL2��LQDP'�)����ăb�Z�́�A�����^�8�ϝ�F0��8��P����aOK��U?S�ϒE{�C��?|ut�EK`R۲*�� =Z����$Q~��3���_0�أ�{�A���MS��(�^��i2���i:��b9��f�icx�Tv���Òa�l�Y���3=k9YAo7�����y��ek�g�R�4'����ƪڟp�`�Nd��Y.P��8)�XQ rp�V�}hyj��8��=�a�:�vkz����W^x�hQ�fV��6���cҴ=$� �M~��-�]�X��n����"��Z,��N��)����u�f!��d:�H�7���t��J(��$��bSR�O�W�7����ts��B&8.��ļ�b��n�I�{>�dƮ��@ ^ֆ��}�/�u�ҍ\����� ����4�u�~t��*~�~AN�bh�6]`,��v��C����Zrn�*%[1�X����v�\"w�Q>Z�L��0���L��I��݂���5�D��M�fe��7gE\=��C��2>l�.Q����cF�K�ѱuW��DJx8�>� -� -�L�ph�E�q��pRi�e�T�ѥ@�,�o ��8u����;��Fқ�z��Y�:� -Db�؛F��'�| >#��&�$ �ة�l�:&�j�]gLmtb�bϋ��DA�-n�Ia�L�d�v���*�vW9���6:e,6�ь�9p�!gyR �g-/�^[�1�%�c��0�:�ةv�z�r��n�!n*�m$`�����k�C��OkvD���r�ȖӼ�}�J�=�}3� c��q�Jz��N�����7hӂ.(��A�y�=Rz)c���f��UFƏ\Drw+�S�?L�������,�kZm/�uo��N���Q-�]4��lX�����Z1q��Pb����/�-8cS0)1�� �)����4+kP���?�[ɬ�@�$:(��{ͱX2�+���LG��ri���yrT�6(�� -bP�x>}h�9�ڪU]\<v�mg����V��a�����씕72{�C�g��� x����k������ڎ=+���E[� �m�^�1�2���^Ǝ^�F�A���+�ՊsIE��C�/+g�qPŴ����eZ���(9�;�tJ�:rp�͙&�l��6B9�)$hN��Ogj�&�G�����n��oh�{�g"=��P��l@��[�M4��X|p�E�<�B�^�O!�B�2"-ڵq�be��/̈́�y�,����cZ<3�X�^X���Kd7��I�`�G���s���'{��� �dd�*Y�c�xڬ�Zi���-�� �M���h��Ҋ��8� �U{}m��幂�"�Նk:w���Iۮy/�T�soF�,!��8���G3��ȆA#06ׁC>vŭE=wp��^��7�I�E,$XA�C������e��Jp̧ޠ�W���;M�ي*�Q�fEc�26� �i���1��&LҦ��Z<g����՟+ک���E{#���2J����ԌZ�5+�[ʻc/�Jd -��G�TЏ7F�3�LJ�#��{���-C��>�7�+H렢��VV7��T߱^���=`��ڔ�y#�J�aK�+��P}�H@�eT����~E���j�&�"O�~��`�ݹ��ieF��ht -?�loT�j�Ù7@�8�%���ꁟ��)PL=�Tzv!��mL�U��5m8ȉ�Yt��]V�۩Sz���f7%�ؒ��ٗ����Mag���##X����øv���zW�q�2Q���ϰ]=�۴����1��~S*��b1�M�Z�@��8ˈ��j�X�)+V����Ot�m�`��Fe�'�x`���~�6[��Ej�z�[J���������bf\f���jVnA��z,�`�FF������V� �)|�<��G 8���rd� �w� ǎ-Q��n�0-͚��tn���#����'RS-r�r������;�V�`��k��1��Ը�L�Q��@ńJ;&3bQY�2(Z�-���F�twjI\u������Ϧ�ޘN&S�o�4t=|��c�s�vV�<�N��Qb�ujвf����j����/�r��3�- [KxX����������:�<�c2�v�r�@ s�+Z�"����;!�b9� J��G�;�1���$x����'�a�6 �*>��6����u���*3�x��~1�r�هZ�֎�=: �f0�IEQ�~�Hޣ�0�Ԃc0b�MY�좍��9(����6�Y+FEk^�ߝ���0-�a�_�G����%��m�Z$�1\���7�Ѕ4�hp�p���Yg?E�����䓦��|�[c3�"84�U;a�>�3�T°����̌�Q>�xf���Ǻf5]P[J �l�f{�}͆`�}C��U���ڱ;�ݻ�5�鞔��_rS,��N��)��Ͼ���?�����H�w����O�=��������O �;/_|�����)?z���'��~�����)�|�S9Z��&m����#�*�hRA�}W��g��-,�sJP�Rq�wx���8���z�8��e�FG�#%�0�t�P;�9^O0 �@J���E�V8����yB��Ȧa�{�����}��0�9��se��y�������y�3,u��y��g�'��UF�9��M։7J�q8J*��Α�K��s�(@���bѻpγi��8���76D�K�j�~�s� �0U*�m��({`������ `�I.,YL��q�5 ӹ��e^� �\��R����9GJHab"��0�@&�[��A��6!�@�� �4A��E:��wRO�=�ܠv�9�y�<:��w�n�ޜ6�;`:ǺNxM:�ܤ�3� -9���M�JĜ��W ����Ǿ;)!�� ΑHe��#��0!�����nbΑ\O�6)(~Kyb�i�Ds�shi�>}ǜs��8���Ce�w���#��x�S�90�m0Ȏ�Djpe@ ��ȹ#�<�ej�v�ȹ����]�!�g�q�s�mWK[ �H��M�9Rjr.0):�ȹ�l�ʞ3���r��s�zs̹� �V'�\�L}*sN��̹�i�r��s��s�O̹X��V�9Rs�s���Ȝ�"3�P�@e�u�y��bk�seɺ{2��f�Lй�)���]\ZQ�@��ku��0iu�|Za�@uM��a�@�D� �9a���� -;灨'��ʉ'�����;�9�\$��a�02Ev�� +f�����a�H�f���,H+��b���;%nǝ#!`����XSh�m�s˜����H�h��Ý������g�bv�� -�AXp�&�Bq�������;.�])s�^�<p�&a�;7��;�wa9��y$Z!����y��q�ϑt+�x{��l��u��� ���~��w�k�U�s�9�,�U�$�q������5 -;�rP�D�wΣKSB~�8�ˎ;7�ܠv�9ώu��9��PF�9O<|0<�!��;�Vr�M��< -���e��97��|�*��9P��u�9���,Ҁ����V�9P#m_���(�m��6��iE��n� �9��j�s$0��C�3����E��Xx�N��ܚL>�G�CX4&����d���<�&J>��"ρJM�#�E�y�=� �9R�e*<.���9^Y�&��k6��`����<��o�)��sx�Z�iJKq�o��}=�W�&{习���@�S;� �9��_��Hݔ[X�+���<��mLћ�sx��4'Y�j]�yn��s�ؑ��[� z]���[���i���:��B`8ǭ�s+@G x����O�0��X��H졜��H[ķ��s���� j�c��@�l��VܹX�7�Q�����wnR w.��m[�r����W��䴭�aDBԞ��Ζ.~�+����Lk�� -��s���BY��@ ��<��:���:�W�9<V���9B]�X��.��V�@y���NZ�@���="դ�[=��3�)��1�y�(c��a�՚;n�A��s�E'�e;���}�+�杪�����?-�s�ڡ�p~ݎ)`x�.q���%��#n��9mHT�9� rZ���刔B���s��t���zge��@��m�ц<�#jY����-���X+ZC��C{ ����Ym�a��u&;��$��&Հ�c�> �9�e�+���#ρ������v�>��P�O+tH���S���� g圁�A�&��+���=�ܠv�@�Z6��ֳϢ�+��!��=� -DM�@�b�+�OU<�AρȲ S.��+/�s4������w ¢C� º�;�C�$؈��s���ί�sa+j�t���Ɗp�V(A���=�A7X9O_F������5��D�!���������#�W?�� _�c�)!�{� �|`ϑ�eǞ#�Ŷb��G�e���!W��EX ?���{�S@ln`�yt��Y{6P��m�����r�ρPD��9�N]K��s�����S�V��Ò^��<Z4���g/6�����m�����s�Jh�h�s��`� ����ףG~`�y�X��s�]�}��s����O='�9�=�w%L�9PD�� =�Y�`�γc<�)7��@����sQ�1����l��tj���V���l��QQ�|<��Y��4��@Q�UǞ�5�WnŞ#�^Վ=�S��{�W��o���k(�6�9��LWi���}n�;�����ݑ�<��0#�9߂:�;��om& � -B7���W n��y$0�����!�v� �w�n��MʂB�# -�g®s����C�Q�H��M:P����BJ�Z9@���q�БR�<Q�H�X� -�T�; -���/�.�܊B7��v��PG���n�; -�ޠ�0�`6���p�>�_�n���Z�G# å_�tz -=��v�}V�IV���e�@��cB�r<@��r�: -(�R�D�#����d�#�BGJmq���� -��B�X�Xif(t��4� -ݤ,(t��Q�1��@��5(t*��a�2��8Q�v��B7�+�3���.�X#Z{:�=�1�@�V7A�<���@�@��s���d�t��S�� t$s�t:Ͼ��Hom�A@��tv���y��o� t;��n�;��G�[N��c݃�y�t�y7�V����0��[@�<�e2|j t�I4��A��a�)+�g/̖&�W97�����0t���HÆ�n���({�I�0t^a�7�-���amr�BJٶ2Q�Hq���G�#������bdZ��o��mu��\�D����^��B���P�vTE���Lh9���?�Y�T�~*Bv�t��|_��@��w����O�:*����@����&��y�f����0�[9��4w�7g1�K��t����Mr�'�0��|��{�"r�y��t�Hȶ���"K����ǖ].g���\2"��ğC[���@;ߣ�v6<��e�'C희���D�ܠ,�s�j�s,x��1�9v��C-�s,����Z�/�0caksk\|螞����-���s��ͫ��s����9{�?P�_Xdg�s��3u�?�`,l��#��Ҏ?�l�:��@�Ő�&�紵6��@�-�?GO���V�}. vB1�����մ�~A�}��s��hۆ�l!,������)�:3��%�=��s 7Z`�?�q��\`�����Ɉ��::d�딢��tR�tdP�t��W�@�����w%���WE{|�б�N����,w:�r���E���x�@��nM��t|��&�>��.�����̅@�X�nб�|���]w���)a���&,�t � uб�<u8$�k6�u��gn�?G܌�w��s@e�w��s(���s�pDx����:����&'� ?���p��s������s���?~�8��� -?2�W��U�����v�t��~d�e~�xd�)��Z������qxM���s8S�I�9��Q�['�\`2v��2�y?��s�>G]��s��y��w�>���-��s`r��?M0���}.����@d�Y���m -��R �&��ؚ~��P�~�n��#�����s�nK�v�>,�M5[^�����Y��s�T��?����~�����a�[A���ϑ�]\��@F�� M���2<�*�ޮ�=<�.h��<��&��/h����9���t�9��pp�9PT�,�s$S���s�0�����b���9��ly�����o���]<����K�����$Zg�s�ԑi��s k�UǟcJO3�8&r�v?�5' O�9ނ��ρ\��sH4�R&��ځl e�sp�2�f�C�#5�^t�)ß�l� �|����_�@��Njv�:�Q���t�Dž3� @G�*��@G?m�������-��-��`� @� n�]"���@���:)L�] �@ފ�Y\դ�:�^� �&N��41����tt`��1��L�m+���J�<�'`��=Nd�p��ô���_5�:���M:.�|A��HQ����#�J[�C�������W��� ��*[�w:O��M:����{:<�G�Ҁ��� -���p��N���� 1ʼn@JLtR�0�<��[t����k��`��a>t��J:EU.���w/ơ��n�V:O�o�L�jZ �"4���Ԉ����������A�| -? �H���t 0��+n@�yBr�ˈF�A5f��ڞ2I�8?j��S�ܮ -A�O� -%7!�@MxHC����\�@�#���"Ё*�gt TDj; � ]��#r��:�5ߎ@��-���I������A��;1i҂A��7hǠ���"����y�~���\h�Z�~�^KFӀ�#�r��0t�����a��K�z5���:z�*[Ii�k(�-��D���e� ���Xd�t$:ϴ_x[��e�-Ht,�ݺ#��W+��$:�%��"��4���D�U�V@�ߛ��)�Ht�n�v*��5-��������T컎D��4��pe��m�������u4:�b �V4:r_�d���ݹ�F牠$o��с���F�T� -x:kц����W�`t^�&����S2�n��Ԡ����]���`tK{����9Xa��ib���Aqj�����^0���{K��xt`�4��xt����0C�.��G�=�HG*,�HG�=: �g"{ -+ �[�q���1��HB�ӆ���1V1u@:Ǯ�� -q�[A^�騕��H��������!���N��P: 슘S��?`���ґZ؊���6t������ -�H��a�<:��%����W8:��U���DQq�ѱ^�LmGGc-Gͯ�!����uE���G~Y�g�h+����Ma�'�ЕF�oK����<��2�&�`-F��,�,4JS�0M��{0:��L14:wR=:�gst9qA�����ht���� 4:U���-ht��X���蠀k�}A���*�ht��Z����<3�ۣ�)���F��<��唭Xt��O۱�H���cс�����b�Xt�ܭN,:Pb���6bQ,:f%4�b�vǐ���0:������@ ��21q�5���3��Ģ�h!���G�Ա�@Q'VjAo�PdV,:O�A�(2�E���YU�âcZnqb�!�@X��Xt�HH�\��"�(�Xt��ku,:�j�Z�] �Po"�薩��`t<5uܹ�m��(qG������̺��)��-d;F��w8�@{8:�C!4���q��_@q����� F�OI�J�#e#���.0˚H�Cl�zf] jnr3"K���E��;�%b]��<��0�;NUwz��L�~�ڝ+0:$�祃�6�SX��Amc�`t8U#��.�^PAu�ht�҈�g�nxu �u0:d�(����N qB��y�@ё��LV\�����Pt�D�$(��]�܄��ڄ�ÆՋ-PtLM��*͠���`�E��ϸ@ѱBS�������4�i������X1ա������PtH��X��hΠ��C��^�ӡ��[[��Ȃj@tةp�u:։�p�H��|�с"zy�Ht��PC:��cIIKyB�i��� EG -���E��Y�y��!�+50:F2��.`tꞯu�ѱ��<�C�&�H0:�ˤG���ifW1����Y�"ɎFg��M4:=���"$��w4:F`8t4:�T~���A�]���Xi�K�ht|^jU��B�ht���4:��l�D�C7N6"�ht,S�籣�1_[dA�āg�c������X�a�w]'��}���݀L�Ud$ Q�lݣ��jou���s-N4:F�)�8�@��T'jVT�pt(�Ȍ�-pt��o�L8:<Gj>L8:T[�/pt��8�M8:|of.k��)s��� -6�&(�3t8:P<��8:��$p��%Y�L58:R*{���O����i��� Hǚ�� -H��tKwD:�͌0D:h�Z!����x���&ȆH�� WX������l��!���� �X�X��cI����Q��}���:(]�IT:ıEk��t�dtj�tXϬ�Zp�?*�;.]�J�u�ҁ�'V�,y��=�BťC�+����g��C.T�CuX�@���� K�d�3���!�0a�#��X:(�, ��t�`AK��O2�=,�g'=M�qS�U��cQ5����t\Q��ttW ��@������W -J�<�`Xs��y\�̬�D�#�ڙ;���o$�*V'ע�ҡ�2�VVP:P�����(K����E��%yӥ������JB���:��&�fETǤ[fq������Q���r/�F6�T:(٥��/�������,b�=f�t�rɊf�5��j -�KG*��~����K���,�t�(�Ht`:$�e���ŭj��K�lXc�R,�$�Q�&�`���?&�����[`��H��Pl���K��p��a�Hd��`�HHlƪ�\�&,�t��{Kǥ�hZe���EU�WX:4�RW��@�. �n�����K�f��::*z��5��ұ?? -�;�����a�a��N��t -AB���0^���#@*P -�L,�t��:@��3�7��ϔ;{P:��tҀq�1�@�P����a�0ݡ��a���xs���4.�t�̐�Tj,L7X:\V`�Hfnc��E3.:,�1S}��xkX:���/f����G?`��������Q�2�t/|[�l��A2�6X:�G���Tu^*,]�L��F,ժl������=c:,�-\d��y��F�K��h��uw(��t,�Cr�KG�'��@HH�^G��<��WT:Dh�C2P:��20������tHo`�0�0�6&���]��5�`����1�PD�4��0�8���1�@q��L:����1��1e�O��Tj �h����#ݾ�J�x��l]��)VT:1�=|�qdC��J��q���d���t���}��t���[@�XxO���Ѥu��D�4RR+�t��� ��DSh��MP:X6Z����6VMe��!0� I�^���R ��َ2G�����.0�~�h�ptJ8D��F�bI~�K� -���d0:R�(���@��_��h���=��*���i�ѱk��nܣ��a�UG���O�@��Q������p�� s�ޞ�M�l0&0�#�� �n4�I�htL�,9M8��2+���p��u8::�L�g��#�8:8#}�m����=!;ܾT�W<:z������h�5+�b�dFDؔa�#٥4Q�@Q�oG�E��T:�l��t���eǥ�1[>����:2G���ѫa�Lw�@߽ GL[�A`:d�+�p�c7�:q�P��]+.�Zc�q�@a�ځK�������κ[�[���Wg���N�\�t�e�p�t`:O�/�Z`:X��u`:������D�d����Y6�w`:R���1��8.�ӑ܌[����'.o�����>i3~Hd:�L(�Z鸙��N�%Nd:�j{1�9���t"�1 �[���>�Y���@ӱ��f���0�/Nd:��0�� Ӎ�c�/�s�!�)%��t��q�E�p_ǥC��f/&�f`�2q項i��K7).ݎl�t>�&�nΓ5�������dd)���t̗*����T�VvX:R�R:.(��X:O�+R:,��/5� K�ƛX:�����`�؝O���NJ*�t�-ǙX`�t�"��t;�zjG6X:&��.1�9d����,������[��t���� jǤ� b��ae��$ւ磍�HQf�A�����N�evH:�g�L�OC��,���� �5����f���8��$�����~�I�֛0$&+xI_0�@N���I�v��t�M���t^�{��c�B)���'������n��ed��nGك����å4��A�p������Ҭ��v��*)��ZQ�l�ȋBq�Q��M]9@��l�Kyi�t�ƕ�����({T�I�t� -l�Q�L\��+M�S�s/m.�z$c�w\:�!��{`:l&� d:6�e�{������h�f�~ԁ evd�IY�� wd:� �K�N;�9h:�ㄍء��ՒU�L����@���{�dK���v*9�'�<�}�w�ߺ��Id�D�A��h�fA��u���8�]+����p鴒���:?8��S���~�t&א���|�r(o(��B�ʻ�C�O�d:+9�t$���t2���r�d:-�7��`:Uۘ�t�� -.��� V�2��L'ْ�A����q2DE�/6Ǝ4���Rh��-Z��NՑ�ɋM��usF�Sd�a|�鴑��d:)��2�V.2��V'��d�� ��ۂ{�I�����+����tr�Ϊ��tR�C��mu��N=���ȶ��D - -�K'�-Pp�8B'����i�}p�ģ4�t���բ��K'[���I����N�ϋK���\\:^� -������s�� -`6�B -c4��&��r�O����0ե�9��\��?�~��M�-ɹtK��tK .�v�Dx�8�D�ө�����yR�@��p���l:�Ć���+��8�t�ɠk֑�����tڍӋz��$2&��J4H9�w.���y`N����:Wh*�����8OP�p��\p����>�Հ� -TҼ�t*��z5T���p:��mgy��v6�H���ҵ�vO�\p:S˻�tT�j�p��2M��|�餚���銵�����q�N���CPl:�t��� ��%���t���>�n '�n��#�߰\�!P���lZ�?��{��0ퟺ��R���>�tK�~��7�J��:���S�~����q�,?�u�WT�-�p:�3�Ίײ�k�����p:���9j+r�֟��I u�����u����x����O:���n]�c.�q<Z�O:��?�{�?{NW�l����+{��i!a� N�S�3@����#RR�����$�sTW�����)J�|�ӊ~���~�p���x:JxT`x:>o��NG��}�E��`f���@W�I�Cչ -<o��'�t�x���)�\e�x:j��x<�N<�VOW�=}q�����F�}X�����* ��t�/l��O�wò=�t��7�p���I�1�t�F�|���f��gO�*1z�)x�Ҫ'L��?��xW�M�'�60A�ޢ ���l�<�B7�t*#�l4�ttթ�b�)�0F��MGnyo4�ڼ�)�N�5�+GΩ�h*�z���T�l�M��N�[��r��0&1Agө���ͦ��*�[[�#��|�f�������Ǧ����F�"�M�2+ �!�&p.�4�p�ɥ^���t��\-*�t3�J'�6$�`���v\;�����GM�ޫ��G7��h��X��C�{�1H�N�L��{� -��a���ck�7��u�j]��Eg��e��@���AtZ*��?:h -�7��iF�^< t*M-tR�(A'��� tz�����[]�9e�<s��l9�:.���g�||���v��pΖ��sr&��'������i���h�Z,g1��@�LJ8'QM�8���7'�m�ホ��fڅ��ݶ��6�[E�G7l.�{�b�iU�Z۬�-����k.7���9�\�Is�YI� �=�0�|N���rO<[�Z��\'��yr�I����抿yr٩�'W~z���\7K�X�c��Y�Hr�J�H.r0�p%[K�͑#��{>Ty��QEN��k4.�(�)9l��X�︹g7/�J,���!�qa��rd�g�� �}5G�r���&���W�M��;��ANO���E��'��� Gb,�E�[��� ����� ���� �M�#k��N��g8 -���u8a^9�>�&ȡԼr�,7@N�o��6@N�oY�6@�ʡJ���)]O5��%|�z���/��U��<�I��Z�TA��^�$��\؏ ���T�C�98���r����@���D�c]\8��x@/|��Mj� Q�u��G[���Ov�����y�vMN���|on�R�.Gf@;D�ƱPT�� �P��̊��l�B<���#�I;�%sF�j���ʈ}�_��%z��������^��Ŋ���Y��a�y����(.4���̴(q*��}B�z�:#N{�=�vz���E���7��1�SW���כ�����/K�(A�[�p�� ���v7�|qvN2����a����.\�}a�B*�z���v(��)j��pԷw��,��`{<���)ě�z���AU{���a��������x�P�a��PE����4d���'�K6{D�Y<��M�{��$��88=́�C�l:�&��/��B����k<��jZ:�ɃSB�B���dМ'G�l -GߨTj��� �s��0L�������E��U����b�v�ഡM�T����{6d��)ժ>����s��0Um1(�e�M�c�,㹠���Y���CHϸqp�OZ48�.5/�<����'�6'}�����d����p���48��_��MyݡKw����n��*����Z�,���㪂ā�(�D�7 �C�,8+��ţ�8�%\8��ߗR�zKfw�o�,�h�[�)M��^���p���<8j9�¼%����48}�����Oh��㨒�t��Pu� �1h9���^�7 Nj�<48 ���]48 ��3n�����Z�!�y���כ�Zkt���\��`p[8Q[�(6�^-/��ϯ������y_�ɳ�)�apK�`pK � 6�$d�� ��Egջ�������-5`pzNnP�(y��U��-�CM�Ҵ�`pf����a���K�7���ͺhpF3p��<�� ��`Y�b�(s�48;�� Nq�ZpL�_'��JZ�7��PV��0b~�<kv-���U��eҡy���q#�q���z^�'�i��48\�0?=`p&��ap�j�p�ap�/3� ��,Ә���Û��m��-5`p�m��G��8��BW�48.Co�1�e���r��o�O�����4 <� Λ#����c����:L���-���Q5pp������%��ϸqpTokY8��ٰX�'��c�88�:Nz�8�)���a�g�<�`�l�;iphc,k����thۅ�S�\�� ��X�;N����M���a�k��r��.;��d?�b��9;�B����Ƽp$���uo���CZ]�7e����[��E���"���mx08�o��� ��^fO���F��}�� -������-�}�q�� ���섾��ճ�oZ�И��o2�H:S7�M�M�}�^LggA��^I�m�%S��7�=��8�#��ڮT�Oj����Xw�h�o���a�I�Fe!�$X�)�o��b�~#ߐ����K�z�ƛ���o�6F;�m� {���ļ�8���s��� ��n��;y��42(|u5�� w[j���{���z��;f��n�����v�N 1���m傻��ݒ�����c+�����I)O�&�� w#Bp9�n����b�7�M2�ހ��k�hLP��&o��nk�]�Ƀ�L��ݖp�ݶp7��~��t˧�i_Mt2YYx�@cq��d�w�^(Z�,�@�BQ�!���-ᆻm��n��ep7�մK��nT�r�ڈ��Xn����a�f���̒���ffm.����>{�p7�hK��倻��(�nf�"��n��M�RB�'��v[o�[HA`R;)�K��t9IB�T7n:��@��na�x>m�b�$��αn(�Ao�`xiz��fO|��&���ºa5���n�z����%�X�C9�n[����l�Z��֦{Sݴ��U��,��馭;���^e��At�w ���q����_e�Y47풟�7� �w�'� �V��2�䶅���)mQ�e��i/6���n%�r��:e��ĭ���p�ʩ -�T��ym,�|W�p����oS�_Xᓐ��Aoۢ��4�qS� g�S���c.K~�ۆGeox�L���_�6��{�`�H:�m'�m��m{=B�Z��l��& u��Y*`���¶m�¶m9�m�*�W�m�-ƌva�HO"l�G8o.n@�YLJ��Ȓz��6�t����m -�У��mj�iū��n�:�l�mt`�>'#V�\X�Em[�S�h ����6,���.j}�*Tj�zY���� �/���m����m �16���^�6�f�irc۸Mh�m� �#��m��>Ȟ�6�7� G��ʦ�Ia�yS۸�s�Mm�B�*�mD̞<?�6�K���6�M�[L8��9�Inj�"w�E��Ty�x�Т��N���MmS`�"�FmS���㢶)�A�覶I����q��\JUsL6���!�0E�)mh�B�7��lC*sC۬���m���e|HNh���AmSH� ���љ� f�6��� h�����mˤd��@���luHm�W��qQ�t����6�6f�M -i�EmÙ�e�Em#¦���m(�)�چB2���I&��m�`��j�Α�H/j�BX��� ��&�)?�ƶ�-��~a� Rď�&�b7��mST,In[f�� �s�tՌMtq�4����r��7�M����Mߞ�iq���y?�6�?h[n�e��ۆ���Fpp��5��l[��f߉mS�Te�¶Q�ҋ-���0��i��m6����([S��=����6�)<������m<����-?��;�m9섃�&�PHp��'hv^�VT�6E��&A1����8�;?�m�DP���f�>����6 ���mc���Qp��W���63VP�ȹm��߈�6-�/nEf�,lO�8�m�L��ۦ� �j�m��fM���.ݿ��D1�Y>�6��2��� -#��@�)�ʬrۺ�A/`��^��Q�4�צ4�Р�i�'��6�\��\�6e���&++��Nj�?�� ��Y7�M��h5j�o�����cQ���ϻm]n��|m�5f�"�icdž<�m|�����v�Nv:���h�Φ�?꦳a>�&�l�t�~_�ٴ3 {��pgWeȢ�e{t6�DC/:a��5s��th��8��}J"/:1�����&�bsAgC�_8-n�rΆ2�p6{'g�m8�r:��f�Ic�l6�JT�l6��7��p#���f�f3z���;g( -8�� -[�gSL����f�#2�?�6�Jd��{lo0k���e�ŋ[˦ 0��o*�f��5�Cٴ�Q&d1ٔI~0�>�l���� �M_h,"� -H��~�l3B6�c�ߵc��t�06RsOZ,6Α��p���J���zg��F��78l��🟓]�^� l@�KY6c�0�6�i{p!�$j6�ĩ|4�ljPS����DOq��0�|뢯UO\�\���H�*��zM����M^SY�ꢢ�HScÓͦ�j�vM%�x~� � ��5g_е�e�[sM�Vʆr�֫�_�5!�1���N�uޚR��D:ik����ᴵ�8�5�չw����ؚ�Ƙ�l�:k}ѓ���Y���9j�8 o�Qk���s� �F�2�/m ����ԛfMÓ�6����Y�R5�cm�n ִ�T�"���D�V�i'�'5�j�()~�����t����o�Ռ<��l5%A�Q#���]B�c�Lu��̞�.�/���&oO��T�u�V0�^w�r�� �"���S��RՀ�?}Ռ��<5a<��NMŶ�Ʌ��z���.�����0�'���R{�զ](5mk�������?Hj$q�����yq��*�Hp�����¨�S�.(j��P��V}w|"�jT�:A-@fPk��.|Z�V��4��� R�U�Nv�B\J�:M�; .�UF��MN���-nZyl[��4UbjizQ�d$��:4�j�23�X3�EL+�`�FT�;���I��l��$RG픡"C�'/XZym�z��Ѕ�sV�l�u�*��S�Qiz��FAJS�K���)�ʈtr�T��`�m�4M퀤���I�E?/i*�6i0��b�!���!M������|�o<p�NGô��BLx�I�f���ޅFcQ�,W��X)=v�o4yZ� �F#s��i��l��>d4� FL�����hd�G�DP���%-EFKTI��:�h&���h��p2Z����L04�h(̛�Fc�fY��Ƃ��w���̳l4��Zd�s��4#�Q��h��'��K݊��o42�j'�I($6��&��N2�0�~��F�����hR�u�h4- -��@�I�0�j5AB���F�%����Hh?�n:ܬ�(q�7s�Ѱ��4G��mL%}�hlxRJ�f� -'���e+���h �M���4�!^���&��ID���O��\\4�m�\4"2��0�:@l3pr�$��ja�4!/M!ȁ�p���&�ɩm0'�JdR����/0Z2Ө��h��Q ��h�����Q��\��H�P7`4* �^^`�D�=� FÌ�� -0�9�\`4�(�W00F�;���FÊ��$�h�d��N��4���h��*���h��� -0��RÊ0�T��EF3���F�W��A�!kŲ�h�m4��h7Mg����n�A�S�n��ov��(�d�4ߣչ�h�@]��F�����F���6����;]h4� � ke��F|O��B���P�X��R4�-a�e�G),���M�뫑'�h�M�K/:r?�h�F��*;�htW̖6�R�3m2 -�����0��)�h�шd4��Zp^d4��e��h�y�:'�)9�* z��0m$�d4�� -���"�)}g��k�*��&���!vt�����&��ת6�h�P��^d4��-p�d4�n(2ߗ�EF��Zɛ�fI��n2/�~�&�)EE�|��(�W�/�h�n� ��h 4�蛌FW�f�EFS�d�qx �� -2���NF[�䛌��bI� ���i��F&�e� FS����F#�9b�&�&e�*u���������)EfW��eƊ�n0�N����+�u>�h����2\�؊��P����i� Trz���V`4_�l.)k"��E�,�U-r�y�rȁE�o��*�k*����' ��f�jC�y���T=ע��+0"�T4�;pc�� ,�n�f��X4��'Mia�PAE��A,�h٭2.(����d&�WQ�`��(Q{��D3#�1��V)�D�8�h���CG��Йh`4lMs>e�M{r�& ���6��"E��Ɔ�m����lU��IJ��O�3��r�Ĝ���C#ÉD{W!�-F�e��@��v����g��� ��<�o���B����(�N9�84���C��G�qh�_yL�=�EC�}��(�8�,7 �~����LO���1n劣f��$yZ�)$���0Zl��Y��b��C�hL���ib`�<4 Ecq�+�N��%���CSQ �<xhz�U�}��(4QY��Ш�F�"K&�y�¡�vsƱO��R��x�:o����AC��K�t��I�y�а��]�c!O9�m��qm�yh��=l�3��;Է�C�J>a��������CC$+�<4��M�������>xhY5�ZAM«ڒ�����u�2�=m�}і@�L�(�ED�3<����=�\���)���j����>��C������a¯UG����"����"+�h�i`6ԟ�ZG�"�!��*�h���$���h����;��� �麑m ���� -'n/�i��E�xk���9|���!w��D�b� D�{X����6������Be�Ѵ�%�@4�Tk-?g^|����7yb��2�z'M��,��Fܔ�T7� J���Y�8��CS����CK�����i?Oui���*h<4:�t�]<4T�s���a�,��x����H�މh�)pH421Ցi+���=�G��آɑh]���w�R��r"�T?0�thȕ�g�R7Mr -�۰c�m=O4ȟD4���,"����L�UF���Fo9�i��V��e��f���MDCƳ*�hRH�-"�(7��s�A�Dc/KR Ѳf�� ?�W�D�2�}armk���B���{��DS�]��,�h[��h��~���v�'M��A,d -����h&�/mˁF�џh���6�' �'*�h(T�M�������@0�2M�t�X$ -�P�<�h���{�*�"�v(g�Ŗ���ʱ�g:�9}�h8N�Z�)��B��h[��h[0���P�v�[�qg��E�b�f�mɪ�t��qδ����\4�D-.{}�����y7�Ƣ�h<�hl�w7Y���!ӫ� 5P4���M���]P4���g�j�m�fG<�ύD���,�U�Z�{�(�h����L^������� ;�l5�Ĉ��R�8���Nj��Z��?���S��BÌ��l����^ʟEBk�xq���'�G��$[9 Ȕ.�A��JOf0�Ԭ�o ��:�lv�] C�4 ñf��/gn����7v��{���y��g����Yܳ�X�vaϖpQ�P�f�))�����F�>7�L�@ -ʚD ��I��i5��L& -��zI��^3�)s���� DB4dzQQ����$�-1Hg�p!���]�n�%�5o�!f�>�9�ʅ9;dǜ�7:+^� �+� 9{=?�T���N���?7�L*�� �I���H�[,xp���� �fjWRBa��� c|���H(;����w�w�g�6�L��V�`3���e5=K8�E[u�������~����v7��oI[��fK��fK ��ܜ -����n��K�>����łh���hfr2�a�wm��x�2YkXb�� �l���� pf}�q�439D��Xfx��Qf�.�h�S*�t������O���0�cF��7��a�a�`�Յ0knв�V��c̶p�˶�ZF� ������^��0�Ĝ����^/�f�$A��7��5'X�\-�[ƏKV罩e�\t;)�@W+�[Ȳ�� ,1pe͛�V��L��_}��������J�ʴ��v;Ae�b?cc���?��������1���˪5��xAʶr=�[H���W�Qf��n�ef.T�f��R�E)��ܖb'�����~���nރG���d���V9�yS��4{�J��%�:�@wV��e���w�Z�2��e�m�'���5���(�#��exd��ܐ2�Z?RF�$/ Hhf�sB�n$�oH����`@�ج�y3�p��%e�b7�^آAw1��Mz��Q��aб�2�ч��A)�?���2)��J�V.J��V�껚j ���a@���I)�0H3M`���m.L�*2.L�T��)����<�i�8O9��)�������iS�2�9�0e��hi�����X�2�oy�/L�T��S�E��)�3A�S�ʌ�2^��N�J��n4y��4M)�2�9t֗�~,T{a��aS���R ����~a�4ig� ��T�i�?#�P�\����L�.��� ��R�}�N�: -É� e���([�#��s�R�N@bZx2[&�x2֕:9'�L"`��1t�![��Ɠ�yVOf�m�;�$�N���u��]�3<�R.<�ROfЂN�r[��E'��P���ڒ9�LAj��]t2�d$��NF���xlw�r��L-e�ɨ�J9�d��㦓��QD�(ȕ6�a�y����d8����d���4�d��� '�쾬��t{vN��N�Ā�Y�� ��*�x(mV6���S�e7�K��� '[j�)R�hO�1��T�� '�&S�h�������r���1����AƬ�O�"�z��P���(r��_6'3zI7�L_�^|�'��p2S�a�JP�ֹ6v�9�� '����8L�� -:�V�W^p2�`�mp2��y��h$Tq��t'#9�Qߤ���M�p5��:4�y���X��&۪��(�Q�k����i8�|Ō�zyN&��o�����N��7�����l�U�u���`�6-�d)l�-�l��:�LE��:��%�z�!9T�)6�:�Y�\l2TmO.6��m�E&c���p�ɶp�ɖp2�X9lWUݒZ�?{��c�5�z)�w�t���SA�\�@�eX�Ӧ�ѝ�0X��dx��Z�ɴZV)��&�� -���d���/4�fmL�L�zEq0�~H3=�d -V+���IƆ脑q2CV�ٝSȪW��2u#e��1��V������A�����d�q��1�6�L2�IK�bV�Xa��V�����,)�S9����#G��U婭ʼnOE魅"�%���pվ��*��"������Z2Z�O��ו%"yǛ�$�٫;sDF*�9ڨ�R�VUG��v0ɪ�zK�(Xk�2��B�sU��ke&��ƒUch�-�0�~�ar�; -�,yY��m�̻p� M�`�Hp6Y��I� 'KFс�jt�d0)�'Kfz��D|&�/�_e�)��4p���L�%�q��- -N�nK�g+�^��C�N�VbD�$B�R@,�i�{�T&f�j��aj>�U6w�����0U�/�UT�/,Q -?�;T -�_�e}+��$�0�h=*l��OR�L��{��F���H�ٙe[i'�l�j�Q1�~;��B;�����C����s�@ſ}���K��K���m~W�ƣ�]qrJbo�����Xv�Ԇ���f��b�xT�r��F9zU'�<Uw�8i]v�J��(P9�+ -�J�d����ڢگ�<�|��3c,y�jֻma�b/}�Ui.�>^;�5wW1�=>MY�i�v�Ya�횦ܣ�=�H�*����pV���\-r�����-��(��2�/�D�P[o-c鶯/ -��R�#Hـ�����T�,%��BQDK -qp^��f�-�Y'sW�&��'�}��t>��j��a�Aw����3��i����o��X��C'bx�+aL��L*�B(n�]R��ɖ�c��cU��ղ_��4��c`����k]�:�}`�o&�:}�{s���B䨥���f��WZk��TP1`x�+B(� �FU@�ͩBl�f�(LU��� U�1��pXuM=Hg�Gآ�����l&i�z���"o+Yj������Q�J:�Yv�����mVL�/ -��RV����n�Y��@�9��I���~fEΪ�~��,�\�5ҭ���Cm�Z�(�V�h�V�0cp�T���b�ǁɞ�#�V�Y�kɶ�F&����`���V����ʲu -J&y�R�@z�x)�_��kw.�ѳ�.m=R����أ1�������}�����g)ڵ��T�!O+��gP��)�QM1;��P|�����>0D�*:����v���i��j��TTuVVk���4���������V)�y+��r!Ԫ��:�w!�PU(��ѳ�,T�aũ��(j���OY���C�N'�fU��� 5�f\���QJǺV -wt����ʗ)���V -�{(T�� �J�gsX��)o�j�k�j�QZ�e�Z������F�ļ����h8�̂Y�{c��l0%��XU���\5�uY����� ����X�L[4�X��P��$�,b���n�]0��Zg�NB�����Z5ؤ�|bMr3<P"K�� y� J�V����TM'���||��d0U}��K�{��W��u&&����Б6�5��#ES9��Y�R��B"<���y�����O��Za�ǽS`�/ښ�KD�9�L�~�Y���Okw�A{)�����>Pyk�hf���2�ږ��Y�w3[�m�p���P�x)�R�$�.|�3�"�����痙ٖ���n�i��� �Rb��.�P� ��8�k24�`]�T�ON�J��ԢW�3n��=�R���l�[ÏVk��d)�;��R�o�l�����G7�-M{[�x �Q�Q�K(��l�>��d� ��,���e�#hߒ�*��0��k�C�{��i�[j�l��������rq�m}���Ȧ�Z崗��3� Y���z�v���zAٶ��p.��i�]�B4 -$���Q�� ����"��l��}�&2����fktL�(�`��j�퇗��eY&G=2��M�`#��չ�G�I����(%S�5�T���cٝBZ}ż�_���N���[��R���'��Z�Q��>O�C�B�lR߅�E�{���P�h���_�&��)��K�����]�ھ�8hm����A�����,�[y��މl;�dձ�=?UlMЌkΙ.�b��~X��X���9S��-v�M���0��OTְ�h����{���Y�0�5u�ٶ|)�e���k�#��ru㔪�78��~[䶪x�x�s�EW3 趪�:V�* Z{ӊ�]��MK�J�A�nz[U;'�qX����MVZ֒[P+�"���1Y״)Z�L��A25Yc�'���N���9�ͭ -�%���H�%�fI)�5��h��g���F62U-j�P��\pMP�{���c59���H .�O���G$x��$c�+��OS�H_��RL�H[�EU�5�n2=��J�Y��X�*B^�w?Xo� Qt�{����7j�����=@[V��h��ZxsU�k}:������iI�0��Sm�4�r�'2���7ɿ)�E&�+��:6����J2�k�����y6�^�3^�v�mU��T_�!ˬ��U���.ǿ/h�k���KV!��Ŏc� ��*��E���mu�y�v2�ag;�KU�qaA��+��5 -�+�zbP��d jQ_1�FGB%����?�t7s�Z�������>��P�l�aG�?�4��E����/���⣪g�~�P��8��8� ]����.��-���hU�1L�E�r>���|C� B��:�f�� R���BA%�JKH�as��#�rRR���`����s�l��p������|a��*�͆;��b_��֤[��V8K+��d�)����jUn��'�W/���N�M�gtS��!Sۓʷc�}(���jV�e�,���T����ܖRǻ#( ��R�I6|b%#�r�'ہ���pI��5S|��T�1�d��հ.Sք�/U�ʎ��hRmEE]3�ϻ/�0Ɯg+�=,��\ݼGOH!m��#Q���x��ǀ�?�>b�ث�o����;�d�e`��SzΪ`�%XH��#�� ���>P%$�� �#̿���� M -B�H�SE9��[2�`��M�C6̸/�t��[ʬ٧��o���'^���UU~��a��ފ���WBN��Uv���D�1^�=C5��g�Z5�>P�E��(E�Otr��BTy-�R����a ���,2�G@�cea���ud���L|�!��~S��R<�Z-~��2������J?�'��$�vee���&�����iG�^#2*y����R�Q��"ۼ��Ȝ9�|f���-)ݢ�ٲ�,T\��I Et3�w�B]���ޡ<�t~�Ɋ$���qFpqV`�V��z��q`7KP}�l���{mf\�~�%��L�%B/�P5*��L�C)Dl��dWИZ��bU�U�o)kj��TM&�Ntm%F�@BV �Bk�Y��d��Qs��E�S��x���� -р��x�b,Ft��h���c��{D����|IE�עu k���z�AbS�&�V��w��;�j��}`�����h����d�:�L�o�N���g�ޜ-1�5B4��1�)WnQ%����������r�F��)���J`+���������e�P���w*�ׄQ�w�,˝�6��W�J�����9d-"f����� �+ �aӖ(;���U�i�f�ț):��љ�\����/�����,�fr��"�ϓ}0 �%Em`�V�ה�����5�\S%%��(���:ન�Q�ogO�o�&k���ɷ!װ���W�v���!A�z V:�a݄�WU�A4�r�f9 ��QVV��J��� g��i5I�9 �����ņ��bE;>�7:�5u�_#���y���e-�U��R.���Z��Jn5w}�m��\k���oB��J�������AN2Qɘ67���3�Bt���b{��t�̦ -s�'юBi���퐲 r�9��z<fJ���T��S+��Պw�͌��-�<�f5@$�2uj (=�6�c�`r�쥃Z�ם�u���7�&;Ka:��)H�Ꝃ]Jg%'�k� r������[�<��@� �I~�nAN��2�rRl9��.��d[��.��5���6u(�^��$e:#�:K_��H��G����@7�"��[/Q"Z��o@�4kRE[w��hݹ r6�6�mm� �m�@ ���y��0A��!���r!�4��ߢ([�G�:;2�U�g*g�(r �n)'V�R擌�J;Ju�9r��2��! f�ju����Ņ�ՀKȚ�6)�잷J������>t��4�w��!N�[B�Ar��I�Ylg��Ud~��If�Q� �p�zLQ����S�ooH�a��.�/��~�A��M��a�;�H�U�.������{�>��֓�ֆ�zKu%T'O�^�d�V����֍�/�ؒ�0�ӡWRH��f$G�ӎ��Z���P/N���l�4��BH��$��$����Ӿ -�����"qu5�lМd�6��U>5Kj^�bŐә�R6k93#&[$o��`����_(7���f5��Mֈ���(<���`�j���R=f]1.}�U�� �n�7ҿ#����x�@���sV��n�b+���Ӟ�h�%$ �hb��KX�U�$_�Ⱦ���oF� xD�v4�hpW���hq�����z�J���2�'}_).l3d�����4�,�-����Q��"�U�Wz�k�Z��G�@�����M�S���Z�<��&���� �,��Җ��sz!Yl̕��E�� ,�F��-�����ELZ�3sk�!̆��M�4�m�\A%{y� Z"g������kl��5�S0a���7���� ��y���e2%V����~|ƎE%Rb{=�N��҂l�i, �r��|3���T��Dz'��v�f1��h��������j)���UA�"F ���7���]��eQ�It�t0�'���k5dm���P�?��۪��G�����_�)U�.�_�z.�S:FX��4!�g�\{)��`.����뵍/�]R��I�����m�U*�e�l�����7��N��\��۔oN�d�����D�������;� -�J&H���K -�O>�}��Ģ,kGŖ�t��dS�i�� oj ��d���&�i��Հ�O&��B�बX���xG�C�5�s���(7<reu��ޖ��ˍ����=�9jl� R��D\Pc�"h���i�?Zi\K�z#i�&�ɴ^x�z�������e?����/H7���r-+��k���V�U��hGs��t�R8Y�+�FD��l�u��o��^�v���*�pd�K]�a���f�� �9B��D��,���:Z�Q]2�/8���q�w6�ByȝElm%�ͯ��U��6.^ٽ���u�`��#�V�����A�ozw���j��\�R9�l��)�_�4:����Yhg��3��GPN z$��apy���J4������1�IA({m����T�Y��|�4��B7_xw�V���LE�þ�<�� -oDW��/my��G��Pm�˗��B�i;������_�Z�oy��_�Rٵ���+2)�����tc�$?/���[}���H��z����Kw��(��/x<��PN�hK�����cqk�P-M^(vfZ��4� Iz� �/���p��$V�L��R�� *k:xy�� &7�����ə(R��P�I�K�.ȴ�����7�W%�1;O/�bQU���n*V͆_/�o���z� -~��I�Xri�HK������쬶�f��zb�߂�p��CIHDy2��N��z� �'[Y-+�77��3��(l��|������2Mk�dS���%۴��aD�PU'J��_�j8������2sV�s4_�@(Ҝ�:��_;�z��(���N�|��k���zz=IkjO���Tw\1��Fڒ��3�uYc�)=�!D1�)�۪�ݪd�z�F|����R��J�-�a��i�R�E��f?�V�����z���ե/�1Y����%õ^�}��qV�I�M!���y�RX>�f�^�(n�ʣ$�� yw'r���R,dY�Y���LJ���&�5��`�VP��_,���R"��\S�p�0�i\�־��e�_e����T��OV����)Qe���Kˌ�T魒!�R�<�j�[\�4���L�+J��x�x%�v����z��W�9NK�O���s�����7W�$�(%.��~~#��!�8�=�z���^%[-����K~��H��6)��'�"!d��9����fkK�|�SW�,�a~Ƅ��#�G�EM�3��]My����#��.�&0�ޒ� Je��z����ױ�z)�|�� �#�x���#�;�)m��`A��f���7�ʏ��a���w����z�սr�@T�|5��J�٦O��m� -�T�ʘ�"����;��_�u��j�/OiH�|=d�BT-��l_���XJ��U�����e����3n���x���*ji���N�{PA.h�2-��-�������J)�k�74WX�W�Ҧn�Z��`Oサ�U?h(� -��� -�b�Rfo1�� e֚�m5��XB3�)�V�Q(v���&3ZO�sPNj���Bi��6h�J�b�H� ��Ҋ�0J9�<t�hÄ��q�(�.R_�?��(ؖ)EhE���9\�I���m�@�Y�@��J�5�/���zQ�Gė�z�e�ߚ�h������(���g�i'xR(�X{JtZ���*t�X>�@V�X�d�����P������ �_����@�lO��!��n����g�qnw�Z#��.-Г+˦���d_(?B�hu�K����k`���Ҹ�k`��L�=�*o��Ǫs����eۯ�������c��O��T� H�+}j-������^�sj�0�f�7�֘piw혞q������ kTW������5ؾ�yB��E�WQ����xO�b�R��Gr��#��e�?�u�)��gdk1k}�rd�d��W��_ )�(܄2 �DN�*������� >�$k/||�y{$�qט���|�ke>��Ǒ;_~Q�D-VrE#(�*��+�����z��.J�j�JӧL�E��w�Y�qA��활����S1��4C�vכ��K�ߵ��8A��E�S -��V�iy�1l �l��Ns�f��'��:��ɻ�tuqÈۭឫG��k]����"Xl[r�FM�4�c{��R1e\(����B�Xc�B���ы��*-s���]>��ӕ���u -���]�U�;+�t>wW�q��x�Z�Sh#�rA�MT -h�Q��)h�V�_���K(2D��I��k�������` )v�z$�xe�D���%��#���0>*-`qiG�`|TZ�!X�UZ0cNV{-���?�Ts2s��Z�t���M�#ᅋ��x(D2m�(Z�&ej|�]����K���ơ�]2,F�쥝���E�k��8[���\�X�6M���a�XfAQ@k ��y/�ja֫��M�ӂ�=��3U�cW�7�����vk����;�̡�#�j��Z��χ�Dj�)E1�7E��9�wu��dF'�A���^GI�A5X����~w�ٌ��˭9+���m���݆�y3,���I�Y�oT��_Ϥ�a���4�kQ�C,d�`�*�J���cC9�R�"Kaq!e'�$�O�f�چgiQ�x�^�W���ݐ�g�d�����p�v\�U��l�����4p���TV�R���K�@��eS*l�*[���}Mn+�X�B�H�*��R.42��u3�M��UmO6�y��k1������_��Ɔ����U���O0��Z`�Q��O7���ǼnpY8RH��I��*�#����l�w��_<����9r��� �Fd �V'�<�[�z���ٴ��f��k�s�a~��j9H&y�;r�7&i0��5�+#(�s��骣?���cO#&+����~� �"?֟D�0)u�)���~7��'Y�He$�-5s�tR��ь+�#H��C(�/�M�hL��*'�WYp?d�_r��ÌBkv�U�Tȟ��`���H��U&f-���%н�\S�w?��Z�Si��O�x�-�ԗ@)�_�_���Cn�-��O-Ǔr������o!`�(���ʢ�!�B��0�o�'��i����qP�갆Hh���;� ����t�ɥ���i�%kn��X>ThiC2L� �CƷ�{^E -�=��`��� f��x��f/!-|�@���Y?�lL�\�Z����JiL�0Op��߹;Z5�����T�V��m�~@��k%p��U���}�=�_#5��yಆ3� ͼ֎2�-s4�)2Jq]ehtݩ��d�UHJf�Ӣ^������6��Ǿ3w����a��_����� �@���]O����e˪i�<S+K���A:�tZ/�o`w8�2b�?�<�R=(���*�a�[�/n��tQ��J+��n2o���<i�V�u�mʟ⡬��6���6���5�NR%d��GH��~ݙ8ގp���=�x�zl�H�pb�U I`(A���O�#�+�nѝ��Q��n_*0j8"��]��l��J(t� -��U�l��c�??�+��m4}ğj�A�hl`�X�Lu����ӭԤlʟdk6����V^�11UrS�{E��z��)��+�����x��龝����ZG[h,T!��#�Z�潀��U�P,�^` ��xf>کFH�e���c��M� �&E�дD��%��ܛX��#(x��R��B���S�R�(�q��.�hi഼S(*X���$�ܹ�����_.*�-k�}�:*b�~���$��;����6� �hqj� d��-�|*Ȭ.�>r9_���S3e�V��t7?X�A��BeRU��^������J������pA��@O��]�"��������ZWฎ9F�r�tv� ��w�q�y��{D��w��m��3H�y��H+�I��s�+a^=D���+�[���S܊ǐ�N��Jؽ��¾=8`��!V����| -����)�d�c[�C�8쐻)+�tY�b�� --��n)�}5���@���� -�]��2f��f����3�t��q�uCరTUv�3�Y��R��w�3r6�h/��S}��ȷoſp���>�q�x��,�Tzaw�b�����+dY�2P/^���.��j�d3��]X���xp�c��:�@�N~V�|�z4��5+SQ"���]��uS�%S ��ߜ�u(��n�b?��"^���U���1��M�L+���I�7Nvc�з��Ƽky����pO��ġz&ߙ�k��#X^',�Y��7/�<�VB���&�e�@t=^�����21��4oS���n� p���HK�:��"��z`����>�)�&��7�&{�P����@���~M�Z��i(��2�� /�l&5:yJޚ;���cPAs���Ž�d7J[�ҳ�L^^#��cC�j�M<�L����B��XǾ�ڰ��M�G=b�Q고�&���:���� �]5A�ų�Dz�<F�T��jT�A$;Om��Y�GG #-��>�*���X���.�s�[����;���!�%�8'�0S���1K����qD��bN}^e@��b -*'� T�=�#N�TNN�������LA���v�ah��¶�� �4�R���^�k����D��ԕC��sF,�i��(��aٙ��[�R;y�cA$#�M�콌Ún1W7[�خhB��u�2g�l����VCӃ+3�b��o%svQ6�H����� -Q��@f������lƍ�����$��h ^Z�QN�܆�n��Rm���*o���m:���k1���)�������t��lm�n�o%��������A*�5w�ѰPz=h�[V��J���IKV7�a��vw%�j��D��t��T��#����(T�siۧ�6*;a%��"*N��H B u����S��7�ޥ�C����1Q�5��(�ۖ�����{�{�e��X�K�)x W������{'�s�߳oX -�g�BV%Qk>�����Q�g記�=�J��Z2aa�7��}m/J1��*hm����f��T����R��x��R�Jj�.���GSLy�Z+�f;�~a� �k�����i�T�� Y �v�X��c9C^�xk�FꚚ�`���: O������c��C%K?) -�ߑ��x�P��.�s�djW;F�/}O[��`� ����f�����N�c���~��I��1^�T -�}E����V�`2f�t:BV�@ -��Z��"������+�@A��ȟ��}�r9�����v@���a����Cns�ST;��F��j=�G��B�y��Ý_�F��F�i7��+�0YC(�&$Be�b��S���R0�Y��F��*�y.^��I�M�]&(㈿�:`�m�t�}+a[J����~�$����}r��I��{�)8x���Y�Z�̢���[��������es�aoE�d���y������a��������L�R�1<d��h���e���R�V���G�}�a��N�t���=�[�I�� h#/����L�E��Z�R���Fۡ��^<�-��Fk���m;���tQ��L�1k ��Y�T6��M��H�;��S�^N�d���*ml�?Ye� k��dy�Z�Z�ZY��Ԫ]u֚tC�9�̪�� ���^R�v�GIa֦��e݊kS��T�w)+k���$d���y�!��CC��el�e�X|�:�D{��>��,���xQځ[��0���?��?��������t�?�C���q�ENGr�-��Z?����-�����7�؟r-���� *j��6�D�:� �1 -q�D�O�`��孛�����! j���& � ��f/�փ�LJ�'~Ď?Ԡ���f�x��O�!�1ލ?42P�Ȃ�lŅ?��=ߍ?d����?��{���"��n�!���ț՜.�!�Xr� [�?X��@BjyZ�����S�A/����[�8{��M�*4���u� �ee�N�"K*�Hց��鼙�zN�h"r��F ��V6Q��"�Q2ɺ J�m��& J�-������KY"8�q'�p��?�/� �}S 5����&���$�DK����0K6"��lz�X���^|?�$�Og! 3����� ��Z61������<o��WJ~Z��D��L�ʜR�e>��I�J��l�E��Ob&��9�s�"� �X!�2�a�%�M@4��7o����?�����{懀�Ukz�H��s� ��w�L��hwk-��h�B��������t����q^����n�*�~}l"nn��7�����F �<0�Q9Ɇ���@D���D��Ta0���M�@%��� A����� R&U��@U������+8_��@����k]uҚ%�Qr}��D|k�D)�ɷ}(��O�@��h�\o�Dڬ����DO�mA�V�z� �ش��� F�vn�������LO/ -�d��AADyzػ�k���Q�o��71��� ��n�Q�t�#ҷc1��ml��[[�qc��=sc���7U�PN���ل@�s�!#�����A��1�11�}c�rb�0��,���A��Gc��AĜ�պ9�(�����.������F��7QB�Z�� Rܡ�9C�� n�� �c�oU`4�Z��C��W%Y�=����ĭ\�Cv�*CZ��T�P���� f��J:��n��yh>��� *2�_d�sLsip�|8���o�0OEM��"�A��WfI��j[iw���[���A,����B��2Q�!#@�(�>�����LM���a�� �ߣ�WU8��;> De{k�}�U$�{��!�+��~@��YV."~Z�9j�1�Ҝz��c�z=�4��!���P�Dm��|�� *D���ys���І�� BĄv�eY�����HL`��A�%H����R�p�f�ƌ���A�|�� B����a�L����������!*��6��P9��kz�U@�TI�T��98�*����>DW_�ap!uk������a�^D(�YC�:P����� n�� n98�X�Z6ް�<?D��>Y%��,���\�-�J���?o�"�߸����oO�{�ӍY1��j'?��AD��"���D�?�ﱌQry9�R~�^6QJzS�p���3o"�����7����Q5�ͼtׁ�߲A��r϶V�O��o��Qk? D�c}>ׁ�'G�<��r�� D�(Z'/�!+����uK�\��*��n�v�y��$D�'|)J���' [�z���n��I���T�0��V�Xk�B�2����;Q��n����8p����xl--m��fR?o� e����� �������l�xq�rq�9��4un��U����wyR� D)����q����B4���B��c,5yw�b'QWc����8m�B���B<dg!6��>�l�����B���U�4K.b���~H�X��|3'!j��Ne�̱7�H�ls9�=(�N��rK"��-�MB,poS����g$ĂI�#4p�)%7S�B$�����8�<eC��V�����9�'B��U�m�Q�jw>k�@!�RC ���C�*P�c�C*mPR��ÍB�heVc&Z��|<_o�6���L�s�YV�ǀ�����@!r}�t�ᵁ{K=X��q���D -n�ɫ�Bӛ�Hi ��Y��<��`n���,D�j����F��s�,Ds�c?Y���7 -1C�ҹ ⡜(�- -1c�M��Q��ߞ�!!궙Z�/�FmR=�$D���%H��sf�$D���6 1�c��U ��� OBPNB���$!RSyf����]wst�e�����!!J�m��F!JaðP����8�BD.�o"/9/"�N��B!�g0� -Q�Aq+q�AC�L�C��?6�;(E����&?����D�rP�%m֡~��P����ǁ��7m(⡜P�-�+V�E�'%����洡�R~cm�PD)��vAIJ1���TO�PD����"J�5y@q�&B��Ly�<��Sc0_�(��Oq(�V.(��h�1���ԟ���d�Z���-"��=��Xj%�PD��S%�"n�"n٠��-���&�%�V?PD����73�rIq�k�E���F�"�Q���h4+�PD �}R��PD�K����Y.�"n9��zB�&�;�Ӥ � E�){��qbae�E��E<DG�))�۶��:ԕ��� E�T�v���7m&"�k���D���ui3Q�S��B/M)��6�����R~#B�LD��fpQ��_�LD�L[>1��[���[&����Pwq�4E�LD���{���`�s15�g�Q�'����9�d�b"*"����f"f�P�uCE$rJ�1`��0�CE̸���8$�NE<�����"*����PD}h���PD��)������ EDI�N("2�.����Q2(������ EdA�G�PDJA�{�P(�V���hm�!9���*m��<����CԂ�n����s8����S�<D��Y���!��C�r���`��h0Ьs|Zj4 &-Aw�B���-�V.▃��[�jK�x����."��G���#I8DP$����'��q�z�l�p� -�����q�j�jś9j%[w���:�d[�q��o�q�[�p�[v"�e����8���!�b� �zմ`+���!�;�q���XKk����o�8DT�}��Qf=i�ܗG�!j,�!p�.�������!p��U��n��<��q�\�9��!J��!�H��)��C�M�ˆ! -��-��T����!*��r��(K�\��d�@�E�����Yp� -$@]8D�[g�8D�3=��y���Bxt�,�2�������!�yS(�:����@Dr,Z. �,����E��x�"�X�Z!x��z�i_<D����x�@.&��DT�Ph?������ ��"[@D~U������[��Qra)<D)�=F�<D��"q@���<��!�P�p�%�^�n" �- \���ׯQq5E�6��;Q���o~?8D���38D�-ԍQ�W��MCT�.?�oba��Ƽtu��L�4DM%3G�f؎����%S��h���S��EC4�T>4D,eX� E�ECD���!���s4D} �mb�/�y?8D��ΊK��)��o"C k���h��Cd��il�ߣ���a��D���!�� -�Xz$��X�'5/b1�lg$b:>��s� -V(ՕMCD~�'y0�� C�`Ѻ���x;Z�4D]��F�n��YM�b���q��Ӣ!2o�wnb�w-� �L<H�9 Q��Z� %���!R8J�8D��jf?��x�cT�5��1�^Y����� -��E��?~���� Q2���!��&8D»df/"�V���B�o�y3�������{�]����p��r��mD�Q��#<Qw���������!R�4aHa��T_"jf>LaCۦm������!��7،�C�}"6]�CĬ㋇�!i��CTx���q���=����� ?+��!fJ��q�����C�T�ӻy�Db�'��n�H4�{��L܍��7Q��J��D$d2g�@D)����拈H��w5�,%���VN0�DD����@"광v$"���8���O-�H`��oG"rW��=��8�?uuY~Ca����� \DČ�i�������Љ�J�?�D��RG� "*�d��AD$��kl"��W����H�9i� "�$c�����2�b� "���X����}��""[pր��D��`BZ� -�����vΜ�h�.�djN$�������jBV2�D�{����w�igT�.$"l*�]HD���0��B�@�R��DD��`E&S�'�MD4��wn""-wD�/39/=�M,""�LZy����Ƈ�H���t�iL�M��NDT��v�n""�(:4���8�tDDaIp%�v��"��L68��(o� �&�D5ͳl\���Q<��7�Aƣԉ�n�u1�Ȁ̎"�m|U�HDŸ_U�-&"���_PD�0�۬1���ܶ��� yLJ�H�QQ�EE$w��XTD�2- wa ���m.��W~Oi[`D�qא��W�=F4��6FTl�V=����\�#*����#RV�e�7Q���'��z�Q�����V>`D��ʯ.Y�nNoE7�)�;�r�X3%�b� ��V��0�����#���]tD�^��HY����#+pDN��v.ڃ3h.7N8����pDJ�_~����<@��F$��'1�dl6���U^lD��x�QIE�m��H"����#RR�-tD���=����|tG]tDlع�AG�ً�"am�/<"��QAŰ -K�xDj�����\�Q�F >��>]|D -�߷n>"�J6K�G���+w�� �����!�]t����}|���l;|D\0J ->� K�_|D�&;���Gd�W�k���Hi^|Dd���G����X|D),�o>��9��#J�[.�����R�t��<VW��tŵ1>tDj�Hz:Q=_oiu���GT���_��ܺ0T'>��l,��l��h݊�r���^�/�&.]����7�M�2؈�`�2ak�d���F�0��u�#�^�%������#��f�8"yH�ŀ#ڒ��� Gd�G~�!�%�m8"E#�Q(�W�W��G̔�)�}�M.3��?����8�F#�24" -��@#�@%{{�Y�j D3�����<T�|1�FԌ]k�~MFd�KL%g��H�P���FD�M�؈R -y g#J�]�z�!�����~�}�%X��B#jQS�xQ���<ͦ��;f�s�i/�l��)x�VؙJ���v�m�4ݭQ[6�9S���^9qN4"{<���hi_�A1P��乽Ј�Ux�%BA��R���K[`w�Sh�Q:b�����g�S�#*0��0�����X���CG�\P5w�(0ļ�����G�� -!؈�J�j�FDy��و,�L�����5o����b#R�9F�lD�G��� j/6b&Aݘ���Sd�A|1#\lD�`��~��=�����BL7𧾛�hF�}l6�F�nD���o����O�o���,xO6�T��@#�P7�^WLJ��������;��"#J�o?d�b�fi�1&un2�Y{���Hm�W^���{PRdD��i�Ȉ���s2"����)kM������a�:�����䜌H����"#�I���"#�h�Zd�b�{n0"c�k�2�>=���H�(�ED&�\D)��}�`D)���EF��f��Ɉ���}�Q�ƶ]�‿[�o6��e�x�f#�ae{�-a໔n�n�ǽ}��S|�2��Ԝ��#�7w����O�����FȀ#*�0����#��)8���{�pD�l;��O��K��i���q3tc�16�/l��hI�26Ѳ}�G�ӏ�pD%���XpD3h� 8"��<3Q����t�pD%�~����c��O 8"��d�h�i�Ć#�X�5���5�?�$�5��tD%*si^�%#^2�#�tj������IG��`�c��=��e��`l8鈅�㝛��SN���i�#�~���/;�� �����C�¡>���U�+��ap�����EGDy��?�H��h{�-���G�M��~������$��W�F��?&#v�OJ� �z/��v@���� H49�����*���EHԭ����H�&� -!QYu�V�����j;#��A� IT��7F�D.3��ʩ,f&�]���$��` �D �M}.��\p-�� p1�.`a�&���$j�2�rAI���-�'%��-�%�}���$:�K�PV�^�Č��������ȈqS�(_�EM�c��EI��}�$QI5֪�H��훑�w������2��(�>܌D�/�v�T[P[�D��_y��4��_��Dݳv�:#�z�w:�l�%d�F$<�(5Y�%�v��L�bu�c����E�3� -Fb�ѧX��f$Pɖ[���P����d��H�<b���$j�(t0GW���(y�!�ZX�D=�CU�c� � ����$R�C�S)���гF�����IID&^�D��AI���.N������$��P���V��~(�����-cS���m9(�0!j����tS5{z��Uz�MI����IID%S�D�6ʦ$Ja�qS�Q*��$VU��4L���/Jb�s��d�z����x�a.L�-oH���=nH"�u�/Y��L J�V�$�X�";�^'%�F�T��@� ��È���X:#Q��a#5�& -�-��/D���^�ۊZ��H��$����p]��$Q�JÖ��^��$ҫ���$"ӥ�D)�=�������M<�mJ"�<Y�ݔD6+��%Q{jb����$��ڛ�h\�mJ"����KJ�)����%�Э*�%�@�K���$Z">�MIT 7Q���L�`)J�� -�}S�0��S�"T5�MIDfvJ"J�d��d���/H"c��#���j �@�H9����kO�#uYl}�Ŀ����\1?"Q���@$r t�@$JIZ�݈Dd�"��-MQ�H�b�o#Q�\�r�%-�+�}�`/�� I�+��4Eʒ�H��1d�nH"�rR�Wjo}�ؐD)l8/F"�2���N:y%���(����H$��/\[ŷZ;���Dm��o�b$,v�،D�E�w��D�����Հ2X����#j= ���k��M����lD~Г��ι�Û�.6"r�c�(Z4-6����-g"�7�9�������k� G,<��l�� -UaG<���e�Gԇ�z2u��e)N8">'8�+��O��p�ٹԌ��6�VR7Ѫ�{�tDܦ봩�r��tD�:��U�Ջ�ف:��M�β�G$(M���/N�U�7��̜�ٌD��L6#u����u��/H"�c�AID!<�D;��nL"L��^�<h_"�Ŀ�~����mH=9�Zי�D�����$��Vk7&;�O���̊&���d&�l�|��b�Wv�>�^�D�eHcq�Ӡ�18���HN�����:'Q -Ϭys���_��$� V�D���C��H�y�K�>Z�IDi�v}�u͊r/N"�;����ue(�Z�ĥܠD�GݠD��Dz���k�fP"�Uč� �Q<�(Q�hs�H��s<N�'/D�c=@�Z�g�A��c��{�(є~sC\�D -���&��;j����ՠֱ1��iaL�V.L���[�r�����$�f�灴A[w.�DJ����7%�`���D�a�(��!ܔD}�Yt%Q -�A��(���%�@Z/>�S��9�D����?7%Q�,J�Έ�ŝ�x(' 퐝�H��=,?TI�T%�MI�mZ��@�t>v�`��B$�( �n��k�I��Ͳ�Q ?AH\�MH4Ya� $��N�����B��EHTI?1�}`qߌEH�B��+����yu`��V"�&�؈D����A$�ơ+��ڇ:"�c���D$fCJ��H�qc� "�3�]���h{���LMc0�d$n��z+�ŋ����~ꇑ�W��l^*�{��H" -�l$��wnH" -�F@헦�$��d�H"��1�$n�$n9 ��0����-7<�����J���c�K����$�"c�8�zh((\�D̄��!�(��%���jN5��VvP�V��!�Jԃf�5AJ4�Ɠ�h�X�ݤD���$B:hn��9�f���-�>�ܘDrw,��q��'���IT����P��hr �?���)AyC N��۔.N"J}��A�+X9A�d�ç����8���C��H�m�7'�G�X�i�e��k�ս�%��I�(���A��P�y>Q��nRa��K�̀W���;M���jG%j�^�@��� P"}9��(Q -{�J<��h�W���*y�$@�x�`�x���*��sOO�nw����JT�ܜD����D���36,�|9�͛�Z�c`y�3�1�ù�0���Q+���+V�0��^=H�1����10���&� �&�u;K`y����*o�{���a���Id�e �`����I�&�0�D[��ta5�����X�rL�V.L���e!��?�G#���$��\�cB����$%n9P� -� �b��XVv�d%�&��Y'o�v�`%�|儝�D�����e$�32�r'+Q`v�-��J0�4[��c�>N��ٻ�<l�b%n9X��4�+�~IO\a>�T�ۄl��9+QQv��7+D"A�`%�`���r�M.�F1�Jl�1+����+�FA<I��X�v����/V"9�ېa%�X#��y���.���kj>��J�ɴ�=X�[9Y�K]�DQ�@�"�⛕h�������+u$K�X�K^md -w=�'�z�I_��D��z���)�!x��>�D��8�J��/V"���y��s���OR�Ԃ%���,Q��_*X"&ʇ,q 7,�d_��52�_E��Hv>?���ǩ��V�`%j_`E�,SeY�/X��3d�,[ƻ)Ғ���)H X"���A �T��sr�����:�;�[M�R.Z�!;-��$*���ȧ�{r����%W�]ٮ��n���9��f); $^�zj�fƏ�$<A��ҽ{��bŇN�M���xƆ%J�@�%�0vy��y킖�c� -Z�2�ݴD"B�r-gSYAK<���x�NK�ekt7�0⥺���n�����.�� -�h]�Dd|".Q?�\��Kd�3���C�a�K V�i�TV.�>��-V"��l�6�TI*єỵ�-#�@%��I�c�@7ulۨD�e��*Q�t��*�a���q���O�mnT"c�̘��D�4�C����� �(��T��%2]��-���˲J�������@%�g�=:*��*Pc*�����z�_�$��6*�� -�?_��_��?��؋��/��<�BŘ�<.��3�u��U�>��l����pz���>����l�� �<�W����C��f�0z{���@���_���:qV�0#�#��p=w?���×��w#�±�p�r]�C��,�0��P(���P-|�;�Q7�PG6�I�Pj�e�%��7球]z/�a]��*�楁�P�'4��a"�#��,�a7Go��o�������b�/�PO���a��E6-���Mf���_�w,�a�wn�!iA���-�s�b*���i�����'o�h�����8�p'�p�A4��'���[���3T�� -+��l���y�?A.|�㔏H������Z�o0%�[� l���ٱ�ZX�{!���б#XX<��N�-\!���5�#��a�4�P�QwVa/>�z� -{��|'j��P� -�Z>����]�N+~ޜ¤,B��S�����.Na"����S����8�(���)L�K�p -��6��S(%���B)��)L�;08�I �Z���Y�vs -�J6 ��¤MKRp -��<������-�_�e�Ҩ S�" /L���&7�Y�B)���+��j"�i�݃Ki��J�-�H��N��o���.����7�Pr}RݜB.elNa���?�B��/L����eLa���@ -� -�JaY7{0Ɇ�����H0t� - TT�B��>77�����($�O�'�'T��:�Z�ZB�Pg���fJP/�Fj@CI�A&l>������/.�r�%"&��_[8��K &a�q�E�Z@��E$ԙO���*����.��n�h��[�{�e -�k6��Y�;l`�%DÇ1uwsE���t����6`�v�ɻ��6Ov��s����%\:�P�ON;��=��'P�2�BՖ�쀠.�n�ك������rLk�����wP�uœ.Lx�,rS6&�.�$Mn�� �`�ݍu0���֏�ݽ�����u�ԝ�AɆ��X�Du7��DܷUL6tP*Cp:���A.�`��Q��ʘT"�I���&�$�p.-UQ�:(�QVbݢR�(�|�����-� �UO��:H�Dwț:h���䁬����pC��J�:H��x��A��9Ȥ֫`RA�Z�0�!?����pMn`0��M��CQ�)'t����z�J�or��>��P,��`'�r���$_�l/���9��`�0ڨ%(PY'W�b��� -���:�[CV0� L��������kK�%hw��?�A�יݟ�zI^��n�n9����26L0Y������mtq_(�ʠn�n9��B}e�u�s-���\p��o�)�@���xO�M~�n����|4�9��8��Y"P�}��9(��a�Afͽ�����0�f�>���<�3�xD(�ĴJ�>4o�s3-#/c]Ƚ��C¡�d�-G]]���ym�V:u��[����D8�s^���8r�PN���9��{�B�Y�>?�A}�y�5�������r ��x�IPe�<����A50����=�l�%4�6�AB��T��T��Y7s�Jsܑ%��%��R�C�ݙ�v��7sP����͙��BϤ��` jf����r>y�6{�tp+t�:Ⱥ>�f�����9�sy�w3������;�.|!M�]<��<�ks�A~�=�9H��g ��!�h!�r!ّ�����h�FJ���9������*�9�%sP{z�q7t����ss� #�S��` jz�~��TG�`]X4�_�n�^���$�O{� L@f��]��T�Bt0�E�� -� x��� LX���e�$�(�D_���x���L�": ��Zys���[o����B !x�<��AvS};nP>�6(�_ހ �E�1֠<G`}��6��p' *~����,�'ۋ3H�ͺ&vN�>P�A��؎]��wZ3f!���q��.��6.��_P�ܗ7��]t��;�E��Wc��p��hA,\*�Y0i� ��B��j�ӓl�o��=��]v-(�����bV�@�'hZP2��@J��hA����K$�N��Q�Oψ�1G&Ui��d����m���,����,�\��k��v2������x�'"�Ruu���scw4� ��T���CL��l�u�2�0���;�p!��D7Α���$�:&�!��� o��I���C9�[�`2����= k�D��A)��}#�����)�B%`� QT����m�A=_�&0� ��~�Z�ׅ� ��\�-B�~�S���p�|ߍT�ق�ׅ�w�CP�M;n^a%�����A -���!��!�eC&��-C�b�BP�x� �1�'��q!oi��Aɾz8Be���C�i'B0e,$F �e�K��m!�r!�A}C -)NL���7B�n�*l�,|T� �>��KrΗL@�e ������d�Ƀ��.�W�P�Uvx� �l�Di��>�C��?�@d�?�V=��$ -x��,]�@==���Z�������&�~bL������T��l -�4�@����<t'8P����un`����j=�}|R���?��c����GEL����}����^� -^�N\�R���a}Ad�}� T�;��Y��|�ӆ���)7)���� -|��8�,��ۘ���kiQ�y�ݶ-��%:"P+V����#؎�d����?�=��"�R�� �J�+O߀@�/�M�|�-�x����:ro|l$E �=H�6M�}��FuP[p��\p�-Pkt��2��dk�k'P->�s�r���PE��6��*Ľ�͛���VӇ�㮋��h+>�@�V)�����i�1Ծ��)v ����ܲ��l;�AAƼ��yV��ҷ�Q�\�g�l��+ܻ�����䘞�a&��{�l@.,�l6��vn8`R\@g$��$zٖ��z�z��k�l@������3�c7�7vκـRh�������d�#�{]۟ET���~�H�$��K�i���W��������ğh�W��P�\l@�>`�]l@�<y��n8O��Z��1-\l@� ������FR�x�i��27�z�Ѐ�4�<� h�<��H'��`2�B���0�F�^d�Ǫ������I��Yh@�*͆.6 )M���l@��0D�ބ�����L���\v,(�)� h͐Y>l@��3u��D�8J��Ȱ�T\RjH�_����j�/8�*I��8 ,��"�<e��M�E����[�a�t@-��=�*����M�WM�J���耴��6-: N����.�â������/�zX�Ho����e����� +vI��D���� -Z< �lMy��^�� -���}�Pi7�L3�ˁ���P�}��(�"ɢ�������Bt@ -����P�~�(�)p6Cu�K-:���֝��Z� �t@n\dMP�|T7�;�� ֝���nAD�r���F`�t�0���ذ/8�v=��-8��v|��[���ӣ��Y>h@�K�6wU����������Jx��T@�[�~���`ӳ&�n�� ��=碋8�l������Z�DN����Db�]A����9��>�����9P���(=�����P����{`�iФP�&k������Lju�{1�UԷ�M0�-}�\����ES_���#�a�R2��@ʯ.tF�$�����`��\�#�7�W�f�R�`�5�RlF6�(����j����D�y�~��6P� s���q� U��Kq�ޡ'o uI��N�Lj�rX= �z�1K�w���B�j���E�$�3� ���S���F�^�?�[u�Z��Y���?H~��7�O��)���Y���?5�� \�?a�;�o�dq��~��`�檧�O�fY���m��?z~OZ�?^#' -Br�+���oD(p�>�,��w1��o�S�a�\�? ����OBLvA��>���������b�u�w���o�_�Fɂ���w�()�h*\�h�wD���L�\���4S������TMiN4�����}��zB��2��f�I�~~��4,�G�d? ��q �h����'\(����u��\T?5��Թ�~Z�8X.��ܴD�,���{v|9<j��N̑~�9��4�~pҫn���`A�K��Z@?��4e��~���7Џ�]S�ǁ~�[������5�����N���f���m�P1l���;�7 ?}ɩ�\?�����|���i�k�o���A,%ao�r�~*զjP�M�[ �I���>M��O����>� �j}ky��_�?�L֨mt����"�i�D����$۱op������8�:��}F�nj�H�|��'32�`��>o1��l<��50�������zw�4�U0x}j�g#x}�/��p}�F>=ЌA�q��NX��XyS��0� ��T��.�s��N�c��?���bD4}^��.�k�:= }��� -@���W����<��c-�T�?��W"',��jr����a?��|���[=H����x�da��7!�-�|r��m(�T��;�J�J��8����D��#ЋȧO2���!$�b��8<~��S���`��0+����i�4`|v1'���,%�F� {C7H|:HOgͲ��bF����4�kek�U��/_�$��>(�z��G��������������3�%���5���n�o���c��ࣗ���>�0�oF�V���ݶ���K$��e���N߅�KؽH�8�{&w��H�$\��N���L]6�|(t���Yxm�q�$��n��m��= �G���Z��4�/E����6��;x��n�P�������I(�Z��'���������u���]��6�O��/�_"d� ���+�bi�2�']���%�:>�S߉�9�|2���;�|�,�>�0,-���}x-o���BXf��Z���O&nC^_��De�� �Cq"�"���'���I�*�}O���s��p -���u 0�A�=�Mߓl��"e�u�e)6�u��$?,����4�Έ�V�� �Ka[ ���y68��_����A���-e�=S"Ϊ����������%��yn��ʛ��{��4>�������i�����i\�v.�^�[�����~�*RPQO��%5�ɚ ���x�j�'o'S~/��])�{���|�Ț?�>d67���T�X�>)p�n��^�BHI�����?������gj�Q�:�<�W�/�����c���=�tw����iY�����i%r���䝳;���P�i��K1#��|�x��M�c1�>}q�֠��c�D���C~��|���o�X9�{��̖6���3m� -�Oc�2s���\<x8�{t1٠^�=+ɏ��{M ��=��J�7�hΜ���y�s^�=&�Ҝ�۳�S-zV.�����1ˢ����3pt���g�<6/C{�Ǽ}+�8����'l����g���7�Xil��~��� C�*�{)0��=��|e.�����1�*4�-+�{�{F?�(�C��sM�NsL/�@������s�g��2[�17�F��^~O5*��LjD�;:��龷�����{zn�=�x�o��B���h:�/��߳�7'䑽AY(%��هPe��}˿*9 ��{��W~�w<��GG=E�:lc���W��V�=��Ć{O����Sw�.���3y5*���&���s������3��7|Oe?̾���}����=��\�=<�5o���|����=��F�er��&��3]�= -oZ]�=};��/����} ����a����g�K:yO�VsyO��e��{rFћX�=9�|v�D�%��ė8zO]a�DA��%|�n�6�6d�D8��pAޣ4�}���~�h��؞v���>�=�l� -Ɦl����W�����O��Q������=l O=d^�LK�C����τy��=b���[�= A�1������e�d�=���LͶwc ���f �b����I��xC���F9�2���]&�شo�^ƱݭyM�LF�tS����_�=}ќ���=Jp�Aݣ�Rm�W���C�0¼�d����f�'tw^@�P�kt>���Uv�~&d�2�L���c��sS��c��.�4��7d��[9�n[���Z�.���Y}�\�=-����˒�(��Gz8�����J%�{(d�u�B��u� -�HV~�=}�ZTP���V��=}%|nֱ{V�~���\Vv�=�V9xϛ���7w���"�O<�A�����,9�{�L��a��ٰf�I�cs�&&�{l�)m\�=dh��Cy���֔Ϲ-�����dT��&YƎ���=}�0M,��Oz��g�)���{S�q\�� ���c\�م�C~�v����ؽ�W��ڭ�?�ݣ������K��������h ���q�f�����ǫeS�t|X�=���MݓL�4�{�k{����Uʋ�Gq�Rn���/ -Ꞝ -endstream endobj 24 0 obj <</Length 65536>>stream - t\o�bJp�.�{��[+��H^�=d�^A�C��糊�����Ɛկ���$"5����;���[��D�(��������,�{�����?>j2-s���EO3'=� K��z'x/E�q��(�.����#�.��?�Iw/A��NO�],&7w�Mb9}ZTkuG�-��cW��{�D�wO -���ݓ`��1�� #�{��3X�l���ޣ�@�Y��2(F�����M�\�=V}Kt�,��϶�{[��{��"0�U/E�ݍ�#��׃ʇ�����gB�����i"�d��iٵѵ��\z��>����I����q�7$��40�A�#��$O�_�;�z?�*9�F��PN�ɖ��_jЗ��qPI�����eD�3nl��-\��C��*�L�3N�KX���C��#���= -�|V�5�a�5��I�K$�g{�)1�&m���S�� ߣ&��>�{�~1IH[f�{5�= �z/w���=j�#o���(>�O���T.7R�s�Tf��Up��^?��[|u��ݫ5���kO���<`����5��������~��E܋zm���0�����<d��aIHy���q�V�=fߟ��:=�?H{���}�����uq��2��c�$t���{���m#�1��{mZ�1����#CaO��?��f��z!�l=Ծ�z���7f^������3M��Uu�sڠ��ԛ>Ǹ2{���Z-՟���R B}?<O�������ɤ���8������I�� ��V�<�����r����SG�־3/^�n�OW1|�Az#H���(��'aZ'��Ie7EO^�,�GTN6 - k�W�`_=*�y.�ވ����h�����F��� �9��[8�X[uv��v�"�?M.�E�]�V �O1eK��y�.p�p���ip�ы����݂�Bֱ����-��晜�����Ak{��*�g��� VO�����Shsn�m�0O��<�9o`�~f�xy -�e")py����<]a�',�pߨ<u��\���snP^��Ǫ��-����$���*y��gnD���L��0��x�J���x��n:^�����K���#Q���W�ѽ�x���b@�(1��'��g�ܓ̘ZL<w�� �Y@<���q;qx��?}����?c#�4��ϽPxIiYc� -o+ח}���4F���,��sT6 ϒ�}q�B2�N-^��d�Y>�TX)��8�w�%��la�ubx'�p��S�`�i~Xx&��k�=[�z(=���:���Xx(u����yGNmsF9��'�阛�����=�{��dű�c��tB�z�B�:g!F' -O��`�'�%�1Px���IxDA�lޥ���-f� ϲD@��%�u -���S)��6O���qo���d��Z?�Tob��R/^%�O�Ԭr��˚һ3&��'ه��G�w��������$c;����Kx�/i64���ƞ�F�'/�!//�d���J�=`xR��p������6�P~|��/ݻ(�/�VH��P�`x��%�p���v��/��o>'�H�TKN�Q� -��m9`x:�Q�Y�;r#R@��eg��B�����q�N�R��ZV���V���ࡦ �K�������T�l;�jz@�Pr��~�}�����i��t�'g�b୵{_�>n��V.ޖ�g��� �Nڬ�7O~*��L��<@��Վ�7O�d+��R-.�8 m�b����S0�r�^�8�O�eoOJOq!����sO2I �l,��{x���"��w� X0���w<x[9xK]<����'�%���{#���L�f\(;��q���C�xK^�V�9=�W��x�s�"���ғZ���xs�Z��ţ�O�L}��s�p��@k�<�x;l�k��'��Ӎ�ӓ� -�Ǡ�����[�3��Q�T+-m��ȹ~x�XE{]��a�݂����E��u�m�P�� R8O�{�N�.�< -�3xw�}[�v\<F���5�0=-ޛ%�\�Cv.!L�A��N��:�50�xD�p<f/�{��Y,ZA�[���ǐ���}|{3NA�;���w�N�c$����ۑ|)�}���LD���g{u�1m\<&�����M�l/[6�+7oˁ�����Ɋ���Njx�q�S�:*, �`��2��M{Ԓ��G�#�2A@ҙ�r�/^z�1<%Q`� -����\<mɵk^�<��n��|Ȑ*�7�d\�wX썿S�.��Pi�;�N��B�Qq�]Y��κ�˸Qw����j2h�8wU������"���������������������� �>��P��X@<�8��,��`%��1�Bbe�<��o��#2������������������WU�UW�ćL<(�s��;����Up�ۺ����S�= W�5�� �I��We���V�DS�g�T���ff���ǫ�2�T^卦�3�Ru��wA�����>B��p����F�Y�M ��F^2�SI����x�Q� ��K�NI99y�`N�����$�Iyɂ��,в���$��зfF�w�ѵ�������Ň�/����� �X�8�m[��`15����V�ϳ�<qte�ۗ�<���'��T�3�"rQ)���^/�WճI�z�@���b�8�$����=��L��Q<�|����o�͵����2�����pGts�Lӝ�$4 ��y}�p_�BܣQ��K�ߗP�<A'����w�W����){$�֦���sU�z����������`E��B�f롪1+�\ �6k���c���"=t�!+Q'�����XK�72t��:^�.�>n ���W[1��>� -Y_7R� �`Uc�el��Uy�=��U)�&}X����MS���ǡf���X�����MÞ�!Qg߆��)�iUB��`�2T{����[C�V�z�����#q>H����0{���v�7`W� -��M��ַ�a'~ꚃ��Vi���L_��hΪ�zZf�?�QD~���&����zú�({R�V(�:w@M��A�[i���ݦ��W�W��}&_q�u����o��{^��P����I�~��He%��_�g.� _Y�dO����,o�3)��eT^���̕�!}UpR+J�n�$P}^�%MW��,B�G��\F�>:��B�э�ָ�����|U���0���b����N�"��e���Va)�~Qh�IY:��R@�5lq��Eh��sQ�84X����%�+H1����?T�q>�KQ�wQ�yX�� -S����,�Ͱ8�V�����Q����Ɗ|(u��5����ҫ�T�*������R�m�R2]D��2z�Cy�lr�$_ 5h&3�(N -���R��CW��0���#��>>��e�-+[���<O�,�$]k)�U|,�u_��S�P�q(�4�CΆ�$�U����P�*��Ekd��x-S�^%Y����x�P����W�tȯ$�Ρڿ�}�;_n��n�<���̆N��p֩N/'�r���M�;$c�L�����=y)r�!,&�ԡ[4?p�������/��7"r����5�����)f�� @�� -�o���q�y ����;�������0]���mi���C[��1�̆Y_��-:�mP���F�!Ǫˮ���N4$?���7P�SL���o�jԃ=��CA�\8���?�fň�]B����X��JƬ�2s,(_��#�r���(����@���H~�kL�A�*_��?� y)ʧ�ߺ��r���:��߯ZJU�4>d[��[n -�Wo�V�Kl -��{�{(4�v^�xBjUT���<k��v@�t!a+���vi?a����qh���K��Z�w�/�q�!�����J� -[rQx��B���tx�^���K��j����u������S -��z�ʕ۲ΐ:����.K�ZZ�v?�| ��-��0��b)�M���È�Xo�d�<VDC�3}Z -�b)��'ٌ-�y���#zP�ʛ �_������G9���w�-qR�ۧFe�P�����q]� �����P�8�C�V�g{��Ku�uz�7,���4ևu���v��̶�y"�\����!�6,l �«����y�}z�LH��0��'w��۲�����w��V��A�g-��f���"�*�V���nʵ����a���U&)� -�W�Ùd%�G*�d�v���o���כ�v ����ZqOq����)�o���q�K(��Y�����1��xleP�-7��hU5H�ߧ�K��(K����\�jIf�Sm��2!e����|�����?�YF����I�ɧ���C~�l3ݛ��4�T4˱�|[VY[�ƙ;���k�U.�w�O���]�qhg�J4 ��ɴa���Yɬ��J$5������O��֙>�C�!�hyש5�k]X����������<,�9�ֱ�|*Wr���At��6��Z.D�Jv��ܠ|�lJ}�c�LD9U*/,�i�j}�{�.���2܆��5���E��C%�zb�)z��L���B2��HCa�A -/���tj_`��o�� �4U��!�f6pK�P�,���|l*:���җ8KLwĩ��^�7��Qu����8@�M��]���B�d��%�?5E1�d�,�:�nH_U�z7Z���T��J�3�~U�X�����i�>3��O�\�`+|(�ql��!6c�[rQ�#�^N��WBh%�Xa�J֏^/-���r�&�드S�V���z����3����,����'&�c��D�c���G`�U����ѹ�mVX�wU5�X~O�ltR6�O��:�����a�=^^��Z�>�B�3��Y}�T -oU���E��_�z_���\I ���d zY5�* B�^�Η̇���,�l ~n{���.{1 ��b�,���>˖b��5�鏎��&9'z��h -�_�b��Dž�W�ʪ�[I�o���6���__9r>�a系&�eu��iW -��(��Cqv���(-��������PF�G��G�Y��>��A+��j^�b�ԅzO8p/�?y����g����:K���#mY{��~��d�:}�|�K��(['�Ņ*�r�'��K��IUH�a'��RZ%ˋ��E�iK糥�|���,�e�m���N,*��d�Gn��$�MA~�]� -o��f�>���/%�k*��=����݅7�O+!�������}���2��dD�����8�x�JȎ���b��/$�ۉG�V���KTU��JeNA+�2�����K�[�@z��B�Ԅ�7$�0�^����uCSt��3� �DQ�E -�?�ۓ� �@;�Ƿ��<��'���ݙ��������C��}U��u�Q��ZڡT�{] Y����(~/Թ��&���ӽ/�UE������8�,���US��s��a�T} �2�AK5�բY� -x������ӝ�7!Z�ԫ �����K.ӭ�4]�9]�,�+<㐧�d��߭�m+�����L�<?:���R��h�m���C�Y:���.2���|�2�a�[�Zu����E�c���V���Q�_`p�������,������"�b�0+�t��������`[ V����y�5�ӗ��O�[~�^ء�* -K�(7�����mY����NE�psYh2�q�*�h�VE����):�Q[�ZY����֓�{�F�R��wC��P��Ow���� -L=@5�l��Շ�Q��=���Y�?�{���O�o��e��u3k -*W-h)~��Ƣ�Т-�N��8ڊ����e�~���0a`M�0\��8��B4��(f3B7u�A�v���]5���&��Q�b*����*��2=W�Uv��_���e�,���A����[8�n�d}띠���sc��A?��o�+Ɇ�?X,�lQv�1���*���0�a�N'/��;ר�>T�e�`�iؓ�� �:�p�{�/&.�ʣ-l�,z�`�i���=3u��̟��ž��!�Q�s��m�dm�T�ҳޘ?�SG�J��:�SHС^� ���*�՜�]PQ�y9����[�Ww[�����:Vz5T�~�����iG�����^6F*Q�U�gQ����Ջorn�k��9/g�T���A�kͧ��L�䯢VE�!͌E�vP�$Sui�f��RMU -�-ؗzs;�������9���T;v�n��!eS�$۫�6�>mc�7m�װ�Z�M� -X��ט�d�8|�Z��>Y���S����v�:���5R�=�S;�ɬW�U=��3S����1��8hiH|�˻FOR���5b�}� W-�Xv}R6�O2�j��г磌"��C[��?�l��'�&E�)�P^{�.Дy���[����H�L3��&�&�����~Cx<A��[m�L���Di.�wnʟ-��d�۲�f;:X!��y/��0���!)�PA���<�|���~�t�B����ҿƀ��~m�ŮZ�|��D�B��<G5Y�RwH�(t -� �B9�ŅU���b�Y��iA� -]x��Rg�V�!2�A�ӲOݩ�n�\��4��)S�Xd�<ٗ�`�Jf�y�g�[%�������M_f�R"G��K)�HP��#��zٱ�a7.�*�ǺR߷�_����j��}U�����\�!�0Y!��2$��.�BՂ��m/Cg�J�`Z�xo�#yk=�A�qO��Qn�>@�~*���hCo����J}9��z��9����l�{{5��n;|`ԞB�6Ӓ�\��2��R6�Y�����:��$�h�����:Ɓ���ޖ���|�IZ!Ğ�������"er��{���k����Bb��E��)Й�I��V���f��th/�~UP�6EX�5��ђ._�O���SS{���Iώ���������<up$�'W�K���'=_).�Hh�O���i�Y�Yh���<�E��S� �v�z��6�=�:@�Ö�v����kޫ%��P۔�O� -ư�8ӎM�� ��d����v:���R�G���)�4p1Q�ӝ��Fc��m�ʏՋ6��ӡb2�ߚ�;������?� �g���jxi���H܊�{�W���ꥋ�'�ӴW��<�D��}T��'�� -m5���pW���߫��.��V����}�,!�\s�z��sj>lX uy�m ����.�����P@lk$Ni�B��F�|�H� �2��(�"+g�Gh�ƍH��\q��Q�+ن����ﰛ�~� �Hf����@j�N�Lz�b�.��:��d�ln���E\���x��f �n��*��ը�fJ�xw�yrJ��E'��6�=j���J�`)T>�ģH���#nR�nK��n6���H��Aj�=8���BɬA!O�O� -[���4�����;���)��$qkp���k���S�8Kw'PC�$�kC��qM^��>Z��л�Y�� ��s5��'/�<b��I��Y�2>s�5.�����} -�UӁ�F֜�!�Ep���KKH�*y�TW�}��-?��t�E ��GVPD�$����t��n�G���٦���G j�K ]�a�I�X�@:��-��a8���g��}a^��d��Wf]� 6�@�d��(��?.���9��.5�3�Y��tKu���?Y<�J�P��ݢ�)wvRa��+Մ�&�pX�c��2�6B�Z���Z�Y�y۽�M�Ut=�h��.Q]�ӃP;�ό�85i>���V3ڭF��h�1؟�߲�/�#(*`Vv��jZ p�Ώ��,� ��<�kI+:�0�����dqo˽T����d��O���F9���H��W&9�۳-L������/�_� dE�̜�U�ZF��ZXA��7$����V��U������K_/�hQ3�����C .���c�%6]P�-#�#��G6�p:��g� �.���،Eӳ�[v� J�kƠ��N� ��^?�b0RK��7�JLfK}^tO�� {Mh�ˌE�@���5���o� \����"�{.�2ΞmБ+4dH��ԉ��*8o�y�fU��p�D>|:�H���O�ܦ�U2���sǖmR��!B�x�Ƹ�� �@�����n���f{� -�I�T��l��Ad��e�y:�k�c����!�Zww�kq��6�k�v���u�;h�pYk�)o(���Δ5n%�X��,���(l��и���.�{9t��P1b���[y�i���͚a]3Y�e���JCQ��BԆH�Xv8R0�5��� I�V3��i���n��WI*U��WO�QV�|�i��T�AW�χ);�Jh�\�.Z�?�Ұ�;��t�n��aNeԍ�Ò~����dnK�Q`9 ��=��r��ٜ�j�������1���4:?Z�\N�D��bd�.Pp��9�ık��%�nz���V4UG)���*��|jj����E�Ja���Mq��ya%��g���[$��=?�x�0ц�����%DyY��ǹ�0+�%#X�[�C���4�MnV�%WI�3'��������f|W�� #��$/��!��^�w�����6�Mn5Qd�a���x6w����F8�O�Y��%�G��mI}�N�T���&�X[G�F-k�B�1q�u`m*�r0��[�O�b�n�Ś��/�����Q�ճD���{�G6{��"�¶�td�s��Z�V�� �:�Zw -�ӽ�'��bV]�A�a*�,VfZ�v�H� �0~q�%4;�l� e|�|�rr���� -��ok��[�A�,����ZW�ȈK��������;��y�n��b�]MiXe��u�=�q���ߪ���"5 t�PJ�jq�_+Z�I�S�ETm5aq���,N���S(�]H�i;vK�?�e%�K��Y��Mi�L�!?v�zZu,�r�e��fFU#��L��A�t/ɶHA h$�� -[ -l��6�)����tҼ����"���Ul�vt�����Lj���z���e��me�H^�A+n�~P �;��@�,�2WR7�O�}���g��j2�3k��t�PN��,��y���gnY�3F.�^%�=_��.���fw,15"G��wXOP]%;f��r?�ߪ��Kyۧ�Y8����a�vP�\l�)s���)G�w�����@�!kU�Y��.O�:,bO.2e@]E�0jX�c��2��Ϋ��J̭z�?kQ6wVK�Uq����+�M��� XC�m"F|N�;Q���J&n�h����l���Ц��'^�.�5���|(�3��/��B���X�s�$(�4�հ�-�u��0���ԧ>�/e�Ǡz�.����&'�pS��NDK]�ݦY��z�{0M���b��ΦJg5y��B�%�a���ν2��R�(w?ct�S���H����^��a��d��H��������&\,S��,�����A��(�p�"��B6������v9��س�B�A�h�R�ꘌ>0 ���"}�̊�}��}#iw�����݂V.7���\���a͏�̩�+#���}f0F���1T�VD��W���J��T� V��عw��Z�%٬�1_v{8-�?��3L���X18 � �����c}�� Beo�57�B��t��]3�]��'�bo[�T`��0����)6��Ƀ2.z�B2W_�Ǔ��j'��n�n lN��ԳТ�_�&���nա7wP -6PT�h6�GP��^1�{�ӽlx0T�܉5�.ѹ���w٪_�����d��f�^�ߗÒNT?32���@JHZ ��>�b g⾐�Ȱ�ܦ�"p�"r�eԪt�ͤ��]��1d��,����צZ�P��|�<�Riʀ>~l)7�aky�ͅ��D�lT�\�&�*}l�����G.�tӛ�\pwrIS��!�}n����%�>!Ѻϛ��G��^v�e-u�M��U_w��պ����ly>�l2�@����i㸣za�vZ�X�M����-� r��Yg\ -[A��mԀ�������A� ��o��{�r�p������`y'-��M��Z��(O\4ew~���9��U��#��t�@��7dU�XH�(�d[���^��k���0u�^�M�R�� �����E'�s�f�]ws�T�ָ1cu@�6b2����L���%,��dT�!ea��NB��U�Mx�4�;K`�/�������~^��㴅�8�ƺű����ȹ�p���Ka�ux�*.��/X�AFĬ0)M1EF��7�愲����V�f���������M��?���7�������jL�*��s��}+�+�^��E�D���0�o�lKs�l�j�n(�э�B��������� -TXkɼN�j�S� 2���A�:�$��:���"�(�)͋9��~Pby��<��]���� -+�P%Wʚ��B��ط>�� U*�R��Y�ܶ|�:jU��I����[�c��[)up�ݽDNP_�*v�uP��n�uad�u�՚�.��Ǹ�j���a���p�/�hP��Xd�OT�0:K��g�9t)�������;���i��=c���TJ���8���ot�n�r����z�\&�C~ܟ��n� }^�^��=�h_��m��� j��]_r306�n�-��oc�|`���Z���D���E�ߒB�(vP�v�t���k����E�(��AJҴ��ɴ���8mO9����c9H�����P+K;�F -��<2��i=��~>����`W���AU�L�];\ӻ������]7n3���A�ꀞhm�4'�0f�N����q+�c�ϦM��z�1�<��ܠY�æ������=���� ?�z�o7��0d��a���cQ���n����cf,nMo�>���♵�j��+� �mG��wAև���Q�}�����\�X��+��5�Xjl���[��� "$_Sdv��R��S*E[y�u�d�h�$��S�բaVg+�ڿ�s*�b���[��H\���|@ �� �D'H�F:t:-7SYg5��+�"dFm��w�3�sф[%�� C��#���a�,��*`7�͆���V��ls�����g��oVr��>��ճ��@E��J�/L1���;"Qe�"����+�Β�����&+��ӓ�u?0�����*�*����B�[��=tG�Rv/~���T�r�>0oފ��Ճ -�#U��X3��Q�P�0�����������&�P��jX�U����}㶒j�U��Q�X�t;O���4Y��@X�ºP��x T!$Md)��B��eE���5���~��2=�k��ȿO�w_�xV�-��FV��>�1�K�j��]K�Qˈ1�ᕐ��9�my?��q~^�88O�^��5�������F��`�M�n�V&��3%�<���bȸ�X�����a�_oY�G�Lԋ�yܽJ�`Ա,C��4{=^&�~� ��Q\�4o/o6�2�$i��_����kh���U��`ɍ3%k{Pm��+����B:e�uy��Q�#��X6�wǿ�ۢ'or���M�\�ݑ�Uu��E� ��Q�f7 ̔]G�����P�mU^���_�{hI �7�C�׆%���SɖR#[���ͩZ�7�ثj<�$�s���n��D���Z֖�x��fMs1B�/��>��*�K����!������g� M������7��e,O��əy豮;� N���w|C��D��ϒ�:+��F�fdO��|C�>��Bp�����:Z7 ��<��l��� �->N��vK�Y-�]o�D"\J���b=8�n���5�Է�4�E��%����::����S�� @u?Vjv�<ͭ��nh��z���%�Ro�6(���A7�e.���Ɇ�U*�z� ��g� %�0c��[��-�3�+�!\��Hg�l�P~"���`�$1)�����٦lx�٢5,��GLOq��� �b��G��wd��QN�$V#�mP/�[vZd�qW��PV�ʏ���c��[�e�E^�^��V8DM�Χ|_X#sz'�`���>��n}��0���,AMQ E��i�SC������a�Y3����ı<㼎Ԓ�n�V�%�3�(��^Qr�4��d�/���9�F -�=wߖZY�q��%+B���)���G3;��=¡~�}���g�E��7��a�9H���X��u��S�6�C�� n������ �<���MAX�=fQqچ�o疥�d3���EUh_�C���+SstW��k���)ƫ�lU:dY%K-C>#>*��PL?�V�Z�H�xC����aXk2��5V�n�C��7��y��-�q����AƯ�K����}�"��*/����*JF~�EYq(��.>s2�U:���C��`19 -���jk�/��:>�4�օr�$o�Pد$oƟԖM0��9��awy����8����q��_k�ؽ'�,Hȩ�YF͕}n�{�9���������8����=�Tk�lٖ>J�6#�ġ���v\�<�U5!�&�H(�@���VY�&4B����duj�K�Nm!u�9Q��.R^�>J�7��m�ˁ8l���y! -�7�Vʁ8ܲ���~$�0 ^<w���i�v 10��']^����@���v�$��ԲE���J�Q��$Y���ς��b�O{�T.e��-��eF�Z�.]*�ߊ?4��~\�������({l�r%S�F^�C�@�����fgJ�\�7I�K��{1�\��������������#��?����Q汒�I)��'�m���d%HU���3E�C�cT�S��*� -��$����Ԯ���A9Ԧr-`�6v.���sȽV����6[�����脹�*�Z�.dud-���A,Y�7�0��T��#�Q��G{������e��# C�xU�Y���������������E���� J�}�9cXw&��L��S�\�Da>[���Y��V��o��߿��šnb�xC,A@D��Ndv A�����cJ:�RFv}i��ۏ�D1�+���Q�jtf�� -�h����/�������=�U�`L�s�3���;r��dF r��C��t+.�����96�r�m����������D:��Vs����z�'N�/8��``&�<-�9���S�E��t��C9A{[�b��&���;״j����B�����sA &�u�n��6������= ���4}F��b%��@L �J#V��h�Cn�s|P��ύ@Գ�C<�@ �Fp�.��V�������X�W��6�P,�d���� اm��V�7fɆ@L��w3��H�b���@L�=��v�.���n"" �@ ��j Ŵ���(�$n"rw�!G_)�i��윰�LO�sӉ&! �pJ����d���>�@�45�!g��}.�V�g��� C.�o����7��uy�?�ZV`T?� �Ē1͖�@,��\�BTn��δ�(���@,���� �\>� g J���,���g�Q����mlb�ز�f ��<��~�f �9�����e,i�@,�A���b ��ӝ �ݥ��l)��#<�s�Q�o[k��R~+�+��k��sS�fV�'���+D_YL������w�hƠ��� J�O���ȅ��MA,�Sr�`%�9�� Jy�ޭ� A^���8�L��AA�f�=�Bl���1��{�s�B6�#���ا�� J��D�'��ZJ���\�Q�落7Q -���AD)y~8�Y�"�N��A̙ ��9��Э�71+o1��9���Sz7Vl+�i�9��xCV�\ҟ���E�M��b��,ˇrp�8��Y3� B�2�M��'Qro�n"J����6 8? D��u�"� л9���RO7ڕ��o"oCp�����us�� n98���-�xCm��C�huj��Tˋcr+�UJ52�~�G��G[����v`8�A�Y�z%����? Č늜/dd��i�t��v+�A�<�S��d��<��8�<����l��jGi�k,�dw��!�� ;��(y<�"�>),ABD� uF�fZ25mb&���!�����*���$D}�~{�w�e��}��&!R;)}|H���V."yZO���Heg��!!Z�'��L��y�MB��;Cq���J��H��y�!� y[���-�n"��9�^�8ٷd�}�� �b�Cj"V%���kn��,q��_f<�������I6��H���݇mw:��S1�g�r��$D�0FS #�����LJ�h �z^(�(Vt!��[���X���"�n��7�{���w&%.�kM��!n�!.y��*�l�a!ry~8�E6�r)@��*:N�� n98�"��6 ���j��n筬��M�8.���ɬ������� Dd>�BDy�J��y�ʼn��(�w����*�O�*�P� -#�5;�JO���9�͊n���V�C��?7Q0�fa��B-��VX���$��wD���F�l�S�����MB,�<ׅ�'G���\$�%/baR�m�p(z�o�;���#a��u����B\��Q�[�~���A8T��w���Z/��2����F!Z�T�I��G%3v���u�MB�ҜMw�u�3c��̇�''!�5��$D����N�����L��P�0������³�����!� B�r�Y�9, Q�TS�"υ�V����"!�B5��h�^�!��N/�a��t{b'��%�π!��R|a�8����!���{��͔N�'���}�J[��d���k���-m"3ӓ�5nm?�B!r���Q�@����{ay���I-��B,�oS���*{s�B,�d;C�d�bT��a!f\�ͼq�d*0eS���F�7}7!1� �×��v�����-"j��f�B:��L���C�oI�C�*P�c��8mPR����B̂��j�D�;R�����I�����B̍�^�,Ĭ1;Oxl�Nj�:��B�t��c�uS���!��Is�%����a��<c�y(��������nb�}�����������!�H��*b�U�f!��B�r�3�4���;�� -Q��=�B!�W}=(`�B�='���(9�^6 -Q��+�6 -13c�^� +��ZO�(�[n�����hg�erq�9 -1w�U<�j*��\?(��a���B�a��(^g�X��ԉ���C{��O�dy�3ǥ����o��r��8���O&��f�:��_<Ĭ�U�����gΪ�\P�-1�vLv�����P�5�C?�y1G�T�C9��[*b��Ң"�'�4>TD�Ӧ"J���yS�T^�����/����2�wSQ^�N*�d�䋊��K�j�3��y��O��|_(ͭ�"n�"n9���;��@�q>���I���d�Z�� -�������r<��XTj�r�S)�v^�"n�"n٨�E�q�NET��w+�*���TD)��l�T�l��CE�l�JPQƬ���C��m*bV[h�!TD��v*�V.*�Qߐ�Ϣ���͇�h��9����Jq*�V*��LM�߱%oءV�^h��TD>��iׅ�e�����7�^��"J��uiCQ�c ��)��}�"flO5m(��߭�~��zv�P�"f�,CE\� E�r�$fe�sS֡j}�d(�"�z���Dž�TM��*N-��@��E��Mi�"�ͬ�"�"�{� E��N�"�H�P��f� �1�n��}aV��,1�"n��"�cU�M�o*bf ~ߛ�H۞9��R���MEDa�uQ�sx��� -��"�6�n*����eS�l�ʱ���rPը���֟�:T����?J�Qu��)���"��`��(�"� D�����"�M���@ĥ�@�-1+�Oە5@��B�?#5����e6y��U�q)7q�D�J^s1G��C?���N ���V� ��`p6���J��#�?<D������C�#g��s���n����0��^I��Cd�+A�����!n��!n�y�f>���Z�i~=���Ȉ�K1R��'��6���w�|����x��FZ뇇X��y��P���C�b��C,�K�6:�h3;8Qo������!x�TZ��hg')n"o�s���Y��C������!�C�˦!����[ �����H�&�w��$TG*A�j5g���l�W� -IP�̓��uX����n8CȜ��l7YM�D��3R(�*���o""=���=-"�>�3��!"1%m "��:�/ "s�*�^@D ��-""�i����Xh|��!"�>��l����@D�F� "�X�3�H��i�����:�����R�����d��D$�0`�@D��+{�=���H)'��C$��;Q����<><DU���/"����R_���_z�i?2����<�C�Jg��!�<s���E���(���!�2|��8D<چ�8D�ڞ-�/�d����y���<��!�i�m\<D��Xf.��3���<Ă5����X�c���σ��[S�4ĿW罼��4D�<�U�w�nFC,o4���h1n�ZL�(���!J��#W��B��l"�Hv����R�c��i���!���fcևl�#F��&�ڍCԺO�i��o�በ2S�N����!r���iآC�˔�Qr�f���%v�X3�p�鿕�:��x�c(e\���Cԟ`{à��������hg�; �Q2���!���<D)֙�x���z�����7x���������x�]��߭|.� -�5���!jwL�y�3���7Q����[?<D����t�!�H�y�8D��<���ܨ��(��9><DYg~�M�<D;(��y��~Pj ��/�1cY����[K-}x�j#��Y@Č���"�LQ!^@���YU"R����}x��n��ĎE����l��wU���_DDj&s�MD�B pQrk$�����5�,%����a���zr��X�LD�l���H�Cm�����-�.LD*��|1�Ql'ڃ���HD�-��Ц�:�@l����I>���B"���� ���J��ÉD�j�r$��L�L$bp�Oc#�i��1×x�F"f���O$��LN3o$"խ�n"�~i#��""�7��NDD��DD)��v33m��]LD{�������aN&�������(;![�`"��x��7��A�V��D̰���L�,bc1&3������s#��~` 3ނj�x����L.$b�;�]��a��@"�䝋������� 3�7������)�뭗e����i��:9K_e���5�HDɍg�D�L^w7�с(� ."���1�|[���_Kf!�ˋ�;�""�����Qw5�ʋ��g������D�A��k��({����\LD5nl[Q*r,{E�뗊�E��>g�T�lC����"���a���E���~QϠy�z_XD5?���\D�e#�@)q�Y��5u�t4�Ɉ,�����"�K[(Ȉ*�֪��EFdh)״Ȉ*����Ɉ�E�R懌�G��,2�ַ^}� -�*�f��MF�����d=�9}��7 -n������4Ȉ��i[u ���u����$q�]xĬ�ذ\ʠv���Y�iKQ?�0�t����Q�c�ڰ�x����o���H�u&�e�>���f����7Q7v+���6�GT+<?�}�x*1t�G�C��xDm��#gDžG�_�S+����Q��u�N>���R� ����b�As$g�7 �G�Q�d5\zR��t%?c[�>��>rZ -@����.~�0V 1q�{̱"#0sl�f߀Ĥ� HL���م��OsX��$�~�n 1�OS_{(%�,��z�7 ��a��d�) Q -���I�wU{ ��s>�)6>��<_]�������X��G��=��#j�k�2�����U�6Q�|��j)�6���J������s�7����#��f����$㩸������ -r8b&1؏�ܰ�g2��G��cQ��>8Q��� ��#/:�nZ�;�FdMs�m��ꇎ�Ώ ˠ#j�o6����I�? -�}Uxi�1�q!s�M�.���1�;�f#���eN��8Xfr㵟�و��R��H�M��>��Hز��Kk�����-(�6Y�u*���pDm~Ǎ�#"�C���R�AG��{c� G�$z�*ui����lD V��؈����7Q�ԼϦ�����وYK�(g#�)fK����+̮�lD���Y7Qֆ�z�7Q�F��7Q]����F�>����ȹ>�6b�l!r�Z��^J�yh�|����G,�Rw<���2�y�qH�$xD�Tj�/<b�~���(������u�) �.:�TvG䥤�pD��d��Xh��fpD�Gf���sKWg��#fr�ڻ�8��^p�L+B�π#��8 8ba<���l��G�(՞G�`gS��#��ul8�j�р#/ �t�����G��?�Z~G�����=�CG�lD�F#��j����?�6)m��шR�:$7�X�Y�h���ǚ�������X�YhD�Gc��ш�w4"#�X�����pB��FK�D#jY`�l��Y�1Ј2ڭ�B#rH"B Ј9&��X�އ��Z4�\ƷO�Y���^ʇ�������R~[��ɈRJ/�Fd�f�w��=?ߍFD!��b#j���7QCO�#�b�q��c���xݪ��P�#��8�Z���TڸAG�p������ɜ���v���tDm�f�s��������o�tDC?���P[J:"s|)���8����#��h]tD�J��鈎ɛ��C�_����tD&3Fʛ�����EG�h��k� �,���t��c����֠#��#-:b��?뇎��Bv�{�Y7�.<w���rX.ml<"�!=��?��ѻ��L`�<��G��l<����_xD�4��:K�#2�Gz�G�q���÷Y ���G$9jvG(.��߁�9��@���3��G��OfD��8,���;s:�Z��$�/%;��e6r�s� �j ������Y�D�,��;!QO����hr�sU\�p����~����D82���Xj�3���$֕sQ����$�6��DJz1�9������I����0�I��4�����ka��8�2�bX�D� �)�1�2o����L��[>�}b����IT� �8ZҶ -���ݘD��m�60��<G���I�a���$�fɲ�0��Ym,0���c�9)�j��W]�DJZ'��w���/��O�$R.O$�$_��y��:$�_���$&��; J"�p�8$Q3`�����6_�(����(Uۇ�$�5�,;��xX�}�m-Rm�fD@���҆$�˴�-zR%��H��m���X��wS�n�6������EIԗ�M���Hͮ<6�k���/5ۊz�o#��0�8v^��٤��{qb�q&�t�)�`�~�V���ٰ�qvP"L��g�Y�F�nL"!z�|�9�t�өl���c�B�f����2E�ҟ��%��+~���pU&���\m�}��J�S�D��m{d!��]ƍI��;|�]�#� 8�R�_�D}!lh80�V��V�)���-&Qم�2P"�pZ��$`<dΡ�&�P�weɁI4�E�nEv6�5�NL"�t�ѝ��N���EID�cC�:$�Ā�}����f$m��D��5�}C���˱ �z7�h/��eS����G�aKfs°G`V�|0���u.L��E7��_������$R�#��$f�Z����U���0�ܤ�Ѽ0��ki���1�<T���(��i`)��J�0���ш�0��9 -Lb�40��$ʗR��\�D��ѭ�&��Dro���;�~�1��9[B.��<E��+��)��(�wH"Ö�&���r�ٕ�&����z[l�Ŀ����z�6�s23 J�W��ȅz�#QJ�F�f$f�[��H�t�TT@��vCU���X���~��3[L.J"{�r���� -L�)�:�$3՞�Dd�݂�(�^�$Jс�$�q�(E@9I'wr��Ci� ����}�����I����`��Ȓo�VIy�Y������J"W���X����#8L1n_�q�G4�KGܪ����8�[������LX�遀#J���9r�`!|à�k�Y�I������X�&��M�mj/^ :�V.?�!;Q���zXl��~��МX�m��[�與�`L������_oc_xD=���#R&l�|�T�x� �l�z�z�(�y�OQ�� ��G�(=�����8}X���j���>�D��ڦ$J�2tPqb5}(�T��{&����$�5���$�h^ � P��͖\�Ŀ���λ9a/P�o���l�uNbN���$jȏ����(��O���Y�zޜD)��oN"r,�������{�O��:��h^��(Q^.|����i`�%��n��%ʡ�̚�,Pb���:�J�9d�%�t�Җ0wQ�p������V�Q��]u����d�� J��=�BJ�;S�/�Ԋ�r�M�u�erj%0~f����AJ�ZE��/Rm}x�R�L� p���xN_�c=H�2�X��"%�\�ua�>��DS�����$b���DY��RߜD�־9�ړY�Dp�rq��D�`����3+t?����}��B�SKڜD,`=0���K���&8�R��L�~�et&1.T��$J����$RH#�1��-ҋ -(�}ZZ�`e����$���x`�D�m90�X�����C�'N����4��B"��MI�� I\j0�j9���p��"Q���S[j�+D �r!M��4��̗�t'����z#e���I�unD"ѫ�c��/ѫ�8#��՚�f$2�Lg�b$Jn�$#Q���;#�kd4��٘�mC���=��Z+r���$j�#.h�x�X�����x�@���/H�~����I�LP�0%���CIDa���$�<�$�@u J����(��|g�������98%q+%�)ch:s u�j��{���&�1�ݧ�d+:8�2Rvn�(Q_���H�0x@���~�EJ�8��d�]�w� %n�^��Ĭ�8�`�J��x�'*�R��rT�I1r9(�P�Y}�-�˃=� Z}~8���ڦy]'�{O�D�*���hr �v����)A�(Q���&���R��I�(V.R"�z@)NJ���)Q���s�7(Q�J����3a�m��^�#\�D��� R����� %rN�v���LslV⥐f�#[�s��&T����JԈ�n�NJT��� |�u��7/H�R���C9I�&���Tɻ%AJ$���OR"�27(���Z�Ԩ�J�&��%J�1G%�n��q`�m��vz�T�'QJ��L�$r ��H -^��$���quq�>z�i7'Q2����(��̿8�R�`zq��;7'���$��V�����o�;�/]�ĂA��-��紅���X��Dr�9$�Ģ��~���L'q+'q��I�1�����z���T�Wr]��_C��D%n9X�*���� K,��`����6ʛ|?`�:�3�{�����D����@ȥ`����Xb��y�!��*��z�����ݭyK��K�r��W��`�:������%��e�ې�8<�%��n����ҵAh�Hy�Q�u�Z�K4�� K�b��O��FZl4��%J�P�%J�9.�ؽx�%���D��K䡏O�lX��S����o��%n�%n5`��� -bў����h7�g���z�,>�%\��C�r���Xg j�|������\� �����~ -~ըw��(y&g#�/�,|�y�V�������;$W� -Z�ky]�D=;�h���uZ�.Z�Ɏ�ld�!����|(��6�]�)ݙQ��%2�F��EK$U����%j�b�AKԟ�J6��%�B3�8-�r+�AA�5��ɅKdQ��}��-�G���m+'.qˁKğT�7z1��N��4��cq+f�V�a�AKD��˓���k�D~XI��7 -���p�8Q���%����3�c������%2n�Y�EA$��R� 7[ۏ��./.�6Z7/���Kd��`��%�?�P��K�h�K X���TV0�����X���"gtF�J�X���c�o!+�����#�t�i��6+1w�+/V�SXg+�aEL�+Q����ܬDƠ���5)gL�@%��gwB �j5A3]�DUb�ݭ�t���R'%n�B%n9X������`%j����Jd�,{�^�W(ll��J��B��W��7+�=��F%�[�Eu���i|���9��$js5}��!��� -J�^Uv�!�B������/D"W�y����|D(�P��O^��CG�������Mأ��q��#��S�����>�F��7������6˟dG���y�\�)Žq�F;U2@C6�`d&S�jԂ�*L-�̒%K�<�+^ -3�� �q����ˬ�R�MOn��{�/>�_�<�@(�dF��!bD�(ꬼ���L:-"�RDP<�[p"B��5)I��Ɉ���6!�pKC|�h{�̴��()��"�B��'Cܜj\\�ƽ�&"�"��7D�h� ��t�q�a�%q ��y�D�3/D;�#���AȪ�v?�Ex �p��@���`�=��܇�>�[����0��}�pǺ� wǫ̇L�o7��%���/�8\|o�!� -m]��^y&~C4_�ۓ�B�b��Ǎ�p5B�Jn��pr�jIjC��y0�Vd&bCƘǙ|�k�~��k�<�|H�鵇�j��b����c����iHt�}OJ�����W�5X6cF�����Ѱ�?��a#���F�f��3 )9�# +���`4l��n- !�@��!$��Föy�K�� ����d4�Z�#F�d7�$pF��*) ����0R:�5 )= !��TBC�/ó%�!$�В��knƃа��dsDUf���斌��S�.g��,�)�����d4��z$�!$�!���RB����e�g^{��|�626��[�>v�6�+0*Y�Q*���ϥ�����uS���7 *C���#����VC6,l{����,�ӓV�����Cd<���)Q��X֝���; -CP:6�6;���0��ΣW@�$ܝ�r�� )2�ǴQݦ�F\�B�-ķ�^\���q�JZH�� -��� A �K��[�B�LL]:��t��")B��W87�+��]#���� -% �B8�t���9u�Ve*�9��5���cV�JPh -S�,�0��B �z�`R��`�J@˹��� ����~LW�ty�5O�����c���b�Z�'l�Բ;(� �Nv�'���|@��A������ZIzBH�-'zBJ����F( -?ac&d�"Y�SA�0�+~�W���$ -v���;?a'�Ao�O�ڤ��k�������'���k��jg��Xǃ�����!9l���&^�'����e$=!�rC�zB$��|��X�%vB��hD�� Ț蒝�^����E!�/֣�VJ�b�IOH��ވ���u�N��^sY��B�������橅X��=�Ia'Li�"hp��$H���N���1��n�� K��(�p �-zB�h��A2��*=!�;�?cH�� SR� %=!�y��H��f����O��n=�y!�g�q��.)��)~�O�`Q�֨U獆P�;��Qم��t�TB��/֤��o�!͐(%��2�֨�W�!�rVQ�;i�MB��rqvBH� �k -�7��k8��c?��$�E3Ǜ�N�vf�nב�}��N�Mr� ��=�.��Lk\2M�>��NrB��I�������))�����A��l������P��,o]�Mc�1rB�Y� S䄸��wp6d��/� 1�qc�e[��Р��V� ��Լ���R��y���H�tq7�9�-�U6H�@��=� 횣WvB�@/ 2��XVgC�"X��d=����@���7��'�I��)z���pOvB<t�m��]������F�� -a�BNhҵ'9!��8rB��^�NN�ck���8� Z$rr�9a��=��}�d.}�서�I�¤D'�)�@'c8�����<�?���\�dlf��^m��S}��'LIU !=aCIж̤'l��]�=a�9M�'l�vJ�������'�a��8�`ceM"B�,�[�2�e�2�ħw�vB -�^��"e�6�` A0k�q�8� �ѓ��DLA�ᗼ�P{� -ZB�����I��N��)![�Ι�����#7�����H�|�H>Bd�7!���烍��u4NF+C\��CD��pw1�ȝ�%��`"�����d�sB��( a -�,�� !����v%!�� [�HZ�O2|9 aC���6�8�#I!a͝H8�P; f�@�B��5Hy��6D>W��9 a[�L$��Y�A� ���(!�O -���RR�� m,��a@� ��_,����� <�$$��v�<�p��DC5X�6��G{� �>-z��V�}�8I�B6(ip ¯c!�8J#@X��-G��1>�l�&�� �8��O� �{��;� ���E6ɺ�=�!!�E%���Pd��`E6H�$ ϝlb�"��2�$�����*��9/\<K%�����٠=cިᜳ�� 6����BDPW�$Rx��fE5A��V'd�u�8�`J -�`��l��m�~�� "b��l�d���=.�6/өd����AJ��L�A��`l�u֚�I6��4�.�SR�%� v�ʰ�s��(��l�`��������Ѡ��F�1��:y T!�e�+��N]�][4��s=W�A��E3H��ă��m�4��9�f��D3 ��J3���:�`CJpG�JRiS4� V�Ͻ`D��dKѝe�k=r)Z�a�� �}�� �1��u��=�JA0 ��i���n՞b�[������ �[`���$z?@�YP�B,�R�D &�x�ߝ�UZA$��z�U�;�ғTM�SRUP -�FTN�(��u��KB�9��I|� ��f�RpcL�� BõsK�@���f�-�!)��ǝMpo�t -� �����%��uٓJ -�� ΙCP�% A$`r��:�5Z��6�:y!@IᨉFP�J#����s�4����v�D���sA#�paW�#���e��A�f�=y�ӓI��#�� �b�#�m�Vfo�<nt��m.���� -�*XSRXS�,���:KP���x��"��Sc�`'a�j兆������'��s��^YQ��=Yy!B�b���=�F�!˾���i�O3�F�@][i)=Y!�l�5Y[ǝ�0Y9��9�E���E�����A,�k\��l�ܓF�_�A� /�%� �Й��`�l��"���j��,���0�)AFv�,�=��E��N�C�Z�k��|�R�8$�`�xkI" �ٕD�����I"h�|s�D��tF������@�v�/��"ȦpR� !���D=�;�A"ؙm��DM�,�� �j%+��b�I�TY!��� $�b�E���<,��ar�"Hɺ��"H � *� ��E���7�"�1�aZY�#W�hIh���F���J#�VTnz����A#��«<���u�����<�P�� V��M���D���;�Gb�v�#��E�$AHhvTA�i��G��Z�#H��4�̡@�G��@��V�Rh�DQ�(�ƚ��%� T%�E�G�4� Rˎ֓H�UA�����-}��� aq�H���]"��x���#��J� $��G��١Ly.^�)AJ�"���Z���݇'��1Dޏ4T����X�Px[���G�1̓GG�*� O�c1V_��Fq�G���8Ơ����5h)8����/ �Yha�O4��3���uJ���%��� ��N0sGrduL۠Du���ARںA �\��d��>�0���8��>D?�E�;��A�1��b�����A�b�#y�'�E�+�D����"%�@�~9�3M��\���0�(1�����It�H�"1#�@:�8�-�2%�*[ ��4m�ؘ�'����u`z����f���3��'�@�_<�l��1����Hg���5!���S��8�#�!aOl���'Y`��X�Rr�#��N��d�x��c�\�4z���0�`�+�"v�+��IDW`C��y�7�@��jId����"D�<}��H��>�*c>yC�D��!ga���8cuo��Cş�,�8�W�9I h�G �@D²�)��}�x� "�@��,�� <#��������� �vVr@�^K7 �ȹ���ㄥ���P�e[��Ԇ?�i=�J���m�[���|5[\���Se�t���<������Z�w��Ǻ��륹�oo�V��*T2�1�Y9��IuƖ�dn�Y�W����]j�B�(���:Wh�Q,W���zw�u8'�@/$�&�a�s\B��ȧ�T����w@��b%�������Gd��`���g)�?��:3����x�,�w�?dR��?� 0��Z�V�����@Xp�5��F�/�R;���9kD�?��6VD�`��'��n0��C��2-�L��4O���!���Gr��L���Q���h�A�6�P�]-�?���u<���%�߱����!Vۆ�o%�X������~f��T���0��ص��7j#A�V�U����*�?����9��D�uX)������ �� ��_�x��a�c�����%,���Uv���D�+n?����I��c8��Ć�~H0�@��~�F`�����V)�~�=@~����n���o��q� w����2b1��� -TD��[V:�mx�D��A0�2��Y#3���k��L*���1�����7��F<~�bnl���%�Ia�B���I�X)�Z��CNX�}a�[=f"��k�߇<��TzI��ՠ;��l� ��0˸r�1�A�����-)�m�>��Eg�܇�K���Y`*���G��/8m+ew�>�S)�}X}�a��Ƹ��4���UjT�>��!��} �=�H�>��`]�����8d���^O,������c 1����!:�z���ky���T}8bx̉��ɼAA��3�y{0�3�D}0�-�^���k��Q6�uqQ_c�N�B�g�}$Q�I�:�L}-J�*U�ԣA�G fNT}�X�P�A<��H��Ƃ�sM�>HƊ:�BՇ�P���c��AQ���s��>�*�>V�Z�����>H�m�����ض��k�T}��jGO�>H<d�i,�.�~����S�!�d�٩�P��k፪��l�8����XU_#\�p���!�̈��rp������XT}����T}�8ww���>H �<}��2�Sy�PyK7O<}�O_c2y��<}�N,h�QƼl�qHx�T�>��Q��w��m�Z(D}�@æ����c瀈����,D}�oΠ���Z��>��X���j!�k��3���S�����v�v<����zw�������]:�Ņ���x�Iԇ�Z�1������� �m��oY� �k�⃥z'ꃔG���L�nI�g��A�G1����dC�CT}-�*U_c�9����|�Y��G��7+T},4]����ﱱ�֩�GY�����8��c��ጁ^D��V�����é���,�T}(��àP�ѩak{P��hU_#�۱=����x�Y6���u�����0څ���y#냠ߨ�ܙ��wrn-��,~�d�kA=R���g�Y�(a^�-CL}Lc2�_��,&���Ǯ��?u�>$��d+SA<�* �>G��b�C��{�2�!�H3RL}d�#i4�W���(��3���1��L}�9�Ee�c�<we0��5i�=����4� -S_�)S%�����?=��d�D��S��eO�>V�s���Fl;FW�-�S�8��oP�S>5����b�D0���ۏ�;SޅIN1�u�xǙL}�tc��>�Y�!�>4���D}��`�[%�c >Kڃ��AL����f��u���<�>���'Q��*D}���\z���m-�� ��<D}��� �c��T����x���)ubO��؎��c���%7�>|;�_��awXe���!=˼K!�3�r��8I�IJ3���L1�>/ۃ��V����~�g8MYv_h����],}6�g��XRH���&^p�q���}X�촪}���G�B3_��z}C�ǣ�u�vJG�[����s��Е��Q�M���Q|'郵� �/�>��Id}��KǺ�����.���k�D�Op�N�$�w��c��C߾zH+���8�A������ �V������Sz>�5tg�Kz>�5p���UܛN�G��l1o�·�ڑ�=�1�=o�{��xe�:��(!>h���5�7�d}7�ݖ5U$�z>H�~��|h�k�(EΎ(��Ac[�T����a�I��Aa����pԻ c������ ���P�9?t�=kf6b=+a���1���:���(6�$��d�g���f�|��a� >y����v��y�~>"�a���F� -~��&���;�HF@k'�#��P?_�ս��eͪ(��G�����G)�(��G �������~>� '#~��������cO�>��[�l�q��:A�w� �R}@�_f�=�5X�����G�3�� `ѧ��n�2+?��+t�0tg�dԝ��<�Q�>p0�QR�(��-�(���GR�m��wg裴�-)�HB�=(� Y��6���X4!�>�'s�"�#�g�0��"8��v6HA�5�v<�(�N�G�>H�5���U3���'�1�G\�BЇ�*�HD�ך'�D��ڰ��Qζ�5��( �}���&+?��?㵨�?;��](�|n�&��P���?*A�q��|�)1����c�q^���Œ��|5��(N�g�%�ۅ���������V�1���c�h��R�(�0�W���Ęu}�s���_>��Z�̝���l�E/�싢�Xj��bnVQ�1d+�[��+C�?�I���P��7�=�w�2)}�^�T~8���f�|x0��U�}�,� a�Q}����"�S��џm -A�G��5)����E# D:���,"�qA�_jRٝ��ڕh�Nч��A@� �I�4})�>b206�{TZ�~߉��D�Ņ�����jD}&O���؋&�al��>�]�]+T}3�T}��D?�� �/(T}�;�� -ι�v`fN������v�����s$U� - ����j��ة��P�È�#Ξ�;Q1?z�e���76������y%X��=<}��\�|��ᗬO��y38����<}���L<}�Ih���!�Lÿ��1&}�3i��2����VB�e����Gi���A`� 郄��J�ǘ�ѓ����c���Xڃ��r�r�>�� �'�^��ď(6-}c��$���gQ�|�#�Uz>�Kذ�|����|��p�Wn>HCgÊ�ϰ#�̷��Xy�X�кh�����]���n{!���փ�8x�`J>�"Z��j�HB>v��c�u?����ۂ\;����B�b�������^G��m�ŌE�'Aa�tO>t�3�f�z{�\o|��й�ã�L���O������EF�ԃ���i��DH�⍠��7��μ'a0�H��� ރR!�{q�Y�9z����2����KI��Kq��G4�:�u�����p2N&)����{��"VE�܃�E����ْo���s{������A���22b�#��r>��A笠��_����L{�P���@�lsLw��Σ%i��!0o�ΝK��o�c���kK)ڌ_ �)K��bOҠ�C�r%�������`�ô�4_�ּ�C{)){-��Ű�B��E���Y�>*�n3���P��(��]oZ�)���M�{R�Mo���z��ٮ�D_�$6�i���}Z=��I��n�}�������z���H -��Z;�$(|z)%��!��`�C��k�L�p���ێ�)t&=��U��(=F��A�\@=~\�=B -�=I���B/�.z:}ChK�e�$9��2O� -w���3���+?-�y �X���#��r$:�x�B�Ga�Q���k�楤l�i^Co sΙg���y�����J�u�k^[�"��y�%X6�����1�K�����i��5�R�l�5��k���:���.(�Cr�V�a�ĚG�H�<��ڑǴ��o�̃�2:��̃��t��N�<���i<�ΟiQ���N���`�4�I�� ͣ�6�ʙG,HT6�3�H�(��-��ř�+�9��ξ���Y��_v������&/X� 1w/X�n�;k����ֽ��I�`�#J�� -�Y�X�d�#�&Ѭ�Y��j �ݝ(|�6b��s�<�|g�3�~e ��7bVhm��9:m/aii��#� ��#���FE��Z ��J��V�!���h�Z4�V�<�qF�6�?%Z�����z3�pv1P�y�K4�h����B��m^� �F0���>�6�!��:'�h��m^J -m^��6��2b�#pDz��r��R�a��_�y)�s�I�y�e�g�HW�<J[����A�����.R*�`ˣ����ˣ���A��؞�<�(�c���sO -���[����./%�./�A�g��ĖO���塞ʪ_C�P-k��-!os[�FMly-PH� �����B��̕�2��נڬ�Ζ��o8[${���O��<�M�8Yl� -�<�r�n�$�Ì0/��0���"�Kɝ,/�A��B��=^�?;�����Lxи���x���o�B�wGH�dP�A��8�8�N��Ȝ�.D��� ��/Ms%˃��q��G��w����/+Y�=��X����EA��e� -Y^� -�<v�39�dy)(dy&w�^F+��H���>�;Y^#��q�!X��[ʯ�|�����A���)�,�L�B*Y.��'�c��f�����Q��� ���t�x&Kn�;W^��+�UB(�W�z:�]/�� �"˃��c�,��I���Q<o\y���Fr�D�r�!�<��+��UG�ɕw�ܹ�R\y��:��%�U���a�]b��Y!�<�-м�\y�h�p���:�)�F)�A����� �k�(i7�\�Db5D���Yִ�w°���Lrܚ6�W[�'���1#�bɖ��[^���,�:�� -WP�`5�*͑�S�&U�~��LyGY`JI�S��c��e��u�<�w!ǣ��%� �o8[F%�CWw,8j��V��-�/�?�����^dy�_|��r[<�����?��W���_����_��W�����>��쯿��.����o�͗����ۏ�������?����~�o�>��ډ{����������������~�?�������|���[���ݗ�}�������������A0,�������~�����믟������o��.������k��o~���������_�������?��n�qN+�Y��� -�}���ٲ���߿��-%l��_\��?\����7�����/o~�O��_���O�X���v"H��7���[|�=����~�?^�&�_��T>�&�C 8+* ���c1�� fr��d����8 -��-@b����� ��CD*<E��3�*�7��p�kw�����3D�QK�2����pu�a,����>F���K�>�1guYk73v#d8[@�ÎyvA�^GK�CĄ�z��)�6v�ϝ�X�bn��ܑ9�I�.�y�Qـ<�`�r�8=n���mF��\�b6�r�]جE�3��f$%yt*���[(V���QE|������=+;��m� q"�5�d:���u�I �@� +t���a�zx��VP���͆�O�1�/�g�Z��f���8$��b^&�]�o28cw�w�&�����P�x8���Lf���&�U���$�9��<HT��K�}����ﵴ̶ۙ�;h�X���U�����������A��w�u�Vc�Ÿ�&�F���;-�5�g7�=rƠ�� }>[�O�$o�>�ʨYH��rgГQ� ���pGP���n0S��dxg9Vv,���316�űU���I�X�o�H��uW�A��"�_{x�������`�����~�P�_������7o~��_��w��p����- �.CjR�m";4�txz�%� tb�w�������o;����-\�qf���7o�1_���$���f�����������/ ��X��k�hF������=j}yU(uFh��ݷ���� ��v,��BXF�+ �z�/��+\�؝i�a�R_�I���⋏�i?�����������sM�[(�:شD�ٯ/����o~������T�������::���~����� ��_���^�e������:����z����Cr���ᡅ���&���|�2^��u�5dk���ܝK�o�='�\2���岟�P#�t��lZ��3.��#z�tP%Ȩ1��6k���ߓ�4N-��i@P�nhN�r!@���b+�;�=��t�8��츽���қ,;�|>[!-�&#T�95�$�4��V�=��Ƞ���0Tb�F���Ɯ�i4�Ir2��`l��I'�i⬜�jZ��D�,b�V��[������ۣ��zۉ)���iZ��V2)F�q"~��"W^���A�2�ϸ����'�;������j�s������϶ߡ#��hY���<k'�1�̇���I��ř]�5&h�q?��%�Ml0���>w:f�p���߅����jc������Vկ1a���==�,�X��^�RT�")z�3�d�@찤�\�G��t I� 2=����:�'��1D�P,�����<�h$7�<�7��! "�Y�1\��,�J����2T���g��EM��m��bo\j5?��{$Pظqg2���{��n1ޠ��N��X�uN�,�ѣ4Y�I��-�*�&G:� ���qܝI$��n S��%`Dɋ�Q��{;,���#:}߯Nʋ�m8<�:ԟ�AǕ=e�&����I�瓃�[�=�u*��O� k���ק�i�W6�BT+���|�v:�pY0�����s���~�zD�yF�(��(��(Ha�gĀ?n�/6��!$4�!9Ao�L�Ow�G͇�b����C�c��� ����]���C�Qpc��٬�D��G�DR{��%�����_�1�����ğ�)���l��ٸlo�����&�K��;���0�r�6�q��o���QaS���T)�ğP�g�J6���K��K�eAl�K�����u�o�兦��E�a�3�kհ�Q�n���k~coĶjclP.�+�s9sVtSv_f"c���(;��Y��7>�M���f�/���E}���EJ���ԥ�p߽�W妭ȳ������ohh=����WݸY����}/Ӱ�4Z+Eo��C_oa~�F�n/��}(�4�O����?ΐ?�݇U��Q���o�t���Y�,�'���`���E��O>�f�rC�,��q�n0v��'��@�4�m8d������[9���g���A���ni�0��Tb��łoe�w1l�(V��2�E�,,8O�#��Զ;�oX\��p�� �6�`)vދ�w��� �N1��<�'7"yL�\T��w���D����l��[�0K23xs$�e�����4�� �w7�}ݼ�6'�ж����Xٹz7�Y�W��C�5_��Z,G�(X4�_�s��ŗ���ᘘ�3���_H�\77A�e;����� w�u�K�[��]~�)�Q?�(�?�e�Ʊ��Z9z���R�A|^�}�md<���Yn�!�.�?q�U���;�˒��<wx�����C��Ipl�VX���,k�r�c��Ы=܃��F$:�G�2�̳9�7/���G'�z�ljt.W�Z�,$X��;B睎�C[v���@k�͞���:n�*���@kC��}}9��#����qH[~ig�+�bV�Á_��"� ]�-�w]��(��1�9ל*v-�&`�R�I�0�tdL�� �c�>�B�᧴�!�Qek宬\քq��o�q��a=��i��n��W�qGpC�ً�B������_r��v%��È�X�u�u�f�4B#�#u�\�j!Ս�C�>��%�Q�f�a�2x�]Ĥ�� fs��"�+��=�����P��(��C��`���W/��ݕz~JH�#l��q>��� "? ��3C �(.� ����qpq��q�ˌx�q�-�ҽ�̅�V¼�W�8%4����N�㙸s0Mu�7"��!��^��q��"��f@��| -��q�9����>II�1�'�g4D�w� -0-a�:�p)��\m'R7!q#+�y:@�Z�ʥ�-K�m�(�G&x�{�>ldc�OÎ�LM$Ox"�$Ev��%�B8 �Bncc��aQʄ��Z/�� ��Cay���Q��as�x��fJ��|V�k��#Јk��t��e��cA�5����7'჻(�-�A/������xk�W�ODx+�P��E�ۼ���XS�� o �[���С@������ \{��6nx�6h����T�/� Fb��gE! ��pmy� Ֆ��sk��#5Gx�D:��p�YG��_[��]ὓQ*����G34k��� ��� o�X�FN����<�$��j�B�P���-�!��"ڑ?Ճ&�0c-q�����X�q�����<��Q�E�4hZ����Ni�êөXW�{�ô66{."��bS�M�Qq��p�� -W�O���u�(|�M��6�"�u�*b���AM���,Z!c��>2�*F�֢�2&*Ka[�5�w��SX�2šJN�WQg�f�]y+|]U���:"����:Uޕ�z�> -����=���:�"��O��z4*)�3Ty���/�Yy��JU�tOI1R�B$GdR(�Rm�Zd�(�!��้�0R�G���D��R>)/���,��M��Xa�)�%Qy�jK*_&�S)5�J���W%�d�*�'[X��j3+'(�ZYC��^,��Ґ2镨�_���(�N�2��3"�Z��Y�D&V����R^7|%e~�N)C|���G�s�T�8�����Ե\Be��kT�z�y]��ʧ�UU�������~8ș�/��Ĺ3��.7߃GR��P�6��0<�;%G3�����vB�ߣ�m��<̣�Ĝњ� -��Ջ#�1?��0UyF�\DIv����@��N����-yY�e�V��gF��}}���WD����\���S\�R�G8K�/�V �iV#~�!��r>�u��^�����>Tjs��״O-�Yj9���/�ɪi��B��t<sof|9�[�������$��A�{1v1��k]�y�S��PK��W>1$�~��t�^gP#G��3?���Z%�5S�I FzW�����UғBT�̿I�*F��˦�wn7�ğ�^��͡�q+���ә%�y�ڻ|HC����f��|�y�|��إ�C j��ycZ���{nR�6�g�nF�� -Dt� p�+4*�w�V%ᠫ\��B����WӅz�P/��Y|���m{���U��� -b�sd�%j'��=�2߶y������U]a�ِ*��6��vk`}�s����F����6�}]��'kT�7��wd������j� ~�5��a����HocKY�eE -��.����Ś)��2#M�e8��K�(��ؒ�������xt�����]�����o�̡�m�/xD�.X��["���+��A�.q;=�͋�i<A�L�B�c��g�g�ΐ����3���(��.q��x>���p5���pƺ4܋�ut�Koh�#ڥb�X^�p|��mw�7M���Z z����Mb�1V�s,��rc]ђ���`���N�l9�ۇ��0�?1�V�W����ȸ�3�G��#mx ��:��O��E���e��Q�*n��ńb�P�7&�t��\K��p�!`���=��o��/��Q��h���Խ|L^��?�ۤ>N�)^K{.�<�f����1��c�R�1M����%&(�P����b�S�Ų��(�'ud��W5�Զ�?��?�����1�q�m�U��vG��E�sSZC�?�f�?ˉ�q"��c���4Z��i�%���bd��@G�;�;0�ѤIn���ۂI�Q����[*��'#u �A�$$zJ�\ -�N�{� Y�.c����IZ��tB8\G������A]ˇ�nP�:�QPz�-�h%�.��8�H��ު����M��Oe*�-�jR�I�נZ�/$�-�rH����&w#�u7G��u���[`Q�̲��)}���ۺ;#�˛�k߷��A]<����R���Z.��-H���}����>@d��t?6�ߵ�D\����`������[B꺅��+�������۽�/��7�-r�?Vc�w��_Ng���';�L���^Bbͨ��̲;r>�b�$�Ꞝ�;G�@����2^,��Ф���հK ��،08A�{*)��h�&~#�13p#ZA�o�KUUe�L����X�$�<��p+���,D&��9y�ߩ{5�ß]܋�wC������>����D�x�5o�q��5F���x-�~�������\6KP�'�X�A��U�e#�N�=F�|d�r� �n4܀*���������&5\h@`ܱ�\C�����m��˱�$�l�dL5&jٍQ/'5�v+|���cu��b=��-_/Vd~B����Z�m ��K��X1�����ܡ����>��æ��\˗�1p��Bw=���[anC\���kp�#�]��h������ș��7N*��l��.����.t�H!qx}3�� -��W!����f� -B[�H\��h s���@[�x���h��������p99����p�~�V�[d����d�B[�O����:z��mT\\�B[ ������H��>ў�x���Z5D[��0���` -6�)��������P�c&�������@�h{�l�ae�X�[�l -�-��h�#��ti��µο���p����m���g��`��W$���>�� �J�`]I� opZ��q�X�F�q�A"!����ҍ�1���Bp$����if �Yp�:A`ۚ8H/ߐ@H؛�X������X�Yd/$$� �T��zsN{!!uf���|Ŋ�D@!�DBBb� �g��^�zGB�`8o��HH�K� -��h�N�qS� � ��8HP�6F��u2��:aM$�/7�F�ĵ%Ρ�+^Q�A�$��7��)���9+��t�kP�E`~�%^ z��6F�����H����k�D��JEx�B�(Z�Ò��%Z� e����w;GFPze<�%Z��\S����L"Z��X�t�h+��ӈp�V��?��E�X����0b�i��%c[-тF��e�hj��ݠh����h����F��n��>�VFD�,�E �o�g uk�ۜ�-D���%\�]�P����?r��/*:�g -|��1SsܺG��in�ʹ3�e��D5�`�j��Uh���֊#�̐�� -��F���ˢ�'�՛{�-v�+J?.~�H+��R��^���J�/y�[���x -z1�.�$�5���_ѓ%B��hZ��1�-��TS�H��7'_�&i�7�K/5)�`�X�5�-���&:}���2�~E�`Ɩ�p�%�~��'NfY�Լ��?�vs;S��hK�����F¥K1F�{��^��q{�#�]��GG4��t��P� h�"�hЇ�H��h�ǭ�eQ:��^6�qR�Qk��AH��l(lgQ�=���@pX�r��E�X[�����I -x��-��`�p�8� �TX����o�+ �-����=�+��3��;��w�|;F�_��#a܌��/�Ɖg��os�r -�hK�� -����#·����%�69���� -�B���y�o�����N� ��m������d�o#Xj<����{X�� �,�I\�� -f�-���� -��ðk -��)�� -� K$:���5��FFHf30�3 4w0�~ǪA(�ɝ��!ȑ�]&4 -6�4_�t�3�ķ���x���]���G�m���-P��?��$������j���A�!I���*�4� -^�1�"]��f���g}JH�2c3F�x[�%b��o�ŧO��%�u*f,ĕ#��*.�fD�!�y�N~Rg����I�?_�Q��c��X֒�%�~��1��O�3��Je��\���o\V��ڈ��@T�ͅ�{�= �����؊��e�����I*&���@.�ήUZ�JzV�ՎcCi���g_��0k6$c۬�ChmP�`�(�n��;H)����J���<�F�깅�!�rV���`:2r� -Z�/�E��$4��,Q�G��?Χ���`��N�� -s�[�X= �T��-�t�����!� ��ǜ��k�h�Փ�� N�ᠲz��!��LźެtÁ*�1}���.C��0pH�����a4�aW�0K2����Zi͉�d�aP�(��C������m�Q�� >8�K��t#�0��0TO�tC�$��){7�0�Q����͇7Z�zwvg�M�L�9C^1���Ja�Νx����ak3 -d�i��p=����\j�_N��>'H밎�� -��e�Ä���5N�Q]}{��5 .{�:gzo��c<0j����9� �%3K��PF-]P��&0��v��&�����Fd�Q����y���y��j����VUU5��9#(D%�쾂����>6ڢPE���ܞ�=���B���!�N����X�ŗGW��b�0&�O���|2j��Ϙ�5����S_Z$�D�J�k(�-}�=�2}=�{3�<}U�>P]����5���7Pm<�]���ja��Z�\m7g7��v�˺Jx@��P�mh�Dr����������Ƣ#8t]m��š0Ձ����*��UK��g5=���ѡ��D��@��yh��u��o[Ǐz�uL�&pg� �:Kӹ�Ou��U�����#�v���I� �7�w�q��Q��e���_v�RR�R�a) �@U�/��%��fCp�b&��'l�0�� k�� ɨ8ٞLH6j�C� +��s+�D�¢0S9�*ܠ~�Z�ٝ�^��n�?04̾O������NB�p?B��g�����"���D��oD�O&��$�p�r�VCq�L�)� TE�]E_��'����E~d| wS�0rH#ǵ��ȿM��X#G���ȡ��|�ȑk^�t��q']�@�II��Iq��� -�$��*h��y����$����"d�����F�T�Iir�"��_�|4�Ɋ��cV#Ȕ�q�\"�z,��ki�E�+e��7iZ抵i'(&W7�bw�X -�i�E�G#X��9��3�X�B'C{d�2���E )�J�R�3EU��S�U�Q�П��V5�p�Ա"®�7�Z]�e)���tP���)���&��<H��<�"��N��z*"�SA�<T#��8}# �Ǵ� � A=�C*� �i<D��ae��s$� ��&�aE�%��H�<�H�䅑�I�LI�j�E�H�^��� �4��r�tU����J3T��b�z�LV�Rhi�z���J�ɐV�N�v���I��lwe úW2�:J:������ �/�ס4��eB��(cZ%V��̫&%h�n�Ҹ��{��p��9����r���AW�1��2���?���~����g3�.ofڋk��w'm��| J�fS s4�7��-��`�;#BP=��w���X��;9i#N��|t%@��_A�2� ���;���e��Ө>�8� w����"�� �W�o��n�>ԂA9+�!+1�ۈ=i�#8�y*!,�g�r�#�Z��G~���D� ���B啓KtF8p -�,��X �J��p��&�q^8��E�G����~�:�{�H>������B}A ��|���I��1+rPJ�'�)CJ^��z�K�n$��'Ł���f��x@y��f?| ����y��G���8>eq QG��=�;lh����mހ����x��o�`h�0�@���R�tU���}�\�{ ��[�T�J��u�5�'j�تw� I�'9�H���l��}|�ޡ{$t���0o"��)�S�����q�5ŷ/ -x�t �*Qs�]��ݾ��m��=�|�Ѝ.Z�����<�c���m����H�gآ�Mkj���@��<H6�.y��`j�☿�HK����'�YŃ�8K��-��>��b��'Wq� ��X���q��Y3~��SQ����^�h����7+��F������7�O��0~����ߌ#h�:\�@T�I�^��9�ף,�����+����}� f���}���qW^/ӝ�%Y�^��&�?pc�C��#H41f�U�C[@m�U�&�)r4�IGx;�6�'Q���j�-�@�½ci}�����J}���^̷�^=vg�B�b��v�FK*��t��_�E3z�Τ���\:M�B�Q��ڋ"}���S�f��k�]��)�3RЏjbV�l�}�ׇy1��3D�L�'0#�o��\����q�B��^��`S�Ww@����0�/O�d�\��4p�1�{� �P}�Ƒ���-�-o�װ���CW?��ݚ3���$%D�HDh�g��`���n�5� qρE��{��������z~�P�����*���;TD>�`J��D��T�w$�����!k� -��-�9 ��`9��#g>/�Ց7�e�x�/6��֣�@�~j�o ����M{���6�&"�]L�s{��g�H�mZl�'f�� N��`���4�!<���tm�C��TS����H7�'6Ѯ�opbsu>c��]����C��e����+<1@7$ ���6�4��`YpGxb ���H<1%U7�Cc)!�'�5�'�j�,@1�FF��b(�za!����8n�b��d���<l�DD14˒� h��E~����}�kG9��NDp��Y��`�y;vd��whbO0��<�Zy�Z�_� �� @�:�O� ��[��]��u���_\��}�/�������W�����žY?��c���q�W�f�ː��7u�NF�|qsklW��S^�Y7������;��p�Y� -0��cE���n�=�\L���rH(�[r�����À96��(%ZQ�ĝ�����_(� �<��wT�n̥� dT'�4 H�UdtZ -�9ׄN�� b�l(9Fn��H<���y��?��t���G�L���QF��"���D�<���٠��u�K�b'00�s�FUM�!}v��C;�0��G�� �� 2%/85,e8������qP�(|�{��ۑ -��(Z��p'jRpu��%��E��0�C�3z(0,q\%|ۍ#X �=g4�5ɋ����?ݏ�9�^0���A�I,B�e�0�*��u[e՝�K��"#Q^p᧸UP��O���e�a���V0�.-i�� �&�����PuM��v$�� -��SȾ���[G"�Z��@�Q+��[P�9�Ժ���Z�C�.�6���Oy_�W�V7��պ�L�Y+ -CP�k�*-|U;8�N��"����ZQj04��ߵ�:�l>���y/{����т�:k��\��,��n��r�Î��e�]�m����0 �U�gx�m�WУ�(0�g}ں5g ���QNg��a�-(�c�ڽ&N�n�����B�[Q)�.�����6�S��58Sk�n��[�Nf���V|î#��V��Ih���0�">�$sO����g4\��ʺGX��� SK 6�\�2�qତ� -���-���̄8���:�1"�� -�Y����?=/M?ƈ���E��B�t}�� ��$��l���X�����̄btD����*�\�(��_:7dg% 8��D�C@[�6 ��=��H��� &1W��NG������ ��ɵ��2Q�c�5A�j*Cܛ��<1�A�ɏZ��b�d��qU2g�����8�tR�>�pFD1���5�Hy�]o��8Y$^�Ki��T 91bJ]QO��`S]�j�."ѮB;+�e9��C��q�9^�E����{.��m\����cKJ��5k��s�B��"L~���.%�1�$�Ǜ$+��Y.���xv�� .^�L�����X� ����2ՠ�v8FM��et��8f!��c�D�\�S�ɚw1,km�������Y�M�ι"���.]Ek��LZ�@��e���Z�* �}����dĎ���ٱݓ]�腤��Tݡd�һh��������D$^u�ǥ,��\�4�ˋ�ɹt��K��-�*{Ѫ�T�NQ����u*wE�|י�G{����p��@1�׃R��:QE[�SW���x����s��`�QY�I��b�8l�$�ʾ e -M�b��]p$�����n�H6����/w쉻��G��=/Dq= -�e�!�F��j�m��k�=o�f0� X�bY��̹T�2��t䠼�=��`}��^����0�7$[Q�[m� H�\�al�¥a� ��S�XCa����kk���Za�gaCpl�*6��o�*�c ������v|g82���;����(Tq��y��� ��Zz#�+S��������!��l4^��A�K}><?D!��k'Mw"��T�ͼМR��ϑn+����������LG�q -N{8���p�����ЮX}{ ��-8���R��{�{!A�g`�����р3� -�1@'��;�w�[���|ԇ�t�v;6w�h#4��������I�M6jB��/��1��4��'��|��f�p��@P �qj���B�4�����Έ�Pz�qO�}}�sqcB��ݳ{�+���Z@Q�!A -�/����5�c�JO�0���3!�k�e��;`�F4�&�2�X����mS -�k9!���%7�c�x�Z�3���cD�V�+����9o�a�gd�և�v�DZ:�ր���=8�Y��-�ǭ:Й��4v��[f����0P�ٱ}C�����W\���D'V褁4Z��A�����~�v$8�;CB|~�倊m�M���^�*�'���W��M(g�䲂FJP$��6�=���-'�h0��ȣc����G�1���l:~ -��pUW�j����U�kX��i�}����tqH�5n�q�6d� �w��s�!G�=5�p~��q�v{������C^����M:)��,6Ȁ�H*��fRf� i��ϰ����fj��J�p[����"!�����D��,<,�s��<������\�d#�_ؕr�^���@6��Ia����� cv Y�dn��;��A�7��A��k陶;z 0@�!l�k/����6`S�F6�Rnr��c~�#0&���Z�G��[$�ْ� ��K�Q2��w���Si���s�o+z|������'� j��亂5&6刕��@��*#|�1���ù�G�� -p e -���1�DZ�&q���2V���u��� -ϔ���.,�%7���o��3j���[�p�y&��Vx�6�+:兏�������u�uO�~�������(�,c�e�Lh�g���a�zP`b@'ߨ7�C7����Y�cJ�f9�ћ(�2�ʼnQ.����O�X�.T<G7KIyj��zE�O�pS�RƤ4 -ZňEl��G�4�!�)�V�R����i](|�\@��JS,P�QAúl]��VR{��u�(��-��g�:�F�{STmaEY�����@q[)�v�\�ZH�b�+E���z��O1j)�cK�*�}��K)+r.ŭ{U����$�p���u��+�?J���=�3 -:�"�G�r�hTCg�:g���2':��]�鞒b�8�H�ȤP����HQ��b���n){%�Ii�jd)�F�L[&ig)8����HT2�Z�������`��� V�Fٺ�F�Vڲ���o��VTFx&K������z%^e�+C[]�r�K(��F$��W��;/�]�w�$t���������\*������4e�ÏS���{���1T�^ΣR�����U��VUT�V�r��D!��e(��79�| �K�C�h/~��@I���y?��ߙ!�*`wj�`�B���d�@�>�B8O��o]��."�p�������{z�d�y �,�)�zZSu ���ɛE'�����"*���#K��{#Δc�����Q���fB0MY �ijP�鏠�k���E�! &��P�5e5C0���ߡA�Q`�pv��qv�N���}`���@ވ��]�DŽ�FmFY�y�3����-�������҅����H>�����r`$�����!�N�u�P]��ޒL^b���s�ż�����(�����d_1Т��`�R�� �Hx�ya������?��OZ!��<����@ E�� �� ,�>E�hcZ�l�m�fE`��N�T�Q�����~�e�i�F���w��/�廻�{��������Z1�+��J��~�,�9�YS7��K���]��6o�����n�>�H)\.��ysS�h����]������Yy|��o�!��@W+�BL�{g2��[Y'����� ]��Bsa�"��E�"i��f1s5�_z�.j߀�C~��0���9�u]m���F��E%�X"l�otON�O�82��b$�$/��+������N��fM���]_���c���z,�S0�A4&L%��EiՅ��~/�}<}O�Nū���%�zy����_ 1�5���ý�SGv#��2s -�#�+b���"�]�O�c�4�=`�^���[�%��k#b�����zb=n�Yn.�gd^.��������fw�����A���f�4}g�r@���Y�� ����%Jmpp�U���D�c�y1��b�_��3���-��X�8�8ڝ����j�?Z�+�;�o�e��eS�U��ۅ�n7��R�=���.^?�g����1��c�R'��M��� -&&*5Q����|�S�����(�(�d�������cZ�GE+�� ������:�q��8��3\�g��<6(8�%������\�B��e��&���D�!�* - ���ia0�D�N��D�ʆ�q�%�k[�ҔeB!��&+6�����:2��3N�o,�ܭ�k ���4-j�n���ޱ�x��{��i^�d���8$�!���m���k �̎�@�D)�\|���_��m���� (��B�pP^�ٰ�p,{<����1�I8���[NJ+cG����#�7�*8$P,�>jD�����>�e���/���jꟈ�ۂ�8�)��o^W»\{�ֽ��t��s)~D�G���S�(Gۡ�V���� -���~��ڏhhkP��vG�c�Z#s�C?�C!���m���1�4�Fܝx�/���b�����e���>����{dG�w����n��1���J��U�=�U���-T�!&�sX��I�;؇������I�d�X��Ѭ7�t���Ԃ𬑕���=�� [�V0+�/�&�d���@�;��e��G`�N��L�$ E�x'�.�[�Ή�%�3��p��D2�WCũō��/uq�����PK�^A� -��c�آ�A�c��v�j���9Q{e�g��v���̺���=Ջ(��ۑC���+��X�z�S��9&�Y��qۻQK���������s"���r�欱_�l�P1�ͽ��Ց��e�xf��|�X���x�j�kH�4l�c��[+&��|�^�i����+k3���D� -+���FE� �D ����τ�JB���)LJ�D�õo�=N�\HP���/!��f�=++��S8n��k�?�7|��*�� ��k�ŹdOE7DP����V�'ٶ@� ���� 1��ae�����.���Otl{�и|.+Q�h8�����Om��ʇ��F���^7��5��!�z����6��h���aU&Z.K��~�B���¦,�·�� ��]F!lT|:�������G�U�������e����g�UAy��x��X��$H'��I�:隣1���I�I�$*tҾ{H(%D@���(%�<�㦔@�F�j)��iCJ��'I�B)XB�SJ`6�����6oJ �"�b�����(J���^*%p�?NJ��y<�c��2��P�CZ��I,m!�v�IcG��$T�]��H��:�?@1��xpr���Ko_��u7.��^�7`9t��� PJ�8`^����S1}��^�^Ϸ���K�>!�8��ecB�3\~z��F������L��%�9{��8}Q�?�Y~�`a�]�����e5�!f���� -r��� �;��p��&�e�QA�cg+�W���I0�,$<����Qc��Ш��|�u�_���j#���+�d�l4g�4(� ���(�=Jڵ�z�:J8�a�?%�H:�?��C�� -��kP�'�*XM ���K�۲�Vo�O#j"��`���P\�o��υ��c?o;�Ն��xw��aף��z��=�.���['�B��7���M��B�:�4�-��R槳}������k���:L�0�s-���k����� -��>���� -;�ώ�~����+�]�����~x�� �"��O��� -)��aͰ��L���&��Jm��;�(��ׅ���� 6zC<0�#t,�z:�4ke&�9�ʉ�rB[���jj����:X�^�#q�s��t"Y� ��p�U�PWӸ�, ����1d0 ��ҁ�����oe���w1��]��K0V�K�b<eP��k��]�服�߯s��e�+������x��>��l�����ZN���F����$�H�3���oG�}�/����#7�b"�ֳ�� %�? -�Nzț�pt ����FOd{Wfm��BP��l�+3�v�z&�1>�8�Q[CsUg2�m��g2~A�Q�ɼ5�l?��u��,2@jX� {�tH7ȼ��G=�qN����Y �d��ߧ�x��Hvv"� r�ȸ�Ƨ�����҉0X�K����J���K��Nd��&�`ÁD"�~"��J�y����x[{�r"2��cƉ�Q�p����u�H٘��@[:fov"_{��ӏ�g��0�y��9���}= ��4��-���S�����5Z�328������L����|m�/��+\�@jl~ �?rV�/$q���y̞1F��y<�nd�:���}�y�>/�3��x ���r������1Z, =������͓�<�ٴ Ǿ�ǣ#�혶<�_�#��A��b���ct��\�� emky���gh9��{�.M�c,�ˌ3-����L�kny �)�x8qҢ��Q2�'2�c_˅�ё�A�b����;������p�1?��k:?�v$����#��^6G2 �X=��!@e�h�#���d����~$c,MG2���{��u>�DV�G�P�w�#ͫ���Ț)xG\�4#x�%��N����������H�;gL�y莝B�i#tg+N6m����t��e����>2t�ѾEe�Bwx�s����+��n������\���c ���㗹(��;<�:-=tg0C�-t���_��~�ㅜވ���8�����cw�}�*v���{����|3�����\_�L��[�����(� ��3~�����+4���Oy����pG돊�.���'L���cx5j8���}�Nn�,W'��kO'�7c�.�\Ѝ���:� wb㥜�v�aU}\� )��+>��6�'"�^]\d+X�*�r�� �_Q�����a���m�3���E��p���݆���q������ޮ��+A���ǭ)��x]�}��%�����qA�2GP���}J7�ӝ\�x��E^n�'DN�u"z�_8�d -[iL��.��~#9�`^q_wr�����2�y�~���<� n5^���@�l� �ހˉ������`7�ެ'L�>�\d�`k6|��7T������Ӆ��n�@��(��(!ԣ�����Q�m5�y%���z�Q��pm��� �E3��/�pF�t7V-F7b��j~������IX�/PD��1}z���6��PW7�>I~6�97~���~�a�����3�$��h��kG���q�ޅ�dQ|�;��ڻ!���~����@���ɱ!}6�� ��S �%9��w�)�fF�{�t���Q��9��e�"Ak�� ���s�������2�W�a���Gɲ(�e|��{���bB��֭��=n(�>Ӻl�/ǀ��C0UӬ}�����;I�b6"6� �掶t����0�n8��M�ט�T�C%���6�5 ��YB@�s������2��C�#��7�n`�=$����R\־5Xv�9V6�4�O���M�H�9<$mȊ�ۄ��H�#��c�v�4����qIx�F������?�(�abEP�C�°�#�8 �u��]�|�O�<�(^���H��H�A��i8k��v�h^���V+Bf��`�t�a����<�=�%8����mu���L�=Nln��ԉ5��wq9K+S�W���V�1p"ȸ��9H~6Aku`��� ���{@n��\/�/03]�����'�zZ{!o�*;-�h�E����j��@@�ĵ��$A�Q� 7���Ot ��PGG�]2g��E��4�;�2t�0a�w,���R<��4�O#� ��t����ZTԱÔ��L �W���X|eVv���J�f�i���#��5��r��f�JNw����b�# -P��4L���$����B��������_��; �@�=�Q�uwcJ�C<\[�1���'��./�u��ș��q������xYE(uP�Zn�����D��Z��m������jZŷ��۷��/���}�� -<���`3Xݤ|z�f��s�����Jx�4�}�S��Y�At�@�3LS�QggL�P\�𰸔@�� �D�+R��CYJ$����w<��^\x��JJ��4�C��~u��@��Ūsa SHg�m�`��n��4�8Z�����=�ڂ���m%��/@�{�}}��j#����*\�:qٯ���Ɍ��fy�c%CU�!��bHF�}�Dv��aS>���<IK� ˃|@r7{$�}�����P,Q=��ak����\=a�����jލ�Jr����K[�q�jTb�ѵ�Se��O�ȱ*���i�i��Y����-�����ieg�,���ű�y�U�} sH�=|~�M����Hs����y�,ZX!��@A���=AzX�rc��CQ�v3C��'�nn`n���%��_���P1hQ0���FV��С�6Blu�����?<D�D`mˋD���Ru7���K��S��BW27[�\&Y�7�d -�^3{���µ�^ ·��#�k�� ��A�nn?$����E��O�$��B���ƪ��������x��i����Zv.�X����͒��s�tj����'P����"��G{�E8�2q�0�m[I�8���s�q!#N�j7ۼ8�z��̼���ֽɥ������]����c!� 4i���}|9��A�s*�=�2��m��6��ݝ�%�6��8W -2�Y��%���@��:���H��d=ՙ��-d��+�9����+v��S��c�䉶�K�[�U,�ڲxq&��� F�T{�:f�6a* @+0� ErM� -Eo�=RM�B�V�GN����%{�� �!����&R�A)V �[����8�c���BIu�B�8}��Q��6^ �4e���c����a�[L�G��Y,��.��:����9�z�MƫT@���bg.�Ř���L�X=S���Z��zP{l�<�"�č 7n�Ձ�b��Y�~>���so���坒�~�jNM��$�K -���\�����&���2ߘ!':a�yX$Ġ�9a�O�6#$V��%�z�e�"~MKYv*6$ٳ+�m�a��-P���f!�\+��ݔ^Y��odn��}9�*�``\ew�K��[\&>��m���Ir�4��}�s�b��,����T@{�!}/of����~1��.�N��uZ�C�Ŝ���ka�2���N&�F8n������O:U�� .���MqܜJ&�צ�<�>1/�d!�{�|���9�%�V��.��� �.1�g;�u�y�Ww��ņz��^8�:���]Lj��}��NR�ܝ���$��$ �Ҷ�z�M�O��q����M`!ކ�C���<�d��d4�����b+����r�X}��U�����>/�z���G���*S�`�������F,g �d9KN{Lg��r�#6���^]B -�i�E@P�Q�òja��VR@�ʺS�Ч��b�us*��]���vzf�FP�74�b�R. -W-���ԕ"�Ri -AէP�T���R��{W}�q�e�Х�j�J^1�8���"��:XޕD�N %tT��et�)�SO �z<*��sTَ<k#+Re�Otz+â>s3�H�l�udW(�S %��R�dҫ��=7��G��� ���� �4��3�ݦ�Z5�K03un,fF��������`��"C6Ӎ�U!�G�)��e5�3��u�@���\��NϜj��w �_���(�+/B�^y� -�D�cy.�0�o���e�:|�Lj�?����xe�<<4%���)�^�=���f�><���3/_4��_����fBx�Y�^r�4wzD_��n�ϥ�ͩ�9ʼnd�ڶ���,�.͌��^b����B!|� -C��#�n�+���(7���v����)JP퐧5"%��5<����dȪ�K�%/��L�,b8������Q!}FD�,��ύ0SI��4t%d�C������|�@YNlD�r�#��Z�§~�B�D8v��,�OhI}��C�i�F������F���eh��&[�p6/D�O�����O��:�O3��f���J�SC�K��N���F|���+�V�o����K�:�}��Fs#��o��w�|\�>\�z��c��)%����0�x��H�:��${���OD��x}���;�^ ��@�LJS��ظĨ��̮"��fvc(�l�N�u4��B�u��n��K�G"/tm�7��O-�+_O�'�|���Z��I�]T:�o���A��j���L�QŸ����䯗����)$���Eq���V��CC���qwpFue�C��u�Uq�����E�H>�r��,;:���F�yƱ5#{8�������V�rtΊ�xbr���n(��'����P��%�VQ�Ó-�w���G�pWr����3@��P�s�ע�w��ђ,O2"f��2k�8f�ӻ�#!9Q��ѷg7�J<���\�PncW} -���=�vٰ�Q� dH��~<�i���P/��|{���[����C�A_���!�ޮ�[���A��y�EN\��3�94XN=���աר��Zw�yܗ��p��$A�Ƈ�D �Dž��p��|*L����.a ���q��ۧW+�dE��������l��+e�2h�M���3��� ->�#1���;7V��S�:�RBjP������DĘ��|o�"�U��t/m����Zz9m>}�vi�Rmg ��|��TC]�M��J -�Ω4�&_�MD�QW��e,�W���{�L����c{���o ���k��k���������o��݀j�W7�Z�� �-��P-��T����b_��b�n@�ؗn@u�G7��W|���v��^݀j�W7��kK��� ��T�}��{���^}~갯��갿]�������{5��^ �갯 �j�φ�h�φ�豯 ��b��@�ث!P-��!P-�j���|��we� ~�#6��8t� 8��7#qv:��3��Aaǡk��qfz��a�gl�еH8y�q��1��,:��[Cg�� -�;���];��[8t�9����p�PX� J$:�k7 ��OJ���}��:H�( ���/ �6�M ���B���4��OP'�Pt`j T���@S�/,,:9�yG�C�k������.5�B�+�h�"x2��Ew-�댟�?_��n�?p����$Z��-�Lx������Cy��/8t���; -�����}l��r:�0�M�3�� ]���+�@���H�V�tI_9HW�V钶HWh���*�J ]���%m%�����%u�0]������D� -u�(]������D� -u�(]�������� -u�8]������� -u�P]��^�VW߇8eR[�J���K[]J��3�1�����҅����ҹ^E�����|���U�?!����е��U���6�{�*��W�"1!$pQ0!$pQ0!<��� !��\�E��,�E����E����E����- -��- -��- -�nQ� $n��d���(x�(xn�:�i��E�An�x\$� -\$�A�Ee>?Q�|�w��b��0�OP <@�����{Je�|�-.W�P���I���Ɇ�W(�E��x�������Z�`hBm�r�Ϥ*�,�AԠ�O`(��0 -�;�Jq���[��Z��!���R��WGGp�`�ѐ�rc�1㱲s��ˌ�n��澜V�A�0�e4.m����s�|Y6�>�k����=߮���q+�)�u�>����^]hVF���v�� 4<3`�m��^;����uİ�/�J��9S�'Z�%w|�}a�!��p��7!�ju�:���%������~֧�$p[��1$�V�6�s�d�6�ccU�/qi��'SLB@��ng$��2���uZ���Q}��^NkR��[�T�\C���a�-s��Y���|1S/�43�>���Qk> �5�����n��og�ɖv����6^3��hGt�(���f�v�7���x#�2����2�?4�����!7d�dg�?|�a�g��3Ip!�d��c�G�s���F0w�@E������y�������:�,Y�v�������N�\ �D�-o�<�H����������]ٝ�w����n��>{��:�'������F?d5�@�(�n>jX���o�� ����&NlL�� �o*�d����z��,ޫ�D^֝����}�s��^kS�dV3+�$�Leİ*Iq -8�R���п:o.Q* �!�2�C��������{��c&n�ȀX_DP��A�˸"V�W�,ҋķ}�M���ڃ���-�[�����l<<Oo��L�V�cUI9Go{�ȞK�W���_���8�(�X���k�4+ ��I����dF�wD�Q9�b99��J fVk�5�r��V��UV�+��85�ӣ�t�K~�9~�lDI1C��w���>X�g'1���1E���3�tE�;�f:Q5��K'�#x��|#��%&�S� -�/�c�`��;LObH�_�Z�h�N�q`�⭭�I���Ѩ��C$㭧Z�)!U]QJ]��� Qi�jy=�S.r��P'$�O��v��#N����L��2R5 -T���Ұ�W^��X(4U;�/�e�\㞸/��,��YĪ�6*�9�� -�*�;��P~\�{��X�E��N���X��ĵ�8Σ��h8oJ!lXF���6D9�"�>�K~��W�@9�K$�$Uy�ؙ$ef���a�M��d�zJrq�$���5���"S�I,� ��@�āt� �d,$o���;���b��ހ�Q<&�Xe}���L����M竨X���$�V�wSd��LI����4U'��<]����^r��0p��L�Th�{��qSd5c���`�$��b���3GCQU�L2i'���q\��ѢPQ<��&�BJ����2 -�W���D JR?�!$L�=��U��`{A�M��^���\۫�go*�� �X*eQ�P����w~7c`��<�:�M�#:L+�u�8k~��Z<���Q�W� �,D �h#T6H��wAZ��}Ab�(J%Cg���#��B;���+u�Ӆ�A,�qA�� =�3wd'�L��kp�R��h��ie���Oٌj�4fMr��o���AX�p�R�����+�L -�3�� �k��84&�� I��t��Q�| �s��9�� �)�x��TІp�s!�� �Q l�ե�z[B��v~�V�V�$�Q���z�(��E��K;�ʺ���|^6=U�է������!���f��L�1�!���h�eN[���9I��e�d�t�� OoL��\�YT�4R�| ��6�����.3l��N����K�� Y@�:��7�t�~D``q�ӜQ`��"��5����%��'�S %��h(��Nim�,/��Ys��l�^(3�>H�eN���(*W���Qx7��J`c=Ȗ�7��GQn�B����Q�+����!Ϲ"j�d쳢f�n�b�ڹ���I�~e���,W7Ȕ�ؤtL� �ӑ9�8��7ɤN�_Y��96y�ڨ~��L��\�p�{I緆=tNl�Ijȸ��D�<�e��E:=�H�=�#q���� �^BA�P��t*�>�^�>�ob.�-Z� -���q>�X`.R�B�j�a��D�v�Ϛ��*��$?`���d}8ɩ��D'�s�Խ�1�sh0�qN�X A1OJUVe��AvM�>C��~��$����T;BI�A?�@a�35+ˣ���!d�笮!P(R���R�%�]u����U;,�$��i�X�E"O�ꢈ��*8���c�-����i� 6�����|j]{E�˙g�s��bAYəQ_�OKP��V�9����u{��}�NJ`�$b�3����֚=��H��$�*�\����Ijh�E*8�3�s>Q��=�䝾Oo ���.P�1�6R���&`�(N�ߦ]$���]���!P�z�}w�M��ݜ�L�D�1��5=���'B!�8��T'A����`K�\���%�l��(��x�J(��`�U�Cك�P�DE�*�eCo|�J���(A�k�ݨp�S���/ �[��]�2+".� ��zzzQ��f!�6�=9��ݦJ,�����e`a�hF�8'[��j���BlPLs���F�(�t�S B$ ���Qm���VJ&��� *��u��i���D��+]d\&!��t� ����A�(��6F�j$T�qV�"������SaO���҈�a��ƌ��y�� SD�i���yn�lL����bā���+�ʱ r�̓X����l �7� P�C�� \x��{*s��"��JJ&��g�N�@2U �]fEL#H�+5SY��!�+�.���s�$:@"����2"��ӪxH��U8W���D�M�D�zr&�@�Hm�gc�+��@��BT�E�/ib"���hERF ���"�*OJ�Y%n*�ީ�9��6���6�$ }%�=W�J������*�:S�Hu ��R\��5-?G�� �K5�P���+�� �+UB�1�S�o̾,թh!z�T� -�o�gT7�Ζ�hQ��O�*(���V��E�&H}����H՛�T����$�'X�V-�h�%ީ6K��8e -/*�{�fL�^P��D{R�Qy�6��lD;ĸ�>��d�?/�N���4��j�eE�h� ��UM�B�\hV�,S�R� v�ujw�����=�Ĭ��^��fi �t�:3�tT���`Ӗ����J���6DmD�gjK�����!�a��Z�f'���v�jk�v������J��n��j�3$7ӟ��N���Z��Q/��%'�S����}���A����F7̨j�Ŭ�S -�VZ#Tf�5Zf�)�3ӰQF3�4;�Ě9�����V�m{J��n����̢n\�pbzo�Fm�Ɨ^�Q�������_Y�9���<�0��И��#f�W��M�g�1rL�BM0PWJ��2�2�9��uLE�VH�FMNR��L�RGTk�Ϊ&��Wk*Ʃ���{�"k"���f��:ݚ�i�9D̓7�X��g��9��+Né�l�E��i�v�TN�r�h��n^O��=:Ȕj*��ZU�0��T�0߭h(��5Ƽ�SU��Ŧ�Gٔ&�<O�+sQ�fNl����=���)n:�9�M-4�T}l E�TW�i��ҟj���7������ & -2t�"��I�,�3��M��6c��{�������hU�QQ���M��T����Uk���ՠ���u���`ĝ�j�@�6�� F����X1�u���qh����ۉ��کY�u��Q 4'����-@�=\9�V�v��j9j�6�0��UK���٪�&&-;T�|��W��2ԄݕLY��n>�5��P�gB���Qő7�y7$��{����Y����4C�>�"�yS ��Ɏ3n�Pw�"� -��=�\�C*D�X�f ��F��:wx�V�%R�H���:{ �jО�oJU��l�DMX^Y�n�l�֕d�Օ�pހH���A"?<�z����B��d�4�鷾f�<�a�:�%B5�uW���}I��H)"��"#����+)H�`�҄��T���fLb��A6ژRٜ�&-o���X�b�ME����)t�i�.+=�i�ɔd���em=��2�ɘB!5� - iϨ�%S0����������ڊ���w����e��l'�V�&T0�k���2�=6%�G�n�"�\��'CCVVQd�}��mg)P%��O5��fUq�,LD�@�b��E�l����TBC恘%�f2����}d��˧�$(ڇ��=����rT�>�d�3�lwת1�Zze�8��7���I�ح��P�����Q�����C�o�<���Z��2a���ۏ>q)��g([E���������z�S��� -�����Rw�ν�\��b 5lSB�.~�U� O�QH�!$���|&s��ZgȺA��6*�`�26=�> -�R�+�]�V:Q�ږP��Vd�`�`?��%s�V�o3�-��vP�JNoz�жȤ,��ѓ����)vq�D)2�$lC� ��F"�Gu��h�O�㭡ރ֙]��v�tvz�l�zCg��l;b��v���t{���9���Rb4=Q�Yz�F�9lS,2Bi�$�s�{r��Z��~�h�� 'qݹz�"G��@ۮ@�7P� �Q~�^#�B�g�P��)IQ�]��{(�%T�֣B�e�Nȏ�x�T�|_mtU(�%HJ���4�_ve^�����K�~Ʈ�φ�0� ���-W� -�yz�/s��aT,6�M�\������/�YL%���̈́K����� _���N� é����lU�N;DɆ�`)�U�myHE��@*2��i&'�"���j���H��ˇ�1�s�?ZF����r�� -�w(D�96����t�<�KE9a](��I�_�9TV��}�����!�����1��^�01M9N&n`��߉���6�8���Y�g|�L�|�Cs��ъ94�K�{�=Wc��bx1�!K�K��;*�C#M/`�<z��`��)ΟA!Z:#���b�{�+����]O�D~:�"��L"�+��W�&>�W� -�i���:J?����ӳmjo�bK�j��9��.��N�)�����T"�c�Q�%��9֙ <�D���e�M�ؠ �� #�D��i�<Y��g��4�$�(�;�NOe")ڔ�}���a���Q�M�����w�2v��.XU6��Cޜz�J�W��@�o�WI��p�ڪl���[��:>�_mE����vA�TD6��+ʶi��y���9�~��'<?�8J�7���G�D���%�+��`�U�|K�o���S�s��T�*dzW0�7��*X��G) ���:�����6'B��ډ����0��q�q�#7�������t�v��R{BHD_#��i��9~���5H�`�t�p��d��#t��;�W�h�;�gI1�#�c��kDŽKN���:j��c��r���Ѩ d��a@D���A��Qm��)'�ֶ��d)����:�8���t`��Ĝ��*���#��I����)v���fc -���#m���Ӆ*�ۆ�%�Mk�e��v����ӣZ��&uܪp���wuu^i�Oq|ssz}��ϟ��������SХ9�0����11w���g7Wו�]<����I%WO��T�\���O+�Z6�?�U�xx���'GW'�Pr��:һ/n�~������'��{�?-�;j����6���$�_\��<;��.�������r���>�k�g�g�|zs��}�����__]���_�����h'��/�>{~g�G���'�?�ʙ�]蟜=���.T�RޜϞ���g�����o����˧�"�O������M=��}��w�I���/o���3�[´w������w�?�lO~�s�{ow{��V���go��?8�y?���Q���+쳽����?��n�i���_���~��t)�(�M�Br1�Cm�%��ɯ��G��Z�2���?��G��UZU:��������hO:`�bh����?�C�>��F���6x�g>t@�TY�,���TΩj��n�[7�q��"<^��E�������:t��v/^pKh�U;�?�sx��R�3�n�y\�9���� �/��'CxDd���'C/�_nj�Pe�ٌ����T%)AV^����qQq)�؊|�� -��p��T��6� -iʖ:���?�\RQ���C���"�⤱�퀁� `�<]������`k��@�EΙ�Co$�����|�@zZ��f �TK�R��Op��e8����aX�L�Ԓ�.�Ȋ�N��������dO�[�hFY�^"8�pW?�]��������s2���4le}�\�N8��4*2My -<���$�/�(���B�/���hP2Z�(��B�4����@��qR��NBDҧm# �N)��,� -L����2��|��O� -�p��zO+���Ő-w<1<~�m�+�#��e -�+C�2��dT��y�<���M@J�B3)�ٰ�����F�W�|(旁>q�@�A�Q�m�� ��ѩ6S��I�[p�.S���w�z�%�09�ݔ7�A����� ���J���gd���=P�£[h!��Nhe]I�I��au�ߑ�������4Y���.P_�U��9��"˻ �႕K �C@2�Q1H^ow/����O� �`>H�yհ�N�k��4�=T���pb>pp.���06ɈN�De%�)!eE7 �J���q4SH_�=&�}d��=�m�j;����@n��w8� -L^a.ea(v��k��0��������`��Apx�F$�*�O����0���6���Ž�ҟD��W��Jݓ�^']���o���B��\��䍏�x� ���^M�BUق������z�� ��߆���Qx:t�O����r���� ���� >�h�m�������N%���n$ ���E7�y%|6��/�3M����!H$Y1t�O�^�߄����t�� �GB�J�t�e�Ka~s���4''ty7�(�\�{��_)�(n<�a�E����+���-��:���@�WmtHF�U�4W�=r,�SBj8׳ax���ơJ��À<Zt�W�vo1�!�E�z����a_���L_�|yoJ�K�>(�]�ϑ �St��Q�$��C�@�ɣǃ����)��P'�9C -���R�d�i�ɗ���{�?#�!1�$����m�ێAF�(�_(�Sa:�"�ADZ��Y����0��A���ٳ�^�E���������|ж��[������"���}��X(C���յ%S��k�67���1���3ۆֳ��H�=R��P�w�� ݣ�t�_�8l���"�M��*jHc -�\9��Ѥ#�n@0��9Qy�S9GNA -��N";9�y��s@vh2CN��!�2�^{�%Ȑ��e�P$��5��q�o�D��e%�M���g]��\����mI��ćN/�~��}Q�MC/�(�8�-���P(Av��B3A�s8�&7��/��m�����D�Q�����l�"8U��l��;��TD٪ŀm.etQ��v��v���S�h�'7]�H�N�����G�q��!�I��7��2h�|O�rj�Y��Y{��rd͇a��s�G�m1��Q5��P1�d�s��*��)1���u�H����I���QT&� z�(�N6��e�n�FG@Y -��U+>�� G��:#�O�ǀ�/�0&�<r�d��5�6�A&����L�w�k���F���bQZZ7y�myn�'¢��AirLy��P����١�bb����R���,������x����ܷ�\�)E��b�=I$�X:"=�u�=����� ��J��j��`��V�m<��GQ).���A�κ�������RN��cQ&�uJ�����06�1l�^gSz�Bf7�F �)�Ƹd�����o�m�1a�v��[����Ц�92���%^(8 ʱ���'&��_�br����P�pFtc�#��B8�TU)�Y5��������y�LhkdC(Mψ��<���z���b애���̳�&Q/F<��T�"�.n�DڇA���|�n�gL��a��}N�(�Z�90y����է��h�ȁ��� v��Dz��e� -̱QD -=�n�g��8�r��;�)P�[Zg���|��~z�ث�DA�6��y�SfL����V͠Q�^ -���1��Y4�Y��r(�:��)����Q-�P{�H�E�\T%o����]n��f6Fz�Q"��(Wd|tK)��U Ж��1�iGq9�h+�B.P£)�̝*ԨQ�r c:*R��ft����#/�]�)f�̪"F�A���������[�+�p8����*x���d�f�ܬ�܅l��`�J���UYO��^��{V�a�d:�u��UhXy�3��v|U�xf�C����{�#+�k�kʫ�%$;d���Z_ϙ}�^̭�G��E��$^�L����� ^fg��s�×ɰ!e�Y��w�w#Q�����!�1u�opq��Y����J@z͐:)���pChP@^`M�DiL�����3�"áFb���*�'=��l�� ��XRQ��9�g��-�`A7q�ѕ���K�y�r�p~|�u�tL�A����9S (F8����H�H��� =n��z����b�4����r���l������=YL���j,k$�M<'D�E5&^�)ʄ���el��W�� ���(���DpMNɾ-�sx��lP���7�${cŗ���!�'é�K�ϾV����1˧N�FX�*Q����nWt���6�Ҡr")��A�c��L��: -4�s��!�zNj� F�H��S˪��!�z0�섂8�z, -�.�FJ%�7G/�/юҀ0;-�ԍ6gA� 9�8��T/��s -*qR�I]C3"�p�6��C��_q:���v��m!���vo���O�FSy��G��ri���c�O��tЛ;D�}�#���b�����xn���R C�[/LH��mNxHRɋ��(Ӡ�@M%$���V�u�����EY� !��1ɴ�my"��������-���8V���\�|YFʪ��s���"��19,N9��y��D�Wn�Tl��� mz�a3�0S�ъ�����,�+7 - N/q�l�W=�i�(���DO**�uQ�Q'zR�!=���"%��~�#:�74�&��ʛ���©�,�f�`����X����Px���'E�HT�6F�w�F��K!������ �b�Xؿ0�6K=��t�lGa;�����\ѳ �{�[{P������!����x&�%����v����ځ�����v�vL�G�s�[95�5����ʈ��wap���˰8(��D7)0kˢ�2��!q��H|�E��I��Z\��k�&�ĵ�W.a���� -�!�-�9�pe�#�w"�H^7w2� ��ʯ��E�Ϭ��q8E����#����5,��q`���c���X�Pp�N���:3y6Σ�b� �L��*�qjޤ�̠���b -��έ0r4�7�<����*ѽ�p��;�2it�ֳ�t�H��^x,�.�ж��E�|��>�稞]/�{��v�)����Ơh�� �����A���[OΗ�>7�e�ע~���Z.�VW:��Ld���,��(�(��J `ɂ�ZkH�B�5n�������)���oή�P�����9����Ԁ�0��Δ�����&��n�381ֈ�`I���\J�Ô�X�cz1v�ld^����~3Nl�`�n���-4D�$OҐܡf�t�]<2�T�E�d�^]7e��[L����)���6�����#�U�5<�&& -��z�1s��e�;���iT���i�27�S���h�-~A<�vZB�4��3��u:��xn��9wݩG�>�c�m�0��1c��;�_^b���\���%ic������r��)S��6�M/4b���M}�nbpwl=�'�m�$|��t�E���L*h����+0�MY���.�K8�����X�ʶ`�� �����K%��R�(F��jMb�� %5O�GC�.*�q@~����4U������������m��%v &:���{95#Rȑ�2��ȅ��w���i��Xx��Tn�v�Gݪy*HՂP�sEt�I[�tځߺ8R8����'!z�W-.����蘥]��<�e4��P�>�4`���v F��=��M�G -�үό9AߪpT݀����`F><���11f�N�\�YQ{T���$�S�r�E�1�7N�߿E�!��?�a�UC]*�A0J;�S�t�t��@�U���2��m��e�n�x�ϫh߱�-$�q,��$3O��-q19��i%�E�w��ca�ˑ�)KM.�N�M\[�ck�"�����@�0hT���!�J��c"?����j�@�#��c�(�D2��!-|��R��T Ig�C���t+�V�2��9�~��{%��@�Hd�Zm%�ͦ����Y�à� -����Pc=��L���G�J^䝠�i�Ҡmғ+ %z{��A�W�"b���� -˷�QD�g�8H4,*R�v#A�:J���n�;�����`�VH6^>G���Aǥ�x�3���:�Ŭ�5�N�r���|g�יs��u��� �ޔ�M�Ux�N��$�!tF=�9Z����[5����B�%�k� -f9(Q�'*q���d����Y1��)phN@$�����@~<��"�$��lJ's-�yi^cӈ��! H' -����Ҏ��\������j���a-��5ԇ��S�)�s�p/6�T�v�K%A}/��z �EpsԩiZ��t*���)���*�V&'mU�K��˝�N�@�rj�f� -�!����e�V�1�n�]/�VIH���'��z(�s���M ����H�e~h�zH���/�H�M��u�4��9G���\/�Q,�BZ�-b٘����v�� -��� ?e�9/�8�������y؋���G��)m�����Y�$ҙ���O��B��E��W��i�^��Y��1�fT$��I�$��l+�y}8�Y`¿ -��� E�)�A: g���!��e��.�L@�DQ��*2�P�np��;��HJc W!Qց�Ѓ��G��C<�Wr��.�m�Y�����@ Y�#&L���0����F����D�v�C/ I��c�O�p�IN��� ���@�!�4>�F�˼Oʏ�jW�����(i(J��w�ATe2���&�$Ɗ}R�BNjSB���£���U�s��z�;��H�b����Pv.^�d����~����a� h�O6�A��^��aV��I=���NaD�ex �\C]���ƒ<�� �ez���~��_+su����������R j� rR/W�H�QBk� ?h���x��}��V�R��4V�`�������:�zQR:�*����J}�,iΨ�)�.�i�9���U(H�����L�{ֆ�0��zD�F2� ��Ad����J@E Il�DQ�^�r��gLH����:����|�훫�� �yE�����F�cN�M�ە�@HV�R��v+����6gZj0�O�5Aڇ�Z��F롏N��������J���@o������bl�q}R����3U���-{)�� -endstream endobj 25 0 obj <</Length 65536>>stream -�1���7��K�A�allīxY9N���K���-�S#D�F��W��TJ\�Z�w|"��1H�Kp�#g0O8/ �:z���D<<꧔6J��+f4l�\���4c�zWMI�Fe�W!���Ua��W�-hAy�(\���t4��,��2�+Z����WY��\�>�m� -a���z�^$�8.�>F>b�;@r�@1����`���WA��%�5����B���'��e숺��PL�Da|끸��;�}V�'��.2��{�R���D-���Q��`ࢩ���G���$�+YR��&'ܮv��������"���u;>�,8֛�%�m7�h��^��Rj7貄�8����ؔRr��|G�T8�y����a4�oh"D�$7�\����l^ɩ��G0\r��D\q�&?�����,�I�L*���2��M�t�W�(e��.d=�$�U2/�s��M�}��̰�:�c�LL��Y�e�Rص�*g���}Kn�zކ��- �-�y�Y�[٥���ѩ*#.�M W����������D�� '1O �����Qȩ}�D>qt����i(_��יЗ�N�:3�&����R! ��I�_i+��.�[�*[L$�pЈ���z���,!�ٲ��t�ih`쭊D�M@�1ԍ֩�3���9V�:�Z)�ԣ����_�e�:�"?�k�����~FH�Z�L�hW�K�*�U;0:E -C9��T(=����a�� �F�O�$�L,��"�W}5�`����8���}��Fz\K#�g6JXm��1�Lm=�Xc��c��ȅ�1�k9Aٵ9@(u:��b F��IP2Yrr'RV��됴q��7(�_�.�7Hu���F[�p���� ���ͬ��"��pz����{��'�p�6%/���ŏz����Z�XiI� -&.b���Rh*K։���*A�C�Kd���֜�*v���w�C#�O`K���T�j������m=h���F�#���J�5W��(��k2/^�z�\�e=��q4]�H�`7�xj"���4�RK�J�Yr����#�y -#�ۍ�B�#�'C ���2�d��1��7��wk(Yl�����:MS�J38 ��X�7>�ދ��P -X$����,����o��E<$#1��]f��+�����ϲ~�e�"��rT�r� �d(*ѫ���}�1 I���) -:�o%%͔Kؠt -��4X+r�5t������X$5EOu�`�z�v������0�Qҗw��� ���Xoib���le&�b�`�Ek0��m�G=0�J;pβ>��?U���XB��X��Lh �Vɒ��4�%!��8���ͼc��w^g���슫B��q�dm�S��SV�m�&���ӡ���jM@��%�f��=@��r4���j��+����u�?��T=�V0�7���%�J�s�4քh1��Q����'�1g�>x#(\��@Um��ѫ1SU�0��:�X聑3�N�;䓅�Qi���հ��f�^%0�� E���[���lN�Vy��VP۫Z�#k�����F+�k�d,A��}�k^d���5ڒ��h��k|1y,Z���rnbC,�GՊ��bQ�^[�`1n�Š:����(o�N��Յ2������>;�z=��rŨ �J�Y�����A��k5 -+D�QM0���p�Fɧ�{sN�?T�����z�=������⿉��1h��3�R�&����!Y�z�M�S1�y�V��),["�� Yvզ�y��WoF� -\���in�`ҀS�l�O��jx��?ց�wz,z �1Q+ �vo�l^��%��Xw�@/�*���Y]���BQ�N]�l�Z��F��V���}Y�c�����j�$��Pq:��9T�_Ӿz���c��Y��<ph�B�����H��,dAE�1�x �#�^D@�w�ju�"�0J��e�u�\M����h�R�I>�r�j ��q���C��H�r5�3���7̓����# -;��+ �K��!�*ie-���-S�|3��ar�X�����s�9�*��1��h* ����2;%�!X2��A�a ��\��j�N -VW��Q".Nm�}Ks����% -�����B�P��9�k)�>���bP0���y��`Ϳ�I�Ryſ�H�X)題��ҍ{��A�]���)#YD3/���{ -)`�a4�D1Z�`��Qݨ��{)�V�*7�}�#CIiIʀ'-�A�f�������OFi)D��`Zn7 4���{syDΠ`T�=='�����0GEH�v��lɺ#&^Oq*�P4��i��hnؘ��F+o�'^,#�4 -����Yc�k�j�ME]�Hʉ-{'j�}���|�^�e�e5Rw���lT�����>�4y��j�U�*^��Q�=�x贰I�(��80�z�0� -�ڬ��q��Fơ͑�K�ւ7 ���f3��P*��q�J�@4:ő�����tx�����"��8�x�>�l���P�Ȅ����HI2Bʖy/'h{�V�� `.Z�Wk8��g�XDn�x0��U-�K^i������!��miC��B<�q��[�6��b -V�$}�^�j�,� O̙�L9!��SZ"��вә���z蔸k��!�Vˆ�4pb=��QDc��>���k��G >8U�4>>w�ղ�8���.���q�|f�p�-F"Z�����fa}�j�fN�Z*"�,�ȱ)�~4��-�6���� j[�0YKf��I`�Ը�$���� �����nӷ�x,�ň��X�� � -V')�6��̜��Y^A�A����f+�fS��@e�eaO"���YWy�P���5�O-䡉��M2�Lj�eV�o5�ʸ�]o�ki�Z�L�&���-��]�`듎�Ȏ��+x.��-���E��� -O�yL�A����3 ����GH:���M�����TyC�����<)��CMU:����u����SL�6~NٹftȒ�;���;z�F�WoN�"o"�]��FJTI��}��T`��&ˍ"(ĵHx�F�P`�fj� -'>~��f��/�i� ���5 -�!�<J� ٢W(LA3:!��2h���f*ͰT��������h�Tʶ��%����5����=�I!�v�7!���k_��I*X��zQ��Q]rMQ��{[_Z�Qӗ��%�J��5�{� ��Gs��.� �:H -�o�u�,��@�;ˀ�iԃ�nP8�H�XRg�h=��������˩tV�f`>h�Kݦ�W�I��o���L�W��1�)��(���� �Y��ޞ����=:Rʬ��%��l���1�o�\@�!�[�^Y80�H����3k�,-U\j�<���M�Ik�J:r�k�B���AP,=XB���Mp�q�^�.�V7��Kd���.���P����3k��WY��^5�=��Kz�&IU����F%�^�K�g��xI�h�Q������k�U2^�ఁ3nk���IP��t���;X�M����lq��c�� 7�$'v��6�Ւ�4�/w��¬�������y�6UZ�Y2� ��}b����=f��P1{MagY�%�H,�4�[4��^�Yƺ %0�mg�u�:�G��^���M��X��5�I q�}�\7e��jl�)��T�rڔ)ۗ-*zW��e�J�Y-�\2C7���!���ۜ�P����wwLIz�,IHTJ o�O�i*��JX�E�W���AC��'�)���Z�k4��4��.Pn�����Kcr�|&���X�A�dɜR��V�6OI<RxU�5�[�Sdu��Q+~���(�Qʶ;Rx��ڒ$ɤhO��6��X�0.���0�����S�<rC?(y�xI�@�D�!{QU��W�]J=Ǽ.����P��Q��~ȠU�Ѳx=�![R[jeU+Փz�� V�(5?�06|%o֥�d� �$[���r*\e��Qϯ�� �J�� ��[�z�\ �r%���ʤU��gAKg傅<j��/��a��s�;�6��f��d�B��E_���� ����\���m��Y��RU`�Q���h�0� I� �UOZ1 ˀ��y�P怑;���\ri �����}�̵�AL�lՔ�F9ZR>�&�~҃������#�HԪͯ��e�N��O�.8��,7B@� -�`�XF�^�0!�[����+��K:ֳ+��A��X.MQ��2r\41�,0�e�J�4/�Buj%N��l��l�Y��NY/yl��<��@5"�-�%&���X��yX[�8�I���25-@N�pF��� C|+�P�|R��<]��J��F4/A�"#J:�� �HH���y����X�z*�`�?$����I���+�E,c_bѫQ�^�dH���������25u��"��2P�)�~�8!y�2��IV�L�i��]��?�5+��S��߅«�hU���T+�0P éR5�Qы�U�R�;���QE����a� ��X��^|Ѱ��W��9�� -�q�\!#��9��5iƭ�9/�`��H�R{\ �f��e �t����W�����P�ЫUb;�����>^��p�~1����CL�0p�vO��$�������gy�5�iY$��z}OE3]k�&8��C� -��WU��/a��i�+ۻ3�%5֎��DJ��t �<��x��=髛KOL��kVcT��-) �0$e�Y�V�@c �y�{�k/�ZU����:�NNo���)^P�Qk��!�v�n��� ��3�ɯ��jbV�)kNU��\(<��.�=aFP��(��;�L�-tP�����h�ʃ1��U���d�Ji���\�i�"22��j��+��'Ki�I��wq��ZN��ڛ]4��Ԟ9�*�F-?�[�L�R3�C -���ʨ~?�g��IZ�;tR��[��d��˩����Z�G{��j��e"���猔�ޱQ<�V��м�����@g_�JT4��2e�7��f�oK���c��[ޑ�M,>>62,w -��%�V/���,spc�5����fb�����K�2��8胪�nPʳ�� jZ4�gK�Pv���V�g0� �����N8�EG���k%�c�g��km�C����L��I�eti���9�tZ��K�$�$�T�* ���!�4 ��sޒ��S;W��m�&��pK�Q}m�&��{���bM��4�%Ti�(J���Ѵ�D"�vh ����9yN��d�>����3����.�)J�YW���Y��ʚ�I�]DzKgA0ͨ������q�i��wY95���$����P��_f{�@yo"s4��A��s�����[�j�R��v�7�Fk���U�rz�U�y�v����ЭDs6��r�#�`��?��L((E�퀳�}��oJ߲�V����r�`X�q�{���rQZ65 #6��(���Hɧ���'�� �df8+_��(�x��x�< �b�T�\.�p��Y�>=5MȤL��"<3p3ߛ$W� �X-H��T�0J��4���3�����3-#ڲ;ۡcm)Xlą���z^�!�uz�b����-��Dv�=�.&��p�T�k��TG�s�SO%9M�n��"YD!�j8p����З#�߯(E ԩ�����3P�=_�ehNq9��!z�NJ���r���6U,�^-t,��S�ԃz��"Mn�As��#`1>��� {���އ���;���<������_<���77�ח}�����)�����<�8}�G� -�s�{��{J⇟������:�߇��?�?�y�}�纽����u{O�'��� ��[�JW��cR1�3q�hGH��t4�e5t��%fTGF:E%��+C���mx�^�?���Gu�^!��^������=DG�8��/�;���AW�^G�m�|cD��=wt\�?Ɵ�o��(M���gr����N�ϵ�n����w����8X�ȓi�NU�<��ȡ7s M�d~<Rw�k��6��@�҃<5m���i� 8��nnݎW6݄��]y�� �W�v�΅2X[�#<ԡ' ������K��=�'����Ec�\�i� 8��n�mݶ5͗�ܧ��t����V���.��o��DJs���X�&x��E��Y�a(��U)�������I%�_�����p��/\��/p�81r 䨓 -,�:m#����0]�uJ�?Fӯ��+�m��<�p��7s�p�>�nD dY���m��kO������� �`�r?v�D*�.�$IQ:מ" �*�2(�4P�"w�j`<^�%eՌo�������$b���Qc�U��4�k���KӵQ�$f����J�S��hu̐fG�[5��o��8{���W���>��9��d_����y������\���?���y����~`?��a��ß����~�����������w�靽�����e��~����G/nN�o��������6'���?�>��~�?��i�n>vJ��?}�����_�W��ꯟ����_}�o_}�������7�g'����x��L�y|�B:8{Z��j��ԉ����[�C۠�w������<;ƴ^��xk�_��I�-Žn)��i���Or��������_\����c�[��<~��ӏ�����wNַ�N�.���ݍO�뾳6J}zz�ɧ7�#����6,�7���6i~_K�`a����b+������t�џ��ܼw���i��{WwÂ�Զ7��_���Z3��������������۲����8?�<9]{K��u�߆��孽����/�7`#���wy������;��D�B�_���np�'��R��k�|q��㗛,q�ͽ�.��ȏ����������w�3�장�_�Iz�2�C#���zq}r����g�����V6P}�.��~�]�qM�����������g���7W��W�/���^]<�z~v�����;u���tԝ�z�:�Ι�;���q���߿:{��RwZ��U>-u�+��Z�������`(�NI�)�;%��TR�XQ�߿��_���|�ſݹ{f����g�?:6�ѝ�[,E���y��k�m[_x����� 3�h���{��mD5��^}����\��ӧ�Q�߃0�'��9��k"����#� -X}�G����ON�����5�|�N�����cT2]��=���zB�]|l�Ѝ���n� g}�wg��^��Q�p�΄�}rzyt&� V�3��L8;�6��`xhǵ3��L8�DŽ���m�X�r���';E��U���o^}�������~��߿����"l��O/�^�AW��p�����=����\��������\�}��ao�ӏ ����Uww�u���O�r��Z���gǟ���*��_o� ������ ,���Vuv�����;�'G����懛,q��}-tM�`�N�����u�:�����������zr|~� d����ĊU��T�8~�����.v:9K���xy��!����xq~�A���{U�/�.6���U�w����|�%���f�.^��`�:o��#w�P��ҡ�ey'���w�����|����FDַ��n�+��{�a���`E�=��_�����mŊ���(l��.�����-,�����Ӌӛ �mg����o��@��v�;���9�0��g��_{�ճ���<�t�����;���7_W�����_����߲����×��c`��_y�;o�Λ������y�w�����;�ʆ7�����??=?��l텞�bFm�ru���}�ݽ)� g������e�u.7����M�_������ -o����� �[���-��t}�|h�[�ϼ������;�Ξ�sv��쓫w^�]��s}������˻�5;s�����K qgnT+��7Zw6ǝ�qgs�j+y�m����,=�˳��Tt�����;=��mf�}r�����~�Q1����!e�H��6_�*�9����ek�/ϟ��T�x�-��<x�����_<���&���������Nzs���˝%�Y���Ė(1K�Ajg�Z���%jg��Y�v���%jg��Y��F��Y��.K�������)��_���l_�p�-F�ǝ��ȫ�|����o����@���+ڒ"4�����W��-��q�[Îߐ -4�g7�8>��ű���'� -�=|��+��y��Do[��W��w,�A��]A� y�6�Yl~ػbp��d�Dc1�����t#㿵�79u��}v�to�4����� �������l�FK��q�{|[�J�� ��߽��7y�~��ޮ���'����P���/�)=��(�ڶ�`���v/��ߢ�s�]E�Z_����٧��>��|��(n~_h����Z������<�i�s�v�^i�����c����=���&k�����66lܧ�]D��`m�²w��[�������|���.��V��:z�QG�;xs1c[X�v��ɛ��mT����M;zh��mĩn�!�bö06�{̱a�,n�b�]dأ�{C�������O��_��?�Փ;�7(�����G/�O/7(C��{�6Խ-���g/�~usvsrg�DF�����7);8�辖Z�^�勋�����d��o��t뇂|t����ק��E�����g�,��zM��խ�(̶��vn���&�u&��z�SOw��N=ݩ�����/>v�����*���u�A�����SP������� -��k������ӝ~��O�D�4>f�t��m�~�����ђv:��Q����������z:�Ckt�x�(�_K��[�"Ȯ"��hWeW�`���ӧ�)��F���(����p��||~u�T��|t~|�w�t�������?����]`6{�q�.�f�۲{�c��6_� O��xi$3�6����W�>fB���4`�ڰP������*�m�0�x����k�럨<Y�y��Ň���]����g6}/m�����}3����~�;�{�{��������Jt�9�2 y���,l'�l�\�-�ν����4�] 䝀���|�+��m�m����-v�|��Σ�핫ݒ"��hK�$oP�yW$�^��c ���<�5�xW$yǓO�=\��y��ႇϓ��2�[�d�X�%? -���f��+xc�t���{���7:�mƖG_ ��C�Z�����A%�6���h��[�$.�kw�Wh�&���:�ߪ������ n���"�-��'���w;>%����gk�чMe���F$b~�%����=��9��Kd�R>g�������o�����K��}��/8��p�`����y '�68�_K�<&��ضP����Oh�6��l�������{����ÏO�|v�`L@��o�?���ɰ���w��.|���v��=���<��6S�Nj�y��{�0�';L|����4���C"�U ~}}|��� ��y���+�T�"0~�4�����}��Ŀ3����(|����rXm�"J�"z��<��<�'����?�������;��uc�D���R��N\?={��4���M��a���N����S�J����}zu~����;��ըb�QEY��;���;������*��_��A�-{~9���-y}yLܽ��b�����������T�`�h���g��ӳ�� *�o�-��&�l���U1��I�;�� QoG -%�V����v���g�mIb��K-�h�G5��<���-�۟���Fm�|�B*o��T$j��m��m��}m5����2�8�^���F݄���c�o4s۞x�6� �2���Ҿ黗%ݑ7v{~z|yyz����ӓ��Q�/�k����ʫ]~y�l�gϟ���^�^�����6�G\ұ�m�[��&?���m�� y���9r���K�2=��T��䀶�:�oHض���_O��~���xh�����ӿ\����]y�����;�����j1�j.���W���7��ql�<�d^!��ka�����*�9�ތg��v��_�� -~q��Z���m�<�����u:�쾖|~vyz|���{r|~� 2�G_���O����>}zvs�r���/�k��W�,����ŋ�#�&K}s�J�����&3��75��S�p��"��z�x#�6Xږ�sv�Kk���Z�7A�m!�ȥG��<o����v�Nn�{�K�����f���]:y��K,m���]��.�b��z�{�b�6!����o��K�2=���.mr@�r�v�K����n��\h����oNt��R��v<����[?�o�N�֝�7:�0��X��ݟ>��������v��w���\��vk��+y�մ����������,���,���V#���������_����.2s��.2s��%�����o˷�������]�o[����Y�d�orU�)7|�B��)�g�OO?>������=;=���&K}r_݀���w��dG�,�8�d6ʽ�����|�-y%w���09^]<�z~���-��y���{�˧fy&X���W�����Ԏ��� -����[�?�~��W�_|tu^�����˿{��|�����|�ſ�����������/��~��{��{�����?{���n��g���qI������O^a���n�g{�g��������_�ץ���t]7�wр� ���7Ƹ�p��'o�������?�v9>3�~�~�?u������`�V��Gؙ�����t�x�N+��zɱA��Y��^=��ܞ�����-{������#�.ve{^L>��|��:���;����|^o��.p�݁Ͼ��oR<(1 �Q9�?ǻ�I�����t}9��xZ�����\��-�]<���?�ɕ�.���>ů�|m�G��M���H�'�+���P*� �y�)"��Q��k���ޕBgUgUQ��ޭ;&c�~b���;p%���0�.��U��G7��}��1�9f�����s�ݱ�6F�����~a����ݒ���^NJ)�s ����_����C��\�=���I����ݟ��r���\����~�c`�y����ٮ��-����N&���fջ��O��B*h���1SY\��j'k;������o�է�9���I�}�{{�Ii��W'�>�opIb����*C���x�5b����~���9�� -�o/6�;OV�@���y�w�~��>|��W܉�<�$>��%������X�������'���:�m��_T�����G��������SQ ��D��q��xvu}�kU̾�Zԓ��l�M�Eh�n��P����W_�/sH��m�O��>���UI�?�Wp���T1EAG �_p��]�OWG_��~Ηp�̠����>d�ʼn�e��J����ǡ��݁��fn�{��f�O�2��R.l4w��T��#0-ԅ�z��A����S,����I}���Bt�~����*4֮��р�� �u2�@���r5�D�l݁�)G������*�|���y�4����$Xh@�~�w.�VSK=������Bi����*���J�W�T��TG�=E��������>>0�J1�{�%��p�UH�"v��㈮֓�]�� `�w�5WX��znCW;���L��!��*���Fɴ� -��2T�@�f[�s9gtT��q�����7TA��QG���n|_B=�\տ0��SL覯�-b���u��W!4N�%������Y�WQ��?xl"Ь"T�J=ơ���"�y�n=��u$�P�Tl�|G��z�ϻC���I~<�gA�:H���G����u�u�!C���b |֥R�J��ܡ˾�����+�9���*��|{�z�*�u=�u�]W�Rp�cy�s���nR�hՕxlI.�憐�*�XQ�yN�_T��X����殞���zʑ`������`�tJ�d��v�%��T��+��;�=W��8ۗrT�Eܭ�,_�g�H=��U��nP�V>jV;��hg�����%L�߮���1����!U��%xڐ:�]�;O�CѲ��,�W,&�A�1�@���+�^A��b��}-�W�����@�`��뎸�����SEf��Np��;!,�gR!jXWR��w��� �( U�gH꙰����� -�B*�ޓ�������D���_�������M����,FO\�K��8�ʟ� -j%x?���KV���+w�C&n -& -bW�0�^����dža�N��S�=��=v�����!���X�� m�r���9���zJ����q�J�A*�#t�� ��;~H;us���at'h�3�R�!�&1��;���T���V`�t��� -����+!t^4��u,�ԫ˳�|��B�d�"�(�x4D>�މ��u�$��(n�0{�d�(F��UthЀ��@�v.b�����3�������"_��u�i��`@��'8�W�������R<�oY̫�3Ѝ�t�uOж6˥t�_��HV���8Dc�� -H`L�F�"t-D���6�Nx��9�#M��3�'.��� |+� -���Q�6�,�ǟ��]��>���3��Jp��s������� -7#*$��BD�7���Ӈu?=o$��렐Z��D���Fb=A��i�^�Y=�E�G,?`�@{\IM��mX�m�vwq�c���DP`�� A<%,�_���$C�ӭR3-��G�\��p� �iU!A�`3ɠQV��;_���rgH��E�>)�Ba�x+���Ď:Vq���U5��W}B8h�c C�i$�b�2���<\���A�B%V�ϐ�<�R�>�UAW�!�T����"��R�!��#u� -����(���,L3s�?�Nᐷ1�B6�$Z��':��V�E�&���b6��Jq��#���UVG�7 ��9�_�t�'����J� ŁC��MJ}Gvĺ�L>��@.�ܥ�^�*3A�%|��w���*���BJ!�\�/����ŤV]HIh=Z��R� ��fƞF�{&�u'�?x��Jn�����G*�Xꈮ�I��S����(�!1j�@(K�~*�?=)��^��D�����bL�<.S���R�h�1䁐ЃQAp�Lc1��jfK>a|%�:\���]��3�ORU0Vp�rx,G���u��\�N��9�6S�Pq�Ӂw}"�1��&�T�r���y7�T�v��{���bW���� ��4��v֛!�(��h�_�%����A@X��d�����6_��o�1 d� R�z5��BG,ٓ�����g�5\�T�"�'���H`Y�kU�i+�id���EԵc��Yq]���8g�O]���dU�� -مN/pgU�vDHX��&)���B�3�`��/?ྃ���ka�Xy�jDOV���E���m���:WuHP ���\0C�~�`b]��Qi)$YTP��lmB�~'�=[�k�����c�+:��ı��s�RUyE��k��>�:����E�~%OM��L�]�{?�I��2D����`�a���v��0j%�]�HJ�U��!M{j�����k����i"���wAF:I�z]�<���c�u��s���[�%d�v+��@T"�]0 -\x2�T���i��2��]2�F����m�Ynj�;!k^� ��d�s�R�OCG���d�Y�p"J��Y7�L\5�#�-�� U��ҡI�ܞ~'VP)~���9�<l�^F�Idq,Dd�6Eix�If�$e� ���a�>n1N=?�f�=s�@��>!�� E��Bَk l,��o_2M,�� -;� ������LD���ֽ_eE��|S���B �a Y�=9xE��-�U�����ZR]�}.Q��|/(��0$TD���N��0�6�;@�q`m��ia�%�XX|g6�ŒO���}Y��b{O��̍�K����BB,��fy֥��1��º�����Ā��pyl�]X�W���"����i�"T��h#�Ѵ)�ew�8`s*�4q����sF�V�,��W�g�JO�]�{��J���R9<�>n��\bw��nA��J_R���|aW����XӪ�փdy��� ���&օ�P -�E�ai �=3�ύ��;S=O��Q� T��&����.N�.�_'���%�����V����h��~j�I�\��'�q=�Ա]"^g����@�t���r�����$��U�"tD�ꃜwe<���μV���ޟ�^������#Ͱ���8�θ�Q�� -�H~"J[����1�t�E���ɓaK�bz���[jČ����0�a��0c�-�.Z�<�Y���=���u$�f�a|K�V�=����Y��3�w�=C@H��3)�`\�z����tJ��'ٛ#�=Ib�QOL|3�(ĩR�*���3K%�U*$�ʆ%#c���d(��z�zԟ��β�x� 7P�D��HC�y��4z�ȉ���G����y4�.-]P������tI�h{7N�� �����؈�ߑ�GВtB��V�*�U\��m�#ۙg��=C,Z��V#�|[^N|x�W8�V������⡳���%h��*���ڝDVLtO����(AHL�f��tHK���y7s��ȑ�O�R -��Y�Wx�N�2�C5����G���{��b�z�a�` -϶b�м���>�+M� �c���)�N���n�ߛ9�o�VM}Z/Ɇӑ�L�����@=�߃Y��;Պ3�)�/�3��9�]q�c?��{�#Ա� B��z��:�˘�@�)�=� -c���j��`��]� �.rO�^�<����˩'>Ue� t�؋+�*�xMK�1����d>I^�l��x�G�8v',V����-7q��'t����'Q��8�H�$y�,[�5�2��B�!�˱�H"����q��� �d�"v�����6�S��kT��8���{`d7c��U�d�؋�a�=o�'�o�a\I�B�S׳����1�xĨ�.�J#�~Cff4��(>S�!kԭ##��!����d��9ߧ�^�w\b4��3�T�=���a��2r�$J�4B�TȾ|�s'�K ������Ǘ���$��/$��Z��˅7���S�[�wģ�I���A�'ky�bN�f��'�GFga:�ӳ@pf�@��M1�p�.��x��*\y���s��2_d����9�������}8�7��"��CX�X -�҂_��^��b�.� _2i��$.��n���N��K�*��Ǵ�ӳf�c�[}����E��6�%��]�{�_��j�^x���mٜ��}�_�/"[T�/p�U���H������X���Z�/�P2̶��[�l�dsȐ�|��,f��o= as�.V��׆:��@�GO�)�n�pͺ��BG����}�Xوy���=1�BC9e��U�=�d��,��ᛙ�#a[��D|�n3�����{"�w�1��XD -��,Cf� B8�>ۃ���*Th|܆����1 ��c�s"����������I���&�G�<N�,R�oZL�i�d�eO$��sl����:�2�'��<�����b]�Q�'���� ?���Y��:F42�@��-d���@!'��SAO�s�j���7��/_��<GE����>���S��$ڍ��p� �$%�\m��f,Tgz�c���>u�"�����wq�k�l逌��S9,�X֝G�c�F�,BTfq,���Df����2d��O �9�}���)�& /!�]V�`n�ůY�C -��wUc��fcp@&���@ �Ts�Y`� W��lm"ܕ>�ψ���EF�y\�Ћq��"�fW"�b|�O�Ű�pKߪ�6䨂��e��� -C!��ViB��(dT�&�����"^t�/��"�*�PF�D��՚��ߚ��e E�l,�j�%w�5+cj�&�0��,�#E�$�� �/ �R��'Q�+�-I��0��Ewd#A�c$SD�� -1�\L�WP`��$"_���zUAU�����s(�8��ڽ#���Y�Dt�_ ��s����}i%b%#�cT�of1KA3lZ�>���N�ReH2]d!�@��� �\���Wn��DG6?WƆH�^ �����;L�f�=���Y��l8_�©X@q"Ոv����HȎ�TVe���PL�U�A,5`�R� �8�(�9�uG2�G�F���i�a��H�����!D�d���腢J�+b��L"�~�N,���g6��B��YJ)7%0�������2%^SO�jr��"���Kll��8-��VGJ �P���Q����f�V��W���*��'�x�Gl�|@:��Q�ܫt+�ȳ�����$.ZE?p3�W�L-��W�𬓑]�<���/^Y�/KV�/bw���/5"� -��t��l�G(�#=io�cw�e�W=����+sXt��:1lTJd�= 6$a4����ŴNƋis��n��c]�"��(�A�%b��&�����8��^���;3�_{����I�ۭac����ȱA.;�B��D/�Y@��V -+!'ոnz�9�,6A�A&�B�qTjLF�^,Y��>������#��Ay'X�ʔ�V��$���p;ᘸy�<FnFw����o��;��|�$6,^4�qԕ��,Q�������D�F!8�HE� o -�qHpn�wB+���c���'!S�d -��H�� ���"�D):���|:x���0<��F��#�f�~|��z!�<�/0Fx�s �� .|�".]Y��$��Q���`=\�ks��qrt�fh>$?��P ؏�����/���w�tR�IR(�h�η���e�E#���~vė){���P1��I��Y0�K:�i��4p-x�ܫׇ���_6��xA��'�0q�sL����/���E�� ��9����>�������lD���5� SG|�gQD�g��X�U���礨����=��j��.���J�p ��3��B6>$�aOD;RdJY�Љ�BWD+����Y��"�m�vB�VlE�Ű�4�D�"��EX�"piCtB�)�@�E(����ظ@��!�G����/��-�n�id�"tge�Ϫ8�e��,��D�y|�2�dhB��1�������8�G�o�1˸���]$_F�(�1�"� )l0�p���3��/��7��@��9�{�]'N6V��X�l�]zvgn>��μ��<�S����JG��_�f>� �2�gA��+��ud,�K�� -��O��Lm�2m�3M�����ZX�囹Yzn��ڶ����Lq��cjD�X�9vs+�����P;���WL8�\�J�)�9�P8�Y�73����53�0��:+L>S�߶�L�Yr����)o!.�m�j���u��ZOƚe��4�j�\�k�P�h�u�4����w����C�߳��CS�DP$���N�%J$��(��D���F� Bl4����+�.ߙ�x�&��W���ZNn���v���3�8���;���o�]u�N7�( G�yH�Fթ���'�w"��I*` VAW���VJ�b���5ۖbG$}��P�EQ�C�Jլ��V-H�I���r�}�,�Y�X���-��9�6x�q*� �<?i?O�����ȥ�&y&C=2�2n�{�`�)�u��D�v��T��dlQ�ݳ�G�h����M&V[�kEZ�0���!�)���94vG�^���Z�}%��^)*��d��8@�H�]AV|J���Kq�P:���Br��w�H[`[��X�%ܧ���I���f���!1�i)��9)�s��9����"s�XA����"lm�������Q�V UG��XA���\iGD/#�|g/)�������f���3�H�5��n�����%B�-ɔl?� �[��#� �$�(>�f��6���R^!���Ӊ:nl��r,�k���z��y��� ���Ϡ��A0�p�8�$#��ۃs+�!s��FfV�ޟ��P2�)CA���@Nإsq|��W��K�/a�����\��M��f�<��ʒ��ʡ5� ��y#��sel��@Rc��9""Z�S -0?g��֪�^��EY1+�;�pQ6w��1B��"�`���ajy�H��tie����@���Z^����*y�m��9��t�Rk]�L6��UA'f�����#.I�QCX҅�8���V�� �� �Ff7dm��&��4 ��o�B� -+�9_[.��d��E}��ym�ћT\��YQ���լ|Jr`��KC��f�.����� �����Ӏڨ��9��B2~xJ %��,�-�{�*�Sօ I�*�ߴC-L���M�I����o(���YH7qK�������91�M�Io���0m�$n%�ű�\I�xͤD�qc!n������t7S���:�Z�8S�S2�Y˒xJ3����������S�I`Fv8����h����p>@o��SA{�6�v�O]`pӨ��P���v~��)��t�a(��a�yM���Q;#a���ޛ�02�]}3?�ae6��jы4���]c[�7�-D����%��f�*i�M��Џ�0edO�(�ɻ���|��V�^qd�B�JZ審�/SR`Y���t� �8�_��l�0rq����jM+���NF�����oe����CszS�5���ɢ���TQC-��4���~9�YOC��.�0m� ��t����}������$�ڹ=Swv�j���P��ѯnm��U��靬Q�e���$\)�}A~�,p����H�Q�����B9�G�����J�4�V� E��MX�� ^���� ����g~��� -��'��x8������CP���N�p�tN��f�xj������d8����&��)��y8������C���)��<��P�D�����O1\!�ٌ�"L�G(��b��Ɗ�$B���6��)��h�,�A��۪L|�qjG7���D��u�c��O�(�Ҡ���tW��C����C�&��I����O@���l�/}�q7}��'���F�F^�T�, -<m$,Pz��|��H1F��/������w��,T�pv���=-��҆2>}�L�a�+�b���Q����"�=����[��ge��/ H� ��w�7��r�SRU�Ճ�ho(i<�G�Q�!J���4a�F�N8�oH�*�����d�#��n5$� b2�kv�ba*o����/��hiHf������7S��l@s��u�(㖔����{:Pe�(���h��rhC��C�IRMឰ;�v;:��J�e@�R����Z�L[���V�{g)��� ���r��!����hv3����P�7Յ�1ؙ K�Xl)���W�QC�5�WA�G"��xz� (�6 ��%�������y7D���`!������^�1�rjA���h���� -ة,\� ���ݐ�5�s�1���wU��M��� -�k3f�t����C���d�k?j.���4�B������� -��}��db;U�.���_����e6\���Q�:��\�*# B T#tƃB*B�����ڄ��O�3��G^D�c�l�U-Pe���R�S��a������\73��`��\��L�#'m��A�d�4[�N�t̜������S����.L0����u� ~pXZ��),�lX�s���ď����p�R��%�Z�81�P K$����5 �>�`(����\9+��A�d�Ym��SF�����ƶ����й��\�fS5���Y<mON(Ć�V�-��B��sE~n P&�%���%\�U� -J�Vz�E ˳ȋҋ�1=(P0JG��a�ƖH2l�C�G�m��x(Ϛ#R��K_���7���Uu�WL< - �yH|���-+�����Ј�bMCw��AM��q��877-˚0�n`s)�'�\���#T�ƙ���H*XXO�(5 ��T-P� ��^�}�l�����]�ͮW%�쌏Gn_|�%I+6��ɷp��7��Г^��v�*A"��G��Gs��'HK�bA0���PNJÏR5�6fP.�&����5��Z�>�y�H�ukv\̅�P�t%�L� ���BM�0��< %�`t0� -+Ǘ X�[�y���\��BΓ��t,�z��B|.+\ŰJ�qڋ����7�"�k۾_�Lt��&4n�����7��]�� ,4�K6��K�"��$[���{M�ED��a(&��&�_^�I�Q�[r�r~�:����E����P�����Y&�!���-6�SƸ�݄�oxRjxW�� ���֞ǒ!^!Ĭ��N�/�K��p���S�Z�;k�Λ�0��qR"t䬹ƢI�����=H坅HdY0H�E�Hd^�'����⟋��H�O7�30D���l�u)���,�y(�(� ��k*L���q��"��=�'P1���ͪ��g�k��ɰ��5���ie ���<��0v -�U� ��-�(�h4?}kM&���hֹ�G��"���\-|W�5��9�j�|.U��#ʻ��(:Ȝ��P�J=p�e�6�Τ8�t�jʩ؆��P&$X�� 14��""Z$�Z{~�ʁxQ)(fs T�wϽ�P�C0�H7Z4e�! �*L�d~l`e��{P��1�[�aPC���+fyR+�TJ�5E��Z< �ɴbO�P�5 4�&�F�Ӭ������c1&9] -���pSPFiFM:� �V=��^1��V�L��Ɨ0G��dy{)���k�����A�b��H�d��������b͠J�8Z����ʴ��+�)"�ZW�P2Q%��')�ȬIvE�K�4{ַ"�A��4а��k�>�Q� ����-�a��q�� Hp+#s%H�CЈʵ8`#���ȗ(��2~�MÖ㟭}6��Ӎ��DN��0u��ۼ����}�W\T9�? P�BL�@�����k�7�[Rb<(�6J��6������`?�@}(o�X{!����n��x�"hnCW���J�րD�*�q�T�U9g#�Q�P�Sw)y*�q2Ҹ��0���]���x� $��ă���V�B��{�r��a]�� -/4��V�x�A�W��L�y�X� �[��'��[�s�2:�<�!sn5@+��~�ʻ�<����$�6 9|�a_���M<��aw��e�],�8���V�;�������r�@"���[��U�3$���,��T�:m�K�X��ӭ"�@:���Ż�r.��1�lI��qWy����%e! \ -���)��oU.I��O���qjnAQ��J���A�,d�oU�ϻ -�*��9�1��[A��*��a��w��A{\�D������8[e^oU.-@��*�:-�fA�-�|..P~Q �.�dx��.���28���1.���R���u��g��Z�Χ[ *s�ywU��UZ{w��������bt��f�ly/�ي} qCE�"�W�ҨaN�k(:�%F�Ev��?,������R������Y*҂��ۆ�3Y��痀� '��{��(�=1R����D+����q_���t衺(Z�=�����b2�p�x�m��)��ْ�H#Hc���b!��B��4�A�hJ軷�!S�@�x�E3� ߕ\ �'M�nA��.goC������j+�����y��|yS���3i��� i=�z���J�*�m4%8ePc��r����W��X˰�A�:n�@�"� �Y�z#zV'C�yCs��N�W����Ҽ�&��M��*z�@�fqV�J�^���u���u.ڴ���U�S40aBBP��{%�AE^Q~�)�ÙG$��������4T>f�y$�|��"4�&�C��i(y��qA����7&=��8�VQ�*8�_n�y9-�.���F��1 �J8�l����Yu�1�f]P"D�yt�ޛ�W�6��$6Aע��$x�7��P�ׄ� �K& �[�,�zI�<�<�}C�^Z7>A?��b.��y���$����h�"����.�au������J�XN ��w��Ϥh����mb�7�/��,� �[�*6��.&� |��y���}�oP��`�S��5�E1�)�̻\�#�bu��p.Ǖ��p��4��kE�M��I$�BY��DZ $'�g�Hd�1�ڄ�D�$���4�o��F5����<#�Ӎ���4V�즈EB/o�2 �4(��T���jNܪ�Z*�G����4�\�M�(F�Z�L]���#�N/zy��S'<��D�M�Zy��Ћ5^��' (�P 4�<��_O -;1ez�Mmy���&�7D���Ԩ���- �^+���:����� -H��F 2�d��y%�� ��T{� �1Ő���+x� 0�h`$H�Ġ�n8q�\#�6�sP��{E�B� BC��]BdC�4�Hp�*Z���4� @�C��i$8�2P�z�*�h%G$w0>�ջ(h��2�X�S�q+5�a�Hz8��)�Ph��]�@"tY �� -�$�T�8d�(��2Q�)ed���\�)mX J7�cC�k"�YK� �ى�Q�2ؚ��k�M��v9�w������'�\�9+�&��`%,J9����6!�����^,�A:6�y��E|Nn��X�k�6jc��$w�1��Mn -[hվh������]��to�O��l�鵷���7��L"� F�dh!��tYJ$�&qHOX��E�V��,��%�ve�D�t�)�vP2�N@I��X�[�y�(0�GA�!�(�x��7�)5l,� -���4�����-���>k%�'��V"�Rf�S�̭<H���"�(�g�ѹs�N$�8��<XĹf[kR�ga�b�tt��`&�X1W�r��K1 �'�@5��Ӓ``i ��%@�%�Z��k�a)�2XI��$��B[d#QY�"���6�|fZ�+[�A��JfS� �L�Hh]���F��#_kb�=�Jb���RtF��Lr�H�Ծ�blHoy$�ւȄ�A#��oP2Nx(��4e�J�\A��Fn�ax��:P<�e �F�: ��'1+�mə���"6� -A�Sg�W�ٚD%,� -y�i>�HQ��K=0��Sz`��ZB�H�btр�������^����Sz$F���y[Ot¦���ܱ6z ���,� V�g{��7 �"���Y -I����ۋ���Es��dkP�� 4\e�p$��������WRߓC�hPAR~�Ћ})&iNs8-�Tw�}>�4OJ��@S�2\f~)����bP�314�z�5��Ѩ�yD���9)r|��- rE[�`�![���Є�%l0�qi����=���C�� t��yFﰟn�g��m'b\���q�vZ����m'b\���6����m�Ÿh;��D����b\�mǸhێq�R�*�q�͓l���D����b\���qѶ�%Gp��D���1.�N�qI�������m'b\���q�vZ���1.�Nĸh;-�Eۉm'b\���4�ڗv"�E�i1.�Nĸh;��i1.:oǸ���i1.:��q��D��%1.Q;����ɖ�Oĸ�|"�E��b\�!�Q��,��q����Oĸ�|"�Ń���!ǸCeca����Oĸ���i1.:��q��D��n=Q��Oĸ@,-C�:�ϧŸ�|"�E�1.:���m&b\���4d���'b\����Oĸ�|"�E��b\�mǸh;���m'b\���q�vZ���1.�Nĸh;-�Eۉm'b\�ě�Tc�����=t�ш�j�{J�� �ji�ImȤƮ�s��km�]��4v@[;�g��{P��{h��'�YcOa��=ֱ�i{E<Z\���7�}����[�5�茼ƞ�$k��h�8l ���[�R�}Zz��ص��ؽ +�ص�����Z;��&4vm'4vm'4v�&5v��R���Z��{P��5S4� (3(;���ȩ�NS���i-��N+N5�tkbhKc�v�k��x�=:;��G'�5v�5�=Dc)����MYZ���u�]�;���C{�hVē�i@�;mv��ӑ��WkR�=9䰊���F/��SL��jZ����أ���LSlk��5�x�]1hRc����Z[cO�#�^[c��W7�z�����ͽT<���=0}��h�*J!��"qD�~&��ӌ����T%k���MP�A�J���W� ebm� -�S�k��J���G��K@�+Ź% ��/U�D+�f"AB��@&h�V�h�����5�z�m�����.�#�m�u�o*ں�r�Se"�e��}j���cR�Ky=��(k�� D�s�3�n�.ڡ���Er%� -��e��n��VX���]���*&(����Gʺ�0dlh=�e݁8i���u+M� -��N-��u�¿(6���qCN�B�0/�Ga&��^x�f��[�[el����c]=�z]����^�C>�c]��N��t���t�B"ǕN=�uɇ)�\hXG�&��5�:\�|���n%Y���ٖ�1V֭�XH����u+/�j�u+b ���[.�H��{��5��#E����m�v�`���u+���n}!u���(��+������Sf�讜y��'�M�V�!��)U���s�PP�-U�bP����ӱ��������[��TP���^�zz��tܐj%��DO/�,�����r���1��@�[э"P����zz!f \E��/����k��u�zz�O��;"�����k&�H��)�N���[�D,*k���� z��8be���f�zz.A&J����W& <��&�D'�cF^,�Ǝ���Au:��͕2R�8X�τ[)Ĥhe+�˄Iw.o��b�eU�R�ɋ�8�X`I����6АE����+?l-l%�"VT����Dk���4b��Aa�,bT�oB�)keۛ�SPB2������N���PࣸkBItJm҈ ���Qq_�i� -廆Hy��T`�˒�r =��sD���6E2H.o@T+��Qqz&~���$N��6P{Ⱥ��Q�!��+��*O���ì!��@1sJ���ZP*ŃZ�gz��T+��g8Q.1]*�`� �C�K̘J��_\�S�)��@�e[�"jXʁ6�㤕��M,=��#��tb>.1M'Vy9-�N1��)����J8�k�lbBAZ�ļ��� -���>"�� &z-;�W�R�.�5�P���m��5.�4�PY9�&�<!��֝��#���s����J�� ��G��J8m*ƺ���[?�Io��k�2�e�Z��ݼn�k�p�Z -#lB�SXN/�b)�|=��7Q�aJ��4�*��|�k^wZ�r����Z*D�1�1A[�oߧVJ1~�uG�6%|�y� ��>j��M��jI,�p��t���D�n$�Ə���ᆖ�?A\���1t� Nj.�!n����1��?F6�D��.-���=Ւ�4����^s��Hx��D5�e[ڳ����ɴa��T#�1(�(��<���Dy (cT ��[�D(�<�Hwr�uz�#h��`o#) -��P 6�P�&25�n�S��N9�L��&Mdk6���܀b[s���MN)��9e:jKϜ�7��`g*�s^��,�&�*��B��zc3�<�H���fBP��Ni���Ғ������.deE�o�͕'屵����/g����0\�y� ��m��vB�ȓ�[(�T�sZ� ���=$SN�Z��M�̩ד�i@�6�@�8��H�?��ƕ��@oz�a &p���`qɦ�?eiC����_�j��� �_�XJ|�jŠf_b�l`�f�è�qx��ֈ �l�}�X����f�ƈ� �K��B�w��M�(zp'�R��;ڿ�>���`�g��D1�-2&XR�����a)�f�X�VM���2�T�Q�1qi -��kd�(���+ ���!.�J�T�(>��fP�(z��$2grz]�Q�w$جu-ꚡ��$52Zp�&P��*c��;���F�� -.˚q)h��<�:�o -��m��\����n�Be�j�WY�^���/q,�)'�!�=�A�R�0������3e�8hG�*AU/�T�m���u�s�h�� ��w�������ҥA'�T�}$�j�9|PR^=� -�;�����e�o�]�;"� �S��*6�X�� -�C��P�H�{��A�6a$�CR鞇e3:��f!��e''��AZr�"R (XK�TCL���#)���X�Jyr6�J�)������y��eB f9_��6ʛ��^ӵ�{1(�����(��� -d)!���es��4��{��I)�>�`��1�qo /h��x'o�y��&y����4�h`#ڳx�b�Nn�n�W�K��������S�2>�n���0j�R��"0�bŦ�&�Yq -B���+��w?gVG�c�Q��o�b�>)���=�'բ��Ӎ�ۦ����3�YR��K�P��9���;g^?���4����E�� �kc���p����Ĝ�H~��B� ^k8�"��{C0P��Q��(l0��K�YYޔ��2*��k�q��d�ڂ����&�_~L�95G���p�l9�{�Q�3���������Z"�8S�[$]7�uf��/[O���FB�� �QhVF�r�"��OKv�6���槐pM燝�s�%��j�P��Wd\��tw��������I�X���&�|OY�֗u�N�(�@k�x����'�n=m�=SN��� |Z|�I��M�V�S��/_��<�����\7� �������݀�Tgb$�~�t��"�;_�2�כ%�(Z%�:��0�KX���U��a)jϿ�U�/M��oE?y�q�31RX����*���1��w;�?! U)������nsm��=���c�J�$=��R�K�2���1$E��R�K12�O�G$�n�6ƩJś��oz$1^�(��S�gj�lz�1F�ȒbR��7 �����w���9�i`�h��Y��mR HPD��|� >inB_�%�'jC��AS��ǧŲx�[ �6*�H����+SU��e�<�Tƶ��=�=� ��� -�xYBB���-٥L0�/Q�o�1L<F�G�&O>��w��e�H�'�|����k�\♲+S�E�ֱ�TEkg���&Q������V�H�M"eo'Q;A�d� [MS�����S��� �L�T�&3A�伄�d�D�6�M�d���ܿ�<�-��>a�7��&7b�"�5�l���^�{��x��jku}��9 X�91��>�̜����/�k���Ak�˵���Qoi8x8��h�8��Ή��-jm�����������ָ����rc -U]�S�f�q��{x�O\�{[��/�6�^�8\�����h��xu�R]E���y��W��M��$�}��>ފ������������/�qv��h��̀��[�ȃfv�����y��������o��W_�ß})�� ���K?ئ������_�w�O�:��:��d�e���C�Yw%���ۦe��Ve� 0ࡓK�����y�e)��bj:f��K(?��/��-3h�A�[@���6�np� -A���D_����������.����Qom��Y��W���Q��ڧ���?�Vܗ�n��(��v3�%;����ZUVYޱ*�n��ss-�*k�~}��w\s��/��J�8�����u��ש�#��J���۷OQ���<�K��EZ��ͽ@�g������L�j�8|�>�yZL�)&��;p�F$�j>jf��sv���ю���!i�SUts<{�le�`��Qwq��̓�+_�۷�>��}iʬ'*�Ӯ^�u2�����Ym���dD��B��,�PR� � -<�Xɞ�<̜�}� �S�m �D��[�2���c��t�Q�ӏ�r� m�[�b����/����<�|A6��\����!��Ԛ����~�e|��(F"�|�n �#J��R�ߍ~�wك�] Z���,��7��Մ'D�X������� -�/�?)�Z@9��:�Y��}�k�N;�ڬ��? ����?3Z9�l�|�W��Щ���S�GKz�����l�ѿl+T����je/�*��b���N5P�d�q�P��J.� -h�l�n�d�g�X��ޖ����s��x�Vt=g)�(YT\��M�]]"f�(�V\�~���S��EN�Y�f�������URfE�t��E�c� -zu������� �l�ϧ-��H���R�k.�x6�[��$��͓�Ő��e/5u�� e���J�F��<H�Q!z��<�^^`�K˩��J�ze��U�l(T��ٰ�M��q��Uw����� -�jN�V����Ε��Zj&�P��PE�J��=���1A�( -�� 踊��v�����=�})�1�g)\�B�ID�n��z�A,%o�(q�JG�Б;~��q������i̖#�<cYMkt�Y����xCL��Xla�#.�)��m${��D�M�5#}����.�n��i�s+u�<J�ڣ��t�?B�[ �ܔau��^Uq�Cjr-��B{����]`�h�O�gʊ_v��d�u%}&�!O��e�~%%����-+�ȿP�R��(�z��r��]0�-�C���@nz��G��(������!��"���&U�6� --Yܰ��TDžQ����9g����ܧ�u�V]���B[I�P7�p��b%�gr���O2��. #�w���VSL%��IKV+�jp{��0�{Pr��6�Tc �s-��}ʬ[MNK�X� �=��2;:jA�ݡYɆl�F�R;�X��K�OK,���j�-sc5g�q�bg��r'~|i3<��w9(E|�x�r:H9��k.�W S��"�<bp�:������t7�I4e.Ue���� -�8[�[��B/Ù��ORK�綐7��(��*rθ½��WA)����(�@~��r'��P;N�Ws�!�4�3��B2�T��>���pE=1W�Q����"/���Y�edHM���#ʥ-$JڑTJ��K���F9j��u�H {A���CE>_GH972��t#�L/��<�r�&���0x��Oq\=v�@����J_��,r�R͒��3X<�,5���3Tn���q�Tp��"�>9��Vbw**��1.cih�HpC*���i6�Cb�'T� �ե��x���o^�\�U�c:�� <wr -xp�(!(%]f�Q�ԧ��a9��`$�Q�y���ʅ㾖�(>H��W&��l�}�_��H�*��9sI[1������Fi"q�/�6�t* -N;j}�!1'���MEB��⎞P�!��.�7G'(M^���Q29Q�����ͤ�d�˳p|X�f� C���� A��+cU�Ƅ�(�(�ΠU���`u�w�rM�Y�(�4�Z�7;�Erْ�U2RX.gS"I;�ݒK~�(��� ���ü�:![��w7����戭��Ёkn]R�v*�h�WNo��&w�f�p�Ao(����0��b���%�u�_i�⑀ǐ�K$�&+C��J6t=��5�������r�դ)"�4tO����8 �@��ơ�vs-���U�\�[�;�u#�]w���)�N� -�R�9w��82)ֆ�%e�u�/���i��'�d���~��� -;@(�b��&g�@U�n�������֬Y�ù���3�']�Xb�y�ꂋo�Ei}'J�XH��hJbgQO7+�䕉.�F�7ʝ+ď�#HC4� ��ҭ�kU!��B�D�N�-'u��ؙ9��C�}X���ؕR�0�T�iZ������j.p%�d� -?HaAɲ �J�W�RLS��P2��r�A@L��*��#e���RK1Ip<�bAQK��*�@苫���OC�1=0g,YC��@ '���]�mZId�T�\UJr/�#i9���DN��f��pE�J4O4L�-,���P#Ҿq�@+�$*�������A�g ��<�n� -ϻsH�;e����N�"��8�^ ���8�D-7�[�;l ��"���U��+��P�x*����9�5Z��V�8h@��ZI�-\V$i��co�T�I q @�b�ba���E��`P��Z���)V��x�T�(��"Kf�1t79)%Zv[%I{�e\��z��v�q|�hA�'��ES��M��[ʧb$)?�T��4)���Gw��� �����k�/ؠK��膂����T�K�L��_еf�Z -����3_�Rq� �$��ARV7$+�2��i�r�+� �&�"�B�xH�C���U�� /����RՒ5� Mꫂ��hsF*�I+��q -AG)N��ͦ�Wt��*y<\�Z����+�@Z���)E.)>ʜ`�ݴ�ҽ�('~��T���k1�ʺ�} ����T8��R��黕f���}$p�,�!%%��a��:�?XV{�GFR�xA$&�G��sü����ʺ��H�uF!`�LKv�!��R���51r'�Un��U8ǀ�E�f���UƂ=��WK!�JJ�/_�4ME�|�J�H)�2�z���Ί�)BQ�����b/��h��=%�r�'C -R���@����D�R���Ӯ�� ��A�vpE�ڧ]vH])��n@E)�Yt}ƏZ���:5����d�CI�sB v�el�n�y2���� -R�c��M��-ѡ8��&㔿�R����s���K.�'5�F�B�Rؓ���T\�DdE��Z���A���*�땶YU�]�yj�n��f��u�N-c���N�#B�����jB��3*W��Rg5��>�<�ó��J(&�hNT�de<kD�P5(�%IJ^�h$�c�>�] Z�3���6$�rm/1��,��sC֞Z�8��<U��3[�W�z�$/Η����5F�l��2Ru��I�2��9������s6���\O�yk�"X1�s��F��C#�?�Χ�dEvsz� K -I�A���Ȝi���'EU��D�q'f[`��S��L��EΤ_�����A�b*z��b�J�f.�bD�wr$�wGmY��#"����c)I�����P�2�H�0OS�2Y��<�QC�PSjM�K���(��2�~KkJ���,l��)r���o0�R�O����!�R�@h��UB�aM��p��-`��r�+���p����H�l�c�O4S���`أ2n�E��dRS�,oq�VF8'tz�~M�7](v=������Ij�3b�M�7��Hь���u�b�/"|��)lU��0����鹕�d�FUT�+�8�j���� ���˅���H^B�I���Z���* -��L.� �^�oA/�q2�e�-@��-!� WE0[��a���0���#j�Uj�%�v���7��sV�N�ӂ�-M�,K���U��k�����!�BAbu��I^b��u_S -�8^�R� �A<��o"a=�]����� ]����~�$���E6����6W��D��-+[xكr��,� y����c)�(��O� �� �{���`;u�5t ����!�X �^q�"���p-%���l��>]-�:���h)ʕ���WDġ��$#��R��~�T�F��_�>�|!��~!N�ȝg����n��F��R'�<�O�nA���daR�0e�T)/�s*^� S�w���{��sv�3����G����^��U�혻����, ��iίC�C~`���X��r0��乳p�B<�!4�� �"��[��q��(Q��7U�S���Dj�B�/`���kE��R��:�����я0?�+ʒW�F�r�H�]"°��r��P�?���� .\�� �D�*�%���"�!8c|Jr@!������T���2�+T��ѵz�"�=,��4[q!/ĊӅ;�t����f9��W��ëJ��#%�rPI12Fr�R��>�W�'��L?h!�l�sh�&�Lή7[����>3w���*U��_S%�n��?�.͂����嬺�������b[f�6I -n�e�R��d-�Ĕ0P����hQ�)�P�_��%�jͲ��1��z����B|�H� �H����%���{��W����R�1N#�;b��!����a - �4;�����$ -���L��C��R�3�u"+�tq -�����u.>ˊh6��M+��Kh'}�C; �;�����+w�0��0 ��̹Ժ����w��Vn�0%A�D�K���(�~�^W��'��R0r�en�.T��9�����ӅX���̰Q�cdL�>�B���u�s�A��M7<�~wH��N������Ctr��A D��}�H�K������tNK~u�1V,�E+߆pca-s�v��LdYה��ev92���ܝ1DY�luX��` ��TV�i��p�R��Y��jA�»,�qj��j�e�L`ds?�,2>k -�-ة�Mw.�X81�@a��j Ч �j,��E5����`$Ft�2`�/��S�>#9��y�v)�M����F�j^�FR���H�Q%*D�:�;Tg#������|��G��l;�c9����-�w�� ��l�R�@4�7�C����I���Ṙ��i� -��@�a��.�b��H�H�4��0��_9���١7��!?��9�12R��ߩ�ӑ�m9�>�!U���ÚQR��ԥ$#�fY"����L��k2����àK�?c -D�}I�`����`�J.8��,xB�i(� 1��s��R��C�H���T�[�a�5�T�����6X�a6s� �Í>�'��964��)�$���|2�j2��TQq��$JbG�{�����������5X�9,1�I��@� J�(��5�H�����)v6�P�hC�!+�(3Q$sÚٺ��uYIO�a- I��D*:�HZ�%6I�9GIX�}�heP��f�7�H�e�2� -C� 1Q���y0�BÀg��L*L�)�:�R�\�6�R�b�c��1��������p��KD�������Bp����M[A6����M�{j.)>�3������*Ha��!��PF$1���&� b�nK���"��� h0���jA�O{���=���MB���������Q -�܊��V��+ -�%���PLEn�����~?�a��]��Т$�qɉ�qt�&-�+ !�c0�?�ʬ��H���"��0ec�MƚW3K��x�C��0�d����*�PhYM���Њa�U��Q|����{�=�|���5��džg�������h�����1謭/��ÿ���P�/�)�I�=�H5F�&=�����o��j��Ow�b�;���ܿϺ�[�<�6����e2�RPY>�"8�&*������n-������_}R����@:-ȣ��l��T��y�X6���b�=�!H�5T� /`��L&YbӐm����U��nI�`�M��>�1��LK2<��U��\Ei�kȍ�r�.�� -h�0kA�u���*v6bO��흤��r��`��)qlP�< BM��P�A!̂�L��3�����<��D��4oK��"�k��RR�V���("-/��ug��P`+��\�J�U��)������9�fΞK_v -. 6q$~-Z;��j) ���L*�F( .�����>e��NKf����vN,�+=Z���͝����o��K7s)j��57���}]͙3S�9��pc}np}���ɋgӻ� `g�T��&k������<��C�"��w�v}ZZX��U�]/���)�%��Y�v�R�BC(cy7|T`�p�"����`���η���O2(F��a�5��r����^M��>iHN4� �kœ�%� Ԣ��*�R���;��+��cDbu�RR�TU�͌�0}�f)�>���#�o���Y�,��2��}��U�k���x�wD q�n.e�@���Mj��.U�!<%�5�AKIA_T��W�е���j�,���E��8)�UpDp� �d��%Y����r/a��T�4��UJ�q�)� ��\p�I����|���j����D��+�Qt �;�3`H�䁉����>�̴��z^@N�T�B`;�����LF�ڐ�ё>$V�4T��a/�;�;4�>鼎��S�hR�yל�9µ-#5���4�=��cI?S0g��!o4��7�`�������]��a*;c;5ΏJ�B2��+�G*�L��ѓDT@x>�� -�����l=�t-M1[䎱�b�D|�|�d��~��Xs(�hE��+��ݦ@-�H�z��ٚ�uTo��!�]��,JAT���974{D�d_ b�һ4&�.�J_��`�'c��Bg3aV ��l��&CDrN�ɝ��X��2�(�C�T����HP��MQ%7�����*�)ngf�fQ�L��|d���D@'h�!"�._��3��H5<�@, -����]US�&r���OV`b�0� ��_܂�ݎek�*��zI�-(����D$)f�@M�h}A��d��R�.$��#Ą&�KBMJk�g���s�*�]��3tq8,*'�`}.\����Y1�,�ۗ�U�� ��L���!�R��_��O�[X�>fɒG��k.5��Z.b� �~ Dh�W� -���Xnj_+�e��(Wl -�Q�i��M�c�8Ȩ W=�&��O�(�7¿:������-�ek��2��ə��ly�I{���u��qNd�~���&��P=f������:����@��\�F�1�z6u�A@+Dt��0EX!���9bu��%�V�\�P�S���U%{��A�%�B)�L�#8��4uy��f�!l���o��C�8&�>��9��C���Ò!��+3l���d�u��qơh%f�L���\�WXE�:1-�V��V�-X�1�7Ƀ�d-�Mg���V�f!G�,q�@8b� �%��� -��(��RALJMQ�fN(-E�UKl-�� �'0�̳�9���7N�Ĝ�o8�'C�9vkB1P�e�6��]0P0���b��ƣ -r?S|%�(%8Io0�Lⷁ�k��c/���ge����2+IW -��@���S����'�7�]]���0*��S�����^���Ŭ�eD���<�q���;?�u�\�5\���A[��Bzb�>dU(� -��z�'ʞ��/b�:'���j�TU��k֖IC���-y��()��0(�rf�|n��Ӯ�J�Qb�$��N��B0y����(�Dg.�Z�pNx#�� P��0N���)FIwf�� A�*����k�ɡ�����0�6t�9O��E�Cԗ)��m����k���,���ɒ�����6DBx&4ï�Hy�3My��� /���A�5%������M̦�25zo�0<�;�k���Qsn�)����u(b�ױdַ$Pr�hz-b;�c}�,��%9/�<� �Jm=*��@�ڣsA�b-@�s@%��Db¾9`A0�: &#%�{�D!u��U�K�ښ�k���*���09\g�rL��7A��JX .�e�y|fI��$BU)���d?k=��Ȏ -��aP� y�6L��`&�#�1Ps���3��厢P�\)�K�%����@�*�C�@��$�c��{��� �(<�dm-� [4|�|�@�i��i"5����G �Q(�.�ex�F�`5���؆�u�_7�(����,���)л�*��Pc)���]J�rN�*���̰)�S8$y!az���] --�!��`/((� -Ԩt��T -ů9�2�(r��+0.O�1�M��bsU���\������pu���FJ���b��uŢ*M��� �VK5�,�Уq�_g�R`.�KtB��]"��K�U��]���� 9��3���FoCH*���C���D'��ELA0� -n:2a -�� t���5G�R�VV0ɮ��)UL���6�i����d�,��� ���E~���0��ħ`6�EͲ-�8�m##��vTA ��DdAb����&�2'�o��( �Z�P�/���� ?j�dl��z��[~k'��������A5.��T(���ޜJ̚M�%�z�2�P/$� q_0�YXq�2AJ2FA'�p5k;�l���cj��*_�������G�,g�QNR�'�Ƴ8˷��<�J�c���!d������ު��@�S� -��À�~���䖕u���=�����*������zlY1m��R؛��t��}��~b�U���� x�zC~�Fvz���yɱ��E~/#��W+z�ˊ�F���6�����l$�w�h�S�l�f��qR�t��~ j�#Q��rp=pd/�!�1>�/4�i��F>v���V�l8%�A��:�>֓X8:�1��?`HT�tH���P&h��-�����L�4y�NH_Rn��ұlC\q��`�M`���N�����c�?sk0~Bo!Ϙ��cO�F6}�q)Z�~���蚛Ж��������\٠�U��k�m�ϫK��[/�`����Ҡ37��|й��V������������Օqo��j_��X_�����V�sۑ�ۺ���N���h�m�'&���5̴>���+�����?R�ֹ먭��>p���t�q�ױT���=m!���T�Wf�_� �V6!�ԟ~�� G��̸?�!ʼ�Y��g]�2\3����h��g���`���]�.����?��ϙi~n��z����k���V�?c���Tv�h�+���L����M�ؕ�����Ǐw�)~�:9�Y�ܓ���&d;E��Żs��ͭှ��������E����=�mZ�{�7�n�bcErI�������ޒm*�������{O�)'`h��������N�u2jO�h&A�>�z�������ѿ��/��%u�]��;��������[\�F�0���_���'�9��v��k�Q':������Eƞz(�+C�9!dYjC4�9�7<��Hk��{��~�ӿ��G�O���;:��կw��;_�i{=E� -L��nۣi���L~�o�����D/�ve�C߳���*5�;/=�����ڀo7k��;�ŭ/�'О�4Z��~��G�^�@y�d�g��� ��ļ��_�UQ����Q��0��B��6�!h��`usui���ŭ^��}d���ڏ��>Z�^�:Xo'�vN\덷�~ʉ��O2M^KW|5��ݎ�o]�ם��L冲���$n%�������a(�f��iiE�)��O����qk���#���ͯ�n�39��n�����z���`�(-e�O�\�)�r}�|�f|���ux�tr�v⦓%!h�������1����4 �� �u'0�x��[Oy�3G���G ����͍a�K��讇,g��:���4T��?�N�Y�z���"���{o<��5;�s���M���#���;�d`ȳۚ����������e���ؓ��AS� ���)I���s�s�h��\�jWt��P�{x�*۳ -�39���6���o�o��xc����>���1�#}b�`�%~τ�2��e���[O'T��R�JV�y�b�\��~D������;f��TCz!Y�����JhS�i�Jh��d�E���-��S��~��/$����o�<�?w��Σ��<����߉E��յ�a#��q����)9i7���֞3�y��N&�%����<�Ͽ���o�����)M�ߢy��B��Ɏ}c�������Fg�Aoy���H��� G�A���� D,�6�b�n�՜��3�`}�#o���K�o.)l�j}��'��<h�7~�ڴ7z��]� ���2�b,�1m���d���1�,����x� �C����t�ۛ�k�s^�t t���# E��=�H��w��x��=�����P����Σ�?z����{B��^�w��H�"0J����(�y��i�m)L��o��G_v.�6>+v�o�%pN�{pƣ��6g{�j��=bS!�@<3������D{�}���� W�h�����onyH\�����is���t����i�`������_�<�W�������_p�� ��������f -��Ć[�רּh��l|Se�3��o��|g��ǎ\����o�|��L�E�l���y�'_��~�_������{��!�90���F����k�*��:��D�ƽ�+a���l$�}ӛ�XS���������,R��xp8�o�dh���\��N�����1N�����Ԉ -��:���*ڟ{���d -M��;�����?�Զ6�N�eo���7Z^_[��`�K��_��4���h��7�^�_�X졼�>���������f�ѿ|A�$�w@#�����d�C�S&f0�!+ͷj���Bm�M�P�}�B�7�B/���<)�<w.}`�w�r�̿�V|�V��"� 1c$�CR5%�{��_��烼�����̌�O^l�#O�F̌X��nT��c�� - - �g�Y9��o9��}Aa�IQƐ4�9QT%�AY�r�(�R���5�}�F/�>7�(Od#��x�F��{;��[7�Y�������QF߁��7?�˝�����O^ğ~_���˽npmj�(�A��Mg�5E7�P���nT/���H�[��]ޣ��;�����z�ѿ��_�r�?�����<�����։�t��:��_���g��(��f��`��y���+9���+�'6[��3[~o��7x�������Fُύ�����'Q��4͵%NR��"I��y�˝G��y�c���ה��/v��6��&��أ�K?��ON}f���y�������J��'��x��߆?~83��K����D�������g�Zڨ�6�u��_�[�_Dm�����<��j5�lތ�M6�va��G��o��0 P1=nzʁ.����Suֹ�~l�F +�?{�a}t��.��"Oz�6��톶�je� -ç͌����#w32�b��� ki���N�LZ��ڤ�ݭY��+�.�*�E���owh�l7m��}t?p��V�Ҵ&!�jͣL7���{+�L#���Ӷe������)k����8��/(k��/c=��IS�Y�qSFO������6��>Ը��vif�n��i�ilzZ<�xQ��mc3m�N�V�[Hj���U1��y�8����r��έ���1�+Y�ݵ�Y��ַ-x:��)�=v#oM=������:��J��0�����ϥ��n�\����4�B%;SXOLھ�ӌږ�%� ��;Rѿ�m,�TJ�Z�<�|g��*�3ρv���;��_{�x�ks��G�d��������W�n���O?��t�G�u�8����CZ�x�;�����?�"Yӓ|����Ѡ��4j.����~�,ΐ��_� U��J*�0=���!l*�t���'�Ҵn�,]ê�Ҳj�,��rJ˲�Ң崉-�4��Ff����z�ŘvK��bt�e��b�D�|��ms�������6Gɚ�f2)�m��S�y֛|rB&���]D���G�T��$'-��fH����[1��Fe���\YX��ziZ����Hj��!�iob_��:�� U��=�r��z/�)�Ϩ��������S����;j�����~�'�����/$�Qğ����̜���?al��?p�ӊ~ڹ=Sw���V=�����w��������r�M��щچ2�=V-}v���6m��m����p �DM&���W֗������kÑk0����.mo���J4�{��g��(Q������x�c�3����VT������{��7��q��{����ܥ!\��Nf����;�UK��e�lu��r[�8غB{r�튛.;Z�6�d��������>K�����ވ���joi88��8�����ɉ���N���@r����������oEOn��d<���c�wqoQh������*o���s�߿�|s�����q���t�?��<8��M�ҭ�lߵ�pu"�4i�\���w]_�>'�UU�<В�|N���캞��-'T,�o]�:�@˛����D����+����bk������d�o<�W��<r ��Z�z�L�/�Fo�+o!�b�6��bsc}�ݮ7\�(k2e���������%]<M���Ι��������`�c��Ϟ��Tǯ��p�������x}{cﶫ����֠��_-i]w6z�qgsum�5����&�c��ί��vu�5�Ѡ�Xh�o-������u�D�s]�ڨ���¹��O4Fm<zxa���7U��m�a���Ta*�z��|o�yos1���(�������[�s˫[�����֗�Q2�� �eP��k�qp~��qf<�q��4��}!�,.n��%���mc�M��C��I�K��3��qB��T�,�e_���e -�5]J?��yN�?Z����ow4nxTs��u���@�k���`xc�:O�gum}s��?�0�������C��Wt��������������w&�BG�N�������E:�N�`ݼ<�]l>�.m���]�����i���[�f��)�99���he��2�\[߈�N�~�!:��/��������Dž��Rox}��=�l�bB�k���ƀP��#>w���bB'��;?\_��d2��;���pu4�l�K��P�ݬ�E��ᙋ緇C�-Ry��5��3yg�����X�P��e��7ۭ�,���5�9n" -�!K�k��];{Mݩ'�Ó���������������7�Ӛ~�`��`צ�M��0�I%2���wz[n����!.�f|�vk��/�M��������s$�� -��b@i��[����]����l7_�;� ����E'l̃�\^_�&��F[�'��6�U�]&H7�Y��eQ���B��r��S�����h%,(�v��~讘c [�Q��������~�h6W7z��k�ƹ*U�N��8��^`~F�/��9���� ��ڤmn�2�eD��S��H�q�����Ii����j�Y`�O4�mUWj��F�U�"8ļ���]�ɺ�H�����v���0�|0�����hy������V�Re}���Ԯ {���1^t��A}��Ę�ez�\�N,�/�#�F����ş؍�mx9w���x� �����n8)��/����n_���> �Z�^r���u��븮 �*>dL�������8)%�O��C�@�r'~�_7�{���Y���;��_�sS�`O���ܸ�7���K�� �:86�Dă_��+�[T�qY��);Y���;pbbC�@���CK6�x���H�Tp�t�?��6�����N$�Bv蜹��j���E@~8��1�w��8��7/��zz��m��@����n�n42x|+� �/'>��8�\G�j{�MH�l�T�ܵ�'�9��o����;n����v�=nt��/�A���h��ԝ�b(�S%�)X������{������1�N�lOl��� ��4�|������E���Ǝ��7��x�����!�3��}��7 -=ɲ|���(!����KG��;�2�ߎ�)��k��=Ok�s�˽�1j���`�}\Z�Z�Cj���6���������T������Ghkr��z㇛���3�����i��6dJ}�=�x2�3ImW#on ��l]��;�n��t���鴱���<|^��<�4]�!���X��u$��L�K��ꩇ���*W�|:������b���k�u��}��ֺ��٪��}Z�������4��ؿ�pl:�Q��~��/���?�������%|mr�����*��=>�����v�I��f�؇�,6WWFӼS������&�$�A�6���w�3��ḻ���6q� -�i�ׂ�4�u�5��ڗzj��rw}�\A�)F�v��N� u�gX�ن���Dx��72J#�i��f� +|m߆�����ư�6�N6�6�Dk�h�ia��n�U�#�6��p��`*]�c �l3�����=�����[H���gr�q{�, ���~�F����~m ��:il�v0RPf��� -�k�Ro�o����Q�Gí�a�uOŮhFh��>�Yڽ�8��Ll���Vwy�u�F���=I#�mn/�k��ܦ[nc�zm6��}/��+��U�K����g׆�G����P��G�8� -���zNI�[h5m������ﵚ��8�����g��u�� -�ޭ��D�] -��܃�ζiȾ�}�:v������Rk�.-�7�{Qj����S�������nu�GFq��f����������p#���ه�P��h"v�[�پ:�Sr#�2�no�7��s�I�)�co����BQ���T�?�[vRge����9m�����`|�o��Bosˇ'\<_���>�co�,�{Fp/����S/�N�蕎�f�'��`�'^��TE@���*�N' -3�Ͼ2 �WG��[�*lG�^�����&&��U��3��/V���(D=th����z���z˜;�������|P_�`�W>\��С�|�����'���'��������С���J��w_v�����3坕���M|����.*�ӫ��ׅs�h6�^]�����m��k3��w���'�w�O������ǯ�_��������5���7ܢ_�0s�u�:�`�]���<͍�YWk۷.�������꩷N��3�'���?������]�"��h}?:?�Ω�-��^>��Skٻ7�N���6V�ͽ���5{S��.^y��Ë�[�ٍzd�o�ۗ#/o���������Xў��E�����뇱���Nw?9z����F/��ze�Oܾ�ʍS��cw��re��3��:^�.�vu�����}p��'�|0��Ź�f֫c���'��}�X�i��O�;����g�:s��x����������K�ݳ���9����̉�.����fV����u����������[ŝ|f&��2_\�og��v�r��`n����8<W���,�a��^;q�6�`����C'nu�'.���X�{m��K�j�ԉK�;���̝��������p�����˧o�)�<{��gW�-�}8�]��������W��{���܅K70������������K7�99��y��?9y��͏�=�䫗�g߿p�aq�Ӆ�#��哷n.^9�t���;�μv��'��v��+�V>Z��+Wnl\���?{�|{����u��8��z��WƯ_|�nv�������w?\�����+�?����G�z���?�|x��ۯ.}v�5{/��t��λ��?Q�f�'�s�����{���N�����ݻv�f�¡�J���O���x�|���{=<~u0|�bq���++��3'W�������+�~���?��rf���;o���o�����������._达������Ƶ�}z� ���Rvs���#'6��Kw��~vҾ��\�o����?�����#��]`�k���W�?��֧^��}�㷯�����Y}����/���+�+٫o�,ϼ:̗^süv����k�-��:��B��~��ř��z����6��z�����������[�Nw��cs���K�v�H}ek���+ݼ�ٽ���N8�b{������~��m7���tU��ֽcz�_s֛�'��,p����)^>�ʝ���7�)����ʵ�5]]��ܮO9�Vn���p�w��yc){���+'��:�~��Sw��0o��v���V��߾�ى��Ͼw��3GF��Y:rQϝ����O��0�|�>{�<�������[����l^~��Ț��,n���'��/^ܾ�ڇ���G�Ξ�-�0Ǻks�o��|���c����̕�n_��މC��������>S��z(�_����-�����^X�q���z���7?8��z����7?��N�ެ��|�ѡ�K?9�ɼ���7�n�~���iu������|z���e>9�r��O�\>5�{��bu���K�VN�{�w�~z~��]�v�\qӜ[>9s��m}wap�����Խ����]�Z��[7̃�S�/��k��Z�tp�ާ�W���هo�~�����ÅS�g����kkŅ���o����ή�/�?�����g'�W�?������_�����K�#_>���C�.~Z�a^V��/��܃WJus����j��[ٽc�?];����J9zu}�>|�S�m��#W^Y\�9b�c3����'ԡ�~r�����ϼ�rq���3���p������?=熙=r��r���^��]<(O���}��goݸ�]���m�zu�#uۼs[��G���j=4� _ˏ���F�����zŮe�B9�]�z�Jy�\ɪ�o��9��%U� �-&\Q� -��3�s��>n�nM�9CO�̠��:� ]�c��AG��M~�u��9h�䰭�SX^f-�nζ}�&س������x3���z�'E�|��h���F�w]��G�a'���:�Sy�nMB��8�ᭊ�'��3+��~�b��A�ɓJ���7����CSe��A� �N^���!J�����;7c~����)����s�SM��'���pw�n�%d3��?��� N�e���/���;nEq$5���,!i�� �-�"3q;�l�/�F�_Y�����PǮ�z�n�3���]h�k$U���^9�Φ���ݣ���O�fr:W���������k�����iAݜ;���h�@�ql>�a�����k�@��q��o�d�?���/TƷ��n����Z�z�eq�����V���N��^ӷnu}�5��Z"}��Cq��Z�zK� �0ug�_ ��Ɵ=@v��l4���F_>�v�9j���k:�>�j>�ߖ����(|A��p�|�۽�:�-D�"���CN�7��%jSP�:�Q���n�>�v ��b��腅3}�c�k�o���GTF����}Q"���ׁ�y�g_�p�>�#��3F�6����0i���d}��e2���n�������=m�8�o#R�h�jj� -�Z��sZrt�@�t�zKB����Pn0�=��Ym��JA������Q,TTi.$������H���d(3�)�����j��*.W; �v���v���\�����u�6�3������n|C�>TP�������+��Koհ�<�/ t;��=���p�F�����~��^:u�d��2�r���a��;=|�=���>�\j[��v���wV��;H\ -�\A��d1*�n?�"���oϷ7&h���4�=ȉ�N�����o��G�շ���O��z�ߤ!��{�����[���M�RRŏ��+Fqh���D�^}g����R���Հ�� � -\ZRO��+�h) -�#~2� �����R�=����"��3awm��h\�H��0������O���З�L�S.m�yҌ�9�`�����f�*�TxE�E�66��>� -Tcx���`jjN]�>��#�gL�\ ���Q�2,Y˙��T>�X�0�5�i�ꋧ�?�QG�F��qu[�F����ל)�������Rp���a�-�c���_a�;`C�:���n���<�e-N��Z2<�F�kpm���=o$� �g�侠mgF���;J�(��}Lj�����0�S�_��&���O(¨�����C�`֞���C+Q�B�l8Ze�}b�9he��-q+��wL �y��7c&˘gT�1N�WD�n���k�{콖������n�^>~3��_����b������b����1�����ք$���f�ވ���^���o5��:Ut$����� -�9�Wr*�F������pj [�y��7�F߉��?P$�}_���нC(���[В �H[e K2�]ג�|����b�����V(.'�/}�?z��m�:�PI]�c����Qi2���]|�{i֝.P�v~(릝�z�9?7����d&'�Q3�����@zF�?�U�@��hҫ�T���x[oj�խ!�?j_����ܕ�r+^[���9[������;ھ��(n�V����̔$o�j��V!Ս+0�^_��.��)A>{J<=�nH�)1�T�]g���N�����b����mt��rImr���^�&�3&^|��Z��F�.Qێ -9h�)���@�6�}�c߶����h�j��F�Q�e[�zhpj���ݤ����9 -V�XN��H$�#Η.�S&�B����mT����^@� -�]u;�nb�Y�+���{��l�3'���_��Q�.�v.9�v����:@�=މ/��z�@�C�t:�&�m�h�"Ӏ.�V���)��/�rC�:��БӼ��V�������<��ͦ�ټ�����T��$���O��mo�,7a@����YR�`\P'�juDZ*������{���|J�jN.�?s5�K -%��5^�k�l�O>�f���f"%J�o��2G�.�j:')*~I�v��ye�����3��6Ui���4R�0�������ݎf.o� -����睢����*��Aw�*�r:Hg� ABp��Y����o�:��Td��Q����O�8U\eQ����H&d+ޔ�VfS|�n{<�>�$',V#�F߇mL��<?/4�m�Eߙ�� -x�~f&�5�1%�^#52��!?�7�}��<�2�N7�LpW쯫2ôI���E/7�{�t㥵Y�*���ԥCK�"�9�̣��>.�a]x> -FVW�={I5eڛ 6Ǝ���Q}�����֝�쯆;��*�R�z,ɝ�J���-�-VS�gM������}���D/|U�B@�A�=��9y�:���z�X3w�:y��&�| ̈́3�^u�W�����F -��Z���$Ŝ/t�� 툽%|��h��U|�t/�\�=Ӻ p�D˕*��g��f���A�����A��� .��?d�YlZ��'W�͒zhQW�Ө�F�+ZV�o��7 X�I�,R�����ژ��-�q�YVp�9k���V�Q��4p9�A�����>����l <������ލWx������7ey�]� ������ -�Ь1��o ��Č�� �p]ՓOoo��h՛욒�sh��J��P���q�O�|�ږ�lej��*12�uEc,^�N[�i0��M\���a�i_{��f���~{�`/���Z��\�Õ��2U]�N�t�s���?.�ұ�����]5�u�s���FE��]�mq�N�L�s����CY���������Y?�+�%�G� -Xnd���b&��w�*�A�}^��y�PN���>��o�yƇ9g�qe͝�/ ź5��Os�[N�M�\�5���0Ӆ���@f��\>Deu��� :a�.J4���������v��\�7�y�H��P��3�Fɮ���X�����(���j�بؒ5$��h(���f����v��(\���T���v�3;`˼�D��7F -����y����"�5_~��z�4��-G�jrתת��5���2r���V ������k=o��ttn�ͮc6�s���B@�n�Q]>t��ugTb@�Go�[>�hX�@ϲ�����XE��6ʛ�m�$��wz�yÀ�rğ�����^�k���4Cq.�&��k�!6�~��������J�0j�XVc����͠�q�l�DS���N�:�.��G��W��Z�\�m��{b{R������3��������4�?z���ޔ�/w�z�욏�tk(���eC��n����z?����B5�(c���p�vJ�M��<�֑�������5\��%����>W�$��U�N�Qmi�v����։�zg^Ƕ}|6���b}��ҮE~&φwnO+��8o}w9�o�m��(�R�lj��iG�� -�>ۦ�S�@.�����+� �G��Ŵ�E����+w�?�1����P˴�s�s��H.۪���(:h��@N�R���E��_eOA]������ ��� z�%��̧��j�o���F�JYkf��>��\��2�3�u�%��R��[3��~�tP���3�ԝ3%oq��fz��l����f��;w�=(|4�q6��I40���x��GA.�C�bi[���m����_�t��BH�9�����jBu�-f�����p��b{�mO�}����H�R��������"��Ar�M�e�> ��aj�.�|������הS�-�A����Zר9�E�<����%��@sR�N+`JN �ac��r#�qq�X�w�.���j)Ġ���Тì����iu�~�/�^# �Cjoe�X_�B@�u���5�����)r�GF;@���~�c -����W���7�tB��� ������^�2z������~!{ۺ,���$��x!n)�Ң�Y;�T5�i%ў���. -�R����b93�+;0R����$ 7�u�=u� >;/?�Ø����W���% >����bF�hbY��L��Î�O?�!��йwTJ�����SYC٣�.J��Z �旳�C���м���D�����v?;�B|?�cكQL<��r8���_m����s*K�aϺ��2����f/G�[�d��HntdK�D��+���o*��6���lu�M�e��S��3� �g��Qr���_ܝ������q��}~FxB����놌���pR���(I5�^��V��6�n�s$36Grq���R��������r��4M�����Cr��j������lX��L����h�e���=��� >�~k�x����u�F���&:������s%c���a]�d�rzW���r��-x�Z�����N��p�M~���[o=�+Wx�NB~[��Uk��l���c�|V'f&��j��c���G�6�D5�� �G<����/<j��Z z�B�Gu7�t�F��½��7��1B+��ƣ}�����R���`p��Q��{ˌy+X���U����y&]�Ã2���{�IFZ������0wҭA��TS����z�����)�C��k�Hz`��� .�7��(�f�A�l:���b�щ�����*��n�K7�um��-�foz�R|#w���������'�O���-�������������i�}0n ��B_s0�q���������mE%��Z7��'WsvB�}�2E�/���8��/���(8�7�4�+�@EG�ST���7��3U���� -�"���_��I�= v���Yc/�֝ɠ8H#��p?�ě���Iojln�0P7�wW˨��m;x~��%Tki���wH�'����|�o�:�&��n2O�:U����Ϙ�O��HWhCg��O��<N�HZ�u��6�sT��]"H�ܵ�� ;5G -<�E������ڻ�k��)gpG��Sm� �<kK�j֕'��+8w �I4��&��!��̕'��/� �����..'BZ̟������XJ���1�@Q����\C�3 -�!ܾUf�*��{�E�Ze�T'R����;;�����:��r�u����{C)W����)����n������"ߑ�6�#A��x����]��h2�ܛ6ү~���Uh�H���7�(��t�ݤ�0������~\���[�y}JX&8��p�E��������|̡�R~��b�������#AK��I�q+;-��T�+���G�2N=�E�3d�N[��5;���b���e�h -z<�����j�W[��I����\b�1J�S<�AA��g -����C7����*�,�֣����%-���F���B��w��t�P<2����O�������"����N��an�C�+������,���$g�7Ͽڶ���@B������I�n�R]gL!�̀��U����ܰqz��W]r��.� ��s�������ظ�G����ڪ�*�L~ -'V�Ztcf����U;�8�l���=g|xPV��/��.�m��I�d-5pb̅�����I�pk -�_Y���lv �u7�:ξI�f�}����M���F�l���穸�B_���#!(���O�x�I`v���Hh#����V\yw.X@���9��"�>��c�DT-��g� �o�����,��\� p}�]5��̊�P�F?�·{�C�B��8S̩����{Bٟ ,R@\��Զ[���l)���ug��Vt�F]�!���6���IQo2�]�O?��7�� �$���$�c����ڵ}�!,���n�~v�Ы -@�دc�t�r�(wic�!حa�(T��{<��GG�?Ijv{�q2�i�6���6H>�ë��?�z����C�<Lm}����&i⣇˭'kRC�\ń!#8�9q[�}�?��s�~��X{]���xvpǫ�Wu��;�v��a㩆��gU2�oK�Zm�E_@��Y�����8i)�p9��v������ݗ�p���L��٘}!!��r�`R.Q�X� -k0���N�l ����ER�9�����K�}W� 0o�3Cvm�d��3�Ӈ暳�����w��Ǐ��.e5����Y*�+,@C���OѼ�C����K ��|���u�@X����k�\^��T��nV[t�娞 %k���� ?��@�ۤ8ߩ6��z����ƺ�A���)|�6(��(�w��O��6�d����s��6X��x>���_O>����n���3�!I��앫��ȑ�m�^�ީ��W�7N�ȴ�����Ý�\�q�cn�S���)�a����v���n�+�j5�����p�}�S�������K�;� ���p���kB>��x����)��:� -�l�/@��?Ō�[�2�]q���u�ˬA�[q�ɮ�Q]}�;X�4��5;ū�c=���-���+05��q���&dle(/ ��ތ����;r(s��!�÷�5��-�Sz��be��� ��^�fq�L���3ξ#7-������se&]z�_k��u�}�2��~U�x����,�mf���}��v5��BW27�lS��P��;ޯG��vڒ��l����R�T�TK�Ԍ���?2p�FE���=��Gr�� a�i��w-Ѻ��V -����|K �w��)e���c���[4���N��5Z=�:v�Wn��eM���� -rF�N�ʢf/���}�%�<�˞�y3��Ի?s���~s�Ɍ4H�=H�j�C��Gr�x�\������H�U`��,z6�Q��E�H�t�I�ߖ@�>��T�D,���?�q�M*-[��.��.���>�TZ7������X����ږ�]ꗹ�@�ԫ����d��9�식y��속�+hPC8P���ھ���G�}^�s�5�� -�� R�q��Ju�,R����T�߷jU�FV!��'�;۔7��^�??�mD.� -T�ؤX~e���T�*�����b����-f������3��p�j�����X:��A�g���V�G h����kG�p�_�v��4��U��d"L_�]�s��Fen�kW0.�z�d�o��Jw�DyZ��R�p،����� -f��1�=��(>�ߍ'�:6Ӏ+�&\o��ɰuy�D�-IJ�cAPw�zRw]<����PΣޞ:��m���� -ڿ��`�v�}֔�R�*������E��2b��['�mFE�D�����u���Gu\��_�vI� �ǖ�tb#7C��?���4����{��r���i���V�P 0kJ���=OB)��-ְ�7�&M'Je�(U�t���˰� �5���Ku�[�İ�+sh�e��sϡ�(���X�6���Ż���u����4��/lK݇5^$�̠{[��hf,q�I\�CMp�����0F��� -g�#����)͜u$��S�O���c�MK��n��1�����|��n~��-e[#�t� -N��L��Ǻ��Y��vnK� �k*G<���h*M�w$�7�p�jk�D��$��Ȁ�hB{��JiY?1|.VɎ�9�G�8Js�����FS��t���d 9�>K�M{�58�+;Mӈ�Vnen�־���ʕj���Z��yh�,��r���'N�̳���ţΧs�3�KC҈����#�!� �d�D���q�L$�1j����qf�-�Y4͉d'�1�>=n.��1O1i��q����(Q�jD|�-��sH��@��y��ʯxY�A=A��S�Q�����#�<���B��.�d�HOw��1}]��~�+߅S&�qɡUM��t0�Zi@>Vc5# �N��.�qU����r���Ͻߤ���!{����>^����S�ܔ}W��^�ߕc���'�R;_���'c��`G]-U��@k��@�s@H S}�X|�B�hvt$��0��H��,\��b?M�����T�����Mh�k��<iY�G6��g�!���d������N����t��J��M��� ��g�^Q����w^+�ń]��qs̈́f�1�y��=��?4��SXRf�`��M{c6��ytƥr�S;4�5gX���oR�-�}�>��`T�vu?��Κ1������L9v�4�?��h_u�`WWD�w�Jf�b�n�ۮq�8 -v�?�|*��q����Ȍ�Z �������B��� -\�5����nL�v�PO���m>=-j�O���~��ٰ.vu��<����j��`��O�a��7:��z|_m�5W�@?���{��UL��Q�H0Y����j��k�ȕk_*����\�7���J�twC�}m�Q�Z�����^�'W̋���KX,����3V,Y�8����vI2��p�I����ký5�����)P}�KϢ�ѽb��,��0��Az%2+ پ� -Ȼ�; K9@��f -�;�F���j��������8�ݤK(y�4�70 ����4費�&u���o�~M�g�D���WT����� ?�ޢ"ϊ���(������'���gZVae�c[�f��'G�σ�?v�&�3�I�[{H����r��w�c?��.�4m<l#��������P�{Պ�н� -vIϩ���r�7mȴ@����tK:`�}��a���h7>���Z� P��U&�^���ٟ��)t���Q)�+v��ʑ�w'�iZ�� ��v�W�iXS�ƇV����d��?��u;3�ٜ�r��h������u{�dgB=�+���E�=�٢��`��<:�Ɗ��/ ��kW��Ywែ+���dm��H�)��U��� ]�(�8k���J� ]����4���Ť��;Ո���K8it��7�`#%h�֠�z�9z�ޏ蹥R�}ʑ4����)��4��J�-®�t�5w�`�}�z��YhTׅ��&}]����K���6�sX��������x�m������~/ ��pm����,������4;�Tj�͓{D�Y��;ˑ�Xb��Cp1�R��U 9ת�9vRQFA�vP]E��}-���;�3��&����T\ns"8���4i� ���B�_:��y?cnl'DAk��� -�I�dn5�S�0q����Lc���}��[�½�5�����_��V<���"�G��For�D+]���.�U[�>W�X��7�p���]�K������t�{�m[�T��p�����/���L,�x�Kh��/��:�0}w��!�f�0��%��z����a!�z%7Mrz/4�N�J�'I�Ui�� -�3��!֎*��'����s ����y>��N�b�^ ��&����I$�ή��Z���M���g�}s��L@��cws�2���͵��WZZ�:_a�LY��-��D�+od�i��P��)e-�ٷi�!O����z�fnҼ�#��~8�/�r5��;����+;"��e7i4i�L^��M&Q����]œf �����nB��K~�*�3��zx,�jO��D���|��B��}��awқ7���b�@��D�)�>�m�0G���H�����g�k��:���I/������vn�C��}h����Z�5��٧9.�Ւj�YSY���`�g�,M�ߴ��ƱsѬ]A����v�\ßi���ʘ�ȑ<��"�r�u�T�[���=���"sk�8�I��Ha"�vM�Q���c��#�NJ�d�QNu`ъe��F�S2&�S(��.���Ƕ�]}�M�zSC��?���_�m��/�b�0/�?��_$D�z���I��������m��z�8�P�_}���5�||�Fu����|*���Id��ω��=��U��O��=Iq.1�r�&���E���[JR��2�^k����E�D�d���� !=��Y�.�I�5���Vf��kg��D�;;0ĺ���ԩ�!��B��{����a;��/6�H��[�Xr '7J�;"�ң�6y�R��� -'-���ɔ�����\c!�9�?C۵���!6ĉd��'^]t�1g/�_G�;�JV���Q��)�rnԳϔ�V�3�b�9�\jz!�c,���#� L���߇�k�͆;� ���������N;Xe���'��3�Ƙ��7;j��unQ�����?���{��L01�lRlt��a�GC��� l���5�(�nҘ!k��?6������z-�ӟr�!/K�z�P;@oJ��W����*�=as�]�u�M�(��K&�.C��w�n.�~1�:m��u��W�KW{�=�����3��;"��3Y2�:x���$ -�X�ޣ����FO���S�z�i��(m�)���9{f}�~�u䴱*�q��yj�(��� G�7�8gZ���x�P�%g;ǷU�4I6�ٯ�o��p�[8�=�Nx�C���Br��?��;���m^�Ə��6|���������E�NxU�K�!F�'��z!<�b��i��g۲Q- -�d�EB�t��:m�c���)�n����Mv�U��鷁J(.�_w]���C[��'X�Z��y@ޜ�G��fr4�� �-I�� --�P��5�\wď�2E�R��;n�xoO�RX3w"�p��_I�g��ʱ��Z߃�8 ���5v���l,uTy�i�����8������j8AJi�%�慨 �G�� -Ops��W�=�Z��(�f�t�*^��2<��h��X�` �w$.o���}G�ŕ���E7�"Lܛ�^["�6Q���M��B-��ۖ�ܘ�\��`������B(�8,�s�a�Дk��<V�$�����l�Fa��?���Ү{�����C� #�?e�}��м���B�#|��Y�R[�e���*�ի��UN��K�%���Px�5�ȃ9��)��Ԋ�*�T�a؋��JrW�zZz���z�$*!��]�6�d�����Ƀ#ڗ���8ޤ��\��nst\i�m�eS$.Qta<$� %�|�+�f��pqJ��~��=f���rG<.���)�BE�$��3�^��(*^��*4�?7 -���ұ���,Y��[���U+}���ƺ���t�`�M}�cn��Y��!IU4t)k���V��h,��w_�%3����y5|^D��.p{ױ��d���~��vi<'�n�~ �!!i�lT%M�)�?�Fu#�����+W��8;�I]FsW�Bw6bHO��x�UhlD��ڕ�B��ꆂ��՝gR=��Q�I��}��`�y^��}��˄�z��a$����j�ʜ�w���O::�&��V|-���}jӖ쥵���\��/� -?��`h,}�CO0F�8�ʫ3�˭ʷ��n1+��,r��+!�K�+˼�|�*>Y�L��I����7���I��}�J��#�f1�Q�����چ� `�ֆXF�o�7o*>�(a|�v�m�uٌ�K�K�vv��翡5�@k��y\���M�P&��L� 3�����bq_�� -��Y��w�� -�b��� 8f����;9�3R���%��}�{� -��r:8��i���pC*��Ǘ[c�o�CCYKF�lL~����6�Fg!���>�q���Z�\�2kJZ�^��t��oV��z�X�8�ݯݵ�s�3 -�R���G��|�J�b�� �&T -����$um�%CK��T�{;L��K�ҟg�a���G�:��#]f[B�1X��&�l������ ���r�f�\�~V�d��L<a��3��NG�('UJχ$� � w�؛`�����D:�**����5��� -(�l��HB��H��a��I;jm&����pOS����#]WJ�ߎ�������p�?}��/��&��@�@_9YX�5ie�����Z>�H3��G��,e�*Ӽ"���Ǿ*Æ���4ד���LF�[3���f����zͨM�k:�w�Y=]�����������@�-��/X7K��Ty�q0��ܔ�T�6\M��v��Ȟz�<�L���e��6)`����%�����3[W�6H,[�*Eu�Ӥ��A蚩�j�X� ����U&��x�!��nIݪ@��.�Л�|j�¡�S��፻=����z��e�䒫V�춪��Wd�UhF=��a �p�ؕJ��oM-q*l}����;w�G�l����W��K���喧�=t�o�ÙF�dLqL�c�n�Ph�ݒ��#�N:!Ѩ���LM����v�0S����٩��tu�֍A|q+Rd��3�8�8r����Z���H�^�ү���������l��������a��4��*����%����i��)��n��E���S-:-�i����>�w��5H��,��R��d�4E�V�l��K�Jsq���6��r&)c�^�y����.��7�5��?b��\���uz�o���c;T��-"�K�I^�.�r�\�._n��RC���<�|SS�s�*��6^2۩�F��&2��nMr�N����CC` 2�N�a�Z�)B��0��%�>t��(�Ai��U���mxb���>�C�VDڄ7�ͨV�����C��������ex�{0�����.�L8���|[-����z�t�I𢁨o��3ڷ�o���\[�q��C�Y�~���>|k�^4��-���f��1܋SY��WF�������>CDߡ�a��Z�(]�ڬ��p��?7��o���噶�����v�kh�jOB�l<ЭE��m2�K�!��l��H��}^�`��z�;n�PZ�C:v�,���}�_��w�>u\�@R��,ݲ~5B�߃ST�ؾ�b��f�� ��P�eB�tw�"������A�)��r��e$4]�f�тk��>��E�6���V���@��N�v�}�M�����c����t�Lئ9�~�,���Ǔ#G��n|��I��X�����qT�����7[yHS���N+Y��D��y^I͑7�F�W��U�JS{���~{"b�.H�ֲ����IP�dt�^��ϑ�9���/K����#' -��R��&DS��6�����mG�zҾ�c,x�+���|N�JA��4؎TY]A��q��1����[6�S�|��x���ܽZY%�����&��}��խ2���G��S���v���K�e��.��Y��]s�(����aG�m���S�ZRU�)\��З�����t,�Sr��p�jV�Z '��ށ�5KG��9uLq�Q�z�R�5����y�y��`ڠWQ��o��=�ܢ��r��D�:����hҙ���n3٬��K5^]��L���&T2 k��b�p�{7��f�����㮗���W�����)Xvbt��Y:��gy/�.t}N�6����k��:�/w8Y[L�D~Qn�)���)�+.=����K��OT�>=�\o����G# ���|�9���]�K1���r��}ݝ<G������sK�̑da�d�E��c�,�Fp�RЫ��)U�_�`��|tx7���l6wr��s-���r��9@۽d�v7!_��+����>h��/�θ��HC�c�[h�h�$��J�����`�VaΥ��\�siijj�_�{�!ԩ��)�v]�����}�( -z�8���Z�/~]��H��>gc�xPq�m�I ��`<��~"��%p���hI)8��z�I�H^�;�ˑ�z9��:i8�s5�fY���2x�6{���c��X3��ٰ���F�[Un��{�Z���� Z^����U�t�{S�Zu�h#k��W}���>ϼ�?YM˞��Iѹ�6!��98gf���\%UoK'���8���ܚ���;�n�PĀ - - -�s��s���WfΜ����g��TW����:V� _� �P�)'�/C��zv��;��j6{�075[�؛�[�Mw��t��X���8�8e������TI�Gm8��L������|c7s��\��8����l�Z��Qf����RK���Y�9k�|������lY"�,�H�����xŴn/�z㗱�蒺�u�����5���:�]�=R�a벀Hƿ��ԯ������*Ջ�`� ������f��F2�+�Z�Yw��tek4��u�q&_f��y���v�&�Lk4��NŴ']��5����ݠ�^ڸn0�V��#�����I���9�II�b/���)�Ra�k�D@kW�E<��Lc�n�m��W��lP��b(��`�?����M��5m���=��+�1��zW"6�|��;&�m£�����Fw����L�����/9-�ɪD(�f�q-&��T'W��v^� -&� -�:�I;��F#��Ws��yɈ��>f2���n�阓�a�u�G�:��r�]�i�;J�ւ�"p -ewY�yɑ�9g��=��*+� -��Z�z�Y�#M��>_�H��å���݆����8�L��x���ޮ����ܼ�_�]�V�O����E5�ָh|�Gm��v�=xLEۜ��z< ����<�pϛ!?%�1���������,4�]�\�\o���@����4�R�6cY���Z��vsC�/�+|�� �/9!E�~l䭋*�������d1�۟�>֊�7U��� K�Z>�R!&�� ����cO<�;�Ag���[\��� {��+�K��*��/N�r֩&��aE��&���K8�m�֥Wta��IKI -�G0+�[�6�F�A��>8?vE}h|pvO�f��?�Ǩ�G �Dĺ�>����p6�pIf��D'Q;4y���z�,*A�Z��K��Tn�?��Ֆ@�g��bıp��Ġ�E -db�P��t+�J��.��8�x�̏5h{�j�@���Z�72��P�xz��5�2��Mv�R�J�A��?�s'��$�Z݄�g�8�������rK$��[�.����p9�`�yh�����v�(���w��L�ZŦ���V�U�yA�fwUJ�T =q1�˒���7�˟�j@5u.��(T� �Z����?�j���F��f��<��%ܸZ�8��sH��ߪ�RϏ���!��E �}�R.^������)p4�����*dM�G�ʼ����xvˤ�;*��'L����;H�VeM���N����7+(�!����6��_9����PU%�w��;���l�/����8Ri�g���E��+V��Vd|�n$��^F/�����\��$�}����ïM����s�:!]���=de4����(_�2���'1�� ~ZY��W�Pd�'}��w�P֏jp��:.z�b���V�V��r<�����w +��҇��<�dT�:��S�0V�2���H���jHy� U���4>1��,��Ƙ�Њ�i��rf�=ᐄ���GŽFѨ��/ _U@�Ȅ��%xŝ)_�fܟHE1�ɹ�2�?��M��MZ<�o��ݞ`�[��+9fs��*o)�)�j�g!�; ���áe藏���M-��+C.i~. +j�s�aKr�u}����U*��Y�U_ D��Aę>)#b�Ш�A_φ�À��.�@��,tvmO��t3�u��(�L��*��f�ŏ�4ܷ�'����/��z� L������l�篠��٣e�`>w[ը���S�Y�~q-�f��s�nIP�p�� -e8沏�X���燩�c�\�r�+��/�:H�y�ǔ�[��E+��j�"Z�ѩ<��Fs/V��'��O֏�v����v��Wk�*��N5p5d�&3������s�;���6���%�he=PM�u&ip����4J�{:?�%��V�f�xx���IJeKmZz�w }'�� k�lܲ� �o�U��]4lY���e| ����е�Ep+��w: .�z$!@������Ck�V�r_��"�7�e~ѩ����cM�[���}���)��G����Y|��V����?K<_*v�s�����*�Oo`'�������\鑭y��g�|�h=@��4BJ���QCQ�|m�v�NN9TӞ:�Yղ�~i���5�H^<�m-ݵ8���.h4{��nt��U}�6�l�����p�����գZ�������|0w}���Y^>����FH�ۭ����� -Z�o�\M�L -�Ì+<cF�iQV������ق��6Z�����ݓ�;D� -7�m���Z�39�Vf��v�m�Ɨ����TLs2w�B[C��K�o��I��`�ELvʊ(�^��z,���a����UK�{>ʒ���R�]6��\�`��zx����|���',*Ѯ:*�9]1}�!O�`����8�����߀Y�� ��gs#壢������t4jF|Z��"�^�2��iA����j5�Ѯ2���v(KUt�Dql֞�n��a�Tb���/fc'��4��5d���"<�\��z�|���V�a��lD��|��-�o�D2�^�|�x�����l]~_*���(v\xO-�x����O���E�5����聍�R�e�����Š�'vd��k�K��:�.��Ӿ3UŸϹR��Sʙ�+g��7觹# -o���g�+�'�a���J^8u����U:�/��d�;�V�5�����V���[�~S -�zÜ��y1"}�;��*�J-�f�>�V�4�+��cgw71X�y<����"����� -�y���yN�}�D�3"�6�a1�]�:��_s��b�^w����yز��W>z.���=Ҵ_�3�ؐ*���5��Ů�Pζk��Iދ�3��6xƈ�Q&Uj��&����j�d�>{���ğ!��5]��V�w�Td����q����+ja��!�5L�a}`<'P45u�(����rM�Z��"!� �N�L=�(�9�����.=h����x��%/vy�"�.�U�%��6�X�w #Y;��C����٩ x��.�RD�BA�p�0�ص�'"Co�3#p� ��)��*��ϼ4.9�]6��rZ�����{�.]�Xb&�W~������������rJgo��̢_�"�2�w���@Y>�,i>Te���хt�,���;��S����o��uz� Vi'�v\�����m���8��5D��K'S�m� �~�n�k!ͼjC�7I�40V'���*.�%g,6 a�W�n��~����P��N��g=%���Hr��M/��b*��L0�v�Z�Yr�i�H� -��ӗ�<��Ȼ�[�g7[L��� -�'QWy �f͞ܜ{�� !��{1�L�����D�I�t�.�g��]SdΟ��j�z֎��n�;~� |������:`����2���s�#qH��߷��2���a-j^��g4K?�jF$�K��l�ː{T���� �|�Y��k`�c��K��t���S/m�> ��d��o\�ܗ:��\_[t������OQt�N��&�O5��ɺ�����`/O�����t���/F���w3 ����Ml�Fє���j�e��irٔI�� �Q}����;(/�0�X6��wm�P���;C���ɓ�/.D�����T��!��C�6��9�k؛¢�t��U��k�,BT����l�j�N���C����y��5)�q'��a|��}����%e�b���v�~=ʸ���t���l�Q�� -V�/V��Z��c/Vz!��ɖ+?����R��fk��x]�棭5�N��yԯ�ht6�0C�ѹ�y�+.D]�Z�~�l��@��X�ճ��u���֭[ݧ��tZ���U1Ϸ��fw��f����K�̆�9>�f|���Q�'t���D���Å�����^��5'���9��*�R� ��c��E��v���P�_0�G��L������U��*;� -�'�I�#���lVQ�,g���T�f�}�jRn��{�y�R,���:텧6��~��ȷZ�wAd.��Ј]NB֡)Э*�!Q�ݳη��O�Frޭf~X�9�q�9���%��U؉<�{��8yЪR��1x�}�y��P����?B����P$M � -۳R>�.C=]����_�<-�u��T�L�jz���iõ{���L:�����r���ׂԏY��� <��?�Y�P���`�ƈQ��`a5�E���3y�-S�U�>1��R��=�����bą�7�����x��m:�sK5�Pڧǔ��ş"�龉ra>� �B�j��,�`f�C"�RW����.���C��c�f��.9�#G���@u�����4Q��P�9l.��ÌZ�P�$8ʎtN�p��#�=�;�WWֺ�ՍQ��a�JEt|�7�ϑ�"�SX��}3�G��㹻���M���D�*����"ϲo���0�:%�œ5c���Fc�*�h]��I�L�8��5�C=�+�Qtn�S�=_��rc��S�4B��,.��n�>�&��[4��:�8r�[��%��� ��|K��q�����[���q�����dk�\������S>FJ�59�se8VF2�$;k�k�����EZxē�U��O���|��g; T��/N>=�8#Rv# �5��8�;�@�_��C�T[�Z.�"���VS]����{������(6�&�[+⣚��u,���d�d�:ş�X���ھ�I���n����8�)�K�NZK8LW�k�ܨ -hs�<���&���7ȶ����9�o��:�i����tA�ٗ��췑�i���4[�z�ᱪ4��]�i�M���}��os|�wF�/+O��҈f'Ik���h��DOFX���yp��2!u��$���ڑbZ_�1��Х�o{K�m�N�术�#d��Ω�)K֞�N�����3�Y���xq�'�1�*0�ZY?�ڹ�=�>o;�wE�ӟ}<~|ۿ����YV�;����.-lM�Z�)����s����Y2dUW�AiС-y.��EI�V���G9�R�1.�2�}�U���8W�)��4eŋ��<N��&�*���.浨��ՋQ���͕����� -L ����n�� 4_�-_4�nxM]�H���6��K�����3�"�����{�ݤ��X[߬�Ah��&�>}P�6�E�O��|~��#prG���)C���/$Պ�g���~ɫ�<��H?+Z�;�^Ha�/C�<ώu��Rk]�vg����J�=�8�ֲ��,� `:N��2��O -��]���?ǞR�$V�'��#v�1��Dr��V�v���⪯�T�P�ԁZ�p�y��e��Q��9.NC�NBz!��*@\ -T����Z��`�l���m��y:`������,�98��P]̒��"?�R��v�-�iI�7y�����혍�z;�&� ��=������v>�#^>Պ���Vl��p��[l�����j�y���tq����qS7�«9֘�F�8�S��[�u�,�&���t��C-���S��y��a�h�_}����w��7]H�>��Ki�=ԕ�Zޔo+��L��V�6��T���mO��*5t4��j�#��j�I�#]��QcQ[K�X�y�� ��Y�����s�#(����\�)֏[s��F��}��L�%F�4��?�y&ܯ�#��)�VO��N�j�Y����Z'��ŷƕ0v��'�ჭ�\m�m%K��j�÷? Y��_zI<[���"is���n�/G�c��H"�d~`MĀ���'��&�K�k]������.��rӚ�q��Zͥ"/�x�1�,3����oX�N�!�n��< Nxܛ-i���t_��4`��~A� K�ћ%GE=��3����ĩ�`7�u��\r���p�6�ܧ�XNԍ��!���a#<������r����oD7�8�:j��Y�H�*'���I�$�Y�z�����-���Q�g�˷g�����K{B��Z?���|Bpٝ��W��`aDk�Q5\-����C��1=����_kŨ6~��/dˋ�=�ڳ�`es4c�5�]���^bW $�8+h� -ijVxcy+��Κyh�1tR��<uloσF'����˾��Z�*mF�c�??Ě���%]��L�V>]�-J-���I�a)S��g�d%I�V��,�+A�|�ˀ>`��8�К�Zpa�X{���^�ֱJ��n�(���8yU���<��|�� bmr7'�5��p��T�Y���TÁ]�75�C�$�kw�5ƕ��ߟ�D���M_�أ��n\�xo���k�ꡬ˯/M9�N%���t -|�����/i��=��G+V�]��Ӂ�R�o���=�Qx���}�YK����AX�����h�弣C���Y��K�P�;TPV�3�w���鼫�%�Z��+ȫ�Z]/ރaT�v{����ʖR��:�2G��h*Xu�`5�豯*��|�+���W)UE6�Al�ό-���Vlwg��d��-��e��E@�4����敜���oU����@z���1��`����xb ���qu�~U��91{Nr�o_��6{��ףj��x��ܪ6�i��H-�l���;͇�Y3c��B�Q���쑵�{�8��]%N���51.�!#xgj�V� �Tn���XG�=0���B9F@�'@�������za��j��^�K���Xn��x�|�_E��n��m/�KH����Cz����\�+�0���4՟`G�gn�D_�`$�� -endstream endobj 26 0 obj <</Length 65536>>stream -M�v�̎�C��^Z�> -ٹ ^�p��@{M!�7w5t�����0s�i�p��n.c6#mH���G�J���\ە�7M�y��D���N�5�u�5ou�m;溼 ��Yp���d�S�0g'��ej�>哅���2��dO�>�%4�*���h�b�5����F][��b'��!Y䑦5���x��w�!�&W -[�v)UE�l�u+��S���dl7[Ψ���|Rrzu����mw沄wl[�����ɍ�v>QY� O˭�Fӗ���r���C)�vV���ߺ�'�>���2�lzne$��f��[*�'��V4��מ*�����0�,��m�+�l{�� F�jϱ�6x�/�Nա�~�u�ya�ׁR�W�l�5�r{V�6Z�c���P\����D,��Y�,sWݢ� -����Eߎ� KY��ބ~';�rk�m����� �-�[����S���N�5-��ݛ�K��v�A�[t��W��un9�*���:���V0�\g}�I�מ3�=��������cq�Ѱ�@Vg�[�O�����oa���{�Q���4�n)/����gM��+>�yI�˵��2"�[��k:��Ze�D%nHt��V�Ä�W���<K�0=��v�<M��v��.�������נ��k�F���f��cj[>��j��T�|�k���8?6l{m���V�.��CkA,D�\�d��^W���J�hv�\�N{�Ծ6�$�8�X�U -��G�Z��)��T��!����cA���F�J�n���ȇ"�d$c���j���-Ӓ����Pr�u,�]��(Ҥ�A�tQt�7�{���G[��m�뛆�!&*7���.pz��>���ϚhW���M ��dL����w�s��:1_Ѡ�۟:�� ������t�>�O�l���]��o �:V��ϝ���� �b�N��d��Ls=u���+��'1a����P)���];/`ݎ�;� �J�Ss(;�ԅ��}�?��-����������;��֕�/�d�!k̟;�$|=�3��s"g���[p���`{�B�0^hE�B�_$>F��twT:���u�v�Sr�"������o7!.@)�<>�06��j��WN��a��-���۳�ǖ���t���'ʵ[nr�OK!��oĮ��.�а�~ ���!��H\��h?�)�\xL�ZN�1�j %/�Ap�nIĺ%��5Ѳ�/���bW�bt���͚��wLhz�9��Xc��i|���r�����,�i��^r -�����On��6�ҳ�_��"�/X�l܉�}��� -��|k�9#V�Q����z�E�����Auls>2M�#��A@�YgT���Mz*�5��Wct�j����E������ �o�H�D[��whJ5[����0[x��kV77M�5N��t� ��;���8�d��}$�Q�u�ib>�K�w�ٳi�b�����V�C�:�ϖ'� -,���q���X����z����g.�_�����*{�hUo�Z�[z@���cr*�]��`=gy�����n�8$�a+ R���������w�Ջ�[��U���ގQ�bXw�x�`��|������A��&����W��֛�]>j�״o��e%?͜��+W7d�+�e��,-�G��fJ]27S����I�����Қ��>� -�.��]X=k�!�����Sk��z3F�56>�5$&2�:QE����ϧ��T�cnI�枏�Ls����K�?-�'�����'IС��D�E��QT�v4gS�4��#}����[)�+:QV�3۹9���&܉��������ZG��ު>��ev���jR��o�<%Z�q���D�k�g��� A��ul�w��n��1��G�mC�lp�B-�H}^МrmZ��w����#��lc"6�xYL��bw��lЬ�20~H��E��N0��]��s�=����H�C<[�����B]��nO�"4�Om)�]��y0�J�<�N�L���V�;���$�T�c��hZn1���YTCtP�ׯ����}���<�C���~�{��3z���5fm՟9z -�5�L� ��~���w���\���͙�=�� ��A���7K�hO'������>�t�nl��u�\�U��*�����;���1�j��P]� - -��G��펷��*��*���WΡ�P�+lF'�m��-��������T@��1�gݪ0%�h}�9@0��w����aC%���3��S����5:��E��7�|�����'�?#ǩ���p�3��S����k�\�uX�'-v#8���)"�Erտ�Q���O��B~��Zn��bu8����ѓq -?��P6��K�xu�(�Le4n��Tל\-4�IT�8�2m��)���1��b̈����)����������c'x�i��]ז��^=��r��V{����L+�](ծl.!;q�#��_$��.'d�W7�g�z��Ǩ䎶����,Z�μ"8�eQg���;.��E�7�O�G��XBÃ�?�����jɚ5}P���"7A�U�M�|;.�t�Gn�5Z���k8.~�\W.���/���&4B]:b?ͭx���w6� �����&UW;f?�b�,�H� -��p�ޖ��i�Q΅P�d�#eɇ�=Q,O��j�,~�N�acO��!u��X-VFj�3��lߩ��[�RgD�h��]��h��Y����Q>���(I��_��˒q�t;�?@���W��c��!����2�����z��� -�E�d�邚 �1��{��՚�WN ���q�\y��f*o�?�C����֊��Fd&�����E��BANQ2�Dt�f�{f�]\�6lLs�_y W-jM�K�v����7D�>�J��ՙy�U�z�mC�]��mq�]9Rl������ƣ�Mw���!'��⅙� ���kK� ��KuDZƂك[�.���p�k�6X�!w���H#/>�ց���Oy�F�{Vkҷ�>K� �� �C88�f�# )��L[lq�imʠ��B��6 �gc��͇5�۲=��利� -e�dZ^��W+����M�r ⸡� �����e6�s���WW����� ;�k):;�#�+���Pzb����jQ�kU�N\��N��<|.)%S�-z�������a8-T��uG �X˺j�ߒ��߁�G�j.��l\SV�+�s��v�Gu�ͫ��!�Q�4��}��@b��L�0��O�b��'��跞=��w�wJm<��T�(]��V���� ��|F�Q�t���n~aog�a���~ҫK�6!��1��o }U�7M1>�f¹�:up�9�_q���!���4�����ﺜ��C������5Wd˾MS=�"��/�V �;u��~o�`}���B�������c���fW�0�3���e�rݥ������u[OI��K�(*O�O����#� I!�ܚ��y��!��.�R��=�05����6{����y�@]�J��11?�]H�,��o�"f/��+9O������W� :�g6��F+�������}�`��"�T���6~k���uj�5����i�<�ԥ�����I�)�L1 -vj���[5����e��F�>Q)i9X1Zi���Ր�cҖ�G�ɷj�a9��͋ݶBC]�f�W|�?�y��.��V�3��z~G���$ [��`1��f8@f��.z�L���/��..��|�.�>!�2������ -���a�u�s�������ܻKEv.ZU�%�e}f�E�j|�5��[��8���(ә~�}\ѥ��5m;l`����J����B8�TfQy�WX:N�no��i���<�y1l}�W.�� � -?�@c~�����5g-l�I�j������IL��teG�os�=DN��\����I���Ee�39�[J���v�>QȱoE�9�$�[��z��#�,:���pu������V��E�,u�.>���7&�>?R�t�E={�b��0ҏ�@y�u�F�)p)��V�-�/H״R��ؙ�❱�'�onl9����M�߸@y��8́R �q����tS3��J�9fuw�����Ca�0E}��@�"C��H?���vy7a���Ӎni@��p�ٮ����q8���J�Fh��v��(��U���������{����'H�|S+T�j� ��-�yvRG���=��C[��>0�ܽAm�MYmp���!_�����9�*p�4th���[2���f^���v"�%�Ɂ2m�Ԩ�թ�O-Lԕ�Z a7U~ݍg��h/����N2���@#��D�q��2b�����8P�� �6-f�a?:*��=���E,�5��v��\A�p�(�����S�֝��l&2ԝw��gQeAP���;t1�sՈ�^��d�gh9Zeu{S��sS�+Isf��6� 7ae����u0c�p�Kt�����t�>��o����}',a/䨦0����>/K�PR�;wv��ѳ���P�����C[�BH�����]@A� -nW�.��g��ǣ�Pf���s�Q��_Ud�ȏT�NyW�]�[�\<�;p -|-5%�p$m-5��sс$D\�V��,T��!��j���:����NO?�k9��bM���ޑl��k���鰎Q���(z�ƙ�T� -�7�*G��7��)}�g���Ec��RN|��Z�����Ĥʃ'�e;�1m�$����9�K��qnAF�� %�>/�x��Qdl��w������V�q[�Ca��!] �A��!������lo�x�{��0���6������@VAŰƃ]��Ve���e�.>����x���o�}G�Cb�P�~j#��ӥ_7��R۪�wc�1��/NT.�zB�b58�����e�8w%ʡC)�x?��`3�Q֞��"� "�����&���6=u�Fy���ɍӴE���y��M���7�=%��X���'�w^&i/-�]�A�+E�f]Cf�(���lr��03���RgY&� ;���cP㧙���*�� -�|�1w!�\���j �����BQ#K�o�*�Q�����{��PN��֑T�+]��~e�[���m��-JH��,mIZ��9��1����p���zE[��6�����N��yi@b����]���u�C�4���{S�fjs��[͑:v<�z�?v�?sa�&�-ʻ��r}���+�kR���h���}�۟~;����h�L�i��|����c�j_⏷�~(g5K��H\ΊH�β�X�.�Zty�7bv���l��μ��o���`��r���Y���:f���:`' ��#�XCr��(9��rb�@�آ��� �X1�˔�7���L���ku�c~͋���p��(�H�8ky��~�ӬV4���X��ז<T�ޮ���h�k��~��ݪ���4'�m��OV��aa�4�o߭�dq�N�x�J���e��i1���� 7��Ͷ;�π\4�`a\b�����<�;��_�]����ip�#�J����������@t����*t룛ߕ��/�&bk�|�GC(����#'����ȷ���H�I�l�w��0��G��{ҽҞz5oR������e��Ƒ�OauUHL�?��G�A[Y�؆���k��4C+�]���ƌYn�V�s��-�0�6&d����e�V=Y�9F3&�.�=�шJg�n\�Z��+I=��N�����`���\����<aȎU(�_zo~���<�b&:7r�En��nR�&8�ǭC7{Λ�̌mqٽ�C�ڮ"�K�#Q���"�������ѡ�w�A*S�F&e��I�:b����=�F�����M�������U��_�N��|OFh2.�{��]���Qm���Y�q ���D;_7��J��GZ �{mG�v���q_��c�;U���Uۿ����U8=G� a��V��thD�Lj -B���m2"�{���vW�W|�:H��x�b��+/��Sm���nK+���S�P���h�dѐ:s���]��4O�p���"�7�^m����yv��(l���BI��%Xm;!�Xu�UFR�^a����d��v��w�|�Y@�����)�Gt����$����Jd���J���;P��z���Sj~��Em�Lڰ�Z�B�a��}����"m��lW�e�ռ@�����Yź��*���xR�?$�fH5�<��玌[��qO��jb�)4L���J���u��e��K����c�m��ٸt�;#���Y'�Q��ƺ\����v�ӼC)����oN_:m���3���]�Έysku%���7n�n�I;��_�X�q�I �{�P�L�a2������V^��FR����I�q}�-ƻ��'�wu�o`�[�G�����Īb*e��5������+�W*�*p��zf<��J�v0>!�TG������o�C�^CN��c��R�6�s[�Q�����m��I�m�lϷl�D�4C�f��?�A���P��gL�e��w�~�F��+�I��D a��I���8��p@����x4 #�gP~��tm>K�?�B�ܽ!��o=ᚫ~�Xar -B���l��;ɫ���տ�3�@I�x+�˼l�G���'$�<ݐJ?���q뗒�QG��A��\�Y�U�e�,�i�d�J���Ϲ�N��5�el:���K�`�~F�o4�N�}���Ǒ��m��W~�� �����/�h��i�7/�m�XkVꈛ���r�Ygx�l�����U�4��b���`��'����ǖP7/�`OS}�8��W����~�V~Rθ�<�)����cψ?��BZq&��[Ǣqlj�8�z��M���4)��T�}�Z�Gؤ�?$�r.�L�����m�+���+�g�w�/b~�|RU�ZVz�D]�CҼ� `���Q��C�%Q�w�m�=#��DYk)�� (��QoC�J���S�ު��6������q��BR�0�tDv{Ҹ��7'9��%.n��=��_jߐb���K٩��q'���4/���U�c?+6��g��Zj�;�pX�c�\����M.���Tg��:�ʺP�t�Ju&��6+vU�i��)�SV��h��pC1���l��*�^"�1�!k�(��m���T7���ؙ7�C���#��v��O]v���6��46GG���!��h@��aw�l��� q�O�~���n��Z����l*a�U����F2�b�G��Nf66w����^�?��ӛح�u ��pI���^�M�3�Q�����Ri�}��w��� -oo��?�kj�Y�{��Rd�"#v�9Y;��N�����G,�&�Z��LG�uۼ�=5) -W��x���J�����+��!j�6�.J�/C#H�g�I�6w�>3��NZK���q$B���Ì;F����A@\��&J�,���|�|��wU�_g�Ueu�����L|rz�`2w��z���!�+�)���d��3@�#q�y�䄺��~r����Rf�����B�+u�bֵfz�]&2���]j%�xݾ��;���H�n�E���U>�XK����D$�O��u㎨�2�<�-ş�N՚x�S�m�q�L�'��8R�?�*ۨKs�:g��\�T�:��6 �T�� �:))7VEm^,S�Ӽ�*;����.�)?�;2@Lw��*��_e�[vD�qܔ�|�:����z�ѽb���]i%��#��w�0��^��=�/ -��f��,{�Y�)]�#\CjH�X0$���H��i������^>b����c��y�Hbz!�Y42�GnU����9)}�ƺx��J~)�GuI�Ѫ��r�\y�G���H0'k����Y4���T�����e���9���`5:{���#��������V�<I���cZû�us.>����jGt[eI:ޕ���)* -�"�փ�Y��o�P�}q�����ټ �v5�*�g��� UBB2m�ƦbB˻щ1u1���z���vk�a�I�}ZМ�� "��R0�[ok��| -[��R��n�a'����:�������폌rt;�r~v���|��������*��u�� Y]��ن�y��sb��1���ʸ��,9��1T�|U�TJ&o%Ƽ:�U)�M�\���{�5�0?���&م3aN��-�mk�[�WG�$�y)�l�ɧ��k���6;�cW���Ю|^+-� [�'3'z�~0��1��f#=Ε�Г�;�wh��V_���h�͢�P�凃Z�y����y/�<p2�N�w���*����&祑Hc�[�GD!]�TY���O�L=B����$c��=fڷ�'�JP�����E.�d~������ЖB����pY;{/WS���O���gYU ͢����4�u�t�} ���<ٕ�J�9�h�1�X��%��<��YE`@���ëԲJjG�*�ӟ��ŷ�O\*�ki��c+ůs�EѮ����l�Y�ai!'VyP4�]��ZK�� �� -9��%���\�ot���s����iر�����[���m��W_�T9U -}u��Xo�-����4hf3��S".ooƹ=��ޭ��@o�%�`�&�r�OE0�5�q(�ot1 ���N/C\t���j�]:RaL��y��<�e"�5Z������h�ݶσ����H�����}�eu浅.�5�]����(���/��uNy/�MQ����&���!�F��ڬj�.�|J�8=�b���-?Zކ��7���A5�� �vmMo}f����hԛ���$�K��L ݨ�Q1QL���T��-�\��?ז�^r�\�(�Yh�^A���t*P��X_�J�_c���7Ut�[���B`�] -]&���H�O�l���(�jcu��"<��n�������Xa��}��:b��o�!�Jj�q҃����B��?g0tiDC٭�:���Dg?�b��s�ƠeQ��Vz�QKyzZ72�6�;Vl�����5PEZs������p��e0�^��j�|�3��կ'�E�r2��êi��D��������+��F�������Zǝ�3�~�!��� zk -+�т8��T6{U��0�ui`U�� �Sko,���V;k �<G��!o�:�Ij�|M��1+��t�Y���5Qx#�G�l�cl[M�Gg�t�c�Յ�/K*^s�@{�{�Z�@O��/UC�v�+[h�#}M��X�؛�d���j�pn�ڽ�ћ�c��&����^s}�����8/�Z�s5p�<F -G�v�5 7~��8��&(f"�`M ����#���-�F]R�I��¾��.����șC��vG����wO;ӎ��9-9��W^����z�܅1>"�p�A�#��������Ԟ����#4|�,5����������SI0O\ٻ�&��pٌ�{wQ���M&�<'�����4�d����s�e��;.g3!�-�V�+��&[�O=���Pq/o���]�?���U�����I~_�ѡ`����-"��~W�;[�ʺ��Ր��W�?@a��>F;��U*�e�Y8.j�b�N!W�*]��!����U��*�&t��;UqRɫ��E:����ٮu�'_,�5��7:6 =���ت�m2t�dS��Q�ۜfJ}�����άƄ�=8*?�Z,7�F����t=��7�ZC�;��Ǵ�)[�NЇ��������ı)���`g�,�r�BZ�^1q�n��D�Y��0����K(��8�nH���#�m��?��^<���d.wŔ���\V4�[�_B���z�i�S*�g�Nj�خ���_KJ;�G##Ҵk�,H�hc���j.�B�\|s��ގ�� -{�M��J.�} ��x�w�`�k�h[�n�yh�� ��&�>�.����=���x;�*�b����n�z(���i�U7N�-�T����V���J�u�Q��ħ��u -ƍ~�:�BZ)d�z��/��tN&��.��$�M9��k��y��HUe��?��|>�y�Bːvx{�v]�l/W̺���k'�qc��!5M�p^`�[$ď'm.�+ބ)d`x"����']ባB�I|�����6[(:�ea_0��F_��^���p�����2����n�����k-J݉�%�����5"�o�r4�۰ՙIY�������h�? س���:�i�%,e[�����w#������s{�X�v{��}۹���!ʞ3����w��D~ *�μ���`��y��St���D����S���30�,(����dVNR҈��E����~q.`t�����9l������vM�j�=�~�ѣ��(���a߹*��ζ�5�ͼJ'a�s��I� u'� -����L �|�k 4l��k���L�Щ͝~�і�"�\��}i�<��g^iŷA>ܜ�.=[��N���� ��˖DZ�[WZ]�4Z2^}~؉�s!8��&������/r� �)U�3����{?��I!](�9؋�t!4&-?QQ7B�0���{�災W>���h=�/�t��Z"WõtȪ5�r/+zD(|X�/��WQ}�2�A�d~�6\}5C��du��<�m��[���U�{Y=4�jN1���Ts�ci -b�����,���o��|�f������1�0p^�N�hjmߴ�+ȕf����h�����ke7���G�~n�׳����21�!�!���J����]��W�ξԩGuwllP�Z^�lG��F��e�P���Ϥ���Wz{?~��j\���� �e -Y���'�3��Uޯ��i-���K�sP��R���<WY'}��G���y|�n� d�fB��Cs��h�s��ZFQ�k��lP�ɏ앴!/;�UKX���B�%(�j��kϨ��G�NL���R=1���~2�Z{�<%�'�Î2��x��< P�T7��Q�8���74O�_Y��]�I�K�1��)E^v�5$�r�1��˺x�V������,v!��/��7�m��J3_��_�5z~O����������+��v_��x��*_K0Ј���tx:3�F� -�s�w��*O4�k\"S��lc*��L�HQ}�1��A�kX����FR�ύ�ɗz��z���Y1����"mu;f���?>�r{x��E��h����$JًsNnY��o�����o�e�^��NK$���;��Z��f��$�.��屆���ײ�Wdcx��)�T�P����{z�U���|���ܜ~���0 8�`5�ˊ�ؙ���r�x&CR���~>' ��U� ��ᘇ��l��:ƽ�x5:|Z�y>WܚV���^���xq�Cђ;��E���G]"��Z��E}ϐ�Qii3o�i�yOq�)dA�9���JS�2ɯ��AE�i$��liA�o�We�B&B��c�+.�cyj^�������@E�I��-����� -;45���ĸѬs{�x��'��W����X�5C(k�����Sk����Z�e�vE�?�8U��Ti�VZ8�BѴꈻ.6�?k��𢆳yx��ޣN`���#�o�����j�������kC��,?�}8�L��˿<�b]�:� -���ANʸq�Z).m�g3)�����:k���%��/���$N"�8�Ŵ]u&��A^��벓�ŔT��Խb���2pk��l���R|��ب5��͟k��6"����!����i��Wφ&EJ���@B+Ψ3�J�v���[�gԈ��f���!�~0�H�qq -���Ǽ�8�qܘ���@s�چS�ہJ�"DVxU2�F�}WJ2��2���,�?$u(74!)9'�1��9C�J�~��o�-iѾ*�G}B�����[��/D��^E���F���/_�(���Ɲ�L�����!�³��7�P��ł��v�̚Z�6{�9�����"Y}_�[�C]�A�U���f<����o� y�6';�b��+�#�$��3����{�O�mAϥƊi �m�U�j����'C2��|������ì��ji���tN��]F����F�89�Ȼ���,��A����8������ڤ�J�h�9y�+�eu�t������=��R`���R��)��ӌa�e�N{���[��w�#sdVzjyV�|����U:�x�O������h�F��%m�Fܒ�D+��r��l�O~#���)?�óU��>9)����(�V�>{h���ƊR<��ˋ*��3����%���j���$��h(���И� -6-Y3��1�T�,�S�MBK�_.�I�e�E��6 -*/�����V��y�������C�u�k𪥜��#G���X)��)װݠX,�ggA�����A*"b��xP�Q��o!)�����f�ȗuɀ��\��Jo�{)�Oq��Eչ&"�-1��%��q�2��؝*�j�g>s�ؑ���v�~'���N�oG7�] �$�����_�nZy_�9Ѭ�5L�Ǵ0'���̄e�a�ߕ��r���>�aLB�mwU�^d��#�UhaX�Ei�6+��q9k���x3�kr蔿��0�T=���Z��'�$4Ϝ�ej<�ӣ�W��/5��,z -F��Ǫ�<~2��.B�_P�R��l��"�{�)+q�@n�*��K��ǘ���Fh�.���RI��Â����4k-{u4�(p$=9.���wߓCX�9`�-�ֳ�F ����\�Q�r�m��=��7䘺e���6/�"�T69� ��j)?� %Om(l6h�Â~�N���m9b��_�����ք��]��T%��eP<�����"��`K%�=�������[����n�gt%Tk��r-`���C���PڅQv{ɱkP�E���r�',?����嫺|������������Ԯ_ƍ����J�ݤ���~�K���* -������N�x��c;�o�ˍ{�[`;��^ -Is��[�Gݦ��4o8� )�+��E5�.�ʵms���&�J2l5�C��&�h�>������?��ɶ%m��ɖ�B,�I�Ә�mMc��X���jrvc��)�!�@���^X�`q��=��T��1�x��˲�K̮�4�I/#ú�Z����g"K ������o��kL|�ۏ�D�YS�3�],��2�^hy2�dx�B����OT$B�:�y:Vs����j����Ÿ�_}�˾;;%����hu�Y3OK�>,%)f�lDQ�PS�����[3��$��W�d�mŲ���NƸ��K-�ֹ��*�F)��`���Ж����,�mS�JlGܣ�(��h/d� ����d�t��ӾDVs���ռ�r��yۤ��~�[s�Xo)S��R��Q��{��n�����j�ٻU!9x��%o��$��J�НZ�=�?V���4묙Go��Σ��13�4��M����n�r�m���-���e�rJ7a�?��wԩ��#$-k��n�5��֕�+���bl�� ^6�TO�[�,���oO -��]F)�e+D��ڸMs�� -V�9�Ջ���(�|[]�ݘ���;��_��L�������x+��r�*@�L�ꌽqC��h�|��?�/x�j���3��:L�HH��4�;9�*�;3���B���A�\D��8yϟ�y�x���\���N�7 >��r[�ŝ7�����Ҫ*2+r'bVw�p���ia�a{}����ΊR�m�N��G3�p�<�*�[_�*��}���V�ܾ���u>�=���zgx��©� �X.����"ۚ������R�w״`+�Ӭ ��Xxt6��k,�"#�����ֽ���g��RJ�[X�W���_ �"V�'���4��W�*v�U��ymc��������o������c,Y�ݶV@]7��$�]��j�f��h�bQ��5����_ͷ�p��G��P�Iu�Z�����ԪM}����l��[��kb�O;>�և�OZ��j�K/p�p]��F�� ��͍ޮ�e��R���lV��К�5 -C��?�ȧbׁ+�f�Y`��v����z��E�G����to�p�#�;�t]�kl���ef�ݷ��]5~ى9�!����h�1�G�ys���5�Fq����֪|>wnڱϫ�[����oe���s�x��ᖪM= ���'w=�0��$"�Z��\FJ��Y�2�Ѣ�z���9g���O"zE����l�4��<�H֭�g�ٜ��J=��͇�e���1=��w�Ii�k�������I&`�j:�>��D9k^�A��_�(�܁ŠY�]�8ĵ��I���v[-tU���Pf���5 �ױN�����V�(����nJ�7u$8)z���=ʺ�6T$e�N��|S�������e16֪�ɕ�`5����\w�Ԝ�֮�Kb7��Q��^�/&����_�S�{���a>�tcD3��J e�,\����[O���R�:= ����b� �+|+��2����{l�(鈫lѬ{�Kf7�]kd5OX�h|2�CQ+�Fձ����:����Y�����.2�[�m���9SI�$+������h����� A� -vZ9��/cd�@�♤���~V�25+��[^��a37/��]Ƽ��Z�x*�� -�,�2jg�g�:������,��h�C�!���8�s:_"����d�4~$�V����a�|�v0�?�B��Me\_�|�>A���!kEfȭ0~���?�T�~���!���V�ߵ} �Q�.+��ռ���dD��#��"P�[��<T��K3W�^~פ@��~�ԫMW)�r�u��y�V�3(@�o���S�+2H9^3O�0=̵������j�qV;u�1���V=��� Z�h�6�ͫ���,�w�l.G�w|P -_��{�7�b�Ч�v��kT�b�7����t�_�#��U�g��6��X��d0E��XFj'h���70 �� -aχ��d���&D�[G�+��ʿ\��m[lV�dkOoi��p�����^�Ҽ�j�2�|��������G����},=��x�Ȯ2���T2D�+�k����c_�����_e?�r$�)���������g -�����F��ܝz�zy-r��+~ի�D~�Y�����9��i_A��/�M�p$"�4�]� ~�J9k�D�t��Z!/#�f2����IS���#>+Q��� �����вW�\{f� �ID�컩{d�?��DI��Mn���O�D�f*q���uJ�t���X�e�.�f����]͍�σ�������vj %�hhv�=��o�ja��:�a��8���r+��µ�lPco��f�M1X���k�i'M1��|�muv��2U��bZ���m䚚V;��H��"�n$�6�+�8�7� w<�������8*}K�k��[gXJRl����$�V`9EF�]���)y*�k��7��'������eI"G@��AL܁�/���]���%�o`�)�����s��D��X�܆J4]RG�s+'~곎 �EF�c��Y�r|*�»�z?T$�*=9/6~���NxF. ����]�Z�vfφ��2Z,��M��n�2ܽ�Ju���qV$o�Q >�jCd���u�I\�M�S��%à�i�oR��F��Ͽv~�>X� -.��r��H��?����b^�}����5�p���c:2+�V�9�<ڣC�E�s�Eq����3W�A��e��k��7'˲!w$���EU��8�r�� -4���W&�ō�W ��~��7���V�eZ�E��W4�#�.��$�� -�A���Ar�U��7�����0Ol�./�������U[����^�����^S��:xt��$A>�Ӽzu7G'��������9������eKv�ڈn�F��r!]��M�tͮ=��������\�F�ff0�?��WivN[��ۊ7��/:�*��t�WO�Q��C�G�� �I%<��K�^��_�C�gDe~�]O�"��v�����-g1�p��9ёyͷDN�ys9�ƍ�����D� �^$�M��1�Ɯ�Si�Q�T��ؓ�'2���hH �����œ��J��飌����x���u�|�3#b��{�qO�0bϝY�YiO��\�ϯU��_�����REHN�?w��Q�S�1Cn����v����f��$��������">�P0�i�sy� -���S�M����n?��SO2�jڸ���k�~Y"�d����0lm>=��ſ�Q���>�͝ڵ�i��,�)n�n�9X��qN�&�G<�u��4��KS�j��֛vJ,X�:徴T�DyK�o���TS�R������]�U��K7.���F"������E��(w\ �Q�h ��6z��<<U_ΊB�S:?]���K2�vl������ٲ>�����H���o�=Ƨ��������k���j��v3t��=]���zJ�v�Y�f����ֹ��<9����M���M \�`M-�}=7:�4��-]�y�jOU��Jen��V����X�Y���<P������>'�VQi�}7B�����g����Q_��L�lztԪ<<.�o�>����aɷ�uY@I�-1��N1k�0�W�Җ��;�r��D*(OX�j�����ٲ���>˛( ���9ۙAL큱�;�B-j�5�I��@�j+�-� ���0=��[P��!�ʂ ��R?�~@�\A^u�z�70����S������]��Ƿ�� �T��ҳ�!V���2��k��Y�[�Wb�T}uÌpi�;��������uG�?ǫ6��Ή�n��)��ԖN�g��sl��祣-s;]��h�R�[� �)P�%�U[��OR��Я�iJ#A/��DO���U�V_ʭ�g��Dxܩ3J�by������Ũ��U�[d�G��tz AH�/�i �c��l;�|��^-�����J�o#���K q��8����vK[���H - �R��ڢ"�_�k&p�0�d��@@�Q0orgwȳ�gy�����|����TW�D��~v�b�o�X�K��<��pb�P/M��"�d� )Y�����d���e�ev ��x*Ab��9%*��c*�.�����z�"�b ����/Ȫ]�ޒrZQŚA���g<���"fVq/ ��! m��^�ֻ �:�La���PX���A�J��LEQ�f�#(��I0� -�f���c���Z��������f��"���P��a��-�j�mc��>~7�@�}��2����I�l�|�+�ܲ��ۈ�w'��V�H���؈{ҟ�V������&�2X7O��"C��0zi�����X������ss�[}���F��0�����ڋjRq�}��4��9�a�����?��?7--x���M�{$v�g�]�\:X\(��q��]���Fl`��"�����zc��]�6�/�@,7fk�s}aa��d���ork�W�=e_Zs���k&�����q���E����r-�dt�ވ<����9�%��R��4�NM�D4�8�V�HljX���x<S���:�d&��U�?ՇSܾ������H"�F_6Mq����^� -jٷR�8�U�)%�q��Թ:qiA�ɭ�tځ�� ��{t[���#M��'�cW�'W��OX�E��9�t���ټ��oyx�s+&���q�f�|�@��8�pF�1\�#.�Xʷ��Y -¥Z���Ā���?�K���mY�ϧ���Ks�a���H���^տ��q/mD���r��+��ލ<������ʅ��2���_�p�BMi8$�:����bBWyo���BAa�*l?��J�G��s��§��%�c�D4��m������v�j�S�;�W�ΦK����v��: ��o��,��n҃[�ej�vi����s!^a��Ʒ�gnP��$u�r���d�T��Ԟ�M{� ��(�M8�q �f��*1zm�X��eم�uG��ͮ~\�p~��2!`iOhV?G�("�����+Y~�]ؗ�-Lit�ys�D~��hz>�� U��T��\��qQ$UGf՝�N�Jc"=��Dm=�$H�t *�?���Wx��+9w�(*ŻjcL9 -�i��e�����SAoZ)rc�4��� >�9�;a�s�M��t��`��f��32����-9������;Oe-D0h���4��Dj��]H�g��@R����v\*Y95q��2�����-�gN:\�����������:� ��]l`�i_j^�mq�</��"aT^�����xt��ֿ��w{y/҇�]l�Y�+���^�����E܌�l�HQ�ݞe�ڑa�ҵN�O�h����wg�9ý�;:���w�vZW5�?�)PԳ� -���A�^j�m �) ��x���>��� ����;%�Q�Y� -@`;S��Q��a'C��5�U��p[sv�Y�xu�� b�IZ��R?/����^�Q��D(��)���)+}\����LjR�G��a1!��2�h��\>r�X����3�r��-w���KU��G~���u�hX̸X�_���[��fa�W w�3��G�cG�.�,<̯%HG�Z���g �6X����&�Ϡ@�u�h�F�� t�N��4���A���cc��[JM�:�%���j���P�Ǵ��~��k�;ct�Bqk���_ޒI]�d��߰�y<H�|:�T6��a���@�'�9�i]5��� ���Ĵa���Y���7n]���qf��|1�rz �3�[���;85��={��,�C0nw`\�54�9XA�#R%�^.�KxI���G�uv=�O��s��Џ�`�Cv;�$���cz~��%��BW��ʉ��N\-�%��v�{�t����!2�R�G�a��#�L?q������x�v^C,�o�j�����g�a�.���ɖ�'�2�������ҽb���Xܼ��v�u��\lC��Q60�A���S�M9+�~p�E�s�V���,ى�%��)J��GV��;��'@��)oi����__d����M����Sm_�P���}QBA0�5�Q(��Fo�ŵ�F����ӿ6h��Vݾ_�"�����M_Jx���N׀˦nDO>�r@Li�0��n�E>����b��i�=_�A�TzbK�����_v�y-k��k��<�k�({oT"�v��:���<Ӽ-�A�[vo+�����G��cnb�f]���[���Zś�P�v%r��i��`�ː�AQO��Af���0h[��9�q�}ˆ�D-�="��1)U�L?��N���9|��$G.��@ yzY�ڠ���w� ����`e����~C�v�Ե>\T�^�����z��c��k��3bWIj}���wY؊7K�;�8�b�9�e�V���� u�B����M�b -��j�B3V�Ouia���,��B|�ٿm\|�Z��~͗�Rq���2\�4��M�7�n Q�����_�ݳc��c`��xN����P�;*���N��Z�EZ��n��A��k��ub���c��;�[+���4���>r���n ʿ:eF4}�@+�{�4���T��.����Bk�����Ә7��!����&�5���˕�7c�B���32�mmaO�s�ķTbӳˠ3������/�/����b��ߏ��γ�h������ �,�=������i� -jo��Չu9�3K����{Nj��:�L�U>ذ�B��� -���Pd-9_�Q��Uo6Z��ktv@��1;��(W,��U�R�Ze�r6����j~4�#��k�zq_�7�n=$�l�+^�=?������5m��As��bB���S�K�D�$y�e�6]*�W't��M�#���c�hW�F�����(u��:h�;L�S���3H��e@8U�[1�ĉ�bձ�߽8W� -(g���h����t]� -���)�[F't&�쾲�r�5{���>ar�����n�����2�A�V���.�|��p:,M 7��Y���{L�:?lR��wC���S�=Q�7��~���^��l��{'=|̂��l @��_��y2r��$O,������6�<��a�1�V�x�[�N�0K�Z�.�x�2�l������P�6�����r�=���.��}�.2��L���gi}Z.�{i�]Sh��h���9<7���10�"�������RF���Umpm -�{�_3��&x0�|lhRx����9:h�W�c�Gf�4�u -����x��i��#!L!\6Sh!� Lo-xOA��P�s��*�W�O8�p��풵",�V1�rc�\|��q�̡��٢�2ŗD˦#��r=.;I��h�]H�=�\?�.��կ���֢[mPǎ����).���\_��T��ꊮ5�m7&d)�@�����$��+�ے���|����|���փ1�%�F���^�N+V�W�n>/Ox���|� �G��H%ĽΤU��e=��(�(���ErY�a쥥K��etm4^^d\��nv�u�b0ϲ>�|ߞ��WGe*-��]dw�=ϳJ�y"������pQB�Q -����㰎e{�!;��y��;����Z�I��+'�8H�Q�U������δ�2���j\6��#e9��<;Ni�ݘ ͦw�k��(�W�b�-�K�8�|UW�<��;���a�����sw;�ݝ\_��H�y��8���x8���^�}�x��������U�n(���2�k|2&�Bs�����>�<����>���V���(sJ�:j[G�_'wp����nЖ��0_[���E���=y��^�)o6�]��Q܇:�b��K���j�#3}�"J���J�Kl&|(��Q�v�B��Y�Bl�=�U������j���(�듆9����!��)�� K;�n�n��IB� �".5�m�d�C���O.�}������1���!�u���Jf;��[�>���#��/N|9�� ��� �~t�"2���r��%�QZ�EgL��j{�hƩH�sVW����쾨�)��7���w`Ml��IG-�ŃvӨ��@ߝ�|\�3^��k֛G��c�KP^� ���0g����{���ɬ;j-�w˥b�Msa�!E7�n�5�����T,MZ�(˓�6L��jv?Y�fL��<~�����A�S9j3�г�����7��.\���`���6ϫ����Ѭo�"o�)<����g��-yO���n\LJd���T)W��GH���Vܟ/��K&����*]ze�� -8۸!�|�v(���(�{J�����}�@|A�g��H� vRz�.��Kn��-��E���O��]��K^]�{��X���y[��'��(�=�*�v��Cu%*v��AXA:c�@�M�n�Δм�-�dR,��Ռ]_�o�A[:Bet?�εo��~_U[�ո�������k��=�ˎ��ߥ�[��m�W�kD�4K4�h���#��ǚ�4B�%@LںadH�+T��~�����Co�{��.��yW�a;Z<�lu�*���Z�$���>�a���Eh�p,\�T*$$-�$(+NQrQ-�V+���Q���@D�Q�vt>�z��ю-��L,�wdk��li�\;y��EkJ��ʡ=��6�>��b� j�v���6�6lV*}]v�^g����ڑI~�EޣJw��^�E����>X�,�K��8T�������躶Ub�� �" -���{S���_����3c&{�$��|t�7�a�m�0�xYZ�\����t1DK����ߊ&�5����w�6���!^���G|�3��u���P5ڷ� ��5�I���'��A�r��/b{��rQ�q��q�`)��S]l�@K�_���eL���6�W��~<�쏘ޒXnn�� X�\&���cųRnЖkq���yZ���D�{��Rv�"�����g!���<^��Dsvүa���M#�`t�nJ륔nk�"�;�=��Z�V��˭��n��bs��/�� �-��U{u� -�"PB�G�4��^?���}F���忆q�V&Cy @"4'Tiz�EE��r��Nݭ�.��������l>H�g���Q�a�k����Z��g*� e����>�7�'�U%�Ͽ_�q�X��*}D�X�Fl�cg%T�PݷG�W7o��AʥSY���v�<� ��H�yi��"��H�s�(��m1��UAcՁ;:\��z��G��t��I�5�U��?g��5G8/�`z�/�|�+b�T�F��@�D}O�[��7),BdOlS�V�KBiR��Uߢxn��0Z�{!������k*5�Ӣ_C���ؗ���6(��]!�����vlE���a}��j�[`��X���O�������&O�H>:ԍsF�Z�ݟ셨�K�UbT�u7�s\�H�W�O ��2-�Rv|Z��Z(D��Vd�����Q.�+i����Qa��/ U�דP��R[�����xn9E����v �AW�"nP�^Ӣ��)A�; {���I��p��}�W?��Y��L�? ���GT�3��v U�ޟ"���������Uh��$y �m����"���c̚.���^<4}�C#��m��T���O���~�R6Ű��K�!_���ɺ��۳��� -�U�d�k+�z#y�;Nz�"�'�R0$��*}���L���=�-������*�ڨ�7Q�O!�ϔF{��j�D�*g�K��e��w -�!�X��$Y�RC�S�u��`Gn{t�y��g�5�<�U1d}��=�X�V ��u��W��Z�5ݷL�K�t ��;ٖ7�+a�D���%�������� :��l'����OF3,r�w��N�}��MC�ߥ���r�tu�72�|~��5:�U����]A{����I�&�� -�^ �R�҃��� -G�HM���hg˻k8����k��bSr�H�ޥ��(��[Q�]����6d{%,w���b���ck�Z�ij*�9��_����w�x���#|j����TS5T�VXY�1����S����C��ۃw�̘IŰ�z_�cK�|�=�o&����V'��s�� �F��1Ӱz��UU�H�ߦ���s;Al����/�F�.Q��z�xs�c��:X*䙿�{���f����Z���\��w��b�R�G �U��u��o=Fƞ��j���Ѻh��r�[��s���@�}���y����V�_�yU/��Z� {��/�����wzE�}z_���b�x�'����4�m�^Db�����i�����P[�4��ãmx�����&�D�����M;����M��i�/�p �)^����`m���*���p��ĩ�=Z��SPVVl��=kSo/�fC��rɯw�py�{U���t�L��t\{����!�O��P9+��G��R����uJ者H��#.�]hQ}���J��a=0�^`�w��59J�M��t7 Гi�Wz�n�XK��P��"�+�S*bL�L�ׄV�(й��'��T1����-����F�+tYՆ�S/�J���#0�Դ��Q���(G<t�<��M̯���.z�� w}�Y�ppd��$W -����O�n8�� ̻O� ��Y��ru���$+�LaI�n>�Ҩ��<�t�eUՌ����R��KYw�`�>/X��x`k)�s�T�c�gR�`��5��P�,������r�Q,�"S)��Yw��O�R˹=��$���K\�I���L�c���-�U4�OH�U������a"��������SD|���r�`��/o|����giF1���h��8��+x�TM��Vm����E�~��#��Q����wm <���H�������w��O����`�'��5[C3�W�Gn����}�:&�ɷ4�LJ��^�Ю�mu-�1MTn(2�s{���Gu��4�J���L��f� Ċ�� 3ϊ��[��&�)�3hݗ��e0y��^|}��������2Ԅ��7:��m�tQ"��݁�:zh{.D�цz�AOɥ~����B��B �������£ ^��t~�?��r<�T���x�h;�u�!/��@R���Y��;�{�j�g����>����ۭ��ԩ�|7n�B��N)������ -}�c��:zZ,�Bҧģ\��8F.�P��� �ZA�����on|�0N��G����g�-�� � -!�KO=T�;���C���ʞ��x7��`��_�[�3�Y��~*yC����@��}4��Em��k�p���W�� �*X��f;��>GWv�WÄ�����/�l��|��*� -D��L�J튋 s����e��<�n�=m�3ڍ���I1ɋ.!��E.uR�3��³�)SA�uT�f�Y��O���tqj���t9�&��������\"f%��� wX�JI�b��cVE��1����3k��b��D�`�i���v!�j������+퀬�NSZ��hՈz0��Te���d���gS�Oa�v7�������:��/��Jw������I�\*��{�� -��۹�Ꜵ�o���-ǩ�L�:A|�� �U��͋(2�F14eY��t����|���L�T%��Bc9�=<�Y*�� ��bx�~�HC�&m�Rl���9V�H#w��[���w�3w����2w)�d�������� ��;&_��s.��&U�����/7���O�|��*�?])���.G�l��Y��V�&���[d)Ru��n����V��ˁA�ps}s�!���2T����H���ۧ��8��w�/��f�\kRI�pt�=����?a���&~�\9J�,F��P�s�bM�u��y�k�h��_X_h��b�jC���b+��_Z-׃����j���g��Րr�m0�e�f�5p���K��i<�f���Q\ގ��m�|�� ��x�871�m��w��4����B\)����c��b�ir��:��M �Ց����6S�$ߐ�.��b:NY�T����*YyY�<�6���-���<�oC� Sۭ�O:��Q��f�i���5u�s���ߖ|Q���s��_���)�̋�U�l�*��+��$\��j�,���Y -�϶>������U�ʓ?�?�@b��K��ҶKl�N�7�(Yc�Z�:���F�3�O?�-��n�������2"W���8` L�k�ryg�����Qف�c�����j/U����mQ��(߰g\���U���� ��9k�� X"���B=Ҫ ��;<BU5j9�G�є�'��L���WWk/˃��&,d�����o�\�h ���c���`#G��H 8��M���y[�S]�RD�4,���+-F;1��Y����~t��6ͺ}��^��u�τp�Q��@ޞ�[�A%o����1r�Z��c���N��-��ڗIs�c��K�-�����Ge_c�|B��u�v+�W�W,˜�6GuW�>�X��R�Ĕʛ��<ت1i��Z<�Մ�"_ �L*j��'�4����Fg��"��h��,H�W� ϡ�,u��4/շV����3�4d�)�J0FZ�z)Ycc8>��%[+S$#�ý��Y-��I�q�š�����J�������ҝ��E�v�u}@�a!���4���n� Oi(+�ɥ�EJڄʬ��*�Ui������� �vKѧ`�4���N/�l=�c<����63o$U�s|Q�g1t����:��|G�C����`���A��Q�tui -V�>A��k���[iI��$�6~����R�G������o�]_�������J����8��%L��n��1��D�Em��G{M��D.��;5��s���w�^����ld��)jn������M&Z��j,�պ$M! ���k=t0��V�WG�Ѽ��r�Y��+�����;���3�#�+V��ڝOa�\ݝ�b�F[���_��\h0N�I檘�5$������J�pڍ��4�ej!�1 -��kG��W藼G�4ū}��%�[�Q�i�=���&�<�.7�$�%9��O!�ɦa&Yߗ%D�0J�U�7�NV U��I4��� ���¡fV�@ȹ�U�q�m�`0`�}� -H ���n�|E2Wn8�B%�?/@#����Ў��P���_��r�d��DtZU>�}7��e�&�k�����*��]�����%MJ%���Z�^�w�@�Ԓ�^�D��Q=����_饯��,��b�^�ъ������ --!Ya�nר��-�Ei[hΦ��v�B� -نhѷ;\#�4���b��X���؊����34�����&-/����笖���诛{����u��O�����(=��'q�_�!�UDs�rp��ŏ$��K���������"�t�f��H_V�4�>�ZZ1��y�;��N%��c�=:T��� ,�$�`3A�Z� -��%�ٗmo�����TŇ����i����0Rox~ -�=�'�>UN�k���uZ�;}G6��욫`Wy�RS��W��oeE������k�ɚwzg�T���Q�2�����n �6���/;�.?t�)��e����� ��Mػ��[��^��wqT��<�qD5���$A<K�ڙ'��֝"�O@�g��G߲v��+��Ey�1��3'XKb��C��� J�!䢤c�����5��峃?H������ -+=s��pg���w7M�Nؕ��Jc�/�4��NOg�ӛ'����˭�|L����[V_S��n�n$��1�`_�i��J���7F귚��D����"Y��ձ��U��_�\ [�#�5����~�p^�x8"�¥��¼���IH�c�rb���V]�D�f2$�l:�#9�n��m��@��e��2��k.�]vk��Y�~�V����S�rI�m����a���3~-p���No�Ge={�9)�֝�ؑV^/�V��I�bw,K/����8J*��SpBѭvĈeׄ�/��^�����AI5��b��Y�����i�\�1���':"'�1a�E��N{$�51����_� 9�������+/p9�� o�}�l�2j�v*����G_���U� �rV8��y��<XY)�m̪��\���@qK�.��g�K�}�r����R,}hͺ�2_cx���(���P��$��vsj���(��O&�(߷�`&��5 ���<���t��'7&���<���'i�S[����K�8�6X-�Է��R��3҇�k����Ns� -]o\,�����5�l�h��L�����z��q�����u~a�EQwZR��\��+��!K,�=�C���h��j+�|`}��?ry,=��p~��-�O<�n3��mYߢ#�ު6M�H�b�Ɵ�3?�9��f���%���'�{��^.�,�߽z���m���S��ϥ�����jS ���n:�Dvg�a��Sw�m@�z���u������F7��3?�F�@-[�oL��ݲ��������8��oQ�_����ؐ�8i�.�_��ҷ�ғ��x�ɰ������R�R�O��|�=:�">�p���v��V�@/_���^��A�I�p�Z ->�j�wf�C�p�`��/� �j����}����+C�RR�N�LY����f��n�g�i���RKK?h�n���9+ �VtT� ]H���0Tw*@'UG�8��{.���ʛ�A;)S��5���C`��%���W�-c���tT��'[���f㹧�mO�W�3o�SΊ~n"�NZ���$0B,ͱ17�G�W0���ɇ���q�����1��A1�X;�:�V���?Ȇ�u�9�A �Pb�Q�}��}biD�x��%�b�gni�����֙@�"�W�ңG?Q�s�?:����R���֠� -]V)�<F��NΘr�Qb=ʫ���H}�d�� �4�_~���m�D����|;������f<��N4�F:���-Gs'x��u��Ӗ!yƍ��M]�,�-�����>�b�3�6�(���◴�N�������.���ulX�����7yD*K��άho ���Y��j�n�\���-��b"'M۳c��^m��p�h��?��^g��?���5�ꋴ�)V�������z��JoS-�������.5[%ַS8��$L���|m�Cl�f\Ǥ��*�6��ӊ�NǗtn�_�\�(?O]s8��v�V����0k�n"��\keA��oz}T��T1u\h�LU�۲1*ΐߞQ���F��W)X�W�#T�K����荫��l]uZ3�[ө4T�+m���k��s��~�h=��%�j>/M�n�M��T�!���ler�vD� -����Mc�lԎ�����d=�\�}�@ ����SMk0�'��v|�����M�Q����'_)�_Oat٭V��sk�cȰ��!v|���"N��c8��Z���K����>��%�Z�%�u����_�oM�Z�֒�����Xm=a�ʼy�,��������G|�C��T7��v6�R���������S�_���ԥ����v�|b���ב������]��I����E �L���#T�+���%�l+�����H����l�(U2�X�7U���:;���S�Q��1�V9�a���&��������˦����ﶚwNs㻔a�ۘ�3�N[��~KgeNM6v�>6ѕ]�@����U������0� mt� Wu������ަ�w�+�V��/.\;A� gH�U+�}����������,�5,�����=��W�F�h�F�%j���[�����X��Rb(��q�Ix��n����l��P��/+�|W˓Y��m?�Zou����o��QY_,6��Q��'J*����s��Z��>���^F�V뼔4h�GZ9��4��j4x�,CAܹ$��L�:OD:�%�e�y�a�\��m����V|��6�[�� �s9���NZ�G~�n��J���^wsD��o!�퉵,�v�@��b�J��HH�s���22�V�O��`�u�.��&��JSN�b{"���#y823q���Y�%�-ٕN���y���Z$�MF��$eF�5��]Y�̉� -��_�gW��B�VRj7oa��x�ٗ��'�1@���F���Y�q��O�n�hṴ�2 �ISY=�U{�0�Bk��\�-��!�l���L�Owt��Q�MS)��#6�5*�Sہ���=�7MC�n]�+x��g��:^\}<��Q��9�:|���nwy�V'�i����:�O��ڊ1�$R��BM6䞦�&��=��b�tǽa_���xH��|0����x[7�cv�.:�W��k�[����x�R��ټ�{��8:��4�����M#ʪKw�$���n�l4�q��̑��'%���&�κw�g8k�]9FD�~�}�Gەu�3H�=��W�g�mV8����zLj��?�V�_��R�x�]o_�|��q3�u}q3�)�wq��˚��IO�T��:?va�:���&4�_���k�� Cqq���uT��QN�w�4��ѓ���t�2|@���"tL��|<j��}"G;�d�c�z����U_WfKP��m�mQ��epB�e�3������I���3<���}3f�[�_��5xk�[v���a{>p�՜�[�7Y[,�>z�U�tC c�g� r�$�����胬,���\�u��ߖ_6D�[��Kǘ�<1�Jm�E�yN9�ۗ��]?s��|ڀ�$�vN�^ ���)vcl}*��;o��\�Vc|.���u��|V�%�`�� ��vo�}�ddI&��ldǻh�!��5�=?U��m�1�����b�T��~U�oma��5j����k�I�w4~��-��${[tM����� �W�z�J�?����c~ �Г���~Z걆x�~CCZ5�Z��z)߬��W5x�y�3h�M�%�g}&v˫g&%��!{_��im��BE��!���ϳ�|2��[7�i����&��<ݔ;�W�S�:���-J�kX" �]^��7-t�J$��6{�d�@Juq������rq�F�����fR:�/��6l{�*ʵvT���I�rlO܅j)�]��#� -����j���d\~4�:�\n�s�<�/d��D{�ȑ})��1?���IѶ��!h�ej��^ݧ��ۋ��P�UۑcO��J'��"l���Q�j�2�>�yռn��t:�� �q��l�:Sہ�����V�d�wu���&o�+�6���b�F2���⸴;��-����k�g����2.� -H��eݭy -�Ts�n����ٕ2Pf���C�v���b���O<?dgd8E3*�#�jIǺ�>��jf��4���^*��d9�t�d�L?w�(���`Ngg�'���tS�� +,;\Q���)�O��� -� ̈�Oy�INńI���1�_nͷ�VN�B��n�Nw%L;�ׁ�2 �K��Vٛ�Ym�^3�[�R���/��n�r�1�UK��o�Qux,uY�"k��[Yt[���'$^�b����l��������D۷ħ�.���9r.R�:�|��ڛ���T�z�h�ݚK�)J�'�_QK(���%P��J����/�0:�V��t)D���̉%����~v -�rW�%S�0�D�*x��륺�Y\�O<���z����o܉��5T�ފiD�V��u�u���� -@6�T*��,햺��𫠲]�hg�=d��с~|��#�z�zT_U~���պ��R��cc��OB��x[@5e1�A$A埳1E"F��RBd�6����#���U��4�V=!>��%��9�WS?1N�<�K�. -��3��8��V��j���+9x�ϵ���B oH����_�\�Tf��hٚ?���H�fgzX�a�}���y��_D˶B���.�fUs�]8���;u�e��Y���hk�����^�t��\�w����3��w��7�S��%c!i���`���=�ԛ��gS0�|���䏏��r�z��m���{Co��_�[�V��$��^tPA:��ٌ�#!�.�'G������_�� -�ޤ�秢�W�{�U�ȍ�I'w���b���Q�o��6y1�8r�X7���/��h�� -���nG_<�J`�m�b�m"�RHI�[�luE�<@|�{�Ӯ�W�7��ފ��I�����Ck������6S���"�N��?������A{؈��B��8|�u�r�ϝ�m�L�댡�-~�_@�%�`w�f��B���nF���2�^�����V��%?,W����ݑ<�p�n,��y�g4���=�|��J����>�.����슃���jD�Fi¹�A�כ%�Y�U~�W~ړR�;�:�Y.b�j�RAk��LO�Io� �i*5nv����f�����|Ʌi�~�x�V�¨ц�%=��Y��HU��W�j��1������h9XO�Ѳv��g7i{tt�(oeI��W�+�[���Z[�Q�Eg#�R�L�`s�#ˍy� f�e����thO������Ug���`�Rt#�S�54��b�I���IL��� ��R/.��Yw!��4#����Ӑn�Bi^��2A�a������6�T���̋�[�.��!c48�y���&�C��yH$?t��Q� -����b[1��ݞ�߆O<' 7�E+����"��X�����T�����������K�uՃgo���M.���#9��[ A�۾],����_�Pc_82(��J �E��ڔ|�2�+��T��tpA+FSw3��<p)M�G���⽱����Gz��}fv-qUu��U_kJjt�O�Q���D�+RՎ��r!���k)��!b������icT�ό�4˥m�k�&��q� ��4-��U�g'o��7~��b�a;��|��B}I]:�î_�h������Y��m�0��Q\�n���\㙔(2��Y�]}@�_D_��0okf����^�.7V%��E��:U'�?�8���s���Wp�_�uT9R���&�[������3̑py��]��}����K�*6DTtyP��X(�7�G�iH�aP�r5�ɺ$����h���_��]?jWY������V�)�d?S@}^�)�(ݙ�(��R���e*?��4��`[���Rq�]P���������Pk�[+�w�^�A�a�p�Po�Г��V^Aw�4��~oǯ�Ӗ�5��,�ְ[��q�4g��hmp�#O�ˤ��'��u9�w�PВ�SmY�Ef��~��Y�7��E��[�`�����(�n�8��1y����̒��b2�>Lٓ��%<�':�IO��Ǘ9g����s�!�g63���N�o#��PN��g#y����jy@��r�I�5��h��N���C0�t>\�i�Hy��0�G���dX�k�� -����I����~9&-�؊9i렀I��F�zє�p���%l�����^�}��ّ~,���AY�s�u�܌k?�ۘ/8�Cz�*���~�G��H��ґ^밀^*/�����~���/�[����T�E�����5}.r'�[n`m7]�&��t^2��t�#�aK�毪���W��R�Y���s]��$�V�B���� HI'6/礩~�SA-�!%/��]��3��.[���Z��w$@��~λK-Z 9������-��i� ���~�%��ՠ��JU��۴Z���\\��8��at^j�h�w�A/�ȻT���c�{�bD�YI�o'jb�q:�{N�KP�u<��šՈ�@�|X��d���@��Z��w���O:U*﨣 ��|lm���������Z�տ���x�*�MA��]����� E���B����n�����=;�R�L��N��_e=W1R8�xM��G��Wx.^�`<|%�:�Ը<y��uI�ڟ�V~56o0� -^�)n�=��n�0/��wn��B�r64�Z.�B�����P�"�^�� -�{���~\����3��I������u}ß�o���k���$���ٕ�_E_����W�smS��K��o��њ�}\[R��JM���>hj���_vӠ��R���w����q3m���5�\,Uo��������ϠR�҄_յ� -�>X���4�Dw*�1�CUZ|_/���e�3o�#����vAɓ��k��x�W��V-�馊aZ�K�g�N�X������8������P+��iWD��W��z�����ĵ�bd��:4�u&=@�a�}��J�����������*:�n�'(�n峚�5�H�����/=�^/F��a�VU7�ΣT��5����NTv��"��Vˍ2�r ʥH%��ۭ"�R��(/�DŽ��I6R���^H��j����i6���.�W^���r,�N�Xզ��ټ�O=u>���k��O3��9��|���sA��ӭ�c���U�\A#���֕ȑ���v����eߘn�/i��S�Zz��#L�i�~�����`��r��~�R������:K��;���:x�?��A�%��;p2^ -�e^\kKC�>,���^�3�W����#����V{��)S!$��:"��Ň�p�fc�}�4"g��5s���,���۾x�4Ol?`��N������ڔR���W�='��`/u��� �6��>���!�~�`; �vY�ro�/kZ|>�z�hr��uGrI��f��`ݿ[\y�Q�*O;���wg烢̠�L���)Hm�Z�)\������i�A��e��;?�)}�4 �<o��qh���z�_�;�[o�SK���5�:n6&H�ifݱ%��Lг�n�o��i�&,�W�ĄS��M�.���Z N(-��&h�{A����3*�>�����UÆkY~�S{��Ʋ�|����X�!��{Ҁmc-��8�rV�]�X�S�H��f������iU�%b0b���<���L���m`��\�L:sW�ݲ� xːo�e��6CZcͬ�[���҃6� -w"��Ʋ -�2Ы���G[&�/��K/u�>?.�"�%��p{�i�'=ԆW�� ��������97V�`�����U�[ZH��88��`���-��h:/���3?�֫&+�S3�I��O맄��.����J�^FHh�a�>5��+����[���a����Vr����j��;��h�,�����KzL��t¨�͝�Wth3��:W2�s�5�z�o�)L�M�I�л�����j��T�X�'{r��.>�x_���q�w����uj��5��}w��r���.�oo�:3L�"�/���1X(3\&��L��o�f� ��l�i�c��rxc��B�UOpu��(M/��F-=M�U�����T��S�����>@cN7��+��w�Ti�W?���������W���ꆠ����d��[J3��ʌl�[��+�����f3��Y�a���4�~�)Y9<��5�{��L���<~r��vV�T����1���\�a����?+99��_Ր�n��9�Ù�=9\m�wL�S8#-��k{[J\c 0���Գ-�5[����f<7��Ë�yx�����/���%�,��A��~}w��w�5���(IL*��4'�<�#@�X�_�G0S�T�bx��dP�j'��\ -p��)cՖ���`em���:���ߌ}�xmD]��]���?#�W�����~���Jg?��'�y!��_���ˌ�����vB�`�q{�tg4�t��5�����g���)�șڇ����Դ��"I�\�h�gU��`r]g�NJ�M����M.W�_J���O{�D9'��"b�j���K@�����8�wV2#ۻ��'p�O�ʊ* �`�/�L9��A1��yZ3�5܊!��jX���uN�"so��0���j��X�q��S#���W�$�������@`7#LA�7�;(�2�%���SS!�G���3"�\=g�.�*Ӑ�_n��g�N;�4c e�f�1��`� �=���������ԛ��vI��0��CW�v&'�KL���)?>W>��E�����uƬZ���Bq����z����(�4���U ���'0��w��F�6�h�����+n���Vg;���*�t���ǐ�ڟ�4|ܷa -� ���4�v�{�+|f�@�����YC>`���rI ��=����2W11��A3eH��)�;��B(� ��>�� ��([�l�@H��,㘹�1�e�K �ە���&|x��᧷~S{[�]�qk�$Țz5E�Ws����]4��d܀� CmE$��4�G[oM���_�%����Z4�ŗ ��(��Jv���س˴��ݏ6L�c��)��M��L�^m�{�cz����1߷���*�um�Z��RI@M�'�q2�������Ft�T��Q�a��'MkTZ�]��sk��xK:^%u�_����.̊ڧ��l-���#LөY��ו`{�Ë��iʵs�h1�iS��ot�F.:s�?��*R+�z;��W2�fL*��CF��\����� -��Yq -[�_ ��H��>މ��T�S���)RH�����_9O �X��R���Y -~(����<��L��-�}���H�^\g�R�=RFS�j���sH�oIl��8� -�U x[���NA�;��~�T>ܘ����d*r <3W��{jڣ9��*k*�~[ٮ�L���Ko�x$���뛾��v��-�w���T�j��_��� ���K�پjY��2w[P�9���C���d��^Ȩ/Ւ�"���������T���C�[&H�w�]�[)�lFa����'K�Qg$ɕ,���,K�|��^V�������K��c�/�wp-�����=Uz-�9�j�4��q1���~�B��N*��.a���P������Ôx|�\-�:o�t7����iο=�����̲��>�^�hц�#A�e����P!�~��|��caG�6xV��0�%�};�3rΌ5�ɑ�����M�����T�G�w��ٟ���.�su)�$��V_O�x\j9�<����=�Ze8�+�ڽl��3���T�������ro_P��=m��?5j�NsZ����7�vϩ�6�V�1U��/�Ci��w1Ц��z�82ZoQ11,�/���i��MC�_�(�&��K_H��cB�Z%���O�҅�Qa�14��EC�B�m�0�.vR�Ɍ7������^�����p� -��F��2x�{I�M�q����7Tc]|�c��M�P [��a%�<_AeZ�7)^q���=��Um����U� `��^t�a-k�P� -��Z� ��XnO�*HreK}1�"N}A�o��>�3��Oz��^����l9�fM��谌�/�*>��p �ʗk���1V*ʎ�Zk����X�*[P�ۧ�V����/�O��s����{��Rr�`�~�����n�����x?U#Z��Uۋ�S�V;��Ž�� ��g�C�}Zk|9��Ң���T[S�/x�}�Nrz�~��ɵc�����i� %�����j.�5s�,1��ImR�c G�Y�;U�z�`�[|1���*Ce%�3�r���vE�,zX���M����������yc�g5�n5L��ͥ=δ�p�M�a��=���N�k��O���gh +�^a�Z�7��5��ca��b����{`|gD������F��j���l ��ܼ��L.Ȓ�|���=�w ���( ��j��;٭��m$^p�Y�blY��ȄsK�����ϑ��6�b�� �u���ug��ݫ,w�V�u��Vs�N�7"�Dߏ���Q�o -y�1��]��V�� ��V�� ���H��K���u�1~���F�e=ʯ3.��wc3�U�]�ͦ��p;G.]wT�!������z���-��r������.�>��s_�a�R��Da\�߷� v��*�� w2e;SH�}���i\��}����0�}��p��u۩���6kZ�^�����^ڇ�@-�K�ҩ,/t�<χ�_���u��'a��O#��x����p�\WL� -�'*�L�M�A)�-�t���WaGqiK�H�R/�fw��_�n���wa㰎�jRT�Qw��;筩�-4��I6ݯ���3����GN�:s#O�]C(��)7aC�F0�[S���o���_贾�����fx��Úh@lJ�j,���r��l�Ӈ P�n(��[��h;�l�{˗2Χ�T��-�+�s�Zl'[X�;��D_�oil���FF62S��fX�D�uywwX�����+b��h�fa�edIĚ8����[����M�l Jog�3"��.��լ�=���x����cD�k��9�v#ȟ��z��L7>h� �7��>�����4�c��\2�~G������״ԥoz�J#��g���7!iΦ����ͣ��y�͓g�'5hhF�Ζ��06)s)=+�Z�(z�4zޱ3�h;��5��lZ|�qk�{��Ϻ��p?Ar�����.����ׁj�!L�{z�/�+b��lz����������$J[WYIR��\��5�T7W3�y;g��~�Q^��6R�?��:���]B-}oKo����`5�*w*�)���t�g����S�y�?:�����k m<�{F/^�^x=��'�r`�s�dVc�ô@��r9��oq�c0c���`RC����=H������n�>�;�LM������3�Fc&8�^V{4��3;u�Z��`�n���}���i�a�W�����X�����lAk/r[��X��O�}�Tl;���%��5*��i���Y���/�������/���S�BCf��z�&_6�`�ޟ:ظҰ��О3%����@���j��h�E��[��CN�6�K�gp����=��������G�PN�VX�����=�o�bG�z\m�x�h���ƘzYÂW������ws�zc-�Q�z�������3^+Q�_s�C��+!/��Cl���sQ�T�5k�h�Ψ��_��%����Z���������L�k��X)��~��)�5ӽ��ik�F�Pu5�=]�(�-v=:�� F�7C^�7��d0n��|�tԙ6�v�u�U�2�㹧�M=K�a��ag�9�H��m�e�������8�ˆ����.|�>]�������I8�b��W5L�9�x�j-X�5��g�������4"�j\|`�܈:�f�}c��8�!����]_:���}"Lcu��z��vk�p� -�e�y�˥�r�O�ꅽ�t���������� �gk����٩/��m��կjW6y�(�.�%��=7�[�J#<��Φ]7D�4�����(\���ٍ}��wH�K��Dv���D���l���t�����Sly#/�����:�(i>q��eۋ��i#_c���?�g�c%;�3�Q�F�bǵ��4$���R��,�m�5r89�t8ԕ��m��� -�� -��[+����!��r�+$D-wv��ȉ|��� ��a�Z��������V��6�P��$$DK�c��G�D6Y9��Y8T]���U�y���6�i͍og�:�C���g�3��G�VV�v�T�t���6�{��P���(( �3���v�m#���H�)��9���f�qKZQ?�@`�'����`�nh�b��M��O����v��g�ܑsF�57�d�5�#��u!�-6��� �9?�A�ǤM�n�~�N�;���͞x��b�m ��ڤgG<��/��h�Q���p�L�ÅTl��^��,U�^�����;� z�([u�q������h��}F�A�P��Tv���������h]�⑼�WǦ^���������l� r��YU4�ƅ�]�ì�͙�O��٩����B9� -¹֝!�<y�\Gw�]1��ۻ���S��'� �����j���ͫ߈{m��J�o�εl�k�/�f.���S��:bW��cqT�<l��~(�[jh���X/ō������=�B��:�O���L��m���g�oC�a[[h,]�䕔��WU'�$����2�=r�`J�����%�@G�E:s���ib� -%F;B� Eg@5�3u�~�qI�^�|'�4 -�����1Xa-]v���k�������v̎��:�D��o��5��[ړ -d?uqРB��n#T����������oɩ��� -X�0�QT�,v;�H�7e||����C����ܗ����� ��ع�x��O���sLL��`ȼZ�Į��?&_ڰ����F��@J'8���U�|�=n?K^�>of�E�v�^�q�~�=���p�~u�/�P��/v��ͮ�w��hm+��;����[H1��}��:y�W -s��C/�_���|#�0ۺ��1��@xQ�'4u���k!ӵT�A�-v���H ��f����mw�wH�:^�7vx<�n_Ǜ�'�F`z��JC�~)^V���йx���wP�q��*�g<�$C�b~��~֝�������;�]�f���e���e.Lu�e��# ��'k������!dcs��u;��Pn����a�!\�Bݝ'�5P��Y��h0�n���4 e-w���lCx{�ݩ��Z�q�B{IѴM��ck������[[�gV�5��� k��|�جWۀvK)�.�A�1����3:�m -v;F�c�$�b�)��G�ɫ���0�i�Fj|�$��x�ˌ��F"���Ƽu2vg��njx�G70]����$퍵F &��ޛU���m"�8\��(� � j���~;!��F�Qٻ��uĺ���Hs<���yN�'Ow@� ��g�b��ٽ1ӷW���!YNI{R+�c���^��~ȎY�����8�r���¡��w7k�J$�a�C��w����.ӎ534��j�Ll�O�`���8�4^4��=�{���q3pq֯�BSC꼝â�i������ȓ=p,%=�M���C;��~�n���~o�@�W���<J���K-����}��ɯ�/p������w�F� ߷��g��i�K}��W�����zHˇ�A����9V��k�DZ��Lt�:�@��̟:���RYO]gycu���?��=�L�R-��~2�V�|��8f��?��,7V��U���H����F� -f�F.3��n%va�< *��+��4&�6�����e.B�'�����ɀA���JWF*g{]�6�Vh�QNU��Lז�J���\�Q�\,]|C��BE�T!N�l,�B��aO�ܓ�[h;�f�ۜz�R���>j���ÊYi�W����XVX"y.��q���\1�AD�uo8E0D���y�i·�oX+��%������tzZ���y��$�Wc��o�K��+&�G��8�1�|UI�O��%JX���t��ta�A���u��1�M}��Lܣ���ŷA�H:Ӗ+ї�ā������8��5����&^{��W�9~�=/x���AgUC2qQ}��A�Oͦ��$2֞��6�C�F?��VS�����q�ͣ��_��M%��E�O�(���:����H�uX�,����BL���U+c�qC5��Gfi�]f���Pړ�G�Xٗ�E���bE��Ct�Uu{���vY���p�g~�/?տ��j(˱��bVi��(��~�V}��z��5)%�qX�'8]]d�W�b� �9� �"�i9~P�f�-�*G��K���e B�S�#CkkQ�eK}��g�P|�v�):Ǭ�y}��҂�T�EU2��0��C ����U�+w`���d���`���� ����ir� -�$V-6�J�[+B�P:=���V�<�[I�썺�����yO���˞O��:W{��'H�Vֽ��i�����:����;ȭ6��f$�z��2M]X�L�`�ޙcħ�:�ͬ��o���$�����R��x�Ĵ�$D��Ԯ5�W��3ҁ��<��F��Ld��%mr�DH_Lm��LPQJ>��� [��4c��^�L$��S�^�p�l�oiA+r��e�#������o�ø7����/F�T*���^�W~w��<�Uý� `�&�۳�����4�l�ա�\3}%�:���+hr�eA��V��u=���ŕ�OE��{��Xe�9 -l&O:��FC:ūL�-�K���*�.!GKWo�C��oR�g �3���ꊱ Q���_�1�*hi�r ��ڤ��������:��@��hftEџ�l�V�sҴ�;gV˽s���5w��z�5 K�}�xQ���^�����-���:ZU�d�RS�WpI,�S�O%�� -���w���d���UYL���Ϗf(^�����j�.g>����m��5�I�x?�4DE&��<1�,|f��zT�9+���Z m��9��չ�Q?����y���t8�{4,�[Q�t +�&�C6х��Z��������Xo�Ш��ح������Q|�Ca6��ʺ�LK��Rü0��w�9�q��4��=3�}]��d��D/p=\Z�����uQ����Z�6δ W�#�� ��]�V>p����+#u�`)�#�_��c_X�#I�ʲ���%S�рHh�0��o���+�]��P4�o�yR�x,UM=�'1�Ӷ����qݸ���/���Jˋ��LL�*EU �[�0}I;Q����`���/!�u�[�h`�m�@R�r4����5�a���2�y2K�°8���aW�L������&�����F�e��&�eC�g�"o���F����T��*v�,�xZ��F�}Z�m�l�@��М���5�,�����%�N�ڮj[� x>.� R���^4]�a�ʼn>N.q����Ϡ���~j�bbi�r����ȶ?�vXl�K�6:���x������g�_$����ˈ*��u��}) &-܉���Љ�w=��M~\X �{JG�g���n&Q:m7��A����i��� -V��n܀֩�,�� -+��8�28�,��cKg)�X�J�zz~^���d��!��Sd�m� ��?�РT�Ϟr�㷘�/���iCn�+�N�c:��)�|�R��a'QChE�w���������`��<j�W/���G���4��X���d=Qp�x��ʳ���C�����l��{ɝ�H���d���2R�r��9�sɞ;k�/<JG�#����d�̞?�PO��c�b�Һ/�_�����W�1���Yש��?8�gNg\��o��Ǡ�U���p+�W �͡Y)#S�]]!�C��lnl���J��- �ZU���ke�°d�����ւ�A�\\a��������o�5�#o�w+�f�[�F1m��NA�7�����)�����l�-�Ua�,X���̎�e<A+�r=YlөUЂ�M�P��j�yj>�a�_C�����nr��������ҹ�K�uo?A2z���Bb�f�!d07��Q!w���q����J3��p��Cs�y�W�3����Gç,'߮m�آD�+/v���l���X�vt�5�1s 曶R�1z�y�����2 -�a�^���{�M9ϥ�a=�n���d����h�3���=�z��4W���9x�4i?M_q�.^�1����*�\��e�9��gyA��k]ܼ7[m<���#}�,LU9w�W{Kx�]L����j�6W]��a�c���o�&��r(��s8�/o�i�|o�5l�n2�T9� �MV5i`�luu�zEsd�r��x�"�2e��|`{L���F����b���#W����]�1�2F�,E=�V3���G����������\�q���������^n�.�\ȲSVg'� m -XgԒ�m/Y=@1y�[��|��k�oMN�Ϊ�Nmp�X5��#�t���� �Հ�k���(ᣡ㔦%��6���o�?6��K� e:��������r���%f� �u�Av�^�>Q/��-�����������͕�1/��J�2�7O��Oώ����c�\�o��@���]�{6w�Y��h�W/�iƧ�ҷP_}h����(F_���#U�r��д-�n߅���#d�K[�:|�ѓv�aP촛���Ĉm5���2x:��R����T���ל��R(W�w��ʬQ�|k�5�r���S�r*��J)�Mv�^bC��KNPA��^-�_USQ���"�)����u�/�TS����;C���~��$�o�n�Ǜ_�K,r� �#�v� ��33Rݠ�p�S�;,�;��C��T�Z�y&��S���ޙ�i�Q}�#[T��������BL�ί����Dھc�~.~�j���O�(��4�"�J]2�<+�̻�t�wpLX<[|�g�tb>t{�A5�VJ�[�t<��J��m]�n#ퟑ3���������?���=�x9ѝ2 Xj^�AF;�m��3p�W�b��:�����'����M��l�J�Q�a���Ms:l�K��s�C�����~������EX)�夕�f����x������3�����ɰ/yD(Ax�E�SUQ��/�[���-�6ڤ0�������]�n*Rk�>J�n�N��*��"��D�݈��\CUB.x������[W����ռ��9�C��>��Ξ3XWR���u�|��B2/ɰv+\Ը�-��o�h۶ۡD}�Ѐ���X���.�:?:���DY��Mf���O����� -�kJԄU�/������:T�N�M����(ȑ�zjF�����T������F�� ~W��ae�U��^{�9�f�9dN�J|�i�7�ߖ؊F0�����7��#x����a�Ȋ����v^�C�Z�2Ysr�T���n�*�9����v�����o+D���y�'�9���o�f��� �����ԟ�Xr�}�5�b��c�Ⱥ�(��Gz��'�:��n$ �WC�yƟ�'[[���vj��c�P���y'K3��!�{7� >�7�����.о����OU4&E�y�����OA�J-�6��5l7�*�5����6&g9]��������89-��T*�v������R<�ߝB_��C�evo�@�r��O��m)�R�4�aCb�!$yk.Vqp���d�/���� �����ڪ4�&��9�^$���h���R%�1��y��6E��r��ﺄ�J|�A�Ƌ�KX���dZmVH�he`}��i��v�;ӄռ-q��k0L$��r� L��K�o:�O�o��y�` -��:�&7�;�Y3�dJ �U>V���k�t�>v�/ #�ݦ�?s�y�q;6!w+�A�_��]�!�(��m7���Y��cU���I:�Z�`�9t�)Zwۭt� ~�9���p�� �ۊ�&����j� -��<�8*��{�ewH��/�2d�_������r��H�0/��&r��G��ަ�b��������ݰ���Nc�9���t������i����w�8Dq� ���'÷�;OEUO��ӶE���]���y���<�����y�m���}ӡ%.J��FOn�]��mQ.��n�&�0���Ջ�E6��� ��2[�V�D�A�{���%��9�;�/��y (m:> ���GD�,�/;WXk\��F*��{]���*��i�ڿ�M�iW��c -A奝,z!��͙哨�K=v���5d���[Z��G�����u^��oo��x�o���{�\"��)埑L�����`�>�JIUosC^y37{_�`�8��D�|gO�_��J���tݒ�X�J�aՈm���J��˰�v�X��ɴ��2�[�����!��[T���g̑@n��ĺ���;�"��~��N�nY�_B(Cc��X���3�N����d����SU�Z�$x�,V��ڻ��o��=�cs�~��>o_�Vs���ձ�y +�%�~�©��{�#� �x����Ž����;*���� ��{s���1- -���d\��\_�:UQؤ�5!�4S}���2�P��OL�W�ӹ֩8��؟����Q����,)`%(�!�f��[�F(qu��0C��ʱ���L�K�C�.��O]m$ڌL��4�������c�gV��|��k�T��a�szT��a��:��Vt�i��<G�l�� ��x�����;�2w�3`�v���~j�K���ڎ��k�j�Qg N�ِ���?��{�t�\���N������B���VJ��wo�.*�`<�����=��c�F'��%Y���X�=�UqH�P�������tB]�Ryͮ�UE��*�厥���_���y��1��T??����M�!����r�MZ�6�j��^sC������8:���d�M�g�2��=�Ω�+_�,�O�û���8�W�DS�K�6�"۳[TS��G�g�v{D<��M�����,�����W��ZTc) �nE��w����%�e��yfMo>?�9j�se�֖~�=�jd�I�Aљ��sw#� �y�_����}pA��ͷU���db��#��� ���sY���,S�_f#�=�WR���(��(w�L�������*6��)�d7�����0P#�Ra��!���X�N��B�5W7�R@4�NU]���P���L�_G���g��hۮ>7U�X5�����Vk�������I��Vyt��G�����x���SD��J����U��8�/� ����/�+t���ix��A3�w��i���R_PP���+��P^>�nt��_v -{ -8�>�����5���G�"c/�&� �iS�<֫��������ͧ�Hꕁލ�O�}E�//��L�����?'���;]�4��:Y�� L_��$Ɯ���*�i�"��Y������o�yW�[�!���s��xVC��yOS;��S�d�^�L�u��si�r=m���)��|,�3�Y����n�TbE�q3l"�S�?�������+���"�M����Ӓ��o嚫)⬇���J* -��H�TL����]����w��r�am��N�˧��� :�hn��V]���Z�i"3{m;L-�0R���8��?4 -��_)��ж��b��iu��U����X]��q�^����U���ĝ�Wg&c������+�<�m֓J�������S-}Dea�m��V04���58�i������X�^��z��m�� ��r�]��>��'u�� ��sO��jm�Wkw���j�-���9^ -c��T��9��8��=�Vd,�\�uM猍B{��%f���&���s6_�s���t��1cN���K�����1��f��W��X3�u'vI�idkt�uy�rz�2�*��g�iDl�/3����g)����i��9��㟽�L5��f� dp�/�Q�aݥ�+���>�˫��-�s�~DӅ��w���eK]9ùa��dF���W�j����AU����̣�4_�� N���8 ���}�)�_��#���=?0��?�^��uw6&����[���B�-�#'w���~�X;�D�Ϭ�|������'䷣��|#��@]�o��6��@��\����ݼ�>��j�J_����toV��:"�S�s�dR��|H~�����r�>���Y�:�s�����z�����j�o�����%��Jk�SD]�[�L4��dPU�Tͬ���ō���K ���ȡ���eg�X�Pژޛ�m2fֺ�`D��q]�z����.UT�}�\&�����]e���̽ɭ1���b���W�We]T�k -�?���<�����fVn���m��ٯ4d�H�B�#�2��c�ղ���ޙC�k�t�#|m�l����HƋ E� >�8��h;���c���*��ji�7��Q������t۠�� -�b���O�2�;!���S�]O�1_N�G:(�C�L�ߍ{�F��A���w���g�?�^j��kF ���;�e��C��_wc�7��Tl}ֵ������.ʺu�G�^��/�bdw���5�d�����:�7B�ރ^���~���쏚]�2�`�%��7^N�!_V��PO$���"�����a�H�Ng�PJ�������j�����Iݕn�$N%�� -z����I���{��_t��~�Pq��}(_O�Ľ��t�m:5����n\�9�%���&����8�S[��n��*��1�����k��{���4�n��P�8��O��pE[R�n�ф���W�f��;<�]�������;a]1̭9,L�Rޙ���WOj�����l�(�F���g\���~hkq���2���'��|{����ܖ�YI<�vT�K�hWn�#�2]�:2;�r�c�;_�m��m�V��[lb�/��E��#Ւ��f�5�9�a,�&�B�yE��G~zL'c��P�[P��tЁr�jeb��b���Ԫ/^mku���4�R�oo�b{��7�^u�~��L�5\v�9>q�rT2_��*;��]����!������C";��\�m��>i�?���7�pf1��;H�^w�<_�'��L�ZS��U�*����5|���Į�a�[�>T�$��(��n7e*�vX^�j�[����r��T�M<��ݿ��ZP���K�/�~�1�gw��r�-�~���֧�� -�����?�@:� j������I��]"T�u��Η��~��W=6J��z����v:��evӿÛ�����O�ORև곷Ҕ -N�+[�) -L��5NR���N��z���v��#���O��O�w��7no �?z�sn1���C�d�?�8�u�\wc�_�G{k=���]DF�hw��eW��Դ����l��f�\�Î�u�|~��}}}N�eE���i\l�5���M��L��-�<.UZ��}�m��?9�c{����Xi���3]����^@P|��Ck~+��w�z�#���9��&��#8�cAPχ�p`9ф��`w�"��S�^:Ӱ��}��hv���Q�/�������Mܣ���i{u����h��%�r@��v#�� �e��?�w���QM^.�S����#w���;��O_<�G{��{аZ�����~=l��F�f� ~~�4����� ��!�����#Ţ��:}"�jwb=�O��8�4t���W<���_H�f���>P;97�;[��� �����ל�"�b@֠6Иt�8E1�a������Ȅ����d���`��c�u5D��0�^B�wxpv�ާ -�ۃ�nJmE����>l�$�aW���_�(7��F��s���ݝm?]�0p��9�;����Dh���H�A g��qͳ{�/y��䋙㟫mR�{=ө�tb�FG�o�&5R'�d� -5��>�c[��'W��;�!��L�l�a���l���Nv�J�\q[I3����a5U�ތ�`�^�9O�(����,ޝ\���|Zh�V듼_99x�'�iG^w�|��q)�������,��nv�N�ܪ�O�n�(��w9R����/�=n6�v��o߉{����:lתMѓ��c����ذ�|$��/ju�.G?�$.U,�����|����U��F��� - ��l�BO�J��+3��p��C� >����������]��CA���T�s���U��5<�j����-�qR�V_��t��]\��Rv�!��U�N� -9Lg�?$Z����y���Ơ���^�Eug����S�r�j#u$���l�U-��f�� -�Z�Z�z[��u@�T:Y.\��_��ӍA 4F�ݣLѫ����Dٔj���F������m�����s?P8l���0��Sc����}�͚����CN�|l�ƃm^�&����Kݸ�/������8t~��~�Ug�����ۘ��b����(�5҈<|��'�Zda���O�)���E�,6d�պ5�w�Lڇ�� ���LqGmVWѾ4���^��L�ٸ������X�;^�X����p+YC�wp�� ��9�b�}b�u��n?'�p�e�a��38M/�7�76�6�<�Q7��i|��$nb[5 -qR�Où�I�ك����ѿ$����+�<Eb�J�|Mz6^��!�niͲ��BiȠ?�6��2��¯Jl�>��%ޟ����q>��MnW����Tĝ+� �k����mb顺ĕr�#qj9�;�#_W..ʭ������NO��M��҈-�m��v�#~�`�(Ѯ�;�\+ӕ>�jƦ�!a��oqr%��hM��9�y��S�)a��k��L�F�i��5\��a؍e[�&rdp��r&p �ku�AC�_�D�(6O@ V���&�4cX}��\�������'�k��ʋn�)�Έ_�`N/���.`猨IY㥣:�����/��YAQu�t��a��_�Pc_�r�o���lD_(6� �]�� ��S0�mߵ于l�-�")�����������~��211�7����RU�*����Z��D�'�&8iH�w6*%��J'��s��r�6_�[�o�Z��B%�TI��� ���u� -�Qj��3~�F�������I�]߁���u���^Qf��f њ�+?-�n%���j��A}k�l�8��9��K�rO������ƺ �` -Bk�^3o�)��X2�i�s�y�LvLݲ��*��~o�q�˟jH/��,fnHW 3F��aɟ��������Fj�9v���s� -����C��+_R/~�P�;��ع�^�EXCn��,-Y�9�;G ��1�|zY���wLN���n<��ε��Au��^���e��v��V��TyM����K��B�m�xt�~��iε[�`"7��i��eh�����A��M8=�W#�t� ���W�71/������ܱ�M���Ł��V�Y�����y��Zq����VK�β�(�J��%��Gx5���y����1��9t�T@�h��nTBf�t�9�1nM������Um�dXD�B�b-���4|�6�r�����f��#����w���r���L�3-â���+rZg�f��hȬ^�K��Y�)��!�f�����}_��8v�˾��H�5n���.O��$^�CS�$�f����"�h� -����^��O-hX���YpL�Y��.C��5�}��|8�{��#��ͅo9�;��,v�R��⺄���c2^kQ���}k�V�Ũ� -0��tvʱ�{Gya=�����������S��r����=m�����N����n�q�d���s�|p�<��Iq��'l�qU4=��8����v��t��L�sn�@�O'�tDZ��Vz� _�%{���yus] |� ~p�/�v�K�9�v�h����U�&hL��W_��� �c��K��Q�V�t'����"�E<�a�)���3����4fc����ïZ�гT%Yr���R�:�~�%���Эez��]���'S!�9�}�/6�3�:T��n5�h��W��pZ9_Fa"ή0���2��ܜ -�����ׅ�{�߂�<�߷�D��$����\v� ��(�Pd�'��oR8X7g�F�����vW�̉�ڐ��O+�.#F�7&�����[í��Kk�i�%գ�s�ID|(x�ܩ�x����Uç��\����9�~-�+�z�0�&���P�:�[�g��V�2F�~��g�����\�[9�yw�����a���K������X#+o����p�H�-���'T7�N���4??��|�]�����\����Y���oGU|D<��T��MfA0�!g��� W_��:����Tw0���"�M�w��͔j������W��K�\������Ҋ;��}��O� j�G��8(�czn8m -q�W�w�gZ��>�J�-�k��^&S0��^a��w+���UW�JÜ}�ۭl\�����uN��G�0m>!%y1�RQ����h�HS�_*����xԻ�V�r((EvgE��7��y�]�[2L�G���ůYe��$h- ���} �aQh����6^�4+E)^�$�M�=�E�q�����(j��:���^-��h�����7�^�Nq1HZ�g�W �*l��X�?vzj���k�=��[���p�}V��� WuG�Zx�y�Y�h�J����c�yQ�XwH����~Y/�}%rB�~F��+� -����נ���*!�~�=�IkV���h�[�)�GA6���T;�1.0�d�.��~����l*����e�b�!�}�\�I�4���������<�Ev��G�m�~�M)����bz�;����<<�9��I'Ƞ��ʍQ�d�[.wL�94L�iq�a7��&�{��Gu#��g���t5q��I��������:�_y)Ȫ�Ԝ���c�vRxo��2��G~�G3����q�+���p�/��S^��r���k��4<���4�ּ��3P<`���d�N��I,�Kν�l�ŸZszdL� -Ӣ�/�\)��q�1�����Hr��a'C�� ����QF�r4��gd/����sR9������d���z��>G���O#���^���8/������NJg:�/�hIE��N�lThĖ� }r���\��S��מo�ݖ��:��}���_.~ɳ�Nc|ҹ�VLN��&F��G��k�e��**@Ӵֳ/?mBO�#�ϡW���|���-�ӳFk�~���j�f�U?f"6���N�������m}�:vC��{��~� ������b���~�!�Aє�l#:�ý0���5m���t ���A2����\����D���"���x��(��(iG�Qm�֟{��^�����?YX��<Q-�J�AQd�tZK���GR�65���=���'��l��^�nw�;��5}���7��A�/3� ?��C�H�9��?�]�Q�c��DZX����܄إS@>����M���uP�]|�~��6�����v_Z��p�v��O',����?o?US�}<�������J�p��Wd�T���sӻ笲���&.3���u�f��~��N���A�n� =T���@ �c�w��3�g_�OnbJѠ�u$�o���f棯N�Ƶ�J$�B��.f�$��SX�����H��:�t$f�u_��H�h82����4/b\��>[E=K/F��/ �Y���dh�z�|�&��A�$�T�FD�Dt#�;�Z�s����fGhe�hN�db�O��2����0��D�$��ղ|�R�ܤ�QS�q���I�n�̯'5V�3\��[/�<[Ѯ����;���~*$W���#��[�˫L.���U���� _��3�^���-,�q7jjlx�KE�./�*�v((;���j3��9;�n�/�Ƕ�xO :��k{�G��IW�}�,�� ��w�8D�b�~j|�� S6��Cr�.a�n<J�hKj� Op�hܐ@�Q�nԧ�����Z~ -�i2��>xg�wt�^TI8@+����\�N_{.�2�]8Dn��9�T)!H���(q�SdM�z�J����F�`���B��m���^^���4��N#�*��!�w*~W5�L��,x�� ű��A9�翸�4��u� -6_$Q�!��F#\�O��RP��P{v�iqg�"��b�\a���W�Ԃ�M[��"�E����a���Y��F�u�k,c����Y�yz�>w��>�Q�@����gЌMQ�+�u⌰^~��~�����;$!�\��`y7�6�'�0V�k)��5�9 -����P3>+�`oϸ.u���8�{�M�k8l1H�lE��T�VJn�-n "��1�O_ء��������hpz���}�� _6ZC[��廝%�Z���p���_�ʀYS���3?�hr�f�2��<Q@g�|_. �f����UP��p��\w�'=��7��\|�l��y���{�m> w�a�=�����\$Ď�-A�(�Po��Xl�X%,.���3^�]�V_�(�-d¬)`1}H��;�����h_�`:��a�lc���ލ�!��Q}���'z+��� ��~�N���Swk���f�i�;�Y3�E>���aE-G���g���1<�s�&-�0o�0xo���U���|v�����D�!��r�����<̖��@�1<��}'.���Ecդ&'�������1��7��"ene��t���-Z;Xc�Dlw�l��-*���4�v8�[b�$� -���^f=g���Q4jwD�<$��҄��{We����NnNH�HB -|�&Ik��g��M'fz���2TG��g%:ٞ�����ʯ�`�Q�Z�k5�0=%os1�����9�iL�+�\�����Ýb�b�j��� --L·,�i���=�T>5�+��n����VLl��G�sCY��� -�ML4�j�'J��>U�~Z��2�õAQ�x�l�<Y�B�����|�@�dc�}O�Ce�Ī�^��D�5����F����g�Q�dsm�|Ap���ܣ��V�r��2]�����v���6j5��0�~��<��R�v��:�R.(�yl�X�kU����G���G���:�K.,&�YD -},\i�Z:�x-��i�v� !��@�G`���5{]+��S@�u!T�����6ɩ�6���2}�����Dg?���i/`u���'���}�P�Nf3��D�!R�k��r�G'�;��߸��7�3ŕ�ݮ�vd��V߸�ɺ�Ф_�?F;��Gb?Ţ������n��\Ȓu�ϓ_f]%��Yp��� g����8�F��kR�ͳ���3���1.~�l��A{86"�����}�a��.����%E�?���;\��~�gC�X��dx���4������.|,�}d[�����k�ٵ����l�5D��H|)�/�]<�l\�k����)� -���g�X����t�=Dxrw�^+� � �ꮔS�7�+|���,��@e��t[.��O p�({.��~p0��noR��T��l�|l����M�]��]5���� � -˱F9�w�i�o�z;�5~G��^A��M!��d!��/��fI��~��x~�آ�R���Xh�&b�1G���F�{L9��u��p/��m��w�q(�s!*w[�u���� �N�L���g��@���3��[��F3�{�z�"�7�ULlF��a��gg������ʜl ���{4ݛ��.�M�Zmr!a�����~����(����ߙ���[���HҭM�m~)3��:�y��T�3R�n^�(wڨ#c�q�>k��pQ�!�p�����d��d*��ݙg鴎#�����ֿ�M�W/xUT�a#����hE߾�[�r��e˼Jwy�ְ]�~>{�����[B#~>5؇�y�ᕆU��x�����=���qd��iFa/S��лF���7nO�VCߓ�@�E$1�B@��=x��\��' W>H�������D�z�1/�����I:ۮL�?io������?v�cL\l-?Mb���(l��f�jD�����L�urê�'m���ˆ��5�t/�~���f��n�K}��2{0L�w�w�_�PG�ۈ�c�I�'�ڿ/D+_��nQhQzĆ��3�B��[O�Umն�����/�dUH�]���Ê�B~#��q�~�H���$�[V�^m��xU�_�c���=�1j��6��� ل刈 �z����p�&��ܦ��x -���V��6]o[R�M��1���^��!,^ƕ�P���Z_��lt:P<<t�?"���:�4^u4'�GV��h�,&��VPI������<?_;��L���4�P�`�Ʉ-�f�vG�-�2݂Sm��F6P���M���f�@5�]�\m,���yAk�p����&r�����H���)����3�[�N�,�*|]�������� �j�;~O8�=5���]�7 {��z�,}���u�x:�VUi���34��Z:���m�d�!��\��!=o�ur�4���˨c��)z�-����*߹��5 ���^s��&�4��|�����_I���o|f#Ő�U3�>������\���haR#_���x�Q�v ��㇕Y��wj�ګy/@�rW�e��tJ�[������w�� o�~���|��ل���ꍒ?�N'iԽJ��:z�e�~��:ݥ� Ӛ��|� �ځ�K��,&&��rEgst:6G䆶l�s���=�Y��{oP�'���t)S4��z�5�b��9վ�� �#NA���+���jV��ݴs�r4Y垷��ʍ�M�ck��7��VTA#^�S��� 72��Գgk�M]���&�y���J�SK��mөb�������KF��嗳"��s����t��G�;�lLù9g�@��;��6���W��{H�����n&t�{1���P:�p�_��Ҕֆ��3��R�Frtd�J;��>K}/�LpX���N -��*O��G�=2(�����]/>��:94_Ƴ�+�V��z��$O���ۋ��<d�}ϯ�����e���ȵ�x%?��Ư;i�;�h�Qs<F]FI�p�c�y�����$��cg�:צ�8�|�i�j� yr��P�����R�|�cM&$��m�i�w�\i��[��mƔ|�� ��>�4��J�g�x%.��M��)�?���N�Ԡ��0��äŧa�Tqնgw'�J�엇!��o�;�k�Up�zm�R�mc،_��1��f�Ǽ~����O�n��3�3˞���������;�(��U3�O�^��zt��u���+{s0�6r��N��5+l>��y�}����^��.Zqmƫ{������Ƚ���5�t�V�}5+��4s�R*|�:@��[�W�ұ n�p|�/w]��N/@���P/xƽZ�\��5�|�Ӥ���p��P� �Ta]��hJ��|�~�t����1�3�*�O�f�y~�_N#Z�}�?������t3P�mɘvۂ��}�0�K>�J���@����)����s��n����~�.��T�������~�Ɛ��TC&ΙWy�\$k)a�g��d��K�o�DJ�7��gP�!}zm�����X�����H�F,f��wa�^F���ܺ��L-��vlԩ���T*�b��0����V�� ��H�J�z������yk�|k���Qeqmb�i8 f���ȶ�l�0����*������@���P��i���F)��М���-�Z�/�|%�\�ׇy�֫��+�K�$m�1��ch�ĵ5m�rw�+�4��~h�9������k��,_��Ǧ��3�^���h)�j�k�9�l��m/S�/�.ż�[g�j��� �v��< -r`�5���~���ŧ�G����y�ݶm�Qj��;*J�wy�tUZ���]���M��ٱ���Z�!�к��e/�cE�%N�F�_9W�)�~۫K�˾�l�2��������e�����;wJ):D�D��`��\�� -�i��5$�xŕ����� �~��<?������аІn_����}GX7~+'!4j���с($�����^?=0�>߂���Ai�5�����XE�>E�f�-����(I�m� �� -�bp<3�ϕG0|<���a�Cܮ�]i��R�q��t�Z�p��T':�#Eb�Z�y��n[B�g�as��91�'�RZ�l(���~����=��{��,��$���P���#��v�us��q�P����i��$���c�BR:�?�Y��?��0;'�8DF6\�*h�O�سv.�ٯ [KJ>����48.�g��Y@j`��Mk_���/�c� q�K��:e�x�WI�����>��)#��u��Q -*�������9�P��@�����!�-��A����;]����W��K$�;"|����c�P�p���f��+����2�� �?�����\��Rg :u��� ������dw���/'!�� يē w���Z�v���@�/��w��h��@A��~��d4l� ����y�������&�杶�5�i���7�������+�`;ʇ�y'�v�3Ч���1��(�0*�5 -(�~ d p:��3I�w�$��H��O� -U,@3hbݸH�%�Kɮ�}@��J����V#������mC���q��:�T3�Hy�?'�+7m9w�+��\Tr� -\��vW#)w"�V��}w���Qm@����������T��Q���ȖR �cM�w��J쁋$�~9-�L��cļ��*<.����'O��P/l����l�r����<^�S���?��1]��k�!��Wq�Z�q��v�mX��Cb�߲��]4��F��أ-�kZ������Rh_e�VE��H��s��`d,� ���ֽ�_�_�,@J����p�Qڄ��'�_�����6Gٽ��<0J������nE!��u���Z1pˋ�؎���C��)�[��ۗ}�78��~g��=�f�bM��B��+I��FD�@����i����S\�*��3G��\��A}F`�=��F}�������W8pͲ`�0���5���V�~�� s���r�j�@1 -EE�f��0xqb&A .��>�!��:O�� -�������E�*L@I�*�8=��=/�QeN��4~R+����Q����(�ngx��!��U]�f�qX�ՅdoҮp��ե�ǟyEv��X��tu�pZY���ʝ�~�l��ڎy�ޛTksM -.���(ʠ������鷭�`HG-�9��zw+L/��q_|�CN��m�b\�́s-�s��!/�mj�8Q��gh������'\���EK���A:A[:���E�NFa�'�En���9�(����=���(���.��h��/�(n#��(����~���S�-�w8�L����=|�@�&�cT�,co������-t��?��jqyE|��*�<�3�F�jQ�i^v������$����U��<�z�1��+���W��ep��G��Br��u�`�Y��k��'*_z�_�b%,�_����o�����A#� ��E�7X�(���Ιp��Ԣ@��+ �غR窆c -���{p���7��rp ��b�T�T�e)�Յ�rۙ,���X������P���1+�W�c���¸OӒ}��&�<����@���2�A_1��Q�������-i��DɎ� ��&3i�̎��ʥٳϷv�'=���Wm�4J\l��[5�q�k�.�����'�Iĭ�c�X�8�4� ��0�-q���N\��;�̕��2�G�u0rXuo���݂䳔���rs-2�}�N�Dx�ݶ��pv��/��F�3�텣��a��%+��E�Q!�K/"o�ڏ�"���_k��8_��-2"fj��Q�5H%���b~��:W��@J7�iP���3�@W��q�}� �|�ΫZ6��GHQO������J����9̋�JT��^���Q\���7��m� ;G�O�w��<��P����}n5n%��D�h����� �[�v%Ώ���Cd� -����q�}/���=L�\��G��Эm��d�^�`�H��,�f*Tۺ̡�[b>*���*�lQ1���[3�V�������9I+h��o�}H���E�S�nB�E43���WL���#�7�Y۞.�Z�A�N֠�yj��D���5 o�T����^v�����yi_v6�)m�̨j�4�6.��U�� ^h����*�ahi�������tMU"Ť�%uף�&2�t��9�U~X/��XTs�T/�B� -z_~��5gg.s$u���V�u��%�se�*?~28�l��n����w�a���BӨ��K�����[߽�n��T���p*�"[w۟NT%���V�<���(�A�SAk�"#(n�P��.���?� -7���8L*����M��ı���\?�1�wk��Xx6�C L�,�R�hT7���IA ��r-�5�R��s/x��_�$���\�1� �W_��t{�c�Yz�;�;�� wU�6?1 -[r1��GҠ��������ė����j�><� n��ZF7/�'���c^�v`��e�4pì+���IT6����Y�Q��{�5�y�_�7�0pguǓ�����ya.erj�e�DފxhYi��� FDI]n�SVG��S�x�-�K��&���L��յ.c�(���2��c�\`a��ʊuk����ʨà���b��'q�շ�xཌsS����K>S�W��)ǁ>�)]����3�BA;��&�ۻ��r>N�F�ۧ�Q�`/O�!nu�uf��+��8��_1�Y�U1�ꗬM�pni8!��X�����{_45�/�8 -m��+�g���s�j��;;�\R;������f���XP����k���0���;�d�j�E� ��h�|2A��N#�@9�7��E9�c����� -fU^[?A�7�_�̝2�ĕ��5�5]<PgD=}�f`���P�����h�x����幣�T!3�9�Qt2$��� -|c��L���ь���Brr�~��ة}�fz=_ ْ��́��/�������z[�0.c)W����k��8�%�\�Ӱ����}#:��C���0������j{�u���`�s�������f�d����z?-�upT�d�y�͗�|S�.{�rӱYR�ā*��P%����}���M;(���tZ�/���|GԿ�(礔ᐈz��>���k^�;���?�2w�ڭ��[��v�6c,�p^�Z�c�_��Q�zW����m'�b[�}�bx���@��3NQN��/�VJ��/�,B�FX���Ũm��nj]�dD�h�-����-z%��h�;�t�q��;����NE��O"��x�Ų-��\�4<��,�9{Y ~� v�ûI��7��K#�'罊BSР�R�����v)`p�����?���&�ē 9�/�[�?����<�1i�շZ� �`2�~��ʞ�o±�i���+�-�#�p�F���=���t�<�Y7���a��R%��L�r_6B��y��u��j�w&-�dS��g��b"0�b�H#i�+��״�&E'��Q��=�c{ݨy0�>����m��n�R�e`2� -�����X<�[7������(��%c�p�͚�f��Ű���%S�7oS��@��&w�؉$gjZ��v��&p��⇻`��/t몛)L]b1�ί���hͺ�QI�mTJ��tp�4u�?-z��K��>҃Ar��Og+�a�Ոp�kЮN���_��1 N��ă�S%%6��Ә09����xR�C�����"�EM�)MrS7� �>O�@F�?���Ȇd_*����C�o����Oh�97{G����X��Z��$�r������&Bu�I�5�ܕ���㰫p+>�x� �[0�v�~x������+�o�o����msE��zU��߾��u������ղn�ãΤ���z/��͊|u&�h��t����.��/r��>��ƴ�F�w�Ө��F�q~m�++�"�v(�B�4���SN �V�����a�%Q��&�D��k#e��>�N ͨ��X�X��P�J�j�:vq'�]�M����i����;�Ӫ���K!���W{9�>NV��� ��#�t�)��bܐ��2��rY�ڪ���!/��c�+�ח ���CJ#�iz�)� �~���� �=qCK��7(� ��J�a�)7-Tz��&�ӭ[�cx�R7L�^G��a$�3�\�[_�p �5i�٪l���YI�ƯM���c�-5j~ޘ����&N�d�=�8��Wݻ�(x�鴜壖�]M��$�ϼ��l��D�$5\%�˸��͛�/WN��(��g?1��WE����'��c�x��_���@�~���c� l&y�\�q��C��֣c0��4�<���9G�����|�]���$Q��Q�o���4���D��'�X-�M{CR�_�S��� -�]�$,�r~�>{�}�P�,)�|y/���sӴ�\o�;�y�+����ޛ�W�[�_��Nѩ_}o)�e�^���*+�I�*�M �EXN�i�O��4��~����JpT�C� �M�#�����˻�(� -e��Ǎ¯�"#o�!�/�nX^�N}�$��SBpǦ %x\����+8SnÄcjV��IX(��z�N<�MA�Y<�؟7f�+%j�{\�jx���=�����C�Gxv -P� ������t�)"MmA�T�?�%�:(�M=�n3�-���Ю���PS^����7 ��QJǭt?�1� -���;0T ���{�0�ѳS�n������:.0�=��s�,�D�o�;�@� ������C\���AL�0�+څpYt�%�w6� 5�/g����%���:��̀����'���u��.���1[���T�:�|�势�!AuB�G�ܯ�����O���Ġ��ϵU� -��պ,.({�%���1oK�w���m���}Z8~0]��ޱ����@e�̆?�x��u����l�4ִV�����;�cLT�J} pü��PFc����/�6��J���q��rj�cYJ��;ٳ����� ^�v�bX[Q/`m�o���� -pM�`�R<�@�ޣ��$��H����+�$�(����C�⥛�����1��{�NV�����Ye��V��FGI���"d�:D�y�U�>�^F�bF[��/�����<���/tVLk����N̰��D-Z���]_�@�he'�($���!gp���p��~��Aq�{�.�`� -����ŌL<$@��DS+2�!�7���S'N<`?��R��ҌM��d�um��5�˝�/�P���`����_K�%nVඳȿ9�b� o�x�Y,ܦ���b ����U[P��%[�a���v�.��������% m�f�9���7�{��P�ok~������I�|��UΝ�{��SQ��fH%8z��%q�YZޑ�3�^������8X��^�Z|�V� ���������N���iG�臗��/ t����ހ�-Z���"be�n�;.<��^ե" -��p��|�{���ᾆ��~S�=��TC���GH��\�FQ�C����������_���xs���ث�#c����� *g�3�Z�'Td|�9Io$�F#��R�Au�_�6�-��\ԭ�E�[���t��c�=/�Iܹ����Ζ��6뽃����3&OԈ!6�]���$�|�P����F�;��~Zu��Lh�_pe�������1�t:uL��U�0�7KĢt����ʇ��p�`j{��~%ܛW�DZr���s)��A�im��i��J�T�lb�����o����Iw��ѐ�q���/A�]-CU���yT_�Ϙ t8�VvE�|0ߗ����X�������������% ������z�����j���6�Y�~�u*5��=:;�o�4��w�W�.=���QdUT-���A�}�!d��{��|�>��si���5_nI�4�}���l��$�Ƃ)�씑/68�;S��o�.���t6c�훏����i�r�K�o��?P�#~派?��%�5���c�Z��nh8�����b��p:y�q%��X����G�e,�\�����?߿�����͇+�½[<��~�z�w�g�y��!�"d����º�j�_�ޓ��'A���GFt�U�ux��m���3+�"��^AqK�{>��S?�2G���Em�˴��Z��<����mo�cG�؝2ݩ��{W �e8h+��Ky?&��a��!]T��V�h1�F��-]��t���sݯ�6�A��� -�qe�&-�Ͳa�t�>.Wd�v�"�:`�˻�52�Ӯ��,���}��I��d����0>|P��*�� -k�Y�>���)y��אH)���Ϛ�B���������,Q�˨\�ګ�fy������� ���A -����Ρ���u��VΉ�c�irD��4�5�L���_���!Sd�V~�培>��b,[ٶET�[_���"��V����˴Yߪ�!�����篩&�Ⱦ��I���_+A��@����R��6^�t���aS�<r���w��濦ښ�gN���u�W�:���|&�0w�1�_��&su�O�Հ�3_r�0��T.�ǧh��������n�[�� �[���-�ۆ���l�W �"?<�L.��".�'���3�^������X?�\��yr -|p����Po]v{�j�v��xl�B~=#h���b�����w���_��_��w��x�a��a����y�OF��S���E������i����3��h�+�~��������ޤ�a�f�����o{�?|o�W&��7{��z�Ӥ�a�f���O�x�o�9��?��g��J���9�9���Y�,�����w|g�t����)��njUk-���Z��W�3MU�����u��o���9c���Z�W��MU�����b��g]�7�S"/D�����i�c -��.�x�Y�w��m{7�-��\L$ח�n�c���WaP�U5X� -�P��ɫ&� !�.Q5?�BX/LzA�y�AK�~�K���M[�b~=�+��ު��8�}A�����\vMu�=ز�V��8�N������W2��%�Ƕ Zl�-g[{�5�`��}�9[�F]�*+�L%��l2�<ѕ���t�͝w�:��M�\�+�37��C�oIT�w?���Q��̋�q�>����;��4e��Z�le�x������u5������R�Ա���t���"+ѢĔ͞�/%�滟���v���m�{�H�]t���>{/�<��;�4{WQ���ØD�:�f���s~��i.7���t�_1t����k��<��|��i�'G&RԠY�8Kz�Ɖ��V^\�J=����z �g�G��>�h�N�r������U��/|WӞ�}���a�mD��9�z�3M�3tz��� -t��%Ua�x�#J�B�e�zb�������PH�;]P�9��D�|�80_Q�b�� �m3�Ý]ټ�3�-[(��!�[r�f"Y��1"�r_�R^i��(~.�^������� �j����A���Sh���Հ�IuDje� d6Š�OzR9��ٳ��uC��jC�fasq�����\vd����X����� Z�|����M��z4�R��5�ٵЖIn� ��m��O�|&x�!�n�̊���e���ɇ���p�$(���i���G��N���p�T"�7JV��Tn��Q�ܞ���KLt�'��s��]�@}�e��h�H�{�h=&(�3!��J�S:��(>V�=��Tmռ��V��7��7}府8R�/>G�Uܷ�Z�/=8(��=4��I�Sǹ]��g�ta�U�ٯ������O����&Bd�4KՑH5���>s����%2Q�>������L`Ҵ�� -�#�!K={.#B����#Q5�x�|ol�(�r�XͣbP�P!k�M1�R.d#}%f���m=��O����(hJ��,�� �v��9_�����D��y����������3�8g �������4�v�Kfz&��=��|G�|��ZA�J#�����씃�K��͚�NE�����|����Ե-v�˝E -���g��[�r�ֺ����`����Jc��dc��e��0~~���&�>��ӑ;W��[��C��;�ǰ��$�څ5�n����^i�A��ãU��r�e�&ͣuh"� eJg�|�/w�?0���t��Ro��0�/��]'��U�����Oˏ9�ɔ�D��|�j~�I�d�Ϣ~�j~����.� �8��k�����o�4�~��ߜ�.�K��_t��6i��Z���W��8��k���|w�_j���_j���䯵���濞N�j~��ߜq�����] ��G�ԗk���;P�8�+��RSp��4w���h]'~%��kܰ�<uwۊ�?̉�������u:j{&%�Zr�����K�u ű*M��^j�Wܹ~;R�2�>�FS�vu��]5)�>��n�;��i���5]�#a �m)�#��aI7��P�/Jd��MT�q�Υ�f|����ޕ;��i�lu�7ʍ6���|17E�҆\�6����O�����JT͏�� k�E*B�����Ad��3��v��u}ݱUe��+��Ʉ�D�M����8=QɋZA|���ؔ$�wT��~����T�Y?#Q5����ޫq"�քLG:�3�e���4��qF�g�9O�<>� -��Y�?�w�"?� -���q��܂�����1���1q22���]�wg�ܥ�ʤS�(�C�lv���Į���V{Jr������.ϊ�Df���E_{��U�o�%B]����Ͷ}obc���WW[�zO�j� ',v�W���eW!�^��x�O��E�s��+)Ô�H��AA����g�cX����QK�h�G�)�pvփ�b��gz3�a)W�e��T+��lۓ�/qLH���$���9A��\Е�]:�F�?-�q�ͤu?�^d� �&{�V�{�w�=o�]�j��`�48r+�*�Q��5�[�[0��H��Q��ũJ�30K�i�� �SC�����:�����ã]�(�1n�>��W�ƒ�V�S������uO�����z +!�Δ FĚ�o��@m�lXo(P4�v�h8m��0j��4���a���z*���r'; - -endstream endobj 27 0 obj <</Length 65536>>stream -�%8�瀾a�jiC�S�5�����<��9�1�k0�֛�[���'�R~����� -\~:��;�53�0���W�x�To}͑��*^�Ԑ�S� ���S��9e��˙��$A�qN����cq�����_���N�\`���eJ���@-jM��y�����J�t�İ T�t��jsߥ�أN������3���Jm��9D<�{,��X��7�h)Χ�?�w����g���3��u&���Bh 7Z�0Ս -WL���b�j��$���-b��q�S��0�cKY��$;�S�j�Q�% k�] -Z��F#>i��|k�2O���l,=�:+�+~�˕R� ���K�VN�3�&R�i�3�"&>�e�P3�gE�j�r�j�W�����I��} �T��:�sԚ��������%w����`\�ۜV�h�G���&�3��s���S/����sٔ�K�$ -p����m�'(�� �U�&4 ����_���+�v�q��/��2'��Z�'mʸ�X�Yc������d�zB��~jb;q������E:��~$j�?$^����O���<�ޭ��u5�GJ3�s"���R���%�v�F�WT7~Lt -+�d�����t�F���&=�5���� )���u�s��(��ᥬ��RB1���Y��[O�d�����@�������wL-�`s�B���hc B�b�x�� -|�ϊ�*�c�O��H�(C���Q�b�NI�O��Gx|~��)�k��Z[�:��fSH�Ds���ԥR?.�6(M��N?`ApFuiN��>=�m�����+����w%���k�rt#ؑ��O�OD#)�튮߅.E��t�����\Lέݑޛ`�w4@ ����;����6;��*��%��7/�:�'8��gQ?��������h_G�g��Z������9�.�k���|����_�?Ӥ�m��Z�oW�����Z�9�+S~����~�I�.�k����D�#��� �*%�p�0Z��{����b������Ɯ�v�3�� -2�p6뀼[N��O�@��((��i�W�M\��a����o��ԥ������*��>VŨ�"���cX` -ޓ��\�ޜ�r�_Y�������q��w{�����'����^�ẖ彟T���.�X�$�����uڻ�5�� -�=|�Dy�f{�]d�T���q�p����<D��~ �|1��B��(F��6g+%gl�� �Ė��Fޜ�=����Ȧ�xC�ǰ�%� z�]>�ܹ�w�^��(~6�nuo�N�Qm�:e*gU�΄��Ǭ|+��1�t�Wx��_q��>�K�hҲԎ8�T4��Ld���{���W ˝�p��8i��l��[�E^�{u����p�4�{�(AY�f�`D\D�'�)��ю�q�=CB�h�{���|w�@���h,�3~ןO<ڎP����H�]�)X��u��=�Ydgw|U]]9�[��Y��Ci��&���#�@��+\����ع��l��?#r����Q�PS��f���Y��/{�헭�"�1� - -dN�L+��\�S�BjA��@,[~t�������|��p�2{D�%^2��ɗ��}�|p�&� �'��W�#皭*���S�_'7�Z�y'X�]���rc�c��u'��b~��V\ڀw�<�#L��9�2�0t�Ӥ?+qJ��EE�L4C �Ii���819Q�J���"�2�H�'����@O���P��1O�)����d��hM�ܑ:�4Uӛ��\�蟑8����o�f�Wc;u���5(�G�h�0�0�ԟn�x�VS�W_�#k=�e��\)�E����ZYQu�9\ZD�$�|�'+��)qj��%��ԉݍPl4=5�ބ��#��`�2)�&���0ط9�����φ|!t�'T��rހ���b��9�z���A��O������)տ���9�>���o�D��<��j�}��#��S�����,��%�!��ϳ�.[�:�1k.Nu:&��㽨o�c783v0|�u$|�焟���{X۾���I�1Ŀ?�ٕq�0�����|-�w��i�` �Y^:\a}��w��]ܑ�'���H�����-���Y�U,W�H��xQ�7��+A�,�1\�S�0*4�֔�����+�sV��s���)#�΅x&N���@�u��o�Hr�t����ᙾ9Ėmq��5��"�H��ĉ�?NB�c���f#�Q^��h)�:�d�/h�-T�q���+b�T��'Sg�}�D�o�Ijj��u|^ɵ�e�w�JK��v���_�#Z�����Jf�A���N]����Ъqd�a����xA�0��^�pT�xa�f}-d���"/#/CL�]����Z���B�u�!-�j��'�x�EB,�%Q5����͊��d� �k��x�NY���T��r&�/UZH�W��.�O�<rD%��78OF���+��j_O���EW���c�]�5�rf�H6�)����J/7���Km�D� �Li�@� 72aV����k_),3����@�s��v�ʯ��ҿ�(�0��q��S3;}"�e�D�� �l`o�*F�-��D�S����$j��'��<�9Qz����v���C�|�0e����bJ�fP���)3©��d�y�Ht+���Q����ݴ]�� ���'!����5zz�g�k����z�\O�33�bBU*졜�Sx�|[>b�k�R:<�Z� E�W8��a2;%��e�@�Sd��>�A�jZ<�B(g����~�?%o�&����h[�Z��=r�(��n�����ibe��C�d��Lc��5�d�=�jvGN���}>f3�(��3���A9i��%VN��gģa\���ȝW�l�ܲWDd"�썘{��a��6�'�l��Kk��&A�|�G��1�&��:�����0N���L��o[m���v܄�G�j~��k�%%��qB&UȲ�VcH�TiOd1>��!8�������E`#b�zN'�p:�ߪ)%v�O��Z&I)�k����F��-�JV~F�jc��#�,��b1 >�@�υ5��5mE�����:��q�����j�{�HDC1O������R@MM.��~���ʵ!G`n]Cz���)Y]���F�g��,�'p~t5�E�g�R�Y�Op���/����h���ߜq2��Z�GW�Z������o�w�����]��MG���߹��:\�?PApzV��j#֜�20��%� -�*�y<�a��� �/([ԡ���zk���5W@�=Rx�RM�C�z7�aS���� �/�P��q�� ��0A!���eКA��f0�?C`�ALkoDM7e$��T!#Tk��J7�h��f�/�8beŨg��uܮ]���~�RJ����%?����c�8��ғ��,#���?����_Aƃx�)�r%wVB�0�ɭ�K%#���&��@��H�E�����pS�7��["-�ޖ��D��y��3^�ӭ���vW�٘���p���rgfXt�%`i� Z����S�;��+����GN����Q��^���S[��"t��/寜�e�v��N/� *�{�/��&ӕ�һ��j�y�Tͣ.6�Qw����zxM�Ǒ�b���]qk��9���&�Sj���:���G�!q����oIX�<��*��¶K��/<��.�X�nv0|�j*Q���b��J���NvK&BK-������3�D���{˞x_.�=c0�N��K�p,>�*�]��>Bz3/ՙ�n�8�9+��[���6�x�,�r庋����j2x�R3��ކ�A^��5���4;NQFD+rޤU̩E�Q���a��q��w��Pq -J۵�@z�~�Ӈ�aA�[��R��k�=@x��&9,zӬ�Y�Z��YZ�)tr'Zy$�oI����g��a�UU4�D��:��Ybx�˼]I��n��������R��;�&Y]� <n�2O%��7]�[l�!��u�\��.�,�u��п�xꬒM����mݢ�"�7&���l�X�SW�i�N-�V�n���G2"6�r<eJEn4+ln'�i���Е62�@܌�л�v����K�+�w[������w��p����]ig�D�-���d�2���V�a(��AJ�&n��sl����`S���n��Zc�o�$z=#�힡�J�R������fҌ�6�����0j��ƪa���ԋ>��4m��ٲ�fb�x�)���l�f�CE.P�Ӹ���m%� �� {<Dv�K �"K�(��k�`X�� �r.2|�g�I{f���^��z]�Ҳ -���t~�B�V��}[7STcIdI��*�ȭ�t��Ópf�'��x�>^o�)o��Nv�vϫ7A�Y��L5M����dS��%ܲ�5����"���-<M��(�`*W%���\��OH���>�\���V��b��y�FM����%N�~ﳰz�/�LZ�Pw����}͝? -i�Һ��3Wn�w4�҄�%�R��]|��.��)��Qző4���iLh��g9�VP�eu�ʬ�8���2g�_�筟�́����R�u���{E=s���t���y��`����qŬ� �����B����l�\r7E�*��S��w�Sf�FZR�ELȌ��x�.a�שh,M#��E���Ũ5i3���uu�_* �ӛ�=M�I���s�F2��u��[w��s���(��'H����m ��fc��{�� �]�(�jȄ��: }p�ޕ�Zs��2O�S�&%���D��y��=O*S�o�e��nXqW0Ε�꠵$��8��M��qik�"�g�u(���B��R"/*͆�6^��)N ٻ́П��.<SY����H�9��0�n�܅�� -���ų�6�X��9�u�F�Y7��Vb���t�2hT�V�/��xK�D��Jj|X;���dۘ�MQZk�-�S���U��/䂁zx�dqڎ��6$V��$Z�=�����-�X�$(d�*Du:4�C���C�8U�cz�LV��{I\R`*^�)p�*n�Mnj��'e3�ܖhn�.�jAdqd{��\�d�� �!�� -=����J���s��p�cf���`9*��::�/?�)a����a��ԓ��A������爲�3�AJ\��D�����`�Z)���&��W�DZl�|��?f�9� ,��(:z�N4�PN����kܟg�^�c k�M5�[Q��{�4�%x=D�[�2�U_�����@&��b�@�K�t\$5P�>Z`Aл�]^�ʊ0�߶�l��E����P�����~I�j�<q���Tiȝ���W:�� -e�1/���q���� m(4��"���x�2;-Xߋ�jA��{ƹ�d�;M�f2���6�,�����&���%�RdCV4^@�[� �n��$����w"1u�i��m�xx���7�ۆ8���������ڢ�%���]n�w�b$ce���: ���eؽ��������V�$���Ӱ�b��& -a�ln#�*PC���_�Dy� l1�7˷�Q�8kϸ�<���\j�u���0r�'�r����I�0��q��5_5w����^n�˗$��}��Op�3�Ϣ~����$�'8o��A�g�Z��~5���o�8��k���|G�������V���&bJi%Z�Q��B ����8�E�8�ɘ� -�Ԏ��0FG����no����� }ImQ�`����V&��;�~�^�b���Ёk���0k�֪m%hX���*6��A�:��:�-�:��|m���^�i�jw��a����w� �����5�6���a �a�m�s6����aA~<R��T?p_�{�M�Pə ��-(푝�kk�Vq� �7���R� 0�*rw@Ȼ����)��}V���U��Qe��5����+2�̓Vf�=�K����C�u���^���mـ�7��N�&�Yݛj�����J�k��c��b�1��W���s��п��/@6�z�W��=���{�<�e���Y��.vf�y��i�,��S7W�н>s˱�JK--�AuYD���b���&u����V ��~6^4d��.Lī�xcދ�4Gp��]*h��/[�����s��+5.j�ĵ�.E��l��Hք�J��ETD�9*t�������R�FqX9a��� -�T/�P���f���_��ޓi#�<g0�2��V��iU���W�O�R�N)�U/^�(RAU:���h%o��H�y?a�%�.�X�.����~Z��^(>��؛t�#�6m��ل���%��-�ێ*�C)��P2RjJ<m�����}~�.�;cj[63-n��P|���5^&�D4c�](y4 -�\No@Na�Q����m�t]ӇY�-�;C�9�#�������0�=m��M�Z��Θ�0M���ΆV����N���$���2����j����4d#��R�rԇ�ك�Kڄi�;�����i=Y-���\�g��1����1*�Nw?�yFTf�SH���"$t)�7��V_��p�D5^Ѝ��u�#�a��w�D����ƺq��l�@R��cF+, N��Ut^y����1�䲋�4d -t>Moo� -u��;�A֗����zK�R����W�Z��� ��ӣO��#�S={ײ�Vm~��9�ҽ{|#P� -�J�&H��_9K���FmZ5C��q��Ÿ��Kù7I,H4;X�60^~V�)��O"���S1�I� ^-{����Mv٬��g���5FfӐV��,w�6�"�%b�Q�'���.E�JkBǠ�Iپ��9^/@��%�F�W���7�kU�_������R�B`ѩG�t������ m�TP3�߾�Et|��V�V����MB��,�m�E�d3*u�r��գ,⡍}<��:>�{e��`}<1K��VZ�)qJ��%ߕ�b)Q�J�ktsZ4��F���9R�KD����,��{��m��Z�VmH$q>��I�Qm�2*�|i�7�E�V:Sw1�UN��G�p^P�\�,MX�/I{�ǿ��FX/��M�5[Eć�d�N�&�k���T�o% ��>PvSsI��K���K��� ��c�F�9*�xZνr���ꜮLr�nk�u +��:�9�o�� o�)����IQk����~�q���+Po5�%G#c/�ʭ��L�7.y93/��&}O�e�,���J� \&�P��,�H3]? F����)�o�C�jލ$�������DB��ศ5�܍;z�V�@�[HK�/�����q�� ����|�+S���m2������X� ;�nw��<D�t"�#w������@՟Q<�]Gn��<���ޮ�V�}��GM�@>�S6!yA�'���+r/�r�J��"�g���yW���q��Zpt�įG-��kkž���t���Q?���|]"/*��jFc���R��ʄ}{w]Mj;��T�/-yA2#&v�!����tk�<��E�:�:.a��;��ǥ"D�"���H����1=�S �(��\}�s���[U����V��!���l�)HL�����z0P�t7q�*nsj���_�OE���������\��=��s%�1m�:�,k6�����&���t��u ����v�����p�8�7)����dt/�����g;G�`�A���/P�j���-S���{��� (�U�cFx�8p(0Ҹ�R��Ci!�\Sw��ӯ�Kd�pXS���5�w�ꗌ�5G��6��ѧ��}�a�z.�̔Q����6���I���� -���X��a��С<����Ӊ=P��]���q�ߕ8 ���B`/�=JPZ��L%��=�����`G��n�"~���7ц�t��� "�D�8�9P��� �q���˳��mK����8� 2����j�0��m�����%�]��V,P�^Cb�~y�\�P|�(oh/�ԩx�v�z��$_Z)��a\���A]v�CYi�{0�#��k�0��ֆ��m��m�yV��g}���x_�upI��.Z?�"4�5a�N�%�3��=Vη�uj��}�u��d�1ˤ�]��������9bې�K���w�u@���e�����3���i���X�j�U,���k������ /�G5��ȇR���2�4?��!��m�tc�w��: N��ep��^�6~7j�LV��4�ڬR�|����r��Cˬ<?-��������o�>�LϷw>͙U�*U�j�D�`>� -��J;�ҕ�?h�Na���J9 Ry�p��=�U}�^�ON</S�x���u}{C;���bz[��vA�~�����%��'8�W��߫�:�'8��Ϣ~����R����k�9�]��Z��&,�JX��CX�QzXMl��*��Q�x�B�A%�~'�Z�ȑ=u�a5U�0L܃�8���C2���g��V���u�S���Tݠ0���t��s�N��u�7�`�����auDգp��y�cVb���G�p|o�}�Й��QڣsG<���2^�s���p긗:9q��u�0�'ns�Ϙ[cr�7�|�Q�g��[����KX���a%%���b�B#�|��H�~����+�Wh����QZ2��|:S��9�ҫn/zA���م%������;\1��e�@�Ew|��(X��ĉ����0V��cXat3 -S�V��)��WFX먅�x��S��=3��.rq�j^p2��n/����d)S+�K��<m7q:2��.���x��kp+��b6;ʻ���@������Su��j��Q����x�����;������K$̣�"��-z.g)v5�نm�j�ͣ�՝6R�;�s ��H{�ʳXV-k/����_�?%N����[������;!p{E�S����w,��Q���M﵃-�-���D�z��wUu�lM�kh��v���^�jnzh+�㺐էJ5p�P�[�D��}I�F+P���h�w���sU]͠�~�����Tg�����ڧ��Y��-x��v�kn�����frd �e$}�W$� z����V8�R�����5�~I>8'>MOi'K���Ҏ8U���D�~�Ky��#&+#�4d�����!�@/���%�U佷դ#հD� �� -�Y�.:�B_r �?cg���g�Q���e�R�@-�xE�:�c2u͇_�2^�kN�� �C!�!_��f -�x�Au_�z���D��ʂ;Su�o\<�F�x@����賳im�,���ї����Q�57��^4��X����c�Iw^������*���澼���^-^X�.ڋ6-�f�*�<���L)��U�L���#Z� ���1]��_��&�%�pz��B��#N:�g�{��pD���%X7`�1�6҇h��O͕��݃�<���/�ZMi���"#"V�K�.b�:$� -U�6c��͆�{o0���S���,,s|�u=�Py<BU9��������S�\�`L�wO8v�|1�WK^z!Y��> -D'0�������ɬL��˽�L��y�������%Ғ�#ҺT�Կ&�h��uV��#��d�4� �w͝���ޕ�� _�e=��jںr���@��X���U�Y�1B�5��t�]Rc]I��m(��=�=Ԋ���@�N�k��j�ݜv~XK�����SQG�)�u�^��9����:ٱ�W+u�)�#j,����#3M��Pz��<A����R���D��Q���^�Ň�u���k���T�Q���TѰj�j�����x��(�*�٦ l�;��BM�V)�l���-|������eCW���b���6d�����H3xbr��)�Yg�_�ũW?����M���$,�F�3���45��(���'6jЃft�e�Q��zHLR��7+B��o��ӷ��X��'�FS�X��~+�2���E��4<UA��!n4TN�_�T��~�'��=0�:�tVa�HpР�$�R��8�uo�+�ā\,s���dvW`D5+�9�s�-�$�Gl�TN缷%ђ��N����@���K^ 9�����[�~,�˿�����,�v���}���Jix���Z1Z W��"�q�[�Mʼ�z$1�Lp6K��\5�GUZ����G�҄���P��5��bP�0�4�Och�% +� �|-��^a4�ڽ��<;3a������B=�MZF��8�BN���4d>���Iz:��kv����TO�!ء0z`� �z���wL`��qtKΎ���>~V"�$�H�=MO��톭�ўNyZS�e͵l��4_H�[Y��� ՙ�;�z�:���qzuj@�=�U�'�T�䑾e�c�ֺ���8��%�fS�98�OH�R��$�l�nX�X*�M�/�I�%�IYm{4��#�R%���%��A-Q�%U%����O��֨fq��9�g� 4"6�㔞����?frn=7���J��~�����.�$a��êo�a�Z�N�DS��)JC˷#�="�����W�r��,�īEL�n��� ��U:�s�Cɝ��:��'�t(���~���>��:���bS�"�9yK�D�KX�V������@ک����6�tİ~���b�.-P[���|b[ML� �ߐDz�1��y䛵�z�����/�`J��>� S��vI`�'�T��E�۸�,�Y��y���5���꜒����痋y��^�{ -s�4��G2�KP�� ��"��e�X����]V��m����qwk#���ܢ��r�a���O���ґ�ʗ$:��Fw���䫖���3{m=�Q���^�3'��c���� -c8<t^�q�����JQ�ׇ��籵a��m;Jr���MϿ��i��×�]���d�̪�?��[ɷ�v��q(�|��h��)�Z�43�ԛUπ��, �NAi�^v9y���5����E�"bk�[�__�y)b+i�S���鼵J U�/{��"?�IbW�^����8%�v��$_�%Dd�{+3�+ڐ�.������A],s`��k����m+� ������^�j���&�U��l��\{����}�fxZ�K|Q�����s_��L���9|�P;��j�d��һ�4y,���u*A���Y��PW�ؒ��ҷ�Ջ�>VӤ.�9�\f%4�Xwٹ�����4/ߧ����3��4�:�N��t����V�_� ,��vX�A��������Q���j�)��d�V�e!��f���e{�g4,7>,'���jp=$����悽1������g��a���;�Gȡ�I����Yb�+�����7g�@�,�'8�V?�� �wP������W �,�'8�.�k���|'SjV�g'�h�fXm%��:�J�_��専!ݰ��/òD���v��K���(<|�qM�u�/?�Y��h���w��;�xt!�+��S\+����X��[|V�R����>}+� �E*r���1�Rp�IEvu�U�AaR5x���s���~�^-��� �}�v<��k�d[�8O��:I�~���do�i{sso���J�oI�J��a��>Ò�G��BĢp�|D��� �����D<���Q�-��=�:{t�'��E����v7���?�r諝q&�T�I����q'K+o�a�:�\���&��D�6����?8��MD���ȡH�����[W��p�}����*��M,gg��s6h�v�Ұ-��{Y��`*�]�,7ԙtn[�=m��y{�ʊ���|V���P &���+V�Jdi9������zχד���qD@��3�+{�*Ö�V�q���� 0�zpb�w�&W����zs�M��BYY�D����ش�ߕ8Z�{�j E�4�Q�D�a�����>�|t� ��Bd]�ڪ;�-�r{nj��7t<��A�ѝd��<I���w����Ȕ6Q��:ןCe�|Τ��0n�/��[��V�����8�����@m:���@�8T8x^�xN;�a�b��U�<���Q�����B�6�n0��<)��QͥĻ���Y��Y�|k�$ �O� <��+X\`/Z���P��[��K���B`�}1Pߩ/�����vq�̛���A�q� �� 1����Ft�l�=TޅZ�KYY4��ax -��������C��%*�6���%Nt�q� 9�{V*����&�AX�LP���E/!���סU�zeG��kj>���8��m`#���Ƅsa�p�?����w��,7B+Mv��-FșMf!ˍ��_9;-*�Jl2xf�ۣ�� �mޝ� �.������L�χl�ʄ���Ɏ|}NFR=P�����+ s��!�%���5uF��fa"U&Gɑ�D�t��ę����H=�{AN�ftX5�d���0@�+�{���Yj�j -�8մ��l0)-�!�sJ�"6�����������<u`g���,�c��/�.:g�ҰR����u����~�~F��#>F�Z��0a5Вa-e}��@�/Ӷ�We�n�����u��=M�%��O�;�P,�T��1⦵��^��7vn��,)^�dռԇ\�R�k��H�i�����"5+̞ ����_���|#�I�\-K��*� ��C�~��f��LZ<����\J�{���M���_P��r�@�L��C�hK'�t/��X���8����w%,��o�*�F�}"֦�რ�;7cf1�pW��n�W~�H���锾&��]�c��:�+�zת)�g���� �ك�^{N�$������U���P��Y���}�7*X�"��.�*�z�vj��l䍸�A����-'=�����ǒ�g��"@k�}����>i��)�ׁ~)gA��e�%�Xjܐ�|sϜ�.c��M&�OK������H��}� ��9'AILd����`vO�G�z���??&��X\�(���*�XQ�O�ߋjd);3tN����ϴ �ޗ���f��UVMQg���ut��J�� -���eOϛ�0{�s�pc<�<����]��,��rW��N����N�-�}�/$��[���T�6զ��� d�>v�EM��dO¬s��ݍ�7�c��[����^ � -d�b���Lڏv�|������ug�c,m���@+��D�~��V<V�E�HL��� o������T1��P�\��c�sZXk��w�z.�����f��!�/W$Gӯ�1Υ3o��\6:�X o�M�͎�-��u�hIJWH�|�8�@�1J9�b�w��G���?Z�*]>~�K�}&�l ;������� -��c{gee:���ݗ�Ϛ� l��}?�.�u��m�� uԸ��$Q�>��I?����h������ߣ�RK������$&�aˤ'-i��������l�\���șTf>ӓ��QU�(J���.�E���N���/��~R����U���e��ap�W�1�ܓ��+����A�8G�3�9�wj���ǯ$:q� ��u�ݍo���f�Y�y��e=�z-S�r�z��u��c�asjy�f�8Z�!DwP�vx��^\LǤ2�`dA^P��� n͑�μߚ�]����[���]������c| � -Ь!<Z��7��G�r�])�FL3i�����y{ύ����?tw4/�\�K��M�'s��m��3�~K, -��n �Y�D� ����� �����Uf%P=d�o��s���NM�D�5P:Z�<(� ���#�ݮx�S{���瑾�q�aB�r����·&�h��ې��vK}��+(�D��fpZX)x{�r6~0/o����]�J��A�Z^K��[��mL�I��<��5�������s�h�ه�(�#w0)^ɽ9H�H���w�gU�>s=��K�:��4z��ћmNI���,�2w5��9:�=�[����ذ�_��|8� -t�� ��$zPp !�����ƚ%�Զ;v��9~��22�ί�j֟��3�T��L+�lB6(�먤�i�f�16+�7+۲ ^�"���J�ύ�ols����2�Vޗ�Q�ѓt���N�qa��Es�b�2�� -���ݒȕ&�{J������3�o��i���X��W�r'���Qy =�ōTF��{ &Z�[?ʛ\�,�-�g�Xm!�{2K��#�X%���^����É�^�h���4?� �}L��Y�c_�N=ݥ�Nqr�,�襾ٝ��Omӝ6R����--�]hꓲ~b�nP��u�^Hc:�rJh -�!w^_HHͫ[���w��+��i���r�} -_L�|�XN�e� /���.��P[U�9��l2Ù�P�Y3b��w��Fr��N��i�����/2������'��[ۡ��%lB��Lㄭc��c�Km�h�W����ԱP\B7[Y�:�%c^s�tq��>2�Y3$��Ǒ���$��� �͌ce��]�(�_���_�o[}�\vg��W�O,��6@��Y'?�<@5���9�gB�����o�k��V4@ -��������p��~�{�|oZ�������ry�gm�Z�t��1�Z�7�RC�Op��������#�7t&��_K��t��^��`���z��Db����YTzFR��?�z�����$^�����u�(�x�oĸ -��K:��}�u��E4�4B�h��7�/�`�����A�a���o?������a�2�X� Z�.��7��l'-P� T�HLĂ@���T�w̧���Ajw���!r;ѯ�5j���=�D!{<��ՃaI��D'*wcgâCk/����i���=�:Lj��t�&��N"�Nw@5mA%��2�e�$>o�1�w�,v��x���� O��Q��r�'��?�b֟l:i���w��{��Cg#�&�ޤ����h�?�M�7���<�5N����fRsՌ��vi��C��r�_ԭ7 % 4s=�P\4�xY��������i ����=�5�����Z�;��ڰ�g߲���^��7eT�� -���4����?���l�HK���P��ld@u��@���b��� ?�^W�;�c2W�%*xjOM��W1[W� '�q��7����O���K�P�T_G��~��W�`Նa�NJ���O#� I���o���դB�v[�3�T� ߮2�D=�1��j����cv����B���A����s3�eg#�j֢���[�Ӣ�O�L���m�,�|y��5�V�IB��� -`�MzF!\$ tu��&������P�(,�҄=�'���o�.N�V�YN��{y���ɵ -������E��n�SSo���21��1�'�h}��J�v�TT ��Rw�K��q�H�.Ť��-Ae�M���������^�aLU� �?��"w�E�>@���\����iܥ��L"kS�}19A!]���H;ܥ�ի�U���PE�`e��C���6��Ο�g ]� -��l�K����������~��^\b��=?Z� �2b!�ݮ�dn���A�TO'��L��~��:}�������7���V�n��g�s�S�ԍQ� �)�v-���@�vM7���b{@��A��]���>M��:���.T�=�lݽi��c"����%���L��|��{^J�[eܾr��s�$�>���Zu�.��Ŧ�7���?' -{���e�.GkP�Dj��⦳]���|D��p���y~ -��z/j���ˡ~��M���l��(@�w����E��T� �ZY\o G�����OJ��ɧ���o��t�����uP��ܛٌ�a������r��v�zv�G��]�ۍ�����i�V ���W&\���OCZ�q��˽+n/���^��S �k�tQ�K��̂|��o ��u�_(�m@���1���l��oQ�}��$ά�O������k�k��h��,���t��BP��[�z��K�2��Og�ӝőo���'Q1�@�A���&�]�H�xJ}pPE�T�F�]�W�W۱pv`����QP����V��z��EZ��\\�@4�J��8,~��Cj����]�{�͝/�;�Y?�K��r�#�� $7���`��'�g����ǖ���R�!��u��nۯ����P�[�����ڮ����MVMʕ =z_v�Z�;�g���.7���I���i�@/��NqNY��V�i�v2���|JmTr|"��1U�~�-����;\��PGh�|"��~���2���]�|�:�ٗ�o�5�|����͡�oq�:�2� ��Q���3��/F��2l��~ق���+RK�?'ɍ��P�h[P�KƛY�� �m2�%"�W�+��z�U�O��j�U�����+[���S���w $�bp˟��6v���� ��f;���m�@G�}|�����������[��{�Q���E��eB��8�����*K��3�h�~�.k��GI�I������]���4�[Z��<�k�Jt�z����ג��}lP�Gt��� h�&��@�FljT�[1�%�S+�����ݕ��K�S��<= �=�_���Q��g%�N:���H�?���}��u�;A������w��~!=�;v�O��ఏ��u1��Е��>�Fj�Ǥ�.�����%=iv۵��MV�����(�_�lPC{�Ju>\�eX7��Y��! x��E42���w�.���n�����r~k?��f����$�)����K�g�O^���2"�?HUG����ʂ��\�)��iF`ZC��Lh\�^����.2�T�M��������.3�r�<��My�1:p�qv/u��V��N�[�]Q]o��2�w�sr��X��+6�t������ȁy���AUk�����un�����[c�k�O��Ei^�B�sgg�U۞4�Kc���e�ŷ&�:��a�5�}:|�B~]ޕ>��v3�&�]�H�~=^��!�[h�'��i�7��h���ī�ֻ����ݤ�������6+�E�+=�|�������+l�V�:sl�+�������>w�dQhfwA[��I� ����DMR�=�*�]�� -�A(C���oIj��U�8�Vn&�z�tz�E3I��s)��)\�Z�Ǖ"qμN_��e�:�����P�B�v����n��xm�n��n���/� ;�6���� -�ϦK��?H�������~�� �����Y��\�O]"A��G�� 䳻|2g�6c�1}j髃����Ef~|h�vH`\���r'����s6ޜ�c~���ښg�C��no0�?sشU��)<~�b��YC���t#X5_n�EΕ�!��|��W9�z�'�0�Go:�R1��75&���bG�@RU�p�E�k�im�$-�n]L��i���J:��j����t��[ p� <� -�3�.ةS�E��1��7�s��D�ϩ{a�R��vt�S�×����*�z��Z"��M�G���l���z��L�>XN}�VEC�k�/�g�^�*�?iNp����R�`V���*N6���Twg����sBԏ<�}�"_�:&��" �}K¯��6��6� ?ka���� �B�D[]-���R_��%t�����C�����nf�(3k�G����o�:��1 ����3P;+�Vȿ�x�d��X+��l5�O�6�.q���~x8�G�c�Y�ϯ�EG^��u��!sq���[s����3�ど�9i�5���4 /I3����X-�~2=V<���,rXN���h$$�O�B���k��z��XNr�>mU���%d���w�O��k�u2�ic1k�j�������nL��X���|ώ�چI��2��6�pY� ��ߐ\柊AYxH�L7� �IXL�2�/�q�I@�9��O� �$�A:����/ï���zt�<*����XC0�g�t�bb��'��5��.R��Q �!��q���ٺ_� �M��e��RCg�r�?H�]9QY}��呤��� ���Hl'�KC��$e^v�p�y�u���M4^S�g�`�0�o�̄�+ݽ�~��ǻ^~�5u@��B�-}��۸�?^� w�� -��t������V���V� �P�9@E�p�{��D@� - ����X�X�oWG$��|��µɛ�w���k�"��e���mݰ�dA0�r����x'��5ӄ�{C(��AR������z�Ƕ�����V�P�=�aN'zWJ���^ok��9�"'5#z9F-��B�>�C��aW �ǿd���,:9o�� � -���p�����n�7���'�Rgnh?,��aImf��}�I�R�6���=ט��>��t��8yGQ[�!k��� ����~���>�;�c��a�����-I��1����W�b��1�8Ϸ��yK�� ���?'�*TP�D�uU���N�9r�JjKoW��U\m�Ȼ�0�hFҗ�~�SB���$�O�u˓��؛$��B:%�M�-}��G�gW�M`�����Q�c��os��� ���h�J[�yNh Ojs��eke���ʝm���y�D�9��aw��Rkd{�^ʗ\�k�9x��v���l���Z/0��hem?Y}>����~�&%��i��f�� �hPM��i�3#�@����JO$��� -�w{����s&Y����}�կ����7\��g�Z�[�E��:� -�g�-/��gw�� -�{v��oss���L�y���g�����F:�7=�)upv��I�T_��b�jf��[��n�a01��w��tӍ�NE�t�u����ٗ���A�\6��5�o&�T�אYV��a.��X��zڡ�~�Ǒ�U��Q��в�%:٤J?Z�ҭF.:����E�n[$�����o�q�j�G�&��\�ͻ�|[�e��X<�7�h���k��P�n����S����t5�A���Y4��r��� r�'z/.@2p�܉��a�V��D��������S����qJ��O�>��~ �4��� �����[�Q 6��tE ێڧ�#/R�-Y�)Oi=�ԬK������F���%=T+�:�b"��2��}�#��ŋ�:OkY��6|�Nf}/���\v���ëF��1���q��럒����<��txUR�2&-��Yԫ�!bq�!a]�L%� $:'I�rw?��M�� - ��c�;��O'�<8+�lU^ܑ���=7qR��֮v��'�� -ؽ,%3>Gr��g��CD��uD���/D�M]Pos�����z�R�?�jr�W�H]%U���º�Ǯ(�1����U��v+�C���#Ibk���76j4�ю慨��4P.��U��6wiUh{��˂�=�-]*�bC ��1o��-O��2�Ο .�����o�G�O���!�+��E龌���[ש۽�u����*sE��,;Л���᠈�<'�mV���)"���TB-����P9��[��]�qQ�(pѴ|O��sn����ܚ���"��LT�`��k�d�w���>�v=��ft��iȨ�n�ƹ�:����>+A��xy�ޚ`���w���`��"�U憙Y�d}y��;�iR��ӤTvO�A������%�O�]8h�K�����\�`���nE��V�9�w��yf5�rh�`�M�ܧ�KX|�Dt�Nw,�T�����OH���q�}����)[�4�96��(d��k \�����o ��� �D��3�W;I�ݿ;���_���h�)� 醷8�hH����� 5s{N�^وĚ>�!����$�������9�4��!������s�2eN�7s8nިr4{�͔�����96�*���nwKt~�����r<>q��wM�8s��t-x�_UL�t��b��-/uvN�h��AO�SQ;����4]�{DQ�w�3�k�ٖ��C��A؋~N�ߐ&��P��To ���Pm"d8�<(�Eɧȍ�7\�2F��W�9G�b�$���^��1�}�M��Bs��4m����̚nGbH�4�Y~(���ކm��N�_L�v��f���^��ir_�a�<.���Ww���^��wL� �r%5wTu�]y2�x%�6��N�<>���e���>����1�x ��g����}4��ӶI�>H7&A��IЀn{n?�~��\�稴��w;<�=�OQ�?&�lt��3ǯ���"�q5�7���fP���$#�r���6���M���B˜~`��s?�[y V2[c�9�w� ���Ӝ����OH-��.0�z�eAE�Z!o��y�� !��]���\3�J)@X�������3X���t�]���S9�4�j�T��_��Ǹ���%� s"���������[��q[�r�����@pTh���R�����3�����(o��X��}iG���Û�?�������-��v�:D�t����'R��n���h?}�^{e� - ��W�[�/��� *S���Ϸ�H�h���8���}_:9^w���J� K�Ȋ��h.����σS�|ڧ�x��v�hҝ�||���j?�*$����TY�[|5�;|��֓��ͺ�ظ�t�q�4�f�������"�T�s�pA�X���(cϯ�����v��\:"���9K�rᖜ�d�� �V�LE�K�0�&�}~��w���lo_�"^y1mD}�i8e>���~���� �h��֜ף֜$+kn��~��%�u�r[��e��Q�JJ��M�o��2x�%@��U��=m��y���^w��p����p��� ��!;d��>���8NG�q�~,a���z�BLl����X�)�WJ��^f%�K�o�+p�� ��.�q���]����Y�Q���o��t����� ^mo_�pi8�ÍIa[�!q�m��.����U�^�����n�ʪh�^K��r���z�����O���e~���kli��5=���K"��94���r���V_������8w�e"�v��*���~o��YC��}����N}�,5�z�o:�%��/Dm��?�7'[�6�ٷ=����o@o��3en��K�3]py_��������$��툂�ޒ�;!�J��;����C�����&�);}�,b���^XK�>�Du��?��� �=���;/[�·N�)��rq����5���џqr�W{�u�^w/�#kަ�zW��,S����Js���%t���D���?��z^s���>��Y3��S��I(D�d���B�h�q1sI�3���x�n���@��� ��y<����r��g�Zm[3�����yl����ui�͟��9�Z�s�� -΄~�q,~�0+f��<ZI�bi4+C�P�s�p�k�̥.�:�����O��i�y��=��Ļ�Ч~�4�ҠTh^@�ڙ��I��s�� ���g�FX ��&�&��H�Q -@�)��>�tlG���|� -���nc��v��+�~�w�]뼌�����w+<^�Wؙ�Ϡ���F���_J����}1�{���X(�=N�*���p�K�rh��ė�X�Nĺ -���@b����Ka���^|�Tg1~��g�>�kӳT����v6+�����Ǎ篑�̥^'(�F�Թ���Op�Z%*�"@ڮ�N����������vL� -�ˈ��^�E!F�����<�����D�t�FMn�����D����Nj��Ş���������%J�t��@I~��4\��f�$�hd�Iʊ�J?�j��$�'��b���ƕM�|?�O�&I(���:hd_�����Q3����� ��?>e5���������]ufZ��?H����� ��҅���E�le���������n;IYM�Q�'b�U\-����}�"gq�ET,�Qk�g~�{Y/�ݞ]¸88�6�폫Gޛ5���J��]�\�-F��@��m=���k�ҽ���b���0�U#�8��X*@�����{[��"r��}�^L/!��A$w��\`�/|��P?ӽ�<�*����Ύ5�z8��ıoەN�y���zv����J~'���(_�I���٠�D]jK��$e=���<�|��h�_"������ϖՇ'���?��{]��[�� �蔆�:\nl�!k���S��s�G���rt�͞{�/�Q�����3#6�ړ��$m4]zn�w��j"52��m*��[Z4�x���'a�r&(W���L� gs�m��i8hW�����U�{�]�Ϟl�|����t����m���[^�f�h���_��i��U� �?� �w�]Q02u��1y���yb���k��]��ZΜ[�����Y�|V������L�y����A���dY��3��ۇ��ZhG��]�LJ�ߺ��w�e)���گ��2l^ā8i��D������n�+;�F��Ϣkt��`�C���e��Ջ)4���Nw��7eQ3n�昫;�6�B=�4�V���kV��i ��]�[S������Q�P���� ����%���N�<v��Y�|��&{���}�ʯԨ�k?�,� �r��8�}���~r���.q����)�nggE\�t�S�����x�>�ciS�� ���r%Ly�ˣ2t�(�C�y0��U�����<K��V�S`z?f�Sٿ�UC -�ˬ�i=�rY���=�r�rϫӮid¹A`��^Zk^"�z|���I�<��/ʨj�䏞��Sx>���LJ�Z&��B<3DM�j"�ɫ�(��T�j�Xe�b=Pə�ȿ|�]+J���L�pF�W�F��������sM�b1c��J�+s��*�س����w�3���������B"��zӫ��s ���E��̿���4��� (S�3�J�;��}�E��cL�Q��~A^�NBe�s��G�뻦�u�F��k��{�n"^��d�2%�"�,]M��A�S��@.(���j5R�x{Xf���|S�(�Ɵ:���s��g?~�f��<�p�+�s�M%w�),��M^,# ���ȵ��-��Ody�`�22Iq-��-�%�i� ��9<�'2g�*g��Y�;8ws��c��&W.T�ʯʂ���?��$R�^\�x0�ѝ����NLr�H0���S���z��iP>�K#���@Z�B�?;�RQk�~ɝy�_ό��(��9��!������x�O`�'�Ȫ)�Ej���?���W��y�����W���\;5q�y$C܌��3H�jh�\U!�/(9��I����8�T��5��I��:{�ZR:�� n��tɫ�)3�J��%�L���L������K�5@YW����$��#�E��{،J7�<Lyl�4���Q�@V���%-���׆��3A�R��Ƽ�,.�A���A���O���`���\q9ϔ��ViÔ4�eJ�)�4fU��_�mS�0���GP�~��Yև.~�^��i��:3�S��2���o[^��G�Q�te�v������e�9~��'���Y���������`���Q���ي��1C߽�|D L�5~������~� ��qjƥ��E&bn����^X��p~�y����_^�BIh���37�z����N��z�.��)��tD'�4zW/��T��xl+TC�,u�7����[�?I��J1&���(�b�O�f�?SuYt���q�f��\^&q~�����.��)V���9���-z����l�g��Pv�*<�z?t���x�A�D]��}���CgXK���_���H��^����VL �-��<3GS7����zq��N{ͅ�͊���o����?2�E]�`Ny?l1F �DT�Ӥp���R���,�9�j�����V�}�19F�gm!A,��Z~������4.�盟y -��嶙�.Z /"uJ0�$��'ʅԥy��?Ͽ8�x�O�Yb̞];n=�O7V��!�F�}d{�/g2c]���9�x��]�^��h{]���o��k-���z��Y"|P�y�bL��������n��r���C����զ��'��۩��!Cdk�c�`W(��t'�5ڏ�т��^�ݢze�X�&�w ��)� ��(m� (&o:w�A����oH�%ZhU -��wi���3�& Տ#�2�䵋S\-~X`����N%�י�e��N�����-}�A���3�O�K�ٮ�T�-\,1D��89�x�p�ol��5P��5ct�ؓ��k���2�zWS��y�[k����XmdLOB�_e�a���Yl_=iD4c�T�x�TG����:��a�{�"9wo���)t���������0#m�[;��:����õ���7�㚝h��-�ٯ���A z�u��R)X�B���y�ܑ'�4"��=��|_�n��BÑ��r���m@�~vy5x�FP��}r#��WfY<�C�sYs��s=@�Y,~*],>W�X��ҘP����N����I�(��h?�������b���yl�쭃�L��eim��zY�-S{yq����D����E�$�M4�h:�o�s��ŋ�u���᠃a�˂Ʋٓ���w5'w�U��V����A��.���g�N����O���gAW�,2�MiS��a�X8d[n{?��*^�w�}�ߐqW*���U�7�F!Z�"�þ9�r�b}��k���K����[�Pܨ>(-f�Ϥ -���~�k����WA�Kȵ9�Eu�¦ߥ�-����ٙ�ov��N��=��D���\�ϬG�0i«XN�o�e�d-��)XB�c� �\~�<H���� ��滹�zI�7eF�9�f&�ߐ�R��+�����C>��(��w*�I�� ۚ9�p�މ�� �`_�a�̸�Z���� ��5�^.n�_��e���,5��5w�ͨ��2���:퀥9�&�=�*��r~�գ�H�t��B�~ yU)���I�J�{�b�t�����*�Z�eD,':�r]�����3���S�����7�����8�F'xK��X����zj�����L~<Tqm1,>��`]7����ҏ���NV�=9�葃��k\���rW�E���[9va�ط��]���O��fM�S�[c&}��F_�N{�QN��a�Ω�u����u�����k'Od���fZ�;�\����o+6z����C�1��5��n�j��?!��_'�X�n@A�@�� ��6mPF�0���@I� *g�h=��4 ��v@Yt ��b��e�J�\�@��D ��O�ƽ���-�ߏNc��r<ڃ�!t'�c�V�T�~�� X��bc��4�T��@�8��#�s����n�t(����D��� �o@�� -@N%��K":0@9l8�I�1^��c����z���̢�Sބ�vu���c�/ҡ�����7�2���8�+@q8Ó�$�Is�S�esX?X��\������� �D�������K��D���-�[���n�ɽk�D�ũ��9;�� �U���C�����>��x"q�w�s���R?TR�ZS�������M`�^�f�OR�a"4�Il����E����S�]��kd��gt�kAԜ[��+ӌ < -�vp������;oʈ���XWya��Xu���j�'7��A��'��d��^n�ǭ���'Q�C|%?��^��+������(,$CO -ِ���c�� �:N�AX�%���3��}����M�Kw����a�DO���EK6��L -�_�{�HZ�p%V�A�rIn��������~�~+��ϻ�s�e1��R� ���ʂ������]�q�Ӽ�S}y�^Έ2���P��}r��M�խ�ܩG�fs�����M��/�VZ�L&RWj"շL���OR��N�9�D�K�.�"w|�B�烳Ԫ��}i�g��ʛ=��^����@v��tsֻ�c��1���> ���龨�}�d���1��]8���Ω����(^�D�+d�_�'7��n5R�dS��y���Y�o��|�#�?K:��e�m�\cg�Ym�muBY/lx���P}Q����s����c@*�{���o3%X�K�"���MkY;�O��������H�%m4��9������~l��A�����U��_ނ�BB��!���!��_�ɧ�͓�p�1l7�j+�N7y��oy����44K"g�=�����T�h}\�C��V�5W���F��g�!P�6�?�JWVy���>@K�]���c0��Doّt�F/m��������t��o�>ͨ��ܢf�Jo#SR��->���ii��χi����^۹�@e�$��J�^9oȝ2l�@)�&�/�$�;;�&�t�v�����:�o7�Sݟ/-9��R��Z��������V���&Ԭ4 $tzո̵C�L\���xmSQeﲡ�/O9������y�ur�V!%�.������������*���c��q�tg���]s����/��w��v#�g�l��L1��~�� ���z�;W��f��_;��X�C�����;�s��䉗U�,�~I���#je�.���T,3g�bv/��?o P�>IP^k��: ЫТ�9l9�����~��b�=�TOp6 -7� -��5�#>�.R-�}˭+#I����8��H�F���ˎx�����x\�'� -��6�d�`��-�M��4�C�d�����/�;�j�T�DZ�}8~L�A��;�j��j�T��]�+��d�١���4�������~|j}T�-��f˃�n0ꝏ'qy>��]��_H-��.�A"�&�X"�J@����Pٗs��S��Z������0T��gO������ ���)#-�}H\o�t`�B,n=�IIS�v�p��S��� ҹ�|-���e�>v���X$ж�x��(����0S ~�=_��^��� j7���Ӻi>�cݺ�v�@�xuh�MY��������r�+B}M�y�]���a�q��=p�h,��!s?�����_�i��<7K������%&������uC��p)_�������l��iv�;�;Ў�[��T��)�{t��&m%�v�}�"��|�������}n�d'�ٝ2e��Φ�QU.b��J�Y�� ���$�ƚx��_H��.%9w��������Փ6P�札|�xD&~��:��+��0E*��X5d����x�v���̸�EV)m'�d��'O���5��G��ǵ1��&B�0Nx��ͯ���D�:��\���+��^vCC��B�<����Z�B��qg<��M� A�,]v��Mh"����p�t�8(+�r���1S��%��L�X�P��5]*O��V�zq*F�������s{ �5 d����NxG0o���y^>��F��̸�TZ�N����)���@�s��m����'����E� -3Xi�<�����ڍe��=������C{�*%��mP��某Z��9I:�) ����Ƅ�w�"wC�G�EPF��P�_�K:�ۂANwu滓@�j�<� -h�.\?�@�:��y��Z�ݣ�-dv�T��� �����>�?��s�Q^Y�Wp.�l�s��l� &��l����?��>k��������=�PS����J���d���g������������P+���{�eD -�BX�_>X7�w�$̮R<�2�쭡9����ܓ��f}�5�{�/����3�_缽������1}R��h{+t?:ޥ%��Sje�P�{5KMM��V��]�RJL`��P3yP|=QI˿��x�&���b���4�qG �=J�C 鵛��|e8>x6�������[8��=��Ҋ�v��v�G����&=6>Cj�f1Ҁ�_m-�.�a}��o��d����D��HLlv�}Г��m;�k4:5�e�����+u��,����|�1����������ۄ��] �����}h�5�Sz��\u�, =%+�����L�t1N�a'��'If�ߑL��#(�3ƭ�3&^FF����4C�(��tVG\t�{j���P���ށ�����,s ��n�'==�>iH�#*�m`�p� �vp*��7�!��4��i�R�i��ڦ9����6��u����D���w��oz��@ךӡB��@�1b�������P���F�_c�U|��~j�[y[��y�өU�=����'�*��o;��ylsӺi�k��:<��u��uH�U����V��X����s�寃Y�oW��i����� xxk���Lo�]�p���&Ӈ�`7ƕ���G�~�c���I��l���xs��7�a�����:�F��Q W�1�����_)�Ni���ݕ�'��ţ����f�o�{���»'ur��>[��s�n�����V���� �~(뤖�b=)� �/�3��9������ ��s-��u����k��;[�2��2��e^��yʩ,�R��J���O��6�4;�f[3�qWn\]a��=��V�JA����L"��F���^E�ݧ[�Rǘ�i��3w��ļ����>�,�t_�f-Z��2�����>���fw�9�v�&���Ay�k}j����tL�gS�-�K�������*[Y��Y�1���� ߛ]��N�֊j#k�VV�!h-�Bk�\{�j�v1bQ6hv~]�y��j3�s����K���O�J=?@��8>Y�ql4wciuw��:{��wm*����Z�y������q���?���we�I���9˂m���͚���z��v�Ky�>��0�tƱ���9:}Mt5R�=z���pY�(��MsX����M ������ j�>���L�qp�hi7P0q�������2�,D��g�~�����i��1T6F�<dO�2�]���f�j�껻w�/{��Gw��WC���|�S@�s�UZ������l�lI��O���Զ�M�*��$�.,�`�}�u���mC3�Q�M�A�?�j�dxZ���"b}���=�����`�t<���t�U�-,P�[�ck\�(M��&t�F�����X�r����U+�WX�*P�Z� -���T(� �_�������-@>_䎎�Y(�����1�O����Z��Y�@Fj%�� ���c�$P� "3���5WQt�y$�<�rX��曝;��7���[�z)A�;�� �:���o$��c�/�5@ʕ-�5'�����/A�qP"�z��O�G�e2���ߦW��5h�`DĶ�HO�7�N�KE��ϯKDR� �=p�(��w2g4�������}Q�|����n�o�8`�Rr"'>����v}����e� -P���v���@Ϲ'@�Y"*'Ub��v��M��ãK��D������;rtíy�ޯb �j��v�t��~��t�COkCoB�^��� �d�DT����b��6w���x��of -M�M��Pl�R�9�2�"�QM�U��8�U�U�0N�������S�m�c�?�f�#�(xJ�Q�"�{�A<z!�;z�m5AI�_Hs6��O�R����O�.�s�����k���uk -��v_#.s�gD���p��ѷ����5���H:=��F��U��#}5�Q�{�|�}��,�4e��,d���ƏR<�/L}t�Z����=K><�9�+�|�����������z��GW����J��S5L�n �O��K%��}��S��ڃ�>�҃L2W�-����Z���=�;e�����C�Ճo�K���/�2�c����ٜQ�J�o{}����, -7��s �n#k�F����%�MM�ϔz�5��e��xZd�p����]��GY�1�댔�5�5o���];�W��/���Ww��Wυ0��,�T'E�?��z����\c�D��.=�c��m�^�W��*lWfT *]�π��2���i���q������K��tݺ������������9���y\�ʮ:�m��"'�}땫�{lv-bT�Y�W�1�������'r��4�g�@�.���~N+j�^��I ~2�����%���RX���X}^��]�1 ����j�z݅g���+3'�6�_��bY�,���&��d�'���֒IM������`/;��f�������S��{%��>�8��������$�19}:���Q+��[��e���l��a��/5rPH���sU��Ҽa���I���x�9�hq�p���s�J�ŌQ���oi2�w�����2;���������Y��,f��5A� ޠ�j��u�e_�����8�{��hv}v�y�{���j���]�2�x潝�/��S9q���7s���x��4�|Xm�u-c:/ � -��bdGYu7%N�"�=���@ɀ�o��Tq����w��+^�w)=h��]ߧN�<�z� �.ﻒy�}�ؓ��ď���>4��&j5-۩��9ݜ+�=���F�e��>� ���R�U��/$5�T���?������~2`.'��zWg�j���-��gѳz���#����I�r;�3��z<j�ڷ�}i�A��EI�s!�T�Z��W�ҥjm$�Q�*�;��9�����숯�������=��@[�U"�;��1��P���4�.�c�u�Vɮ������),n]�U%M�F��x�W��R��<�_�\l�%jު��nx�x%��Al��H�GqA�Nj��όZB����x?K��(>1P�|�Y�?��e�����O���3|xG�y��S>��n��t�ڦ���ū˥�+�`��D���(/���J�+�,v�����L�7�[��'T~�- �<���M��?)U���I��Q%^���<M����{��s�Ξ�آX[��� +6�Of8ִ��˷�W��Eȕ~͖�����xh�A(o|�d��H'��7�2 \����Z�^;E�O�<�-��ש��ؑQ�X����A+}�</4��wf�fN�ܤ�g�աj�L)|'W.���9�N�_��B�/aЙ~\o�8uӮq�>�dns������. ��� ����j+E�O�|�N<����������]d�[<�s�JF�<H���^P�� -�(i���)��q!W7S���!A�]WB�qx�|fY�8x��K>[�[��/�+� Y����Z��Ի�D+E�����������f���s��E��K�R�#�4W���P[�8�ٖ�5��&)��AoLj����d�/&{{n|�ţY` ��fn�삩�z��ټ9;�_~v���g��L�� �X��������p/�֭;�管T'V#豧�675��xja{��kyߐv�b,�����ʊ����r:�,���hƋ%�`n�}c�F���s����Wa�m�G �[�)���+*��8>4��.�Uc�����wJ�V��bp������3�V���9�]��UXJ���X8갾/g�.[��'���x��P�-��kt4ݏ���EQ��\�>�|��C� z(�� yͿ� �252�K=�D�t�x�]��O;�w�X�X�?����*1'�l�$���g�5�r�<�]��b~j�cnX;o����MǍD}˝5��>��:N.��L.��O.f���a��������������4�6[vu�o�b���r�4%��}��S�-�c%d����G��ߤ���c8K�<�0��uw����&�*�y:MKdP=�8�� %�B�(�H�(��Gҗ�D�� ۻ����Mf;�6tH�k�Vo9�D�"��:2JM�3�B�� �ʾ�`}�!�W�� Sۨ���*���?��B�(�i''���ߗ��`�o���s�mp�f��� �y�/�)���'�(�����Mխ�,��ߎPS��z��l1�*���CB<������Z��A����֮�V����c4Լ1T�"��2�t{"p����rl�6�l��K�����e��k�,��ZU��V*�;y��?M�<�UO�#��ՒU���Bv_B����c��i'��kr��6�U�=*?�d�k�!��V4e�x�6���u'��Z8e++���W���A�JR��*�:�U�2ŕ4[���7_�}����f{{�h���*okpUH�g�Kzn��N�[����y�����F����,��A�r�w�#�!�V��bS݄uD�����VJ���¦�ԗ�h�@v9��/˹ˇ��� -/�W��)�=\yA�Yۮ[֕+VQ�K�*F]��%W�9�k �B\����5�=��7�f�>{��Q�b�-Y�������t=�n�J�A�LY�Eiy���eZ>���&Cc�>w_������ ^Ϲ��>s���� ���J��(��v+1KU+s��sO!������w���6�5&�a-����J�� -��x��9�2�D*�m���j�(e���|9o��ڍ�me�4klf��dd�m!_K�s&�$X��I0�<&A������R2��܅U~�)���3y�[U ր{���ܞ\�SX8��9�`H���d�5kD�h�2�Ӥ����k^�t'�Y4���z<:N���;����Ad=�zr~xZt��e������Ozdk�j���"��aE�|�E��A��\'RΈ����<��g��W�I�v�)7��F��Y��14rG�rzA��.K����F`�Ֆپ�,��_��R��=�����{�e�M���쥎�������a� B���:Ϟ:�0���VƏ���R�� M{M�}�1��h�����)1�V9S}�a0����z��q�2Y�x��u�Y�h�nI�����7��J��ӎ�����X�^�"4-Ĭ���F�X�O�Q�-p�l�%�x��tg��c�#u� C��2����]�����a�촅y!�u�b37j~�xڜ��ycY?-�+������1�W�1FWƖ=sƗ�L) *K�(m�s��b�u��D�@Ak� �_���p�k�����>ȍ;y��^�d�ܬA��}�Ƚ�{�kr^1 Ƽe5��I�Mb��V F��Q�N��=�FT�����Y}@�9�lp��٠[v����� �������s�9@��@ܜrM=HD\�?A!@� _�����H�>����\l��j�t;.V�Q��*r�9*"v}.�z�SX���wk�{�A��?,{�/\]��jz^��4�'�t��y?C�/���>����1A.�@~�&��A^�� ��W��<���D�J��q;�l�axk��~�DX���gwz -�!~��)�y`�Ǟ\Wao�< ^f��_Zi�7�d�"i�&��5@>��L� -�j�/�R��f��(���͊���rxDUH���@Άup+��2h��i��nC�Y�'�~�-%&�X��CC��7��^�y���`�<W�<�����wϿ�{�p�7Jl3���.��|չ�<��Bk?���<t&�K�(��N�x���˻Ͷ� ( �A���������ŵ�OQz����&Ly����BZ��D���1��~���{u&"�[��o�a;5���7�(�Q��8��yd����<ZR���O��ۓ� -�����lX��Gg��G���zY����Ϣ��������ۛ>QuK<pX;�)44o�2�^����һ�?�cMɸf�u���o$�(�H���L�v O�N�m=��;Φ>������~<n���4���-�ܬ}y�Ŷ�p^��Q���;EwJ���i�v������W��J����9_��D��f�������6�o�^/���Qx�����Y{�D��=@3�6������f�mܼ�70�N��~bSe� G�N�w捩���e:ޥo���g��Xt�b��B���Y(���u���*�浼��=��z�k���d���۟�?�`��� -�,�f�(��A!1Ø@K����y��*��m�~���ٽ.����~eϠB���t��gg9h��m���.\��R�U�V-��S�v�)�� ԓ�0�SǷ"]�>z?>Z|���H����O --�O��dJZ�_������ת�լ[��[j���ԝ�g��bM��3y2o��l�G������hu���a�W������Ŵ8$ӈ@m[�~��:��?*� -W*�t��o��d���h�m�P�3�wWj��2�R^�kЪ ��L�)t̼ܳK��ª�ڤ٨�8�� ��zH_���v| L��6ֿEU���xgs��ꌲ<�M�Γ/���\d�|�s[?HS��z�Ab�\2�T�MN��6�st��O����9un��i��Yg;�� -�.|�e�Z��hv�) -�鵠��Bײ٩�����y(ӏ�U���<���i$��)�EE?���㸭��q�_}��x�I~@�Z"'a���F���5�s2�x��º�1�tVD�u�B&��%6��W��́~��MYvHU�*Bƒ�2߉����\�>��>3Ǜ�[���7�]��|�vs|/�� �~N�|R���ry;�}���������녲o��a��MDú�ŧټ˟�P�ҽ������P����S�sw'�M��.�IU�p���_,��6/pO�%���%uNG^k~(�B~h8���@����� n���o"j?H�E;ME}\xb�vH�v�d������#}�h�4� -�՜��nOZJ��j���۶dq���IBp�\������B�0X5���q0�\V��t�9�8���~?�K -���;��O�f��wkm�.�JY4����0rt������|�Q�]M�I�#1?>��F�V�N찾 �)[㏖�rߌ�q��%sХl���&{S;��&��u�������e�"ޯ�� gd� ڤ�hэn���*��>����cfk��:�*�6{�O5ϛ_����r�V�;GF����acS|̋{�ː27=o�� �<�5�ݯU�Ⱥ�g��&I��1|b˘䱸�����!M��m25�G���4(BU)|>^�m߉���eѰ��bq -WSVϜ���j��J1��ҍ��#�l5D�<�����Z6��\:�vj{�پ�.1�ٲ�FջL�m��4��wr�`j��'H�������[P�TE�����(�5�b�4�b2��D���V5욢����\/��3�D��W���$�M�Y��Fk��o�칐���,{ah}��fn��P����`�ÖS��]1]��zIo�z���o��6������{c���E�lW�R����6�,Bٜ��\�r��E�|�/�Y��t"�'�����vrXa��~3��,9�p̶sWwO���Z���r�L��~��ʂ8�(�����t�"� ?�1.1N�E�l�������`�`2�nN�l�M�/�r5���y1~ -����O�"�\��j͇���X��c�d�;�kӺ��l=�7���'w�R� T����Nm�ʼo*v��(E��ڔ�^xg���5$�'��a��i��k���w���Ņ+�jt��\n��65�Z������^�h� ��'��G��\v'j���Ȥs����M��:_��\b�)�u������� -~c�����Jy�Nj��gج|q�q����|R������r�0����0��[?w�0�j����h;�.+z�5��9��ߩ���]���8�x�Y7�6������x����V�����,��:g��Q�g9�����s�e����%���j}(J"����[��;Î�]A�[q����4�S����F�ߐy��Ef'�t~2����=�oc�uc��1���1��c�n�'@A���ξ�b� ѐ�K��Ƌvx��u:{����yU��]0�(ۈ������ӓ�%K�{�4N�p&��(��%N� -���{{���>��k>S�V�|�ZE��vݣ3�*��̼4;���!x*��Bzy�}b���[�b��g��� �W��v}�ܦ�������N� ���N6&ppL�p:��l��iZ� v(*��M�̺)+��\��d&]��W}9�er��g�R�u9e�矸oq�d�3,}�I��zr��d���Vv����x}�3(���@���n_�6-=�ĸZ#ƺ�E~-��դ�캫 �OW�!��B�-��N�UO|�m )%�����"o���=���v��+\�(!���E�8�&%3�'�W�=�Ћ,|g�M�UO�#XGг����Ji\�UV-(K����E���9��]���X���Ea��j.�e�ͮչ2����1nC��Lw��L�6ݝF���T���oX�F�'?'_�\^�|���Ϯ���$�\�0c��,�ݗ9�H���sb�ѳ����z�q�2����tsR��`��O����IW)w�!���b�B�aIC���]IF2c�Gc?�F�`��S�ŝ��{k�9e֙��ߤ���?��������Δ�d��P�(7�F��LM��i��硎�Cdw}�q��Z1�w�Y����A�}�:���ZzX=��F��)'x�z5�H�u����?��yd�@�~χ����9����j�6�4U'��5�隝� ���/m�/+�c���j�A�f����a���ߧ����qK�4���2)7UJ�6��sф�c�qbU��� F������ҳ��B� �nu��SOM�܈�r�u�Y��t��a��|�.�/��6^�2}��9�ö0�OZ��_�ƅκ�ʹҫq���F�m�X��\!'V��W�ֳ��ʕo�6 -�ݢ���g�V�� �h�Y�$e��Z|�6`;?+]��9i�Z�~�{�]��;w:����qS�<3�e�u�����/k�hS�ңU���ڵ�a�6Joi�K�+|�aa� -���J$���E�Y�!u�"��0C �o�����0TYw��^�����������C H���~�~6UZ�N��I�=�а�`�L�D �ll@!^}*�U��ș2�t�nC*��W)��?���?v����_ ��AvV���w�P�YX�sI�c����=�t] � �2��&�@.��k�����L�@�"G���^>��U���4�����}7K�tj�������c����zGMu������D�\)������B�����P������ߵ@������!.��Z����ʓ�=��Z��a�"s�GM�$���n��8��4����IB~}I)�m�R��]�;s~A;��<5��o�Y�PPOD��~"%LD�d�|G�k^"�� - �9�ؚ4���Q��I3 ��Vy?8�|3=��nFO/H�a\��nfR��ڶ�y��V�|�\��<+�s9��K�ج~��'�8���3mA�PND����Wc���nH���r���]1.�Nx�.���m�-a�ýt��fC�o�z�;xR�}{cQ��ԏ��{͔���g��Ӏ��sD�N� �vܬ�-:�?�/��a]��+@��$rF�@����r������'"X,2ѹ8ʇusZ�â��n�gOy�����h]~�O�H|��������Q5�~fG�{%�[��Z^��C����s�w��I��F�&�3R��oH̓��x%�:�eS{��y�/U/.ƽ��y���7����|4_z�;~������i���R��{u˷��5��*t���q����,����p;�����wt��8Ȫ��Q��X�kٴJ4d����4���� ?K&��4��2` �+ �_���������~�S���1��/+�r����`r�u����<*�{WY<E7k���o`�At�i��Vz����Y9����U��f5�ݘ�O/�`��s�<u���f�� �F� -@6d�n�t��H�8����~ ���M}/�z�.���G��]hUr��v˅���YX����P�r���c�h^���Y�6��=�{Fs���^�{�ԙ5F:olI-FmQᡩ~�?�����8� ���DT`���"Z�ş\�{��-ozy��W�(����9r���-S�Z�z�Wq��2k/jm0�>y4~8u쵤�7{S�_��&��P�P^�n�uz�o�Ӡ�+�5ek2�`ٞ��|�*?K���ɡF������'6nI����Ι��q��гΣ�Ǥ�2j0�B�t�}=��s}��qM�[�]yU=ƚ -y_WY� -i���3H�v��\�kK����R ��Ռ�@��7���T�nT����~�!��hq��`��ic�=��(ͧ9y/0�*z����}�X�EJ*k�hi���l��%��1+c��(��) -s�*���H��Rg"�B;�E,�GKf��7�dR��|�|�U��X�"�;��}bG ����rVk|gU��`�k�ԓ)O��G-��*R-֔�:�.}˶����7�$�����������x[� ��V����i����W��������'�"�9����R�ID]\x?qT�G���Lgb]*a�ː�Gr�ДE��X!R�9����_�.e�)U���ٹ�����{�cXY�5��g�p�Q�q2#v�I�Er�@��,��SXM������ -�fP�O�ek�x��9��r�䬌k���-�ԯ��cs4Es5u�v]eM\_�����n����ͤ'���T�s=����-?��"'�{�m�σ�ݡ�k��Ft��F<��~eYf���`��@�N�@�Q��3���9��g�< vɡ�F��-�o۩����+�ry�<���cs9ɉ�I�,�<�F|�@F��}-�)|'��q����f�zۣb�:�W��|Y��3E�-i���Ό��� ��&*� -��@���dJ��^B��Ȭ�1��z]����q-J� -Q.�ƆT{������ - }=[�G�{�S<��A}vX�|o* -�/@���ׅ�(�t"`�)_ajpo���15d`$��)���r�X��,ڢ��ӧ|��g�[�������nC��>7r -Ӄf�z����;�ό������f��&��aR���X���=�(��k����Fqw;���5������[�[��� *F��~R���� ����ݹӐ���ey�c��ت��P��yN]ζ]�PĕT�Ϭ�o��}�c��gz6���9�+xy��� �Ĵ3d�Bm��@��1��������<�>-��(-��)�YZ�<uZ�f����� P�:��q�|�i��b�u+2�q+��m�Q��FS��&s�g�[A\�G^�4��H�q0�\��g�,{��le�l0u��gn��KA�H|�X�D���I��n���29|De��/5�Χ�oH�}� -�{r�^s�;?@�6�L�M�����}K���*�vv,U��`�ԇ���VG���>K���ݗ��5W��������)=D\��GW�ʨ_��E� �d�w� >4a�WRQ'a�8����=$SS�Q*ֻ��S�v�ndr��6��/�̣1��a��}No^�s�e�ή���C�f�b��4^�x|�m���c�����C*��V�\��� �e.�F�?xi�kᥞC�~~�7���~@G��k��ɨ ��D�t�)��I���ב���9���b�o -j�^>G�y�/"Ӥ#c�a���7}���m]Q -��5r�S{��p97��˨%l(�koj<���\=y v7�Km>�V���F� ����;-@��z��頏E,������h}�r��{_>���~��ò�m�O<�������P�e�(�h�d`[Ke�u�6^���J����k��և�^wPy��^V���'`�u�L*��4m����D�]����B%�*�x;%��-���ҋ�ew�`Ѧ��iN͑7M&#�H��鉰�ݕ(!��'��wss%{t�L���Co���b^�հQP��-�C����h���+v)�dy)W9�7<�i�G��������r�F�W���l|ފ�S�KD�ʸ���W�<^���i�� =y��,lOt�X����ˮ�Q\�:Ls��x9��BW��E�_��"��!�4y�@��@̛87�Ư�Ѧ�hK���Z�|�?cQh�Ã⏝/1�LE ��V6��>�#��k�}����+���q�{X~���TY�Z�&��� -�b��g�:V�c�;/��qj�g��bMo��3�~��Y�i�ӫ��W�y� ���opHP�~ xxl�\�ӓ{��)���bu\���Xz<�g��x�Nf�#��ȵ��P�n��1m���i�<��W5L7'Mظ[���p�9b�O��_��G��6ǽP^�{�O�{���u����>�_!����j���G���MǴ���ȋ�>�O�U��:�ӻ����h� ����١�!��2�!t�Ӷ�,��`9|V��`Է<��c�H�cz)�QZ�/��~1/ҿA��˯L�������P�\��cK���0�-�����y�C���!����e�o�7����5�葭#ѫf�lw; �F �:�G��ǫG;(ڟv��ZQ�is>�>�ck�*?��+6?�z���&�� {���h��0 �����d�13�No۾���^Ń�}'�;/�T�t*�N��Q+�ijֈ��M�MhXf��խr?l�O�K?>��M�ҝi�Z������Qf��f]����5ΐz�i_�/�����e�E~�M\�^e;�Obz�^��#x瀪v{�y~k����7��Z��[��rZ�V��F�ɝ -3��ʞ���]�[��-V��ma��x�ğOhdn �Oޒ�4;���z�7���� ���np�<��/�N��G�\~Et��l 5dۄO�n��r�=l�*=칕�һ����-����������w�]�5�]vw�<6IJ��T�af�A�ײ��f$�2͌{��:d�����/�@&�Io.A@@��o���Td��3j��%��S�h*4��G�:L� x@j�H+)�:���D��xYռ8w��b$�ƫt��;z�#n03����Qf��J��W�� CY����D�}@�^�~�v��=8%P��r~6Y$-Q���k�۟&�o����%�q��*���;N�Qq;�s��C��B6���n3 -Zr�wE��DTd���c_@aٗ<ݤ� -?� ��2��� -�,�E���E�����cxy �l��]�/��4�g8����!$��n-��"�};��{;>� �v�g�6�*��x�Fu���9�ʖ����&�Ϲ0g�Ƞ�>Pe��6��v,@���Q�p�7p���I�~?��� � �Q�-G���I�;z�������D�*O��}�?�������ŋp��������w���yO�O��L�:J������{�h{�!&�mQ�8݅���ss�t�@pK�ڹ&8F���lX�?�7��tJR���ԋ���=���B������t�מ�c�<�x�Z��������u^�/�WY�߳��ܿ5�n�-����Q?Hw���� ;���j`�/�6@&� ��� -�rM�^���ݤ���nȓr��Km[/���Kd<X��(�K��tZMt/���u{�o��w�23ȿV��Y��UW�����ҁK.e/)���5���ǒ�0f@�mz���Hv�KD��u�[�ș�Sxb;�t���g������3_� ��+�m�V�����n��.�Z��xC�`L�\9# -nfnh���g�*�m��e���P��s�r�ƶ�!���'O��� �6����x �6���<��s����{�s�x.?��~^듗Ӏ��+�!�qԝ��uG;�:�G�j�/���$���a��&�BNf�?�-.~Nϰ�8�W���),�EІ��ӄІ���B�������DT?*�\=�\($��5���'��E7�m+W�sԼ���p������ۃ� ����Q7���5��xl4�8�&:�&��֛=X�U�g�g-¢H^eU�;#5+}6�>A(e��(���o��t�粢����g1�0��䜓`@E}��{�=Sw��?���j�:��^#0k��FZP�����Q�~�i�W%%�չ��۞Aއ��G&#�/צ��)����O���;�W��}ܶGCxd�^�ܚvе��ڜ���g �Nd�?���^�����v�m��j���N<�5ܴ��dE=D���E��Ζ���������x5�S�+���}��rǹ[�p�Xӳ_0���n�n�pj��(���S\/o��vȍ-m�h5(�J�r�Ǩ�<���Q -��qk��v����@v��NV��"-���Cj��a9'�N#A����a�`�v+c��_�a�e|o��jB%�e�� �{Τ�c�eK;p��V{̾3�� �T��c�su6��:ʑD�ea\�J��-�؏I��RW�Y%�i�r�?s���C��J�siv�ow��5��������Y���}���� �:�H�ڱ�L�:v�T�����嫣��c$G�7"V����P�dUً���5Kl�0Y�fnE������i�kdU���a��eU]Hv��F{��c$Wp�ѵ�����H�����[�v���`�VO�Pm��� y@�^��,�������4v�����[�?D��7v�¥����۱`�����(~���(.{�b��]ȍ�#c���yc8��lt��^˚MzS� ����g�F�+U���N�˓��O"y8�%�"t 1}v��N��"p������W�K1�j����7nӿT��`s��q��R\5��4�=riq�v��/'o�8�5����3p���{�G���^#}s}�Z��Sj۫JJ�X�d��8Hj����+g��#Xi\�ɻqnėZ��p}�=>�KH�+�3%��Ώ3��MN�o��B���P,�6 -u�[oY=w�\t�ZݳG䭣3��n�e}��=�q�j'#�~��ˣ�ː���w�C4�/W�Q�&�u�ůA-����m�Ϟ��m�1��ISeښзY�y�4������/ĿH�F�ϼ�b;�=��it�� -��f����.ƺ7�k$6o��! �qY�HI]e��d��s�v���.�n|�ʼ�-��ؓGU�&�wf)|��}�9[���d�aM����jT`�Ԩ�_Q�����"-ȳbZ�TZ�j�<�@�z���¹g�7mdwзR�{P�{�)���D��#T4'kJ��,���)��p���h�M��a���DG�q��3� %p�L�@���ON�M|��^� %���{�|��H1QJ6f�}�V���[\v��x�'��3��z�^�F���QUҶ�@\L -K��p��<w�W���C�l������X{Ӄ|1O��b�L�zv: xE��#r��ŝ���;�Z'�F��T"�_�_�žR~��˷ �b%��M�rq��h�O�9Sp�!i{�(Bc�#7�����յ��`AaB���x�ɞ��%?��NR���g�0�@��&�ﶠ8���5k�eO�-��Z[�a���y_�w�_�E�\K'�yjl���ó�*���N���-y����E���� ��;aiLFNtH�� -Dz8��bF�E��^�Y�˾��]�/��4�n7��ls��ĆƦ��Eؙ?���6��i�q�v����/~�����}�\�^�V������JN���|�k�X����i�1^Pjސo�bIcaj$�:�A/)9'|77ox�q�mU� ZM삜�Xۇ6��W�<�\�=�����|�����y��q��N�vy�]�� i�O�l`��v/�����T��9�?-��h��Qӕ�$��qL�q��U����.��y��nc��-,�49���L\G#���k��J�?�M`5����#��NM����}����K��w�h����j�ޫ�vf/'Y�j�'�����y�C{���s�qO.�����{�lCI:�i!w�E�n*��ރ끴W�u�Z�Y��jw���t�,�1�@X�!���!P�C���D�����M����A$vb�8�C{�T�F�i_Ԟ�D����r�ܱxe�*�3ָba��Z�wFm=켆��[�&��}7D�/�w�:c�6 -�����w$�����N�.K��x�����*F�*�ދ*:o-�c^T[��8@�9q9�Ћ5:����ӵt���Ѳk�����#w V���E�95Bl�E��Qs��K�e�f,���]l����f�<(�Μ���yS���b����ہ�5�}�',tL �� �f���O��_�.R7�̳鄳��T�O���w�+�T���輇���Hͩ��}zή�م1,��h0}6���L��X#�I���^yX��;M�[|�-�U�z��p�+��5o� -&4������_�����N�����C峰H�}v76D���ֆ�K"�)�^N7��_�����������eT���p����m��V��dp2��u3 �9hjb� -m�>��~�i��hj�{B�7� U@��ɳ�w�j�/��h�s59�#�/���y[>S3:��l���K]M��]�:��j��� {"}��1���=� R�ٍ�Е=���n��f]�l�����դ�6�9z�ݔ��Uu�l��r���H�� -�j�4���q��Bo֎��<�/��Z��D\�'����g�_�.1��u��=�>�7�I��;��.�����^�֭����VIy����Ul�p��h<��:3ik��lQ���mU�-��S��ްkej�P���{e��Y�篬�8�̿d?�sTdNL��3C�<����Z%���8xcr��.W�Ґ��nA2i$l���$�<�_J#X���5{LԻ�^XKQUޔ�-yʟ���X+碸PBI��M�V>p�>�n8'��Mn��`J;P�^7P��|�T�����'�#?b��ݵ�a�e��R�'���v�ϓ�^�&���7�!4���¶6�;Xu\ֈ -\7��-H%��)El<� -uҡ��hd�]�e���9�p_����d���o$ ���*Cc�7�[����� �T�k�[�I�` -�q+��,ェ�:�3�] -0�K -��$4��q��j�~ -�8�a���]�d�K��z�/W�٭��JE���ls@L���ޔ[wNj�=d4���4��?�~&y�s���0�xA��YLAh�K�� I�a���l��l�����,'VZo%K}<x�@x�Z�(�U"��s�t��&��gU/|���O��.&A�� ������Hj\���V�T� ߦ�~��?I>]�|�(+"����\KAS�ེΠ�i��x7S�1>��I����{.��@�|n���5[��t/b�E+@�ƒ�GgyP�Q>q�� ��¤��a�B������0��mw-�|��+|����YEzaV��=s�;#���0Ns�gܥ4���ę ��:��O��1�'����l���82�û�HgQ�H$���h�3�$��<`������6�Wt1.\0�j��< -������x|��������������������n -5W�BB*�Ё�7��į�c�"��������#�n)�l��۲Z��(\��,�d}u]}�Z���eS��Y���zg�%D'�T�����x����y����=���]��H��q>��3-f��R�d�i>Wg�Rk�����;��� ,7R�x��D.9�uuF�/�K��g����np�Mw�S��/�t��Y\P;}�6��~ހR���_�w���*���jm�뻧w�7)XeE}q - �j�7�4�I��:ע��s������[��؇s�o�s�| O<�>�����$����aJ�{#<��s���\n�#Ng�(��֡*xe���j�,r���w�ީ�ts���S�����:��]��3��i^w�i��Ş�w��GB~�r{���\�1>�q��c�~��\|�G�g�CZ���kw8�6����\J�������}7նn��Œ�`��n��30����a�;�_d��ܲڜi~�n���y���t��J���t&����Gف����hQ>�u%�����{^��u��W��{�![l�%{�[�2� T�����$�&p���y�T��'7�|#���Mtm�A�in"�)tT���P`�lV��Mۗ��ǧcwݙ�����E*N�����-{H���>/ք���oL=.�͙��VبKn�1�=z��l�KhQ��E�ժ���6ds�\��P�Sv�/Ҝ���|w�Kc��3�F!�5�~��� -�绳�����۪��R��[�xțo}���*�6I��G\�&�z�ڞ�����PO,8T��U."�m -3���MC2��Kr�:�I��x(Idk�/R�~��y����'��Ҍ���>�n���z��݉>��;��ak�p��~����%�k 4���i�e)P�|;R���K�|F9���W�b�re̤���tX�bR��D��*�S��>���/������"�[�O�Ym�z��\��gz��s������|Y�i���zd��R/�\������ͳm+lv�����]�-�S,���-ɭOIL6������Cm1�l�da�&�B���yg�i�(����YmD?Y����r��[L_#��#���k�߁-��os��U�"��k�]s���Z�>G�ҝ�EY0C��#���$&��&½�K�<HX�̺�K���jk�2��v��C ���J-��&���~�"�Wf��@����m�3�,��yeey��Is���`~�jl;NA�]��R��;�Ca��,��Vz�s�4v -��˖�r���s��!��j-�/ -�YT�=�%�{5��c�ǜ��s�ݚ7F_�_?|S�����1"N����z����߽Ym:�b��}�p��v`9�. �kCy��si�kQG<R�� ,+�λD��ŝ�=7���n\吼Y�����a�26gZ���Cb��\|�݄�g�F_�_�Ȯ%.N�9�x�t���S�0�#'�t"M�םP=�c@����L��ѩ6qTX�,λG��K J�6k�Lj�Y�/��l��v���a��ޠS��LQ�Z�����SB a��qf\�I�O��u�Х1��\Wtw��b��)��`�����Dz���h�PK����6mB�/��8���kL��),� a�1��bǴ4��7S�>Rb�����9&�7���j��"��Z5Cf�?��Mt}�ia�~�r��%��{å�v)۰�W"龲��6%���I{��B�8������>k�\-__��E2�CW`:P]��Uˡ��wl��Z7��,>�HW c��y���m��p��?p{�q{=�g�_��Ȯ�*��OpTɻ��ӆ�B�0����א����ຎ�NЛ����b��-�.K���ang������Ge��B���"���1��G���ޤ�#����j��ay�6j�5�5��k�����EZf��f�s��v-��7�5��p�����ty����ô��T�Wd���IG�m� '�)��d�[�m��9� b�=�a��p|>�X3�ߖ����ZȦ�h�X E,����v.t65������7����20�#�sw^�i7��F�XIe�r���x��{��?>�(��Գ֨1��Z:���l�{�% ��p���_Q�-v��M`sl��μsu�6�ac3[��'�f��i��0��Z�z�����Ө�:x��lg+��5�X�T;{V��wx�;7�a�E��Lm"%��Q"��Sw��/�����jeHm�ǥ�i4VF3���;�m� �������t2F��3����C���w�<9u�i_;�",j�CN�.��<|خ5K����W�/��zȚ���LX�������v?���ݺ�!�L�g1.�1fpA���]��*��}���aΜ������t+"��#�A�l"�Wx"�Ɩ3md��/����]P���V۠���>���,�W�����4A�}%��z��4'�3�٪�5G�U<�5Vri4X��k�j$�Nc�@>�G�I^G���wxu�l�hY��£K���E9ꪋ�1�X�-��(�NcQ&���q��r�ro�f�v���#�HH����w矽T�+Z��UJX���Ca�|C���@UZ���D��pY��������G��Dao��a5���M2�{� -:3ݢ��Jt��5�M ��T�����J*��Q� ��:ю�|�ׇ�1�z��P=�M�.N��)��g����.��3k+�wY)|���0���p� ̦�YM����_y�RIw��o��}�cC�wdž��yk� 4N��d\ʀ˿p{�d���w�S�ݽ��� w�q{�����kq���n��g�4�J� ����4���nl6��x_#g6�F(��� -'�3���x��+谜�����߬�����A�O������)w��֊�ou[�>�)�>��_X抸�ھ*���"x�܁,�"��!b�~@ v�Λ���=�*���ʝ�ye���3����^/w�w�vEz��'�y�w����:�7k�"�6���в�ܧ�t�~�l�m��2��#�e�aT�Y�g6��X�Y�Cw䲶�6���EA��͛��l���p�f��͊��{j5Z�z8jzy�h����,k�o��q����W�{�v?}ꁔ+�)dީ��\�"�֝����BL�Ձ�V����<��SE�^�_h���!��Ӎ;W��:ij���ͥ�?��9y�,'ˌ���v���M�����R���z+Z5nA���YӪÉ�T�/�k�?�������vQ)�b�{�h�̟��oļ]��b�S�tf0ԙީ_�R}����B�/������S��S��N4M��s���FɊ��L�|��U���U^�?)Zw�<7*T q�������o^�&ԉ����:�q;�p���`V�ɜ�p�ɈU�mO�����OJ5;�E��ҍv�=�I{�Ju>�v��v�����:�e�l��I��6�BYs�d��f�{|z��[�:�E{r�T�۠�����_el���x�YQg�+ʊڀ2�vV��"C�3>�WF#�I��iI���$u7z�&Z�OJ���8�f8��琌5�d1���Ϡ����K�g��������TI�O����^��,V�W��ϝka����<����ط��/{8�n1�D}T���^��\T~��rc$���N���N>]��|�V.��N+�����u��㒏P536��#;��d���/gr����#������Ƴ�:�a��xi6�k7e��3�hƩzg���-���m��?����(�艥�r��T��!+�=��;�&xqH�b-I�,��]�e��X��X����,���⊸>�=����x`��Q|P�>��q�o�����{�)���"�Hӷ��Rn��:\�p�B���B��ٿ���*<,'�Q�E��^�aa/��%bL�A��^���:�C��FY��?��i���q����V���?|E�,Č�����H��\�F���^h��I8���.�kJ����im}�ߜ���y�Q<H��?��<�Oj�o�R.��`����(�$���|'�u4Wn^�H�$u����D��d�^w�g'���8��ns������ew>@�5W����y�- -��tk��o�Ǩ�e1z�� ��O0L��x�����M��Z�/������*�Լ��YЙ[��inw�n52Y<�}�(�E2�$np�Ƈ�KA�x|b�r���N���z���~������aLB߾�>m��{X�{�ి�[�m����(�c�Q�A�y�@N��N��~���K�y���V�w -��j -����bë������9��uly�(~ -7�u��pP0@��"�����i1���A����+ p�Q��:�.����E�ɲ�m]��^��&�-i����r���AsZZ��H����{}�J9RȺ��n��ߺ�ָ���}�6�������h��hE{���hi���4�$�,�s�s��a6�������Ko��eK�k��ffүS�T�m�,&W#Ǐ>:ws(��?�M�4Ӝb\SH$�4��מć�F�+ -]�}�s|���រ�.6��o���1�q�dO���,�R-y�E���ꛉ�Lތϑ�ζ3L���ỡ;Z{����ѼSE�*M,PI�V���A9��/RP?)T��i�"~Ҽ<z�\w��z�/7ߙ)��Nw�Yr[�fێ���z���6Ff� tH�X��f@��Q���"};k~'kX���������]u��eZ�C:���v���K���J�$q����e'���-ͳ��y��^�}�k�����e�;����N�z2N��fװV����������4_�1�fҔԣ��Tb;w�F�<*4ńJ��K�0L�r���H����D�ċ����z�a�ރ�/��>����zQ����-�X۬Jm5g=̟��/���R��%#�F[�>��*��T�gWʥ� -�8p�mt�d.����<I�4�G�ի���!qr=��7W��ك�������1���?��[�)1~��gQ|�ۗK��C:P9=ŤK/#�z���&��c}�>sZ5�jj#�� -ӫ��=X����$&�Q˪�(h�!�����y'��Pxd ��B�s;�Irk��r�������֙wyv+��_���>�$�nsfa��_�vw�L�~t(ќ�O_���z: o��e9�-)��}i��aQ�~�@�S���W��jW����̢S��<�pk�I�C�^b��b�ք`�C�a����4�)�����~�B�#�κ�m�W��8zI���-b[K]�i�P�Zm_&�_r���K�˦&j�+�z$̟��wV�/�}��!Ǖ��������� ��놡�V���i�f'ؚ�|�&����50c���"k��kv��K���wd��؏�xk��3�ƀ�=Ɓo��J,&� |�FYX,fm] ����L8l�Y���ذ8%�L*Cq�I_�f�❊� D �ަ��|�"����%��!�K�g�|v�~����=.v�A�� W�.I�MXK��[����*[�,��/���M0��ws�_\�u��ywX<��̹��*-l�t�k�4��k�%t�x��T�%���6��O��[�B���9C}g\�/�_�ylu��֨s|j=���oI��c��JV��Q�˥ �͍#�x�ݹ��6Ŭ��S�9K��JN}:�^S�����.m�a�ʒOo�����%ңr%f��wܓ���n��_�B�'��}2�ȚtF�i�S�a!Z��XRw�<��Kc�oUJ�s�gD��6�ռ�m\��]������S�9��S��ԥ��nL��9�\��yP�~/�8lظ�Z���TL���]ݮ� �-!���%t�nKc6�����9�%Yw��r���~�|�.��E^ˬ��!�t������wÛ�Uɷ�\f�}C)���5<���:D���� c���1��+�^�H�n�v�z�u�}���e�����M��c'�Lcd��Ӧ��@s_�_ĝ>>����˥�����0� 8�5@�E�yt�p�/2�:t��;S�NvÝ.b�0�O����M�>tz8"����s�l�o˓��'r/#��}}-1�=!���=Xwp�Xw�[w��Ǻ�t�ugT~q3��m?�.���"9�(�)^��-2��f:p����� ����Qf��pg"�x�p�[,w�n����9еѦvkΰ���1�O2���kv|ڭ"���9�Ccg�A��C�u�F�vCeИ-��R����M� ��"U�RY;�0AUV8_.�a���Cj��7��$E#g����w�M�N��%�������#�F��\FVB��W+�J�����rF�V�����B���"�����0Ҍ����~�)���C4cE����}9�|g�V0�ƃ���f)�����?pk%���C���d%J�<�L�T�-t� C�]��~�1dF�إu��˥�;�/V6�Է(!�٢4��E�=>-J%9�p�_'��Y�aZ�h��A��2Zy������qw��66쬹�ZI廀����ݫ�[�y�#�A{/�����?���N-v�iw�(�)d�?8�|�'�����3R��Y.��+`�`�&�@>|q�����/�\���3�d?C����gWWC�T���� ӏ��VIk��V�&b���W��Oȋ��s����3��zϚ�,�}G��ݢ�4���x*�Et:�/�Ik�Dv���xŧ��l��Ëk"g��p'v���]�l��]�Z`��iD�Pj�%�13��u�?�K���1�ȩ�9N�U8���}�����$M�ZՙL��q��h�z:.vnd���h W;�<0�ݣ� WD� Kͅ=�h�۠:C���)W^�PT�Ӡ -��/l�������ԓ@�s��ǒ��H�����Z���d��d{*�Tyd��p7�'Ò�}�{��7��Fz��^�Y��%<=z���:�~�嶭.���� b�MH���ܷ\�Z��B_l������u������A�D�=�)��'^iԈ���#�ՠ4��k�5��r��f`'�_�v�\u��J����}�2��Z��ނڮ��ΩY�?ߙ) -��G�5*�ϸN4FX������G\k��W��k-�F�z��5�^��E�{��2|���#��� �����2�������7���z?��B'����-�:�~���3�*uڪHkuRu^���ޠ˪�Q�S�uK�t) ���_)����<�����Q�oDP�&�N�`d�|-X��_���u���IJ��!!�*c��J ˝��u@c8�+f�ha�3��`sU�$�K�WFm�^�Q��gщ��bqU��[����:��j���f; �۷8l�o��3 0��;� -�� -L��,�0Ax$CB�"��{r���`���V�, -J�������u�{��S�5���]���Wyʏ�s��ٺ>��c���hu�0�=`�t����"#��R�"�L���R����J�5�̘ʈl`ʒw`ʕ˿��Z@�����-���Λ��|�h�z��ڳA�۹mT�-P�매�sjw(��P�'@5����Q3���q�p/7˘b*�}���77Md�B��4�O�τ^��_��$3�J� Id!�$�ob�ܖn���[�|�@��J���^�X�y�}�:pIb�$9��nοCx��7?�Ll}}-*��;���!�ܳ�s�1V����:��7�>y�VM���V�l?���}(�w���/I��t���w7���IJ/��/�3�$I/n!Iմ���x����f}�p�Q�^֨�� �l?]��?W�4����c�QWT��8NY�Q7����84�)Ֆ#�SFn�C���^���_���|�k��e?.��|:�%I[7&I����+)y������������(y��dz��?~�g�H�3���>�am�n:���v�{�3�#~1M�>��n�-߆o���`N�&v����;ا��D>��� ����mc��?�%+�RL��FH>]�J>W����DUJ>a�I�mn�*x�2>,M�A����>�hY��7ͼ�}ͽ���<�6v�p���9����/���8c=t$su�M� \bs��k?kg_�}~�����?������+��)k����@a�}V���Ѻ��获�7�]لz�B_�1�_!��\V`�8�H�9W���zp������DZW�~">+�ϟ��Y{��ޥ��K���zܕ���������ɭ�Z��3=JYEr -����o��#(A�B-&h�����ızW�JS��;��aouj��c/xSi���/#/��m���A�� yu��ts�l�zy��)�[����<^����H8MRl����ϖ0������M�������`5+`���J�W��6�qPEs�tV�0�*A���T9u�u� 禽��B��9[#���1m���cy~W��(�{�v�[��\'��NN���������mѺ7}k�N֦R9ʆ�Hw�.��Ϻ�U���I�FȤ`O3�\������u�ʱ�D�\jZ�d�����)z��i�W0p��v�x�8�g���ɇ���g�xM����i>�kϜL������1�s�֑�F3���e����J���AzQ7��V?�E -O:��Ns��P�=أw -��6i��Y�議��=廠CG���Fq�A˒J���.`s,��[��,(�M^�7WUG�[�Q����oZ����v���Z �(��f�kif���:˝�r�������)X�in�tRh��3��EXb����'y%��9ż�����I����_5�ҽ��I:�3`��m���dk�����F����0�+`�J����l���;�\�"=7�U�� o����$$���MIA]���s�{�+ru�y�Ւ�w���z�s��\��>��9�h;)h�h����*��)�Z����_ɬ�rw�2�=ߒ��z`��2u����a%/�y�;J��>�[]C���CR��/�\�e�����|Q9�y�<Lg����m!aɛ�͛�y���VY�/�\�l��R�e��T��}��I"4�Oz6�6��7���� ��]�s��-����9��v��"�X�W� c2W��9���M�~�i��Y?��0?�p�jn}��@v��2����ZЋg�D�;)mH�˽��ĭ�yW'�[[��[_���� -JK~I��+��|kIsn�(\i�:�>��M�}�S�o0� ^2 �#ӗ�g����/~N����˶W�������t���gy��n�V ���7��I�w�x���9 ����U���I�ͭ���+i;���7�݂ Μ�ϐ͕�4�։ff@Bw�M����waţ�@�tǧ�������_d;�G�WE �����K7 �z:É��c���J�xȒ�z8�|¼�y� -���kGr\)i��-�i�5`�e��|�4���\l�f�KE@I�xk���K+&�f�JJW&G&I��I�s�r��Ϫ|��sK:�#�����u8@M=�N��z�W.����!�әm �������+�O�6�}�<��"�Ќ7�S�SY|J��\��<E��I!G��O$� �ѥ����Hu7G���9���4�@��2�vGM��R)]��4UOx���<?EE5�3BA4��n=v�B&X���3)�99JhE�3kd�R^���� �F� nK���\<��C�����5�?�ߢfoQ�2�I��t��Í��J#ڥ��֚�|T_���Pq[I�LX�e��ؚ��ˎôʵݭ�@w�J�v���1D�9�Bh��"`*@�3�|a�CsDغ9#+���47����`��`DZ��7�e�����w�Й'���(�b�9�ݕ��\�r_?���'9��m�����a�S�~��q|&���N�\B�T=�[�q_�X�tm�ugx[��o^`�ev�im�M%<�1���zlw���&��22�S�"���$0/���9ږT7Т�V�{*&'�ͣ��;��#�C�e(��*���.�o��7|9�^[W{�լXܖ�n}�!�������j��%4֘��5m��uy�W�"�Z�J3|���b�U�2�`F��/70��C���]�0�V:��|��Sa��-��:��Ѕf�p^ljwk `6�<���+��l��~�l�v!�x=+Xëg��� -�X��X�Ï��%�=*���f�:ʕ0t�*:<~F����:��r��?S⁀1�.��m���z��D�I��L�f␘%� -��̶���fW\�tX������֝�n�UZ�潹�/�T��at���W� I��^�-?��}i�>����������w��g��uo?_�<�X}h�c�k�5'���䋫�0?Ӯ��|Sck����J���G���:zv�H�� D[���g�w���<^.��2�Ę���:�\�ϡ0��-<��*7�H���{����8@y�D����W'Vz�5�.|&�#���:7�Y\_���*��+�Yj0H�*,/ms�e�o�&�����\��7�@�jq�9��yE���,�gD Uf��z��4�t�Ї��i�g��}��b�?d�閪�҆{�"�D�AzD���G����N�o�?O֘֘�DF��Y�&ځ��b�}���9y�M-�̓}XR�ZQh[ 8 ^`��8��SM����dVכ�w�K�f�S=�D���&��R�2un>aj�b$y��o�F��dDb[:&���ALa&&o\&&�K<�f�A����V!zE��k���ӓi-�X^b�T� "�{����Y����Ao��0}Xlh)$ʲ!��=� 9^����HˇA��;�A�9<x0U����XT>�0n�Ѱ���� Q�p���2���ب0Ҿ�$�h���#n�tћ�eo��B�4�%C�^�Bɒ�t�qy\�ǡ����2%��(�����U),��<6��!�<zcT47�~O��W�$��ʜ�w������]��*��('����B�i�8�fA�/�?��ؗ��؛��a�co�gv�0�z��-'Kja�0"R��4����,�����~x����>ݟ�v�o�ȭ,��ʭgwe��y`�"�~�� ����vh�o�!.o��1��b���um�ǹV�����r�7�.��!��B4� -�h�#��<(��KF�vL��O�W% -��Z�xN�g�������BEJ;7+�lȏO�Q��n}'h����b�܆5�-��v�U/_E�kZy��j� ���h�콊�2�_ -eڊ��7ke�6c�7�7�0=���%g�����á[��Z(*��}e��Aܡ��W�b��u>y��ج\�ۚ����l�%��O��,_-�+#� ٿ15���o�WcS�7�����[���;k�b��c<�-�C`,���X^���w3��&X�%v|ZƆ�.<��6�����ɖ���Q)���b�/���ͅ�W��l]�e��P�1D��)p�"���>_������`��_i����@�S��� ��wM �y ����ay�i���k���?�Ӥ����ގ8b6�O2�;G���j��Z9�/���L8V��2)���w��<ԕB�N�<��n�e:�@��e�_����Q >y +�� ��f���ru�����6� -`nƟ~q6�vsw�2;��4��G���sk�Ѿ��K�>T�s��nv��3��]d�w�Rk�� - �U��u�&��r1�Y����R�����@��#@��y�n�P��r���qj�A���q����f�MӔ��*���i���0�2\�4ea.��>?��x2�����ɣ8��w�L��M�X��.����������سӋ�����<ۥ}�K�78����j�H/�������u�_hZ��83�g��2� -@��a�M�ʼn~�ʏ�#����磨���p:�7����;<_!/�.&��.H���v!I���u>��RzLU�x���a�����=���ݒ�[�J�7�fa��w�~�:MG��Jg3}�0?��I��#��,��y��d��(�:ݻ��m3걷��'\u��\��_��f�msN���Y�u=���=�=�[�eYs`�t�ͭ�{c����|��M{54�בj��k�!M_� C���������O�Ni����9}����/���n�o*~�_���}�7�������8����{���Z7�����~1����^l��s�mk��P@6���5v��WTG�JC�`�y=����wb�c��� ���1��?��~Lt�~<�����EA�v�2p9���g��,�6�Ρ��}����v�Kq[H�˛T�6V�[�;MM�x"��T�nV��pY��"��K�sQ�[�HI��|��s��r��<�C���_I��)��ci�p{����YR�� \/g�#������/�%����b��� ����������+�b��!j���'�>�|yg/V�����BB�q�,�C�,[�|h����z���fئ6 �JaL��4 ������O?������.�P�lo��juzŷ�PZ���KN�n�~V�ו�X���rR�������K�n��D�u����- ���U��0BKdz�� g��1 �R�LWv��t��Z����_#f��z�eo>���ֻ���ڱ�Wnt����58�ٱ��V��f��'+��q���yGڋ�f�,��W�G����������$�����W�����<��(QV�@�\��.L���(7Hv������w�����S�_�Z�������^y�g�J ��Lnp;����E����C��8�&����s.�7��b�����Zނ��_���J��S U�i���\|���4+��;��G��&��0�;�Np�+d;�����z������D�}Vp�9F�;�=����E:�r`u��^���u����3��A!w~%/��^�uY�5�(O��ic�t�{ ��;|���Y� �V���;w�vƄ8x�����eS�S���m�b���+��O����/��g�8βe9�_�Q{� ��h�����JA�n-���^�~[M���q��7��s���Tǖ��V?8�\������E�ӣճ)3�O/gX̶3�@�Y�""�q�Q�\�0k�u�0+��ol��?�K�y -s�=����R���w�V��7���Lx�[N�<�@�x1�p�f�=NO�۞?�ԉ?6�\�O|�Z3�,0)vF��%GL��e<VMQ8���bcs�/F�k���bB��aɞfp=�Tӧx�NY�=��^� ~��T\o�:s�c���ulxK�M����R���Q -�F�6�B[kv�.V�������Uޫ�U~Iy�ʥ� ��ü6���\?�QQ��J��Us��$���iY�EO���Op�����/�U�R��j��ԁ�~�ؕ�"- �<E��Mۯ�&V�ޣт��G� =��^�d�yj���ý��.]�@������G��gh�9��k�Rثφ��F���PT҄]�݆����g��wV7ϕ��e��o�vT@�١��QtO���<B��{��Y��b��e;0ku;6T�M���w�:�U?ݖ���N�v+�9�a ����7c�i.�Tr�|�G��{�e�~��>\��F�<�_���O�9������a���:���b�Hz��a���6&�^>?v�j���m��l��?�fl��tx �����zgNl�'�5l)�էh��i{UT�룡��pW 4�Qh��ste�'��b����]ܕ�)eX9���_|��o��zY��~�͵U�)��D����qO��Mw�^�i�j{��z�� ��҇��j�V�1����X�T�)� -�k����*P�Ǔ��'D.����DQ#U4|%n�u*n`�-��(�롕a1�\�7�� �,�G��_�>����gʴ�G�1f����H�Q��t m��9ud�^ Gj�'*e�B%�$KD�D� -�Y. -�]ZVI��e�2�aq���l�b�`[���|�����ޣ`��y��F�N��Ț >�ȱrm�gw���B�MZ�Y�~�F�H�ˠ��:-��c��V{��a�7NSf��(�l�˃����C#���(n��MT��[l�[Ea�`�>1n�8��雼9���6�_�½>�npy���в�?|�:���w��g�ʋ!�w�^1��=�4����3��"K.�C]#*gT�<ެ����Ԩ�$q�M�͛E%�V�Tw��������O�;ϛ�N�ojwp����i4���g�c -]� ��q����&�ې��:n�k��pXK��*���3� -3qS�0�:��m���U�&-=5B� -J����9�% ܊b2xb�\v�a�9�On+�GDo;������F�V�s����r���HB��w骲���؞��v䳹�XdsM�as�������;6������s�R�e��\��P;��%-K! -���*��b�cpa�2��6��(�Y�n ��z:��w�Ms�5G�k{�͑76��6g���y����\���C���ǘJJץ\�;ɔg��d���0w��=��<\ڞ:���k@MvLX :����&�,a��wI��p��e�j_ڃ1f�ű+rT��A����`�y,���M�xs82�S�z&����Ө������bk@k�pDCZ~�a���%���@C$�}��?��2�����P&��ַCn�5�k������_�M@��i0N�o����\\��=f}f�e�����]w{ -���݄�i��v4��^�����ڍb���i��R�!q��s� �o1,A��B��; \�>$�A��ò��УDݯ�^�����5�w��H�)"�c��8�Y�m^�]z����.LаK|w��F%E!��n�Nm�����$�We��|���=3��M�:���!���s�.�����@�f�A�B_A�k�!�T/�%�Ӗ�NΒ*��t���-�y�l�����L����$&���� <(�p<'Ll��$��� 4�J�?�>Z��ew-̾�M���=:[�Z�h����c�m�̨��� r��䬳D�%B��( �z���׆��۪jU�.3�+�D�NC��L��E�+t15ƾ�7u����m�V��kW�8������;�c�M�M�6@.:�"6̏��i��d�"@`���t�)C�s �f�-C�2kF�����C܁�e��@�U�,.���X�c�yb��K�y�&%f�1/�cv����qf#���\�D2�:�� -t����B���@l�5����+�,]����z���;�$����JgW�_�W���k��sHWG˶]�U���-��W;]��v��o�?z�MO��+���)|�'���D8�e�=�� oN<��w@�[B�ڛշ��[=OO�j� -*c��Wp����Z�.��ҧ� K�U�K�}5�bT���e�*�����O=_��}P�<������ -�kT��\[�� }�3����1UP��]�7���(s�N�S���w �A�4'?q]������K��`�m�ȝ -��3�2�6��J7��� �&1�� ������`�,�<�ɐ����V��_u�K�a�.i�������ݲ���.�n���U_n��=QB�U����� ����y�@^q�mN�����2h�.c>�M�;�7�/� ���&�B -�������n�!�x:�����3<��G ��?��j�0�3QK����_�_0�_�g�����E@� ��SaW3Gf�݀*�'㭝�]���Y�pg�x�͌��b��������@/ .���T�u �u��� �\ ��H�l�Ȱ{��i��3-,qT�$v��Q�Ma�R\�;3y6�D�X���U)�]����2�0��p>�o1�����@��%@�� @��+@�"@��̜���N����b�څ�z���@L�!p�+텸��G>-2d��ȧ���ȧ ���}��'�GQ���p����h1�m:�u�o��fs�������� -y��r�U�d�8�/\?=w���ɭ.�c��zG�ˇY[��՞@Gq���!���OS:��P{��IuneV��F��Wfe��dֵFf`�m�g�Vw��U{��+4}�#]�/H����l��B�9݅���V��*��H���a�}�,���\k�a�Զu�D/�j�Z��(��?�k���^��062���Dv�!�?Ai��G̬�~�~��J�|���;�eu�����1���#���� ��Q�㮻��� ٱ�*�b�A6Q~m7Rhu7*� -�t��Ԯ3K������J����Lӓ&��*�L�&��k��� 2=\��|=w��ㄯ*�#���3(_>�f��/���Nx�Z[���Yd�a�&P�N��e�����X�Du�L��rI�x��o�lw1��KU�ft��x��̺e;M�K8}ES.}��(}7��D��o̿��Ap�7Px�W������!� �U��n�G�����gsbjঽk��ܬ�~ZMzdg�ڲ��׆�%��R̊�Q\���z�X �^��\�Hx���!���9]f��qH�t�Hߠ���MML?��p��2~_,�>>�%����v�m����ʾ��,�� -��|_��{ �`̅�R\d���p����Y�F -�"�z�c�zS���!�L�5�]Bt7C��\@�0�{W���p)ya���T�c�▵�}>�����/��X����"� l�!&��f����ѱ�X���6J��SԘ�s���2��QG ��As��o=�}���~3�(���3�9�p}M�У���kk�FPf"]tyR�)�����?�oYi�}͉�,)��Fv�!�oEW�&�ݕ��\̳�%��x>��g��^�n�y?�v�K0�o���?~�T -~nW�xs���8�iOc_#�B/�&��A��z�u�������oF0>v}x����s���c�Q����O�I��̗�~mpV��. s��խ���� Qs��J�{1�����>����[�q7�0]��e*��d#z�D�F�I�z��:v�]�X���%Ō�mCv.׃�\sa?��f#�a�9����-�{)��x���}o~�4��ӻaw��t�S0�m�������Zx�6X��6���v�7���T�`�҅�o��;�a|b�۸#�s�q�l?l�����Cl2�,ߨV�Q8[�#��F��f�O�ez%k Q )�?��v!ߓ}�?�:���ds���Q���r�����_�#ނ��� J�f�4�b��Nߞ9��[�ϲ�ؓ��h�������/�7Ƀ�y�Q�k2㮂��=b��rK����vngӯ�B��b�� AU�/�V��&D�v.��B��\�R -�$ |n���4ٶKD�̗�����j�T��@&�����ѫ��QᥟM����J�}���hԡSm�}5:C���c���ƽ>�w��.<�븫ޕ� ��nST7ߒx�r�op{b�b/q���'x~+�T^~A����W���q����)=�,f�e��|����7+�un��ce����PK��!��Q�r:��7M�8��[���f������Q�Nw��r��EFG�7N���*�9��������_��+t���p�>���2_\D�}_�-��J�(�ԁ)B�blL��¡;�C_?��BoW��v٬���/�=T7�TB��Ya -� -��#9l~�r�h<�����l�Yi�y*Ңv��p��n����� ��X�!3j�x���v����q�����ˍ�@*�f�1F�n�CX���Ѵ4���P�m�� ʋ$_z������ߪ�l^����>3�hOmR`�j¢��L��"Jx,��ks~{���Yx�x���������2m��{l3�٦{��h��+F���pXY�����4'���Gp3�Wt��*��9�C����.���w�*-��#����W�ZK��aO�1]�6�PP�5��&���%e�8Si $�Igby?��G���K�WqOM���d�5�bV.�}=�������[�)oP���P�per�f9�YS���sq]|nDiU;��� -�θ h�',4w=�?��7Z�Ϸ3��̚��~����g���p�SqCވ�B'Ub���4�屃�_60�N�D����4��n�bU��+��R��+F�$�7J�8�kKa"lE+��z-4���7��q �@qfѩ@,(��qw�>r���ƍG�O!!CW��$�p,9����^d췛(U�=ٯ�3���h1��f��`�T�Ħ�0�#$�& �e6���� -�������<�Z���0� ���6�H����w"�\��q���TK&���,u�VXj��><K�@��� H�b��s��#}ޠw����ޢ�2k���[-f�f�切5�~#ũ^7U�#찈����Q\���͢3��s�n��9a����z����M���<���l�������|��Q�w˰�v����E���j�B�I��p⒔']d�=_%�pz������P�l%�Xy��k�,��~�o#">�m��yO�~���������>a�z:��Ⱥߟp�~�RfQNK�`�#LɎ�� -�X=�{U�)���j�W��~��3�?l��%��_�w�ГC�~}������<_���,��ۭ�jAY -�lv������{���$L�@�r�����N7����L�t�{S��j u���iUO��t������0���S-�P�� -�Z�j/�Sp�~X�[O�_� seڵN��5&E::����;���K�U��� �_?�������8�ǘ2w���S^��F�F�7ؤ�cڥ�=���rC��˙�j�7ٹ�5�6��Ę{�B�,���f!���(���m�XV=]�L2���vҹ ��e��� �ʃ���䞥d��т�������s�v٬�P��P����N�����o�*1ޜ,W�)�bD�O���P�6�"�$J��G���2w�r���r�ep�?,Zj_���J���E�y���!�}gO�wR�ց��q��bs7?��aD���̵�H堊{��>�3��&���1�֤�d�FKu��z��V�ꩳy�@���������6���Q�� �T� ���6\}Rm����0�B�E!�'-�IXү�rQa7��pCg�ӂ7Lœ"��Z����=�3�͠|1�vW��V��r'�w�N��w���L���x���������t�f�r���� �lE��F(� -�v�_�r�?�@�osx��M�g�|U����h��"a�WH��wI��ۋ�.��~�;v���ZƎ㶑W&�ՊV�[p?��F�O��<L��U��v�af�O����|Q���n5�5kT��m}��^�:4j�J�_�6�I b�[Mӕ��y�Q�_��3��;9�2ז�e�J����,�?�ؔ]��$�ڇ�r{���,&ҨQ-,º"�6�F�z��N;��VO�^m�r�<� -�4��S�21���_��Kt�~=t1ġnaAjapc��E��.����a��ajX�m������~^DR.��`��~��Sʒg�cIGǹ~��I�d��|>�i+O|J,�)2,�UnZ(�Z�_7�$_�.��.��f��rVk�Zq@g�,�+��[�un~�"�=,i��:O�a����ˇeU�gy�N�=��[t��$n��+�G��!�b��mX�mk��f�N�g -3Ko_����������2�6���g@w����c��-!S�@\@�g@����u���P��˰��`��*6��K�W�����ق���S"V���P�g��{.+�_��ߦ����5I�U�)��e��:�S"��P���4�]o�� >�~��Un'�k�>�&��6"q�[]e�;��}������x�����������G�����\xM�x�l(��Q��Ч���C$�*��|roM�����yQ�����?�r��.�'g]�0r��t��]�C���&c8x���c���g��=��K �`^$@ئ dX�&�^�\i�٠��m����z�z�Ji�}������φ^Ek �nM��SC/��;��r -endstream endobj 28 0 obj <</Length 65536>>stream -�&��4������ѡ���^-�?���]�\��th�/�E�y -�&��c{�r�y�����5mH��\�ZV�������&�$%f����N�����2=�z��(�qN$s�r��M��ҭ>���W�����+�M&p�.g��ڜ���x�7�翥�~�0C�t��ima��/N�Jf��c�Cf��̰�f���������$�������} i�ۦ-Wouǀ����\���_��� �ϗ��?w��t�-����S�H���a�8��{�vNvB��l��z>ы�9�Sr�$[~m7��=X��5Qi�t蟐�Ө������i3~���&_��U{��$�_g4������Y?z5>|4�}`�bw��������m}Ve=��Yn��L��ݝ2_���L�FnI��f<#l4f^.����!�Nof�+f���̭�ĺs�\�=�9�Fpr���H%�݁9��=wi�v£�����+�sg`s��Y��k�W����\���L�3��.o&�ae1��h�}F�h��/Q%&ss�"�z�������6���(����ij��oy� -G��K� �y�0k��}Ѫ�ۺ�u7G6��a<_w7���mX�e:4�=�K��e]c�n=a��Ҳ-�M-�|�%�B��l�ui��N �����;pʵZ���M�a����y M�~)}q����t�~�n`�����Ⱦx��F���^�a��%��[qH���B�Ei��ʅ��VB�d�_��HN�C���mW���#4��o�Ppq���M :(̽�ir��ڻ<�\�3-�l�y����!��e M��A�T� -��ٴ�M�v~}�(��{Q1�%�� -���\d�!m7 ��1�]��r� /����կ~j֟�G�9��Eo��M<̺���i��'�:�M�8{s�4�o���'�8'.��!}i�z���iq��U?�¾p٪Gh�z~*��{7�2���"��A� ����></�����ث�Bq�DžL�Ft�����D"��I���d���f��'no�@�m8��8����r�z -۶5EބE^���m��ҷe������iR�g�߷�cc����7���}̇����[�Y�~�x��C����h"����jDn��F��#���#��t� `�NI���e� ��i����CV�#&��fԨL�1n��ɚ4�d@��H���9��Nus뒟�I]�����)x�l��}z�i�P���V�Ґ��Ta|$&�s�F�][L컶l�/�Իn,�~�G�9pʹ�kԇ��yN���Xnۂ!���0)!�0�)���v[�~�����N�?�$���o� V]/$��ەkh��$�n�ċ(�3�LrUW�ȸM�I���Mh5����5 -��d�g{��h�epf�n� �sz5���NU�OՎ>픠#m�WZ7������]B�z��t��1|����}���[=^K�o�M@���X<Q��}|��E'{��+��p��#�:�&�_��j�����=�v9���r��Vo��E���ֱ%��o!*�~�6�s�7�)e� -t��;��r�~p�)������=T�j{ ���qH�3���ye���B4��'�Y��R�s�Y�G#�C���,�YVt�>6��nٚ�M���_��q�V�z��L��C�#/J7Xf�&%��̣����->/�(�u�Hm01�l��B�w�`���%��\�Ix����yѾMk���O�zn?�8�z�gj�ôh�̧9ԆŮnX ���V�A�MIkHv���X��ڷCRRc!3��N��U�:xN*έ���n�(*VN����fAA�6�!"���%~8���؎���(�����{B[��k�u6θ=�,ab3�[W�.{9�H̪ڥ��;7CU���)�5b�s�d�y�dp���At�b����֢���b��|�#So���\�����x}g�yx��0?�Ne�����ߑ��Zn��o����>�"vkS�����ܨ�Q7 ���5��P���m�;�ɳP!�|Ag�����F�DQqu���A�=�!Tb~�p^�L�<���I���Q�&�Gu�]}��]GI�s��6��Q����$��Z�ڸ� N��9���E�nh(p��v�j(�ޕ���*Ң�@RI�wDqS"�m0Bc��{�P�aY��������K4���n�����9�@�F�<�0����;�OJ,�_�$�����~x�)ݟ[�y�A5�+�����SD+߮�!�ѧj��o�'���^�XE���jw����㇃=8�^�`$��A�K��.�9'�����ս����g�������·��;��ޒ-�s����W���'��� ]Z�������f����h���щ��@��;q��B�����ȗ��>��v��i��n� ��s{�����_K�`����~Л��L�~��vt���&Ñ��k����;e��*�a��!�jկX�ʝ�f��ɾ�헾��+a�裚�d�:���j"J��Z���+���_���q��ň��N5ᅬ�^?�B���{���i�oI<S��E/NgǞ�}�$��M�B��N6���������֤���0DX��<��2��pG�*g���j��4Q�����_�(�{X�k<�sYP�`��C���r�>�,@}��P���St$����W�C��'�V��9]��{j�DO -J�2y\�8iJ-��{�;i2�7iv�u�D�.i6��`� jn�j���.�1&Lr#~�_�3�m������R���\n�z4���T��D��)~ƅ���_�Wm�Z��q�A�j�� -�*yj�l��'3��z� �C�t�����B$�Ē��_{��Ȫd8#8!��?ě~6)�p������Od0�^碍��F��?�ù��d���b�u�Ϸ���j?D�j����)0 �Z��I�X�qzJ�Ī8�e������\OX�L��p�j�l�9�B�>E����S���B3�������^0�> ��jV�\���|Q>zq#�Ϗ`��Z�~X��ep=�jT�a���-�[�8�|�ğ���㧂�"��nPˍ -�2���ڍų�-osAg=淝Z>�[�Ykk�E� �}��|@ߒ�vs?��Ž�n���9�8�'�L�ժ�����]�h�I�S��>Wv�rCQ��F#�9Ăx�c r_�\X9u�����W��l�ܑ/�ޡ�N[{�64�YA4�hn�n���|�]D���5�B��kP:�(�+o�� -��n�Y�N����Q+R����1]}T:�/R5�l�����0��o�"p��Q�};� �ۺ����.��.ZWؾ�Б������Gz �m�/$H�֢o�B3?�x���4J~��Wիљ�WIT�ĵ.�|]�����hď�ޥc&�IEG��G)��M6;����?����g��e:�}/���A��4�'�66�ѺQV�c}�����!ԶH�TS?,\=�x����Y���e0+��塌=�B�9��% -�Z�iG�n�TzJ�W�ɔ�&':�����1Z�aU -VN�����x���~E���F�q�V��[g$C�ٖ��2DW�3�t�а��q_�_]��ǫu]�3�p+��P(&�J~�r�|�0e@y깝C9۟���+f���G�Ye~�à?�9bGԺ_�o-��l�';�Ggz/.6���,� �A̒W�J+�+�w�g�* -����S���P:���:9x�A��N�r:��+ì -t�o&��S�4[���@j�(t��贌E���� -��>���7��C(r/O{3R0��c,�aE�l#����SW�ͯ�Uq�Pd��#�tR��a���%@�@v�������7�5�z�)t�f�{�*����P�@w#3�~u�ݤ��a�q-^w7B^q|����*[Hl���?�k��WWǪ`Q����Cz����/2�N��4��Q@����2(���Ta��j�'� ko��u��u�+���������e�T&�@}���I��b��q�-I JO<x#����Pow�}��#��!S�ý�=�7��[���a^�l��b}� ��}��?��F��g`S���v �ypn���������� -� �B}>b<m���D�� 3����v�)�a��:$���4 _Jr�CY���'��C��_��5�m�ҟa?�?f�剘����4�������{�8�?��J�)h�\�����U��ժ5��L��%��� ��ێ���Ou�G�_�tj�+�J����3��Of��>�]�/�B�^��|+�n�q��ji��B^M��vA�Tٖ}�ƭ��.�����G���a�>0�rm��[�a�a��V'�m=�Ȭ�:� -���o��7T~��� J�����n������N�p��sw1>����/��v����T$��\���f�'�cG���nEc'���U�b���js������D�uw�AVw�W.°���?d���I�+@N��1�/���M���|�鲢J�{-؋ -҉��� ��}����Z���v�8*�9+sb�9�˛#K�E���g8��Nh�����q��W�G�c~�7w�w�TS�>��[�oRZhn�X��,`�,Skt;�V�a�,d�,>��tA?��<�M�sx5�%W���T|!?�ѡ��_���C��n�a/�W�� -�>8������p��qZ���&�w�Ɂ�mxYٯ�xZ���eE����̽�����)-��|mν��xI�d\�9n��Cs�*��Ԩk��`���j��|���ȧ=0�W�����3TZGb~���{: {ۄ�� 4͋�p�V^}9X>�$�-v�-�k�c��3���^��% ��6���br��b��B�%Y�B-<�H't`��ܜ�w�q<��)�>�g����D����o����ڿS��+,�n6�ȏd|^�^,~dn�0������tg�5s -�l'���8B�0<��y��v���.'$ ރ ��e7�� ������=��V����$����.��@���_�֗���ɣ�m��S�l�������>��=8k���u�Q�R��Q�C|(?�ٓnl�A�A�8���q�s��WU��)x�-��'r�S��n��B�7�k8uΨ�v����qR����ß.~�ǧK��Y�L6�By��ܾ����8�?�߱n�0�E(��M��-|�{"��ڏɢ�&Eu,�*�qu�v�D��^�ُ�4��!����fz!���\J튃I<�.vH�Ǟ@��_c6�����W� -3��ï�W���-��@��,���U����(7��.~�q�0��u�9�4`H�;�g���r���G�gm:���:x�����lإj+����������-Z�_l���L���k6��R�el�^%:HVx��<t"��7���*�� -���X��fS��pC�ez�T�n� -�{�R>�.yp4,V��U�p?������Ӳ -�n�֢e͆C�*�܉����Z>}����Լ�4�3���id�"��SS�#l�����9�FEC��Z�р�ب�_Z?��c���ݴ�ē�\xA��� -�,��b���5F��!qyH�IӦ�%bŭi��C��ά�>�@-}�A}U���[��R�~� �:R���1�ְ>]S�}W�Ҿ����B ��w{��w%��,�:�?����j`���{���<��GbYs��:u�\x�����^��ۅ|/o�[<h��.�1v%�4Z���|�A��͑��h���w��cZ(��S&���K��<�DLf�9U.�&3i#WI�GE�R���ʼn�_��i�/-�����D�+��Aw>n@��"��vH����\Y��v6���=h\%��j��+��˴�jx�L�c�A�]�יQ�>#�J��y�tw��E*���H���(Y��5^��#��ڲ -[�u��\��]�?K�����y�r�a��}p�Y�6H��6,{JbCs�2�@��o~c�Y�wD��θ����NZJ?��JN�ue6��r�x�Ң�2�J�ŵb���&k�^�_]���>���, -�9�<��O<L4�� wz���_�.�����QU���:x�[e1�q���mU��`д -�~b�����3�S �NNɝت̡lCZx,U^�wYד%�k#V�n粠�/K�x,�eu���É;� �s�ġ;�co����L��z�#�z`�Щ��S�]k���t4��A�y}w����c1|�y�Z�k�ǚ~���F�Ws�ߤJ�u�ʥ��%�q�(&�"(��bSзEX��&ΛO���K-����9��������\���K��&�|=&�#���{��M?L�U��� ��=�#h���j4~�o�6?�G��_س��7�N�2`X�4bе�(/D2w���$)gE�4� �t��x���ٟ59G��f����7�5�d�#�fL�B����4W���6��L�9tAȥ�~��}Eͮ����o�7W���5�ڻ��oK������:�� ��̷����!=�S��c�<�v����s�ʕY�<l0/����Lp_Lo�`�l+�l���C�0��\��Y�y��J�V���[��mOF�WO���~���ơxω� ǝ_��������Z|}�� �W�)2�����#�x\g��y$ܨ�;�w�rg�0-1���]i�l����i���I�W -�Ao���<��z5��ݽJm>�2�mZ$�Ԓ�� �$s�����#U�a5ej�lh��*E����m���VMkdC0���κأ���{�����y�Ô�+�=MO/�H~ -���Z�W�z����hONu�ڒ>Oi��N5�M��W�E�N���_$O�#F)U#��$;�ٍ�l�9��������1%F%Яo��L�;d��~!ȱ2�t <���nVyn��(lw�zLn�Ni.�l�bY<�����S:1@mgi�ҹ[��ʥ��r���!OסB���!n�ʔpWڞ �w�������%C��}�\�aQ{�0���{�!�m��A}������wO�0؈2��S� y��K���W�sЫK��o�i�@����^�Y2 -�zU�M�k�(A�?=<�A"�D�l�!�@� �?�N�Ec;�D�΄GyԚ�|#D��B��Q�!�rd����%��]~�E{?s5Ǽ�¾j�+�%�6<�/n�>�J��=L�y�[_��Ĩx���5�W<���x�l��C�0&���`(?_�h�,��Lx���/^���W�FС�]��]��! ��@t+��7TǸ�k�^ٙx���%�T{��X^vc~8ru��Jr)KՂ7=�4��t�_�<�m��XᄭQ�f�h%o�:2�;�֦�h���Sn��n�I^h�ʉ��(�i�Z���._lა�0kC����zm�H��!�Գg����Ln�i�*-9���W>��o�N��S�5�O���M��V�7ө�p�!̓���00]�O�ݮ=�k�,�Z��-A�[��Ѩ�|?V�f��f��lsH%����� ���P�/B6�/��}�xn<�0��|qW��vӉPL��Ԙ�t�=f���M�vz-+6�Y'�}��h���4��o��z��fD�&S���i�������$^�k�P���n`�n��{aZE��r��`�q�L�цA�<�%��Ʀ�����\���F�J���䱣=��I�Pf�lJ��l5} -�g�Q�%���*�4O��yP=�7�j����e�.*#ML�����o�c3Jo _+�Y-�4[��,T��y^6��| �B�Z����~4ί�����j��¯4�"���p<3+�?�~�0�H��~��V:�e��o�-�lR ����`S�ʏ�V/rٰ,�N�%-��$[������n�~� -1`Z�-�#� ��@%��0��2�>�3������g���c0�?� l�R��b�%iW��=vdB�Ka�=�]g���켝��|Cx�hZV~�w���*�!�� �`!�����;22]��Z3�ѽ]��@�p�d�+�b��7'��� -��`/C�����`i�rK���B��X�-�3[�?��>�L:[�q�Dm�7����Zd�_$^��>���3������`�#�T��tl`����$��ui�ӡO�;çt�z@���a:�y�z��!�h53̢��h��JF���&�t�o�ޑ���]_�i�6��&/ ��"�#�K�>����d�:�J@K]@ Pn������x�����t9�������`l��Q����leX��lWzd��?����N�=��~�V*ҡ�FC�̈E�C�2�Ej��C��7���O�(�ܒ�ۯ.$��#@��.�Ձ�?���~�|�=�̾�{�8\w\��T��T��Y �e v��E1�R>��H��3������6H��ޔ���o�F���Zw�����!8��v��a9���?}����z��k<� ����S���[kg ��g������#@����c�����qV��o�*������ʧ��?��#�[d`u}D����f_�����k��5���e�\R<�.�c�=�l�3?�tff��),��M�iG���Oe�����ve���Ӯ�)@Z�l�����ۤ|W�CȚ��"�U"�����O��l���a�+2p���U9oo��~��w�?9y�t���� VN���@m�'�B��[*��.�.q��*�ߤL/k_�Y���k4��+�]-�C \h�-�ۭ��x)�/�ʦ?��'���:W��\��פ��xg�MO��[����4��ϛ�yWrZ�mB��zl����X�X�%��v�Yeu��ڊ��Z��^c��s�����!Olj��=��"x�%"[���1�a���e�M�w_#䶬���~��s�f��dqcW�m=�}�5&���gQ��n*��/�6�\R'"YD(�.�^6�b���K�K9�3K8�4�/�����E��3����v���yrs���#~1�����a������Gb�U�y����}J�9�YeO�Zz!/~�y^�Z,~v��]�3�ptg���[rjT.�Z��U��³�JC�ݸLnC�=!nr%� �B��#������|���/����Z��CP�BWZu����O˧vy,�����a9��acV�}xj����~7��>:�˅��'7k�N�Kh��zP����S�ӟG�����W��_��ɣ�MMq���x�Z����I�Q襣���������KѶ���M*�����%/\���p7���%<k�sr�op;!�V5���������7�O&�no¸�T5O�WMP�����hG��hPߜ�.;�{�/�lq ��~���ݽq��&�}����eM�{�C���M����X^��Dr�i�=��Q���t��}����&Fq,ܭ���Z��Tb��*��h�������k�sN�������z���+s�� -w6e�և�A�N�]3n�M���,K; $���w��Ó�����7wim���;��zEѦ��-l^ n���?�cq�9xʽ�p�J#7��V�9{�������b���#��U����6P -T+Z.M���=���ff����0�����ZM'�����9�k�5p�l����Fis>���u�1�:N.�917��pC��;^H�k��tt$��s���ᘈ_�+�|�,Ծ��T�4����6�A7+���6-vPk�Dc�� �IIC}_�C���֚�����"(�.RBU��+�c�� -U�2���{y|��5s���W��+Y&Z�m���߇M�RUeOk�i�u��hR��]����Kro��n -��{��⠖�UCo� �y#��az�=�����~7Z�Ȫ;.�*!�|�_�s��OrT��2�K9oҒ�j3�4�V�����Ǐ�����]�m�\�>S��"T��f���:[��fKm�9v�Ru33��e3���9�C7�E@;w+% �Uիך*�v�i� �z }�3����F9}�O6fR�l��� �D�V�{q�M!/�| - ���0x����V�w/���������K�=BCP��g�� -k�g��wB�By���A�a��Y%��o®3Pzܢ$G�(�uS�/��*]qU�1�tT����Hhl��߹�o��&w\-9�V�����4�;�)t�?���;Җ�i��n\!��)t-�<>oՊ����e�^��=R�d.R^re����Af��4�u�Y�G�ĕ&�Ep/ք�i���aB��[=����6�Sqr�W�y�.po���b����_�m�R�1�w���v0��US���yI��l���rhOKՔ�M�0n��y�]d�W���V��"�Yv;�q�����(ְ�U�Ə�{?s� *����68�N#\glv٫��Y���������ٸ����F��uw�t??�鎞й�����s��#����~��&Z%^kiN���7�p���w�IE�ruZ�!�.-��H\�J$l1q!4�Ô7+��O�V��I��Q��_i�y��-��8C� -C�k�w5�η�E��]�\ԯ�K��-��,��x.?���̎軬Ba��D���14k�����U��zWəgJOY�}J���>��3n8���S���_�ҟ��ëU�i7n�y����-�;��Q������-�f�Ep�s��k��d��R*e9���V� -r'����?��]���ϛ/v2�G\h-Hf8h.��]F����$2�O����?��7<.G��K&̓;�jyN�����s�����^�_:8���Z"=8w�dV(U7m���n����d_�h'�����tvM��Q=d��?���Dc�pkk9q�"4�l�����R5��K�+��[u�ǡ/Ue�[զ?uL3=o֏���_.F������p���Am�r�R�X�jhF�k�����y���r�!�ls)qѳ�fD�Q; "�yb[�;K��r�*�}7�y���Oi2�g[s�ME�=�}Q-x$o� �ޞw��i/�f���֨��{a��9�U� �H�cw�n��=����Dv�`��$=�����!p��c�]1L�7�~c�ͺ�M�#����x�6Cz�p��5��L��quQ���%5z�TM�=�pb�N�Q��4ԟ��'3+��̏Vww���m�0%�yyIv$%�m�B�C��?�q�����_0���?y -:C#�+PK�8�?:QlvR[�,z���:���`�:��q�U�����<{ﵙ�D����:'Eh �ލ2���I�_��-J�U�� ��.h��9Q�Op_^��{��~7X��&7t6i�QN��h�6�:���;U�% �e#��a�y��������u_ú�YºU���y��ei��e�4�A��|Cu��Lli�-�Z�l��~�d�,f\7I���'��a��U��me��P3�AO6�,{¼#�I��Q��R?���@�ȝ��N�C�����u^J��;趕��-�)�?�6Φ�>O3�a~�]gx�X��p�_\c��]�� TY?o�4�Ij�t;$j�� �_���*ܑL�F�H���`c�v���vҶ>�E����(�rZ�\xB�����Ԝ`E�ٻbc�]�l��4 -��S��Z�ԠQ��_��Q0j��[?9��Τ59���Һd�u��O���Ӥ����ۅwm�a-��:<@U�u9Z��_��G�&B]�d4'��ٷ��w����y�� 7�"�8Յ��]��p�&_E�a�^�Au�L�f�V����ǁR�q}^����o:~4:au���r nc�[̄s��Fs/��I�1t.�mx�^�[� -,7f�R�U�^m�5��Z��)��&1�[5w� -����?VP�z�������;C����Nw -q�����J�-ĹB�,]{n1߱�Q7�SG�G�woKY�`���9SzoS�D/L��_qD/�p�5�z3��zMmt�G������(�j9��s��"Ԙb�ו������K��,��_i�1j݁>��-�������]�z}�g��e2@K��F_�hv���b ���}��f�� �mqDb�=�T�GL��l��9*!M��7kj4�T.J._�����6?{�UT����*����<����S43A�]���{�Mh�zx�� -��a���K�k��6 ����h[�m��=;�Q^i�M��)3]��^s }�5� -Z�5���n�9m���&hA�Ѻ_)��m��'�����((���`�)�����͐d?���2�?���|������ 7���~�j���1�u�l�R?�\�x��ח,;�y�,nh���P���j[.�*�`���*�G����DE����f�lj�, ��tr�ЩI3��9��}2��@��@G+Z�K����@�+�3����w��>�lY -�(u80|��,�q���<�B�)�� -AW<��Ҥ�~��;�:��G���Y��%��Ph�(�?1�x�j<��:W�����3�m�+�8��V����o� Z0s;��k��,A�����x�R���������� �'��ZŎWX�/�9��t������L�h��G�U�+ ���`}�� ���- �.�����Є�_�"�k�����b��q�^S�����>�_1��-W>a�Am��(n���S� W)( ������ӯ'kI����z���4��*�� ��%@vvw��U�<�$Хn�+�s�k�w�;y7�n8b�nҰ2���?�~.�o��?������ƗO*���q����Vn�A,^�]��8ݸ@>�:�9����A��9�;z�9kZ8Y��T?m���츫��s�ڪ��1J�Jr�t,��=�����>kk����ʧ�\��!�Յ�3�1ȍo˦�=L�����߹��^��><�F�@m��?�e�����F*�Wͭ��6F~�$~�'�j�_�۱��=o.�v9\RWu����7�\�/��+�0��<�/����G^T�?.�Ŕ��B�:kx��T�Q��J��b���jc���_I��l���-�5s��s�~�ܲ�����{CH���^\�]e����4U��Ԩ��Sh��àV�����(��qeR���~�t��Z�g���\��x������YH��MY�˧�K�8���y��k�s�i��R`�b 0qu�$3�h��Tt�n�)�x��j�s�yp���ᓛc �O_������d�%E����v1$>7P<�N�.,�:����X,4��NvG�61K� -5��+�Ԩ�)��H�U/��V��l4�=�F��� &ĵ3 �������G����O��1_j#�R�Tk|w�yխ�p=�gH�� -E�{y��㑈��6� -�]���cs�v�����<��N�)�<�#x��Ct��M<8nN�s��D�).uOI�3>�}�}���&�c&��K��I���U���U�J2J��u?E�B������<f���͆�{�����{I)_�7��>7���+"��yܹ)���Sh�y�D��mୃ����Ϲ���y����f����&���n"M�� -��Q�ͱ���Q��<�������<jXC��lO��6 ��Y�d�,�Tg���[&��YUffAzO�ȧ��l������~,>���(��h����Ā�j?����c�W��.��Ѯz��Z����� �bqxk��ƛ���a{�Ȅ�ڍk2X��~�&�"�YT��@x 9cME�Q�p�����F�\e�CPe��ɼת�Vw�=�e��E�O��\3�Y!��=孬��jS���I��i2�F��h\n�?�������R�Ƿ~��oTj�lׄ�R��,�lwPU4�PR[6��q���T;՝f�w@=�%XuLXSn�x�u���p�w�d�%��E l���w@zU���A=�5�N�)�\��C�Z�֞X��/��IHM��N�e��`Y{�����_�w�u]oq�v��ع�:ʵE�~��M����V���[�,M�T�X��b�_eXl~8���,��[�!�����*c)�Ѯqm������ձ���[�7���w���ܞ��F���nR���\��z��^��:����в2��u�� y�u��X3��H���K�ʲ�����% �|��1�5�e���z�f_�OKF!��e���E���׆up�Aؓ9 [S��2+�z���E�C���zz�u6�\��õ�S�N�~g9df7�n���[i68$������ -��J��z�Ђ2^�|Z-��~�x�y�9�8hp�س��Y�/2���Fg}�˭,Y��h\Km���W�v��Is�dɝ�t�Fs��iqT�?�8��Od:"R\ �$;Q\'�:T�rS>B�s�|�'u:���}�s{N��7��3�,�{ere��ScH��1����y��/���!�@�)���y0�����K�O��JBH������P�+O�a���R�Dߑ&�ҥ'J�!��y!4r�7��o�JβK�c9�R3@ӝs'$3.�"��u��tN3���4����`��Y�7��(���.ҿ�zl���it\���⎜ -���r�dh��!8#� J�M�鶺R%$IMh�=�7��� c�!��`G����&:0�f���0����_A�HO�^�.֟N\�� �_��iO�T��J?,S�|����ΠΨ�=J2ěU�\"ן��j�ro��?��Fz���5g�J��Y�9�����҅�@�l�e��%8b�`�tqpJ�Ȓ�SȆ�U�S�8�>�l��"[���=��*��d��ӠP���n����Ct���*d�y����u�k~�AM��=u �d0� v������_�t�L�*��`��]��*B�$��0��kq�3x���olb���tԟUVA������Z��{y��+�����W��6|�]]���P#�xT}��ݔ�{P�8k�8�F&q�"�t�Vzߺ����z�0 a�Z��k�e@4�^�hWν�T)��B��+���Y|��L��24]�}����P�{���Q -?�zy<�n��mW�_����ȋ<�e6&q�]b��V.t�z�M%��n�N��N�N,�A��'V�ul��u4�K�%H�h+o^��yR����i��vԸ!�+3�ݺB�����w��x9*��d*�̮~l:]h�NH+'�$�%�Y��x=�ۤ��=Yh�dm�c�(b�g8P~��� P��h�MsZ�G��4ΰ[͐���as�t�ה7]�kN�-Τ(�.���l��4r�~���T"�X7u��;|.�iiI'�8o�6��1~{�"�[�W�dS,���Xo�0�k�Pf��i�v杆����S��;�"��2��+n�,ʳ�e�]��o7{��~�t��HSǐ�KՄ�Mw�d�i��[�-=8O5:� -y,�0qA���"���C{������D���2�e��mgn��!�`�������̻�Ԛ o�n"�(����]�z{�B����e�6�,ta�N�}��v1�\���5%ʙ��`��,�IG��~ZO�bw��r$�r�X����~C�\��CY��w�wH�c�D��E�"5e����~7��[{/p�v{=l�Nʕl }��W>�R�����{�/ �5ڂܜ�C��i���yl��T&�H�]M�!�Zm�d� ��ӾF5�h��<����ʞ��X�Z�B���X_ -��Dl�6���:�J��pO[h_L w� B߿��1_iRmD�5ߠc|���M=� -�ه�y�l4��5hI����0�}�[Z�8�����65 �w�{$���^�����Tƻg+�_����g�����A��a��k���i�o�5>{�jD�7n0�!������y�֖{�P����Ш���j�Wհ'X5]cr���t�s�[K��xw�y� �ıc�����t���AP�+B����G��û匐[3�^ fW+��]�Y/��NmEk���9pc��qܙ�]XW�y$�c5�t�ѽ|9��2��Xi���$���>}�>7E�n��~��/��s����%~`�H�0+G�A[��><����:�ɜ�ܪH�&m$�?͉X��+-�T��{�$q�Jg�|t�m���,�O�{�w�狹�^-�ĊS(��Ӽ���|�~y��S���b![��h>�;�<�[�.����-�i�x�R&_=M�K�#�y����{]����g�:�.�6������ˣj��S� w�%�����ȩ52�z���!�2|�:Rj����]�:����^�өd�@�t�������������j�`�n�G�Bk#>[�LT�I�F�@�J�@+�݆�èQYr�i���C�(=��)���7@k�m3�/@k��@kkU3$-�u��D.���Z��h�k�]�& �.!���.���n�~:�{���f���R���3�ȏ��\X}i���Krg16.��~�"��R����T��(M��=_�S�-�S�=)e�Z�Yӫ��l?u�}�a��R�\���\��O&���[��/���}��9���:��`��3Y����?uꂿ�� S8��WTۍ5<_$E�T�Īh}!ͪ-ȕR?s��2�p����>�8^<8�fM߫E�)�5�i�1���}�a@��@\d - ��g�E���#i���ȣ�e���� -��*d��2���<�((iž;v��oO)�`}%����n� ���B��/+9(*~~���t�D)�ǭ�#�R�:�б:� �,�Y�U�s썲�*�h�>�p+�i��4�� ����u��!���h�*�zv��|�/��[z ����)0��ٰ}\iM[p��L��h��s�~Q��O��G�F!���3��U}��*O��P^0��X$.����xoVp���!0���r�g� �d�D�oe���>�h�~�D��m=�@���{Bwe���B*Oظ�����}s'��L��|�+���:|+��)���>@�� @�/�TF??4�0@^�.�)Э�E��6q�K�e���F���S��s�w8�?ez���ǵ��j�k�ߗ�|M}h�)���<��[������ߐ#�:����j{���C��b$�%�T� ��6)s��XJ81���[��"k�w���W�kE�C܃^�{I��פ�_R�3;�������XF�C�F�}Դn����ü�I������1r?�Pb����Y���²'d�4��%uig�5� y7��@{-�9���B���#�?���+5%���Z@�u{�M��p0��7֖Oq4�G �I�h�ݤ���X�_�E=\�Tz���|�P�Xe?C*]�7��Ze�G,��,�ʬ����Δ��zdA�Ex�;��鸅�r�^�����WJ��i�u6���)��]�jۺ/5���k��+"m���,���H�9�����hq5����س�|�LSa3�B��$:(�Y��Ux��=�/Ł�� ��ڑ~Dc���ө����q,��?��d����Sq{<���p����*9Ѕd����cڗy�i�x����V�Ӕ�Shŵ#�ơ�Y��=��͢鉇p\��b��7)�?>g�e:~>��Y�ǥ�5��j��&�Rq�-6JMY�q2�.��9��~]|�s�u�]�K�nb�.�e�����){�������Mn��y��� � -���>�Β�|X���<���B�[� �UWe�M�V߭o[�(�uT۱/�G�j�>�_]��N�&su�z�L� -��� -�Jd�J������%;n��VD3���i���Y�p�5т�3�1�qo:����'7;7W�8ͭ'��(U7��[���`�w���<<O��Aߐ}�ވ�u�]�������̯.�A��A)�ԍ�@�R6�e��F�t�����.kjy���*��D��*����U�3R�� o�講�2J�A��:V˟϶?�Wi{�ͳ�g�����h�����l�/��o�9��?K�/�-cy�C���)>L�a���ٝ��L��G�6)�j����oW��ks�ou#d���^ ��q6<��"�Z濯"F���8��1�,���fX��Z@PL�gf�B�Nm��r0dQ�5�}�OZЛΧ��EQO&M�Cj&+�Mn�������+S��>D�«3̰K�x?���\���� �et\����>���I}� -����ײB$�3-��<T������}��~���v��[ V�zƸ�����r��B�bPФ%� -�n\Т��C1N�K��݄� �Pb���߀��+���|��B�bp�%/��?l =>����[��Ln -6U#���z�`�Ҷ,�:����P��x&�Z�J����-�i��ߤ�Q���� n��Dm -+9!��y�$L�Y����$ܸ6z���gOE�f�E/�!9��t���A-��u���mk�_�̲U.��������K�nD�/�2�x����o!�4��� ˑq>�c�Rb���`�B ���X>��v�s{�q���aOR̲�k~�\g����X����~�d���;���F@�*��S#�"Y�嗣?�@5��(جpA�h2��v<1oud4s�x�A��:&�Yiʈd.���J��q(��_�S�z�p�s&��Ӣ�c�b��\��q�M�!4��}�OS����u��� -���sEAz�H,`�ް���е���{��|����s�bHFFF��%����u�c!�T�)�Ǯ6�.�ڼF�.e����:??��S�ܒgkU�.V��"�7�S~G��7�r�;�����+%�T���ʵ��7K|��7�;4��xf<�����ٓ��RO}�8�MK:��ڴ�U5�5�m{��{e!XY]暲�r��XYm�����>���ɻ�a�ԗ�$�ʔ�b ��\y��owq����[aOZ�*���U+���m�J�7��\�аbS�l��nR�P�^�D�.�{-��njr�eTo��ʚKJ��� ->l��.`vr��dj�s��!S��Di�(��t<�6d�n�e��ٓZ��O���Y���+9Ϻ���u�6y��[�K�dL��H�Vn��hӴ��4�5�?�z9���o���W�Na��-e�T��Y��\�s�L˂t�������k�� �=���@l�3�twB������\wn�8��_S���?Ϡ�Mb�Es��M�ܽt�sO��yC;��n^1V�Y����T�5c+�Ik�a��ݼӑ��~,��{!��x���[����Sl? N� Kx��M�W����e>����w�Z�پ!(�\wtu�Q��C�f_����r�VZ������E��ڵs�U��wS�/8$�vm6ޢ�b��"s¼�r}v3eڅ=�qƪ72Z�լ��>����z*qT$���jO�öbx(�-�I�ys�>�9�`�!@ܤىRl�Y�J��h5F��[��=-�SΛً�S�= ���/S��7}��Ϛ�v�j��b��ez1R�n��u8�Ŏ�u����@ �: ��w��Қ���Y��r�;q�S%ؤpRY���X���2+1~0+)��y�Y�V�)����N�:lvE���V����Z���K3�:�'�{*<� ٹj�5sR���"3��x�B��R �'o�{*?��J��藹)��9�zns���m,Y��<1����� ���ӦLלL@S�Ɛ:��uXg)��R�O��2|+SeF�;݊�2Qr+큪�K#�u��6V��=��l�Ou�V/�s�v���h/J���U��:Ǐ�w��^Y��uTb� ��^Us�����7\Kztm}��T��QG������@^t�'ۙ�G -S�G��ݑ��qWY�z��!���P�4j/�*�ʆT��N`�H�R�47�?�Z��J�?����͎p���a�K%ϬK]�����3����j�oK��^#/s�C�LmD��ɊP����>��<�b%l����SշX�F#�xzT�qwqp�¸���E��&�v��R��eK7M��OyR�K�)�*y^r��ͬ�Y��ޝ]�Sᝡ� W �w� -�����o�l�C�[���1�&��8�k�ba���e�^���+�i�u�*L�*�dSdc�Khq��cc�EV$y�x rV���b�~��4��g��{��r�K�}d��qK�}I<����� ���&�0��R/��!SD����뇉bU�G�1B�~{P��zY`&�5�"�<s�K[x�S���fM���|��i]lP糺?�g��%H;�0{�4���`��A����R�Ot��ē�W�T�$h��k�4/%,߅�{�X�S̷V �q�r -�;���O��|3ʚ����r+(���G3h]T���A� -` �L�VD�0R�N�^�6j��y���G����h���#Zg�=�O�\}��(~&�w{�g�5��y�&9�4gْ�ٺ� -���Y��L���`}�@������B�):3@s���2�p��.����ҹ�@p��K]���� -�:Vۯif]��?ɜ�f6��������WܼՏh��\����ى$��,{�p_����dj�� �,�BcR�4@rP�z�I�N�i�o�4�G�Y�n�n�R�W)&@�F�cTG����w�V.@��CWT�g5�]q!����L䃲��f)������;毣�:kU�I燀|d')�%�r�s��P$�K�a��V�O��Pv�����㤟w�@���Sk�8�Unn���P&a��Y�M��|��M���TrU���f�p��Q�<���U���������1_o�eJ�Pc{�K��4H��.B�&�4�/�ZR��%)E�t+S��2H�J?<ހ��ӏL��O ]@�@��0��"�b��� ��V��ZUmu�m�ޭ����t��$��^��8Q���P��f�5����G����;o��+�w��+��t�����f�Q�O!�hD�%�I��� -V^�U\�nq�b}�WF��e���*:��g���)��� -�l0���Q�F���w;'*��7@��SZ���G�fq��$����Ȃ�J��^��+8T��-��.�5�6��p'���P��'T�����9���<E��Ht��dťVԬ�[tO��FSR�Z����E�Z�R\?m&�V��*�C�J�����-џ����g�����ĉ���K�����]z���;��`ZK �sO \�3N ��"����@4�,+��b�����lqG-����b!��B��)y���§5��������[�?��j���wl�����砻o��/ء�/�{�ã��%yC@���� -������ț!�b�Z׀�����$Y�����o �?����e�a$���Z�67$\�Xkr�������ϩՠ��ࣴ��g9���&�#Ҥ�P$�;�M [���7�(���&gYw�,�p�¼��V;ӡ�W�'��Eų���ǧ4U�֪U�!�VpŚq�B����&%�l����=��n����ݭ�zy�&'��*����쉷%�枋��A��\����|$X��6�<�{S/[���lw�������ut��O�3�^WT�x�u��2�A]��=T[Z[�����)� �����BڮI�J��Oc����~%թ�άg:M�8����b��e\C��ѡ -#f/�3e�X<���v =����Of�} ?�S��� �.�K���河�}M�Dz�/���)�s�C �Cm���=���B��̢������U�E�?V٘+���1�����'��맞 ��;� -D׆�F���N��E�lΧ~V����.��#��[t��<�Go5'�� -�����et�zߕ�����}��LH�>��-����\o������+ݩ�]Y]x��c/�wV�[��m�Q{[s�m�8y�".����j��b#E�{��e/[V��u�]�r=g5bm��\4��|c _q���ǽO�/zƋ�u�:{���{0�����t��:ؚ��*��(h�܋x��t�9���Uך���4beVmH�|/��"�̦�p� -X�Vf}ZA�)�v -?�%�������8�;8?:8����$��e.�����Z��{�M�6��������4nj�y�9>V�w�4���иu���|ܢ� �� ?��0��P��u �&�ZRY�5�,v�����b�ˣ��4�JM�h��,�+�K����u�&D0���0b'��g���(�;�� ���&75��}��bc�qk��ϛ���֣�:m���4Gf���tT�:�Y=������njv;VW�筊?�ʶ��+�ԓ -�T ����[����-�fM���lYV���������s������ĉ�]�[�۵��l1�N.�+e,e&��u�n��F-i����*���V��U�>_�Jm�����c1? �M��H��oíG��|���k�ɞNN}w< �nI���y6#;Ό�R�� WjӤJ �n�u�(צe�� ���^��&�j�mh���z�z�����V8���&�Z�I� -�J9�nͿ��/�w�r�8��3;��Z~��|r'm��¹�������· ��8��X�[[ry�ȁOX�2g���(�]���l^�M�"�ޟa���L������ڲ��V7R�\�Gs(8���}�Y_��U�r���ȹ�(�T=�uҩ��p��ss<q��la�qoc�n�=[:��x)���h��:���Ю���%F�MՌn����M���M�v*���7��P12-6[G��Y���� -�T�A�~L8���T��5���l"���\u���rP�����]��X�S�cQM�;#(5 }Vb���٦gSx�A�,EL�ڒ>��}=Sz����R�P�JmWM��R<��¬Y�ܬޡ���oX=f��Ixmi�]&��5�+k���3��k�ɬS.�E����]� -V���qZ%�z��:���4��|��w�o�X��D?��୕�WD��EAu����S-܄���{�WbT)~ )���U&աU�M�/�uq�\��$t��qO��o�q�����u���)�:G�Ҙ���d��f^��W2��G4z$�ʁ��!Kzzf�W�I�jj�}���{��z�������6we����e2�w�(S�J�$f�%fF�Rx�+�s ��)?�y�~��a�:�+��:����˛���[nv�FV�VX$7��d�͎W��^%(��G�� 5f��j$i0>�T�>*��r(++,��Cuy+Xc����C�z�"U͉�Q�[�BI�vS�=�����%q#}X�M��^H%��k�1�6��&J�~=�seû�\ץ��f;�F[�-�[�4�,t�G9��M�|V�Q�;K*+�el)Kr �5�����+�O�� ��P]��?<���k������)}w�����Ae�*s<��f��7��J���F4q|�g�������C�g�b�Gk�J������͢���7�����g9�V^|W<��uG���[�]�r��\��Z(țU�ՠ9���=�7�!�+�M��ŏ�(���u�,��3�Y4���ʡ�h�p�4�ʹd��ϴ���x�T6s)�W~chL�Ц;s�zCa��5�$S �.52�؆K�p�J��e�4�~PZk�N�O�ƍ��� p9t4agWh�:��,�)ʔ���*�������������K�ӛ=�ՖD����W��6po��s�6^�K~s6���Z�᭯�VE��T�U�S���b� -.B��}�oP�x�B#�x�\��p\Nm(����B�0Km2~��7^�U�s�������H���SL>�@����F�� -��P����%���f�&#��Bp�]��M��o�� �zv�]��[K�z�>�8�=��J���ϴw.7[|عs����9�1���,+J�f�4I7<jZ�T8���sVH��&�0�%nj�#b�x�4�𧥟�1�8!����K1��4��j��|U���cκCå�Ny�I���\���aA�)Cn2*N��N�2˷~f0�����j�&� B��9A1�G��o�䷄E��r��^��w.�S\è=�� �6�f*}�>|\����L�x�,���ٱ�a�ϙa}�Y|��z�3���K��E!N���S������U\�Z���^�7�Q�uڑ�c�J -z�M�2KȖ��/2�q-j -8`�:f������h�A1-֨w�?Pt�."kבJ>i"���Ʒ#����53gS�����:>���1���b�[�j�3�~�!��Ȍm��v_�P�<e�lfD����!���m-q=W�cc�yŲ���T�⬵����.�0�RB�+��B�R�w������ �^�\���+Qa1�Vs�K<��r��!�×�+4��1�6�Q.��!���&Y�q��t��~+[��;~�>.��S�ӵW:?a��g�m�XhN�hq�] ��yWؗo�B�c��)��|S6Q�:�(X*��J�J�at�9�P��B�Is ��g�˸��� P��q�蘙����ۃ x4|�C�6o5�f|�!�y��#G.v�G{�ُ��9��L�_O\��}t9�څp���8w����}� �e�e��T��<ޙ�,�d��AP� �[���Q�4I�9��#)����&�v� �lv��l�j�Z|�$u��f��\�;yg7I�7>{�Le4XSܲ���~sRD�[�p�ٝ��1��C7@p+ts��o�H�:;�x��U�)�x����87�X��+�M�K��� -t�>�o>Ngy�8�*Ҭ�{����M����k�j�S�G2�a�D5��-����'Z��J�qiA��X���R�]@��y��-���~�À����P�D�Y{��f�i��]�H��1��_� @���O�7�Wߩ�Vz�K��D�h��CT�1}�sˇ'͌�gB�]�[ϗ�c���*�=�0��-@�� �J��b�������{��d�B�Dl��ȅ��w��V)vo@��4 /���6�y��x���LN|��W���(E[2"#]�F��vA����M -�Kq��Ƨ��e6�8��� g����!�Dt��0�r�bt�Tz�t艙b3�u�@Cn �ةh\�8�M�!@�Y.E�Z����{�y�!:{�X����kJx'8a��qz߿�TJ�Oqi�o�f�z�N���*xZ��0��C�4_m��+������c����C�%�8o0�z3�k -�~�@<��SLe��0+b�b�̺D�ة��B�c<= {�tI^E�H $%�sG��<�?��P��-دL�� 1��]�ֹ����՟�����ɞ� -�mM(��i p`���b���|���F�.h���;����6�,�,�"����m�hy�ȗ��\\P�� -b�i��Md�����W#O���LJ�S��a��=����~�~��?��B|,W������[ ��_#����2bj��� -�]\TR�i��?:�^����>�5�`�� ��Iؠ2�}�O�+�?��w�?�������L���Ko�@�# I�HS��e����Q�bud���z�;n�E��ύ��7萞¯<>V�z@^spK���եz7���͟N-�o6�����}����������M�#�M�Yq����9��`����a�[��.�î��Wt6�t.`n�4������_~�Ø��-���5�ߣ������>���O0��t}�uo�h���ա�����|�l!����V���g�wH�S�;ŝ���c�����S/s�'+�&آZ�0b1&���(�3���S���?M0�q�^��ͬ����v���(nV�YX�HZ�KY]H[�L�#jύt�5��wf=^�i���)2:6&+�� ����[���TY<����qȟ�� ���3> -��T~����Ffx�����W�����%{�rHV��֍7�$�q���cfݛ����lj��Z� F��J�����$�s˪��e{��[��՝�a�kg�I���S���L'��l��mF�kn��?F�oK�!i�ө�g��3���N0m��y����d9�շ�������-��Fr��A�Y�;�����!qb-����I.i����~}e�v��6y��V�j�� ��c�i��&h���E�IՏ��)��%{���������<7�-y�������@:��z�ݞ�迺� �f���n���Q'@�y{��v mr�[��[�ɹ7��ʻ��\и�!ܐ^!=C*�2K1Y'#4�t�>-W�uxs\ג���|Q��<_CN5��j&���'���r�:���y�!2�-'FR���ޖ�r�P]-�p��-6���ڷ�KP'z�w6�k�wa%��2 G%8 ���9���y�%s�yi�U]ս|5�GTe�<+�N��C�� "5�'Σy�o�6E]O1��~�n��O;M/�G䅇䵃%�u��n�S�4b�^���~�]>4��B�u�n��n-)/��W��j��Ҩ�U��ne������ -�'�u�-M��B�s���+�p^����{���M��j0요E����&�O�ގ�UdZ��%-�n��<z�z~�>�&�[N�� �Lk��+�0LG$�P��D�DO�mi�-e���{�o�kX��k���� -w��+��Wg:�gۡ);9uM�kQ-k��-HZ��V�XaV����˃?StwZ�m���r��EqZ���A�c�A5��������Ь^��o��{������^<,x��J��I�S��f����j��7��^tN��ϭ�JC�Z�>�K�m�3Ov�5��<0���j\Gbt�kt*������)t����H��_Th��c�xUV�}���;� ���oc�ܵ��7hH/���!\'F�\�@;��&8���~6��q��%U,v�0 -��'fs��V�q���!����G���^W�t^=�x�N�Y��S^��u�3G�7�����4�FFo����˩�$�ڏ��{�#�}Mz�A>���ߵ5ǭbw{��36���z ��A�R������f�Y��[k������=��5�j�O���`sQ�R�y�Ql�V�K7VЎ|����wh��6�}u���MiQ�ՓQ��3�_��2��ĝN�#'�j]YGcۏ6�IΥ(8���dMA#^�q���i��v(�����e�����i���R������'�⯀L%տ�Z�U�� �o�:��SeR���Q_K��J4@ƃk�=z�K�.�Dn�:��8hr[;QL�Wb[��f�ސO�U�[�vV+QD�5���;VYLv�R�]^�PG����/�:R����'��b3�L�����5 ϵ��n���r.ő�I^����!e�v�J췯x틗S�;�(3�UoǣR#d{���:�"G��g�S\�珪3�WeYD�J�Z�� �d���RtF�� Y<�%��]|�z`[B���'H6|oC>���*r��r�bq��/9((BdV�;��ׅЛb]�%*Y_D\�|�]�/i��4&[k_���<��ՇT�������D���V��ݣ�� n���b3�C"/\��}|/#H.��O�������'8F܈@�Գ�����X -,|9(L�q�'�{�x�B0]�I�V��]�j��qK���C��K�.5۷�x���nP�X�3���<m�:�GR8����;�&�'A�7��_�����I;h�p�.���:g��Ҩ1�r3^:�`P!��4z���i��j�B��N�"�_��`���8X�����Tu_��}�ʿ]D�.ցj���c���;:�`�+��nI�`���2�>����?���9�v��]�\*<Y�F�������� &�.�:���Ojggu��YOs�%&˓\v��;&. �$��%�W�;��ԋ���0U9������6*^J-��}u�hv�5TR�H�գ) -J�ο0��9r�rs�ڛΚYH�#���7z�]?t��ԮC~�T�r9��{و���t��f�c��y����{\ӸҴ]�-@���T�<�]/�6'�JX�ܨ�V��=�??�܌;�V7�V�ӡ��P�ֹ�pw� ~��u��GB6�;L&�t%���n��8���\�I6� D\�-Bt�L�X���[���Աw�`Z�`����(N��hQy��;N{��)�-J�p}�ˠ�9LI�Ϗ�?�yژ���ĶZ�n��5 -�٩����#Tc��B�i��W����3���� A�ZZ�7\����`�p�8|�j�F��ug�E��Y!��3E�t�څ-̎3�?���.��%���(n�����{yr�K�g��̥F��oz��J[����N�n��P�{o��};��m4ǠZ]�^�M�� �2/��jh��RU�"�PӷN�����N�,�>���Â��sq�Q��={ ��7ܲ�|��JEk��/�ulU���:�h|��������.}��&f��բ��9$�UP�>��W���:��_1�!�[�Y�v��*����b�������u��}�O�����X�ڐ��,�����ffBfxbg�Wy��)�=���z�7������R{��� ^�b7�J�_��>b�y���j�l�����j��,�~ѽ<���5�=��9š{���gu���Ʒ����kf�ڀLA �<-2�h�uP��z��C��PE/4A��u@Q?nAѠn���rڕ}�I6e`S9�o�e��B�;���nʈc&��}���� -F�⬉אJ���W�mg3���U����C�N��8�{ ��xL����NJ��0��+LТ���� `������]���!E� -��O4K�z� +~Tl\ef��V����Q�}�7���1�S�&R�/�<O��,dw,�����$?E� ���Wq�>O���+���TL����N�QHq}<�؟@��.�q0x�x.�-&��p2&Mu�̧_��Gٶ�1�����_��S�ݧ?�r�s��B���5hZ[� ���;��ɼ���4|��~k/S������l�5�%��A�q�����aS,@ �(|M\�I�[�N����)�P -���*=�q�vb*�ن�2�9f��1R��.|�d[��+T�������� �t3E���l�3���#���o@2� 5Z�Rg�)&@�.��Y�Yj�� ���2�ۇ�'�/���3��)u��RYX���䍮o�i�Y ;��]����PA�4���,�M@:� ����q����{tȇ���J�!@YAP��8�T<���)�'@��@�Q5�*� ăp�!�T��[m�SM�]�ZѸ�����;�����洴Gv��V�$㛋�o�v��_�q[�j$�rǿ�������2t����&}r���L�������å ��%��aA<�8��#*t�a�M}^�������m�Q�g�w��G��3�l�����[�?%������WN����� �,��{�]����w��i��R��-`o2�������}�c�>�p��\>/����VkHW��%�V�-F*�c��ٗ�/?$<��+������{�ޖ���M�5�T8��3�5;��3��s��)�8�@�� P� -J���,8#!��^#v�U��a#R7lKaI���@E#<���o8�?��9��n�9��"&��P�ɽ���$�}�vX ��2�f�1�����d���r���$��p�^��ϐ�cK��U;��0FW��L���8����x�V�����:�썽w�M�M��Y)��z�_>krrͮ�d�_���7!�}]�O����cПـ:����O -�?���߭����{!;]�wt؉��C��vS�P߬��1�krlE+f�7�-��,n�]H;d��ih4�4���q��gS鶚���n��y����x���\��("V��9���W8 �k37�u��sB�����e��uE�N��J_��鵃#��j6�����G2yV��|�����P�i���q�$��x��q�@x��_�ZGF� �mQb<�5�t�yu�~_j�{�����"ރ&c�k�^^ܶ]x�\��G.?��K�z�6?/�z+ڋ-�~r7�ͬ�<����nd��q �<F��[D na H���?[5��"���֘�H�������hi]��qb'~�,�ag�#������J���C*�"�d����i������Y����ן��)����u��u5� 6Λ��p����XH�c��^w��'�Կ/���j'��!N�ֹ�r�N����mpy�k�j��\�1�Ŝ ��h���KZ#V�n��èo���3��ő�Df���g��!���˻�C���&'��K [�a��gU��x:NLR���dt��嵷�[m�[�ъ�j�<7�~����Fښf�X -ѳﯢ��" t�rw�o����Ch~��m�V�%��y��WWa�R���~e�n+5I�*�s�V���%�'��0ޚ�&��[�uV���&ף�g�ٯ�&��X��ѳ��#���3B��̢~}��u���uxI�j���5dK����0���T� s]�q�)8t�G�<g���OG��Φ|�$AY�3���\V���~P���d����Ӵ���_�?��9�&�G݆{P-�&H Yω�r��~�U�*5f�V��d����~��s�]��'B���]�M�W�����;���,'w��>.�+�i��86��9�?�����=1Χ8�Om��V<��¯."oC���t�����ڭ!+iT �jZ#���0���o�q��_S��S�,gV������~��ꎼgɅ�ϯҙV����DF�F.չ�ý�=s�Ҷ_%J��aRi���oR�&�_@?1������D���9��t�Ko��Ӫ�d�ڷ�������ڤ,�`��K����[g�DZwZ�>�� ���vR_m��P�jx���ʥ�7K��훇w-2Y?[¬���!�ۈ)�}M{�s mPZ�5���)��o�W�����R^O���3 -�3.�7rX����_u+��>h��¬�����^d��F������5i -�RS��u -�1n�&��2�e���;��}]�i mHM�Zf�ԉ_���ۍT\Ƿ���r5;�ˋ�IQG����[�CGoU>�|�� ����[B����;�Bǹ7{1�*v*w,���J��li�����6��q��!幬�X(]�J�6�hͰڒ:9����*��JR�� -��Fr�'[[`/�Ƶp��a�ٓ�9�}�7#WA��'��o���|�$eS~v�3�܆��j��"�l���R�t�Z�-��Z��gc�}��Q3փ�:%/�z�J�^�C3��j9ω�����Rgu1Z��ȶ���zLE��N�c^ި ^�ѻ�/�3H/;f�f�L~�H����z��;w�$Z;��K�p��LT���xz���i�C��A��H�K�DI��VA�4##�&�M�-�v~V<�UD��J��{Qh����[�u��'��#��e���ʖ"8bs�r͖�W�I>^��;<��~-����sg����%�j>6�q%��5�ƹ�Ug��+��P��/���D�c�D<�&�}-��ep� b����7�[¸w��qzc��4.���li+��\<�1n!f -�L/T�}<��>�\�>�{��r��ˠ� P9r�kYϾ�N�&�]j�Lѿ��`�GZI\j�c��5��ǖv'�"oXK�l�/�er�w7�{K��<�#�'�;;aG��� as�f\R��³���E�Ac�>�6�Ł�N�yȯ2\ڿ��� ��N�3�l;<���_����]��s�ڕ#�G��ߵYG+m��ZP��\�!�D��p��<�%!�\Vm�ȭ���]���j5O/��[�-㖘�ܫ/��`S�ڢ�H�m�"�IHFpnH���h��!d�<~�R~�o3���/Dz_�VK��u�|������:��om-�Ĕs�g�ݎZx�C�*X�xZ���i�xU�,Ψ�>k�vĤϐ�^e�������%U}o���! ���(q�M9��_1~;K<Mz�\�7X�~cZD�`�^:��'��3g��샦l�#���Ȕ�Z����p[jVs���C ��y�Y��s+Rgds#��v6�T:�^����M�*�=2�1��ނh������S�.7�y�e�46��j�rq�����,�Y >Qߓh~_q�|�~e;4}�љm|�b[��N7SΌ�W�Zkn��>��P=;TV�Җ�<b�S�FQ�f�A�Nc��A���c����6��%�\>f��:6���g�c1�<�%i�Eg�#�:eVD��Fʝs)�Ƭ�~���ʲ�3g���E�@��]��><��uT��<8�\s����-M�����MĎت�u^�e6_,:x ����t���x�#ZX�v+X��4��v=.�J�1��gs��^�H��� ű -�\)�����<}�jp�t�<ls:�eC�>��0'A�E���.��V��R�Y1���ݝn���������] �l�3螵���3�k���l�w5#-䤣���E|�]E0��,T�����q>얗��]�ü?���<���Ŭ:TDh�Ș�VfZ�����=2pТ2���2�$���>�TV5�Ų�(�fӱ��l5{f����6�h�:f�lM�p�䦋�fD�h�{�w��?���㪗�u5�>7n��H�넄>��2�I���7��x�@�HY��U"�[h�^(�NR�n��k�c4w탴�XT�^��\�bk�"x��'a.���A��Õb5�7�Q�<�(�vq -:� -�J>j2Bv��"� [Z&?jY`aN}�Β:@�P�BM1�S<��M���ɒ�Q�m�E����`�3���j������݈��M:���N������/�lt�*��&�m��a���#ߠ�^\%�Y��A����l����wp�}%>ㄞ���,�bзS�+�X��t�疪�ڵ{�AQrN�DPŜss<��Ԝ�������}Vk����a��N��z,f����!��#�9�-�2���6bY/�s_��3����z}��H{3u����60s��U/ޣ���إ�W�b�t�q�+٠o�f������v�8;Y��RRs{����F9@ -P��K�6 �m�i�,�+@�X6@68"�n��&��҉��ob9��Y�I�� �����Zئʸ��� ���7�R��FG^K�0Ix���֜��z5�X9��SVt�àd���Ndo��l��I:���>��yPʻ(��ʩn����P,- -P��KPT�@������O���2���U����"��Õ��נtS#�|����d^'�*��x B�tT��t�u�4��m@Mܤo���*��+:u��D[�� -#@��+@7�#�G��� � ���1���_��ƀމ�XN�����j����!��������)����"��Y�e_[���9��F��4� M�UO�)�Oj���� -�WD �'.�C}�B0Z���q�����_}���⋬X6�v��X.��,����Ҁũ|,Q�D#,I�-�A�b�Q�h?2x�.|���.�$�?F�G��͚��OmIY �����p5q����` ��b�t��W�р��V��օx��~m7�8]n� -�ݵǀ�e�w��9��^x��5S�jۿ3mK��F.��*|��x�ʭ?�%5}08|���'���|��~;�'���tO��M[@h������: -�.�Dz��W^���{�ҥ����$���B �zi.>>_�l�:� ��M����Ϡ}G駶��'�"�I�q�9N����7a�����Y ��}HN�R�\�2��g����� -�3��I�7�FҙQ1�����a�%7Ap�غ��C~���'I��8����{��I��"&�;[���~Xo��<���l��A�/γ���.Pxx7��VD�����%�I`�Bz���0��=��Û{�;�� ���9:���4����0�������wi�I\�j}��� -�u�k~��VM��.;4�\<k��ln�un��rӅy�N�Yk=�1��@��ȿ�����>>��s[���e'j����Isj�tQ3 Eؖ�|~Ygg�DG�C���3����h��P�h��O�5]Bw����ď��㝫~���ڊF!<���Jg6;�ȣ�@y4�]���kد�o��=*��f���hSy{�U��� �����_@<��'dw��N� 9m�� �ō�bI��|v�dLm(E��Uִ����U��F��׃F�i��d����>��uyYk���u�ɨy���n\�q�+i>,��L��>!ǻΘ�G�Q�ݖ�&u� :���6v~[?�ZeǠ;��҉K5�D:��Oܲ�����lkW�譀��(��&�:�fv�jHO�Y�O������ -i��9�Ɖ�4�y�d�����Q�6�7;P���i=��{x]��.<�ZlN��>F�Z;���^6�j�9z��Oܱє�KC�q�zG��u奧�~��j�����[�J Z�U���*�̂E�>���v�I�l"��7�BHbd���4��.<ԟ��c���P�fTq�&�j�)ߩzC���z'���g�_ �O�^Ya�f���4����h\�W�x���:���PY�K�<>_��4�����J��|VjZ�tI�_�u�+ˠ�xy��VLU��~���eU�H�]�W7��gp5+�F�S'CU�e�E.��[Z��+�Y!#._ޅ�[���s���?���>�(�K�^uP�>�Y�c 6E5}�x}��)����EU��U,;�0�3L,��|Ď�_v�:��Z���N��<�2��Tm[�U{�x:=|�Qc��C醔ΝQ�^g�x<�bDŽd�9y*^߁�@0��,yϵ��B�|��0�^�&��� ���]�w�Wd�|��̚�n���<���Є�'����.|v�q\�ޮ�K��V�E�^j*N�x����������z=#3������Z���B�}��P������ぐ_7A6�˸aﺬc�U��aزBs71�3xo6��)c���ۅ\�� s*=�%H��d@;^�y���Yuj���.��C�����xT��Hs��,[r-�P/LK�n�UG�",�8�����!O^gg{����@ٿ��Hś�B� -���7����-9��r��ܔ�F��i�(=�G�ԡ�%�M�~VsT,�g>��I���h�>�{T��X��"Ԫ��%9P����g01 -�b8X���P��]��̣B���|b6��ܔ��&ש9U{_�F��gR��4��֬����AӜ�K?y�\-�����'���Hv���l���g���,�þ;���F��_U��a����w�o��&pJ�V����iW=F���G���7/<V��-���=�2^Us`��D�JK�&k[=}������k(�I��!�)[R� -97��λ��Q�%7��,Y�MjϺ���BN|b���j�XN��m�D��U��+��{^R4f���V�!��]ۡE���8f;^L��H�i��fv���r��n�ɖ67ɞ����ZR��J��[�R,��k훫�������!�~��UI�6 -b_9�E�M�Qq|,IC�ig��4�i�Y���ϵ�m�+�jׅ�)A��Zv4�W-I}�\�v�ƈ{� -�:̪Yͭ{��N�}e7� -����e����h"73�x�"h'u�$>7�S엽��ytHa4��BZ`l�W�<R~|����#�?`v���l9^Ų��K?5���Ru����F��;m����iY�]���_VE�Ꙏ��kC�^FV%/wAaw5�#�7���)�TURu�)>?FO4|g,���R�����.DW�9}><2R1�H����&[v� -�_��L���3\� ��''���1��`J_����?-( Ƈ�J͗��s=��n�mvU�f��yI[�X^�_��[DΆ��(ˣ�,u�KS���_�n�?S�����e�rE�ذ�i�i�-�'��3LuŲ�u�E%Z�)]��M6T�����<�d/[��G[���JgW��ţ��hT{Kgc���euo��lM/�����}� �pt���KR�/�⠫��x���f�([p9B�Jl��,����32\?�O9���z��k���=E)������z��"�S�c@���E@\��'k�% �2$L�نf��(��z�Y��2�7��Z�{��jŵ[T�ǵ-_Y���@S��'�[)'��W����oK -�;<\��'*����nr�P�ZS�m���r">�&F����(�S���g��-���R�;�MI];+�'�}�}�U-��<�fT��� �K��3h��)P+�^�6đ-���&�fU�w�Ǒ��f����LM�%�gP���!��H�C���2m�<�����~7����[��>9]��&J�H��� ��+�;������A��x��ݔ4�E�bG_ZW��R�zK+�@�9̓ŷ��| �6�"[�V,�MK���ϑ�����&�K.�jel�k��������4A��E�R��^ ->:����iI�R��y��]-�.q���@�f?�V/�j��?��='?�9��z��X� �(���hd��Ǔ��e ��F��'1�1�h9[b���CigM�� 0|r�zn�M��Q�yH5��ʷ�z�4ȴ��<}��vi�>ݡ7��PΫ�PJx�)����2�qf/�f'�Me�g)���V�n�/S�Dy���tn�O�5#6�0�d�z�D+�X��YV�e�������n$V,�8r�n�_δ3��~����־ �����Fj�E�Tf��z9�ܒ"��ko� *��Ј�Ʋ���?zb9��"�k��g;�ga5�F֍��TCu��ZY�s�P֞��������F>յ�"1I,�Bvr�8xF��8d�T*ߙ�)X=j�\,��i/�K��s0?�e�V֙X�9�U -!���"��7�`�@�K�b����|8L0<$nx�+�J8z7e����Xy����O=�C�ܞ��3�CbJg�����Z$�� - �E.�Q*���2j����W����t,=�"mDzl\r�������p�R8��������!��ÅF�g_��̿Fxa�-J��}U[&if*��5e�>�1, -� -{�A1�x]��|���.��*�p;s�d������o �@Rb,� P��z��� -$��C ����٭�����}Yp 2���G[JA��'�}�iȠ@~���/���Z�{d:є��=�4�!i@Xx�U�P�9��|@l�: .�> �i��Z}���v+d�%@��> L���"j�Os̵��|m����m��<4��Ʋ�KuQ~�9�����y4�ύ�?���`pq%��*���$@����d� ��竀\ۀ�#@!�&��P( -�R�cy��� -�px(0�_M�I��|�t��#�-��ڂ#�F��lۘM������\��t�V�9Z-�NF��zʥ�r\V�|�*����M@)�������.j� ������s@��=�-�����$4� l@+P=��Ъ�����x�ƌa,�c.���v�9&�,����x������ۨ���?�$�ٛ�����O8���5bR�c@N��������{�AC���0�wL��d,0KƋe��J�fx�_�$���2`Z�i�ۍ��F�W�f���AXb�+��u�a�J�ӟQ���N -Kj�V�C���x��p��_��������.[8�� ���3O�Y���� p� -8�2b���c�+�q���8� -5�!لq\�^Q�W�`�4��#���Z\�L�Im߲~�/�c;�:��xi���M�R߄�:�'�0����LNs�`�[��W1��ܞ@`C�T�z��G@�QH�x8�o�ۅ|����M"�a-�@���?��������0�o��$[8��?���N���^ .lH,UR��_���DG����x-���<�d&��J;��ɻY�fo$��"�-�T�� Il�ߡ�!��8�$Ͽ��7_�k�o��/�}ǧ�lm��E��K��e��*��?��h���X��/�M���G�}�����ۂ�nXuW�Tn���3�Gez����\}�7�:\G�j���骉���UY.���v��*�ܼ�����}M�>:Y�w\�ߴ�q@���(������(O�w��ׇz��k�<d�7��E}��FdM�-DFYGV�]I+��?�zt�����ecͦ٥3�{cw�~���ڪ�rV �Z���R�=�w����[}�(7���e���o�0����a_��EL$)���6YF�z�a��JZ��>�[�G|.�M��m���ć00�Oz�<<�w�P:,����c�y����H���E�7Ҧ��`Nͮ���*l�i�}R������ -8m1g�mF��f��_y�>�O��*����O������5��F�:��J�tcp� -��rR��O_��Xw��u����*{��}��h���/�l�h����*�)�[�hוw���{/4��>A�.��,�S�Yx�[�6���aq��^'y��.Y��Ń�#�.<b�6#����v�\�K��t�͐��s]i5���=��om}X�(椮<��o�����mmd磚��\�S�����N:X�`H���Y���m��kU�@)�e��A6����_�<��&,�;�?4Cں4����w�2>�u*�kc��Δ�͌�ԙ}���`Q��X�+�ʬZ�e�BF�vy^�e�r���QY%a����K2he�{�Uذ���ky�����Kz��%�����,ك��GBx�}53�A�ɨ�d���î�;앵��U|��]��zc%�̜M�e���{^.55])IoS/ކ�U옕���VJ^�iW�ϲ�tG������Ea��|,�E��8^|f�"U�����t������§�A6�?�tU?���ҹKMe�(���m�c�OE|�^���=F w䥓f[�`.����C#L>P���Ɋ��e#�nz�<y����h���6������t�m��g6ݕk6��̔ �K55�� 8Ah9ty��O;z!���l���re�B�MZu��L -�n�t�~�`�}�_�/�<y���]��坲�W��\��͘ͼ� 4e�`�:^�ͩ��n�+��0�`��iH���nIfU�n����}o$O^��� *�)Qzt��s.�l�x�d������y�R)�A���F����?��y�榜��r��v��NG�:\�~���?�OJUL�N�ME�oG]t3������O��}�S�2@�eQW(�%_�d@lu����c�m����OL�w�>g3w��B͓��04Lj�s�S4��N��G��َZ�����t����p=�����g�z -tV�}�������A<7-G�=����m�B����~$�!8�,=�R��T�~9��C����]��N�fJF|��d�U��\���9����~�e��&^fY�XӦ���9��U����ԥ������^v� �lpv��� -�0�\�K���G �������|+ -���R�|Ï/�o�O�Q�X��NN�+��e�]{��� ڳa��=�O��|I�0wF��EL��ٯ�BCj�z[V�i-�K����reW>��\/�C���V�8�k&vO�^x7»`��?�"�/7_�}����O�,�3����3f�A,�^�6��+��{D�U����6��k�j�i���eƨd�&� -�A��Ad���� �P�kC4%� -b��J�Ζ�T]�-J���d���r�rD�rX��b�Ԙd��2���h��Сin���W��.]���q/�� -�rud�a��Nx�%=ů��sPl��5�Ӣ��L������a��Q�I4HQo�$a�\��L>�98�"��9����k�n��`��ݸ�b���DG����ᄀ.�@��6p�{>l���"^�Ƈ�$��'�l�f��s�a��n~���{P3U�Pԧg�R�/OUX�"��O!��v���\6`aT!��o(�Q�H�e�]�M��03�M�aX�W���U���u��%��=yo���~#��P�?��x���m���30��f���5�K��L�Z�k�R{f���-H�j�ϋ�RzZ�>g���R��2��\�BZ��<�>S���,U]�Lp�����D -�� ����w3d�d�F�Z��9�&R\���^��+�s9�n�h�NQbt�#۬G![�����y#(��M��Jc�/mK��U����}�v��DP���%�9.C�v ��N�;��ߴHZi�� I�;�Y�M�1؎t"�>;����}��<�07�[���鈖T �A���@*��vv��Q�tr�I��H��73��T�0 -��F%r���M������)��Ȕ[��o�,�@q˧���uN�������}��T��(��) -�D���CK�0���j�qQ�^�� u�`&p���0��>3R;�.��ͪ��� ��r���s)H_QJ,�/�Ho�� ��u�s�]Պ#�˞Ħ)���"������9�V��ҧ�"M>v҇x??/|��>��c�B�||�&�w���"��)1�;��4e���u�ZZE��\��!��T<s�)K[Щ4��`�^���гX�O��-8*e�n��&���V�qi'h_��%O��¦8�f��Dޠy�x�(�Vf0��eH�c��)�0/�| �����n!��M Owvz��[:Į��&?�v@8Gy�zs��3�|��#�m\j���HDz�Nq�>m_ė�鲭��C��(�J��e -�Y7�c$ʳ�~���4��}��!�%�����}��m�LhP�R����җ -��gJ�E��ĥ��5(����X"��kT�į -� @�B -�$"�2-y67����S�3N辺=O��T���ǐM.t�t�ń�SѴ��HNH��|`1�>�7qy�2^���0K\':��X -j,��7��U�Tmt7�tO~�t��@��Ll�F���cz��,~�ʋ߾ƪ���Xm���k��Dr;���i��Ɣ�{hWe5�5Y��V�%�D�����||ޫ��)��H�U>gO�����\�X�.Dz4����X�Z,����"��9�g`�� ��A�=���D���X,� 0�����:��Ա���V�Y(�E�6e%FxF���8|6�Qh9��� �C�w������1�bG4�W��82��^T�k� �B��5�b�X*�XN7��. -p�$�X.��ǯ�v�[!p[q��}�mi�� �Ko���@���ɭ�V��KZȳ쮪S��;K�kW@��5 7��V,:t -�* �c�����w�w "m��Au\@X�* �z7!���,����+�-'��r���N��Jw@�O¤�02 ���6�q���3����"��9�ЭW9G�W#����ʈ�F�;���[�y��Q����Zq�U@bk�Y�Wi2�9�ֱ�/��Ӏl�l,G�M��2��X!��7 �%��X�z��tg�V�|�:�`�n$�1L�B\rx-X�/O4���$��� � �O���Z_I,��f��'d���Y���Uyw5�g?�ԝz�Lj���f ڀZ���Z�i@ SJ,�PMk�m�`�ԟ�������_��&[���@^E��y�U*�9�js�ٟr~tE���|�j��M(fB}tЫr��� -0���A�v����t,S09��|)w���C�s���&``h���V����Zv��٦^rXË������曧���Dž%:�����v�~��?���'��� `'�p�& -8 ��GV�R ����e�'`{6đ_���v9�����j#�2=I�� �a�ɮ�S�z�b:��/��W�qb7���.��������W���i��O:�r����O���#��r��~��~���Һ(����d�'tN�m�{��KnAΝ���IR�/��?#&�O���Y��I�H<���q�����H�� ;�G��+��m�>_�'H����M�~���7�����O<���������>E�O���{b~0o����d��+�.�.�9�� �m��������Q�Kz��7<F�x��_;� ����t��s� -Փ4��Q4���-$�����:�K���U����������x�G�m:�*���G�)6�Nr�|�\�G!(�s�ԇ���D!t�|:1���Y�������x3��'������ ���ܖMf�D.��Y��P�g�lV��=N�b��2�6�9Wc o{����M�n�����$f_�:2 -�4^A]���]���oð�8٧>ڽ�zT��x��Ƶ���V�~ޙC��h�Pk�)6�m'��"/�=��e�6��s���w��d��~���ki�g�q�;5�T����������hmr��[c��Q��4��y�8�k���2�x�L�c�����%P�Q6 ���CH���/Y�J;Q(����s�ާ]��FJ�ݝ�~gaF������57K��-f���*�u���kC���zG?u��"a?W�j�IO�Y���N�t� -o[�`X���T=�x�����t���O+��aXҊ�P�����Y�^;䧵+�X��L3�O֍s��[ru���h�a_��g��~���i!�U�9 -��,��Q�/��Bw��N�����C��*����m�՛��t�k����m^wY�プ��ĉ��!�������߫�|Tx�T�ŕ -<��%H�6�����]�r�?L?j�l?���?R~����R��i�M�M�~��u?������0����ßWGX�G�g�U�dO'U���[��p�*<�[�uiW/�]��?3?������c���o��꾋��;��������8d�Ϣ#����o\h��=�� -��x���`���������OWڒMrU+t�},Ǵp"�?��?�� -�kV}�r/&��x3E�S���ժ��ٸ� -��k�����Ma�S���O���D=�����R6m�:"bj���A���\Y6ϓV�l����m� -s�����j<������a�g�0�S�0��^��/�yo?�V�Ƶn���_|�+�i���B�b�Z��d��yl��5+�&V�?�yt]�M�3�L�븩kN�Ч�\K�aP -���yF����Mۼ��tM]t�#���I�>]LŁc)� ӏ��n����3a��Ւ*]jBJy��rv ;�j2k���h6M��ݦ�>ͩȨb<��a���ٮ��� u(�M�ix^h���]���6tT�'vW�}5�P�ó��Bʲ��-G�|M��T��]:�h6�{Q:�v��}�b��~�'#>H��g����n�:��aa;c�=�?��-�,�С�j��i�y�W͙�b�����˪B�g��n��d�>���k&�j+�W�Yj���xۍ1�SEd�y��G��oD-��� �y@�XVY~ԧ�F'��_p�=�>g�l�2c�*3\�bF�ޗ���ouѨ�T��@_m� -�r� 7���,���r�����l��\I��xۼCQeRm�q#�0 -k~��y�����Sc8g�̱K����v4e�@���s���[�B����j����,��o�5üܧ�1�ᬞ1Z��<�2�f��}����Ȳ�)�Ɩ#H:"D��)vFgFTː(��"=&'�7���:>���^�k)��9+���gf��:��&���o�9%��� -4ye>z��K�^v��l���m+�+�=ﰂZ*�)��2�|�T�R��ї��kg�>�_Į�>�W3��\����BH��S��fõ̡�Zc��O�%4��l9��ЫɈ��� uڋWJ�ji���r���l���}�݄^S���x��K(��E�i��⬸�gIB�2����aދW�Z���-")u=%���K�7!��O|>�ܸ9?9t(��i|�-��8SQ�>,�*]��mꜺ�()�C�}RY6D_<��Ƨ��`phy2�i�X���e�.���x�E��U�GP����v(~9���LK��BE%7G^>�8RR^mX�L�?ՇOnA�snG�4X��dt�#x�����#FI�%;0"�J��D�����>j���I�V��bӨr�c�F�)�D�JOG�c��P��ޓx��w�SN�\%?mw��AD1�?$K�TOQXz�K�4J�F�'& γJ_���i� �QD���������z%��C���?��[�����E�f��W(�K>l��h1D6er�P����k,S/�r�r(y�V��z��Y�z��[4�)5����|B�줊�����%+����7/X�<�,�Xf>�x�Pq�y��_;^'<�L?1���0d�E�"�Q(~��H��M�]�ڥj�������?Og;�#�Ш� 6�6�Vj��5S��{�J�w*�R���9k���ŵ�;���i"%�RYY^�9�_�8Y;1슼S���Lv +"�������C��%��)�o�@�pu��3��I_s)�݊9��<H{�����}�2 u&���U���WS������s�2">O�^~W݄��-j�-2�u%��4�����sE�0�Y�C�$t'�Q�;�&re!�f��]I� RA�-̧�(���+���/��2ө�#�������I�W�Y�`{�pY��y�8��X"��R�.���-��Q<�2���i�@yRT��[�{V�8�G�ΖG�}BK�����]q��G������͜S��c�]jD�`��o�f���G0�Ix� -oRq��r5�c�Wc��� (`����W���=��L:��RX��=��C}�k��oJ���;��rŨ�3��I8��'a���� �A���mZy�7)˚��µ�Xw��K�e��Ki�� -��k���ɣ�H��H���2e�4��2=��gG�T���<���gjl!�A���ˉCy �.�f�mX"�~���0H�P}8�Pr�|8T�QZ�����s�ǦۋK����9p�(=*0@3+ -�pJ�E3�ʌ��eӍX� @���Z���ſ�=� -��D�˞N�18�q.��I*�u�{ޓ�&ݺ̭�o�e�I7�7���<������F�iu[�Rc/�(���Μ�Z���jC��<�d3W�X��Ʋ��+���X����K,�L)0-�9�?��{��$��]=}+;Bz:z�V��7����������K9�ڥDQ�)B K�2n_�|��gZg��������v^��X�y�x�u��r �x��c��v:�v�7��l �(x, `�j `;*����Y?~{ϭ�Lu�U+�yU�7O�ZH��VU���H�t�&��X�Pa�㗩��B��LT�L(&0�E�7��o�pV��p�3�]D���"�����i,����O,[�/����������J|L��o���4ݽV; -oW�9���?�|��,��k�8��r�`������wU����0_, |V�(0}���u@ -� ��*����!HHb�;�L]@<� �׀<���̵�ݳЕ�\�&��$8c�a��Y��샌�i�{ �v��>�%���S�OY �N��9>�H�����Z�j�\@y@>�o�Ķ �@��3>���~�R%�Ux%��r���l��(�D��X.=E�gn�kը�c�p�����c�$`~�"Q� �R���k��á���-�x��~��4�k�=@��k���-�c0��L-@_�:���%�w��g4���?̶6�w�������1��P$�I9��՞����3h?C��������z�����n�����oBnpo�����p���+vO�;��ۋw���N�4K�����F����*c4� -�܋���ÿ���{mǿ�����|�o�Z_��F��m������SH�1��>�?�����F�x����f��)v5��ϭ��t5�7���$��4�7N���H8����_}���Y���EA�*W��D q� H�-~�\р���Cl f@lW��W�55�r�-��m���8)��П��'���'�$���������e�I�C{�|?�O�Cҟ-���-�(�yY���]f%�\|�-�#��>'������y��}[P@���w�R��=ϵ�x�dZ���g����Qٖ��ʆK�չ� ��Ү�x��B?vs��f��љz@Lֶ� E06��w��s��R|<:r��?{�o�j�o��7��$I��3�����v���h��O�]���f��?�ȼ��Q�矐@g�����!1�5�9���cf��0�Ṣ������@��N_��Hw�4j���l��xzk��9�&�Zk:��6��}����d��ƛ9������'�|�eo�)֏��Qwdn�P��bؤ��A����}��>���3_�� �G�Iy�p�����L{핱6�)S���~��͐�*�ss`7��%�wtyXW>�Kط�Bh��z��8 j>�U�l�t K���%=��fW�[����K�̇+v�zM��?�/��£S��Ͷ�6��4Z̮�j�l��,�+�`�Ҵ�Q*��`��g�8L�Q��S� -�#"�hX��ky���Ny��ˁ2�R�r�:�D�h����8��� �Q�gH���� -�.Y!����jH�z��ܫxҏ+�k��n��-s�����o,bY���V�M�BF�N9�?j]�~���ҹ/_J2(��{I{����ޭ����~v9���H�������A�(�j��aP�ڥn�pK��[�6��+��p��,Ux��&���"K�.Ɩ�7�oCX*vLR��S���c{��Q�\h�6�_�:��v<��;X�ʯ;���5j�'?8���&���d��f���1��/�e�%����(Ɨ�����;^�v֞� �g� j�� -S�*��n�YTǨ�q3��N{6�KG��u.�hHފ�B� �e���)��%w[-�\�o��N��<Sة�@,��>���4���fv�z}5:Y�+_�ڰ�ۭ��U;f��u��}Q�ػ67�%���>��B#�}��@�b6m�n�����k���5p���Xà��>����um��L5'k\5����"�XNk ~k���'-)1A�Iu_��/>e�*eE]�����u�6sfӜLZ�r�鹚�8φ�\��F��� �����/u���4Gd�|5��x)e�o&]�}�,ew���F�et.G��K�Vݬ�^#��<�� �O��I��um�Dž����-�\'_�υ�1<�ꣲ��N����Nk�����7���K_��@�e���c;��YzM�hF�e���y�$�#�m��N�ϋ*�i �s{+�[JF�E;A��M#�щ�8�� �M�L -�?��G�����p�]�$�b��G�UV�W���5&�zb�MM9��4Y�'�t^�������ms -D��6�g���7�?�➷��=S(7�$�1.���>���gv1��X���LF��J��]���]P(1�j����e��ݢeY����֏���_��[zK2B�m�AĎ?"��O���?��̏k�����M;O�s4��.�R���ʄYO'�\�/�/�B+�N�R��fijAE�ŋ�"�M��E�َ�gdK��a<[��쯐BݲO��?\��Ļvۈj����`-����9�/�%v�<t�~�i{����a�=������=5���+8���r�9�HI$�@����g<��c?��d!fw�z�E��H��YLO�D=���~jNmI��O��=w!���N�7��F�R☸�U�CϞc��"�?:������Tj���qez��-��;���5�x5�ɴ���$�l�{JY���p̴b?��#��'ީ��OZ�fd�]��l2o�Ur�~v�r��HO����ќ��8�oQ<^�0��X�g0�2���3O�ur2�3�f'�~���V/�Q��_��~�}G����+;Ȱzf��i���tp<���ZH-J�����k��q�s>Y.�b ��Dm�M�ڴB4_F?o�%��m -��!�sOt��a��$=�O�����8v�o�9|ӥ:죴�jݓf�~�'s���/�w�W���Y�j��0�R�,�r9YHn�#��k�R�|+��S�����o3��D�:�L_1I�Ҙ���uбEOQ���ȜlH�ճ�mS� �Of�C��A���=F�=�D.����s�U�ߵ�h����+$�y�[��xi'CҒ�2+%5�y��F�ģV`�w�N���û�&b�i�@�lT�d��(i2�S\d�ε�f���%tPwt왲��h?c�!���U[��W����%�g�!��G��Y�6�}�VU�^�do�B#тs������陉Qfrۀ���D�U��v�N�=dĆ��N�]�Ž�dr�5�o3����pa���I��t�1:�l���2� {��2 � Ra����Vj���_^�,��ۮG�r�6�1[��S9[tu�ȷ�i(;��i�z;�|er%���$��Z$ڭ��f7#�b��� .��7�D��Y\�=��5քrN��T���J -ıb ij�(�K�A�.\@�Q}��6W/� Nj:�'gp��-���7�:�o�e�Jg�̕��$�L��#el'Y���ijd) P[K��%��м0hR�Q���}�<צb��s��+�W�{�@� �s>z;v��g�A\�' .�� ~}����[��WY��eF�8��@|��q1m>v�n�@{���[g`��Y����F���$����3��,�hߦ��Y�6D6�>Opy�bLF�A7�� ~w�(�� �� ��IBD��<@J�@��@: �n^@ v� �u�H �q��M�Nd�Q��{*v*F�T)K�L�6xFem(e]��d�l�F4>T}��ɨ������e;MŘ�}H���f�6�M%h�Y���hO�(H���J���q��gY��4�2�r�| ^w -�+&�DT�+���<;���I�A3�X���OKZ��#�=A� ���,�SY�_"\���ػ�f@�iR��Ag���2@�K����e.�@���j�M�X�� >��(3D�"U�n��N��� ��@��l��ȸ_*�r�����Ī���GJ�[�U��P\�\�ް�F �."�gG�+�(����>��_<��?.0:@�� `��`�M`S?�Y����3ؼz@��(���+����&��������땅̷� W� b�tf�o�>�/�߉V������i^6��,��X�p:vŀ��"�4־���%�ғ�������O��7;9����Ǡ�Vu�O�`^���<�)��w^Y��)�����^��6�g����jb]Ύg�Т���"�Y�Hd�ǥ�@��>Ʀf����'n�8�,i�xc�|���N�����O�j��ȷX�d;Z\N,+|���a -@������2�(*w@$�@���ը���U��E2O���C/'x� -c���=ٺ��b����&Fh|~絀?B�� � ��OՐY}Nq �Ĺ�����ġV�y�����G$7�<H䯳�;�F�6(�#�yT@&��h� -��a�_}���>{xo@� -��O�0B���������o}[��l}��.��@�Z�-���A��4H�p���]�)^��>b���K�<tE���y��� N3�z��d3� =Pxb͒�J�@b����ۿz�C/m�a~� �Rá��I�-@jܡ@ʆ���ѣ���� �6J �S��f�S��+HUO(H�: �ʁu�Y^��X�d����^�����\|��?���I���)�_^t�����7�Y?��f���&�܇�������.+�Y4E����ӏ�Qّ�fk�8v��ύ!�_ d���éOBy�������v9��VSx�"�+�7r�V7>�E���&���S��=�I�� ��&G�3���\d�N۹Ռ��0*��R�/y���+����f���p��b2���7�Y�ҽQ���M+e��#�jp�x�(P��tP�)1P��yP�oCPP�(����7�d�[��Ԋ��Pu���.R�#�v�9C������ݑ�8z{�Wr�G9 ,�C�ꍊ7D2�k;���}�����L-ʲ��Mp_(���/'+a���ɧ�2�ȇ���`�ө;�抶Y�բW�Y�ֻ��{_�g}u������W�1o��:3�I�T�>�<����y�[n��'vE�gƞ{�n�z$Ue�'��˧6|zD.٭�K�ѫ��ȧ�:yWҴy+23��>xm��B���BU%�����>$*���x�8�}�I��9� 슰Qƞ�S�*���J��JJ��>*��H�c2G����8��I}JC��ķ�6���M�uWTc# 52!����"�(d�)�'�Թy<9��"0�$�e�F5���稺K���.�8� O9"���q�z�E붟��}�OT�6��v������⛢�g֕W���Z�����K�_��I-z)h�@���L�諩~�ϣ���uh�"�͏Wr��Z?j�v_��a���3� j���i��$���L{���������uŗ���I߮�oi�|��iن%f����5옭̥�{����&�Ll�o��iڕj�U-��}ic�Ke���<�p�V�A�F��oՒ�����VT��,�oz�,�1�������1r�C��\��s\��f�ԫ:#�X#��N�N�71w�h������p�Mn��ʰ̸�$���R$�.��~/_q��=��<�=i�.F/����/o���&���ik���k��� -�8ɹPpԇ�m��V��K��j,�y��s5>n���-��;l�m�����v��K�� �`�B������>�v����6�˻��,��<�ٶ�����c�`Ŗ�K����j����&?ʳF7m��@�¢Ϗ��~����8����0#�=�R��IA�uɻz��{��I?�wۤ�p;����-���[%$U� -�6��qҠ�ޱ�6�J�]y�z� �������%�A�>��z�:��V,E�d���μ� -����V����W��������ܻ�imە}��O�C��4�X�o<.]�� �j�����vf��!���� -<\��v�:��*��+��}WIB��)�B +��pR֥����f9i. �m�C��|Z��f=�V�S������/�ᣈZ/Ϫ�,��2���~�X�ԓL�|����P�x~+��Գh_,������R���k�X8,_v���2�jA]���3���2h�e��$�U� �ى���_�6�{uXLy�z}ȧ��2Q!��L�Z�<o^*�LL��V�K��G���-�G�q�\-�)TӉ�ZB�a��|#?�roG�r�J�A�i1�F(�l��ye{�}#��d���<��K�D�����v�|���M��*P����v@�<�%|+��)&Q�&�P>e��FV*�� _ɵ�z3��~�W���`R��[]&C@g5������D:��j)]��i������v"5�C��쎬ۑ���# ����v�T+zq�W��a<��t�S�<�{[)ǮKj�]�Yl�,�_=���1� -d$jg� �@ ӓ��I'�N9�Kh+5VGIS�����$�iR'���,�)�T8u�[;%\/y�IEq��h\�Ҳ��c��:ܔ�rf�܊%����AαO��U� -:cz�H��)3�w���i��)�I�6R�p�R�>wO��O���̓7���x�,e��{k��4��qH<\cMP���h�(gY�������;�)�}��]�"i��];Dn��J�Ox�]g`�[��uQ��2�8��I�G}�k�T��l��u�'�=E�Ñۙ(&\�|IV�����6��|�:W` ��>.%n�mLH4���hT�:JГ��jd��gHr�d�ч6��o6۬�9�*/�{�e���Q�[9Ε�,��K���Ȧ��)��_w�n4^��Xw��f�u�*�"���fp.N��*Vd^L���X��:iuD���H*���[; �T��9��wM2旭f,H�u�(���]�A̶�F���z;NJ�Q��Ѓ�I�im��S���,x:��=��Ds�^�����N��c�����GeF�����DQ��1w�/n�|��8\��,�?� T�-DZ��v��'�`��e���k�h�\�#G��$[��D�:Ⴏ��3[�3��V�$�d��8'�W�"^��?S��v�����v����<�|d����9�ot��oFC.(ڟ�+T�C^�Qm����J��ڮ��K����R�� ����Z�Lк������*c�lm�vd馟��;�=�L���ӒvΒ�ʱ����D@��h��%{����I�3x#9��{���أ��^OU7K<xSs � ��j��t���I%���A�����K����c�Hi���K�y��j�^m�6�\uܙ�E7������g��#�lRf����T��-�rUk��� -� -��V��S�7&1�#��{2�(��� �p~���8�n��� � T `6�th6~��+;�<��� ���3�{��1�\��"ۅ�L쐅Ǩ�e���b%m)��U�(�cq�{>aҰ�G��A�`���9�:�\��(՚���8�����J���K�`r�e�J=�q��e�81�@<�,�xL��]J�������z�$�園J��Z���LA�%K���7�y�N�0�ڣI����6��e��MxSЉ�E�Z�x:�����۳��=��y�� �,K/� .������.��Jk �!��5��X�R&U%�o�J�x"�I(r���EV�����٩H���G��p��Y<�pI�,c���NG b��}i�ڜH̖�B�����x��\RK�@��.@:�e��9�� U@j$;�/���K6X̮W�5I�Y9�����'w��-��అ�d��Jd�E�u��Ei{e&k���E4-� -��� ����229�0�@n{;���7�y�S��oU��A�������&�!�Z�W�cp�Ү� ���q?�5�DF��Ժ�x�'�n ���L>��p�S�u������&;���#@d=���Zk��]�� -��~�th�D��?�c�n�'q�R�zK?0��Uf�1p�{��ٳR�X�Z���gs���C/O�!�'���N4˨��7�0���3%�������8���e��� ��"�E��t�u�����<���0�Dz�2vW{�s����<� -0��E�^��O��:��M�Z�Z�Ҫ�B,��d1�$�?>$0�u�Mi�����`F��訙],h�)�>�;�[�V��*�E[�B��h�B�' ��Ѫ�?��?$/��{0��J��A5'�b��I2?���<�\l���m��C �]>q����C��;�R}�`Ҧ)�H&ˑr;�� �~/A�3`@��i Q�=A"}!A��@Cw�x) �y�ϹV��j�/�t.�`z�0E=���! ��/A��EbtH�Q��~�8��= �������M��E��*�Dn'@>�R���$�.�rK#�x�S��l@�q���!�,�jT�%*����\<*ȂX��ź;�S�"b,�m�����Q3�e�������EfM���ww)�0A���Aj�$A��k�:�ވ �� i]� yY�r��jd�ʹ��r�P�N���@�$ -A$��ԗ�n���I���n �_�Ha����7�����ܐA�<@F� �C���G ���@�5���D:��T@zטGo��Ꝑ�S�f#= �}��H%��i��u�JLD�s�R�"<��FE|#�7RXåKFZ��|�V�Tpa���䤼rC�i��|d5��zk�������i�Ji_�i0�|��\T�#�Gľ��.r�FL�h�Q-��o��������P)���6rО�P"�#(�^P<��Xϥ@�}n��ZB�(����8���\#z��Q�#"��vq�hE�������2K?��SwQ�"�͔��TR� ���P%�ӳ�.� �4�@�6߃�;������?Р&��wY�൳;!��t����í�h�^8�%�tMm��f�d��U:�M�7o�Ep@%�U*�����g��L�w����m ����|�"N<�f!\J��ḧ��)�&�섬R�bvN��f��֓�tۢ;��q��>�`t1Ġ�Ue&L���j{s�܁*��KXt3����S%��Ȫ�o��9�k��LM��n�e��$�Rx:�e�5�:�>����Y������'}߈��\�̷b�� -�߈��\�'�~%p����I�+���ޤ����D��m6�ƪڻ��1>@R��@ǧ^����X/ߎ<��K�<=�Ys���6�fV)g�y� �6ss�tG�>YP�a5��rV�kz6qm�� -j�[\���_`�������Ͼ�@SCRw[�������>�Q |�o��;��Ғ���͍���;����on˃éB���C�r����]&ͨآFz}��s"����;wB*�Z���b�ƬJ5K�Sq�L���2̡YY��ֶ��n���sa�3�Q�b���1Y�� � R�=��ٻ��`�����+�%����ڴ�Z����yCA��� S��{�F��b�����Yo=Uؚ ��������w���v�a�k�o�v������{�7p�K�"z���|3���{�U��+ -��-������}�� -�:��b��HI�$ӻd��N�X��*o����F�5l]�lk��f�o����p��u:|��:�%�c��h�lC��O�����,�HJ�qu�0�6P��ȃ������d�d���>�9�OE��M���iNZ�L.2f��o�0�����j3���a-�����[�� M�ct?�g�4_Z�+Gآ���3�qu*.*v���,;������B�����-�lɦ���dsÅij��Q�m� -����+s���An.�[{�v���A��ᨽm�>*��.���0U�0�����բg=N�������߈��dx�#��;�A3ߊ�ґʒ���U@*W�yG�n���r��^�fF2h -xz��<4�p}��c�aT�yT!w(q�a/y�-��%��yYn���{�__~��w��G3���3h早�h�~��w�S����?������M����$�l�����rH�f�p�>�?���-��g�����;���0�l�G<�f�p����ސ������G�O�0�l�G<�f�p��b -�PH�f�p���������@�+�p��g{��S��:��/����s��<���zJ�Ѭ[w>��t�I��PmH!U��'~� yIg���/�ҦW+�2�|���� �grf:RZ��A4k���ӓkM���]�0O�O/e�(�.�4\���lb;\^2R��͔��aM���7B�Ӳ���t̜f�W�~4�" -��v�5u�ciGC闧��SI�$�̓,��cБ�*�P�|G$��f�w���~��[�j�|�ztR赦.&�R��^;�ܼ���<.:��K�!y���$�X�D7�C` �#�Bs�__��gY��$�Ѵ2ٻ�83^��؈-��&͛:��eB��KV�*�zI������%V(�%�w�7�ұ�f����`+��g�S�I������+�x�xv��s^��7��pW6֓l���xb�T}��N=�%f�s�y=�B��;w�'��=�ꎯ>���N�hzb܈]�#=�m������ѽ]2mۑ�ٹ#� "\�(V�-ԝэo�Y�ܽ�m.8��o�����N�û�����hۗ���5b���Kl�� 쬎�W�b4Ҧ�2xK*��XMW�AL �O�v7�!t2X���}������$�t���I^k�~ (���=��-���K���|�/�KNv��օFc���r�r�eonX<]Xe>S}T�~�f�c�ۜ��f_��h���z� Z-B�\f�&Fg�bHuE��,!T-�p9�={���p����Ή�y��� ��j������:���S4��OՋ^I� �;�\�uF^OL^���Yh�4����ɘF�*4��d�B1�&Bӫ��P�����PR���s�T��� =�j��-/�d/�=4�oy(|�+�kZ�g։���aڎ�B�3���������S�+{8y����!h�w0^��Dnj8I�Tr^��r�H{�?��mZ[r��<c�C}F[42�v)v5��.OU{�>>'���=N�ÅFڠitԱN<�����RBœ�Mi�LJ�)��i#fq����=�v;ex��ᩖ�����4:6���[$g߂����;@�p)]2G^����+�Yy��3.�lq�31Y�۠�G�����6��R�c�d�� h�G��^���OB����o�o����/������'p��|N����T�g5������}W�����NA |7�erG��Vӎ�4j;���4�.�n��gV��$:K"�iQ���pQy��[��aL{J���;!]���b�cU%�W�Y�n7Pl�m����O�������ą���W�l���;������w���Q���m�W�~�(�h��| -�xH������9�����0�l�G<�����S)�! ��?����h�H�G<���C! ����������)�g{�G��z��/��+�՛�}�����m�ԓ��� ]j��P7�X�ai�Ҽk0Ky��\=��q�h�&e}�~�z�����"ٴ��E�U��Φ��*�������Y�Z)�t���L�)� �W��L_X����4�ȵv�� \2����@Ќ��(��u�F@UT?Q���Eb�w���b<ȗ��Q��c���u��CsO��YVd�F�Nʙ����S�A����'�<��g=��<р��dBI��T�����h',�]�����|jH���FQ.SG!y�^肔w��P�\��PO������ }��٬��wm���GTM[���{Z�.�:[}=��Rz�* -�$�g�e8}���|$� -�S����=��*�p�qG�|V���m#]; ����T��ՠ�Woװ��+���4��EOz�%]�r��ĻK�A;s���B�[~"��\�5{r��y�t@���z��7��w��HA��m/.��"��}Lt�*��ʷ���9>�?�k�_~���h/�T�����-S�p�SH��c�q��ͽC�i���?�n�b�[y��g��\��� f?ܠ��~xud.;R��h~�J۵��� ���I�tF���,�iB-��P%{�x�����!��l���������bԍ���7|�:{7C���hJ��M�?��L/�����(v�_���j�w��-<(���r�<��؍�'�[c���ǏC������˜��0m7nFa)�1��u�����<C_�B���j��l�҇0)<L���iY;�˘�I��7չ��Iku���ˬ�3�<�!�PL�-|)��ns�[y�7�WY��P3�C����&Vg�ʳjG>�b\�$F�X�0C�Ob�w�B�|W����o� -n�Q�a6v3�����e�(���� pK [��J -��0�o�gO@c��y?� 7�*�ct+�2�~��)�Lm��������BD��'C�~˜[���γֺ�5L�Z����5U�ڌ�&��$'��Ȝ{>FqÛxd'���̭���O�cԌȞN~`~ܻz��y�n��i�t�F�~0wTO���S^�r�6�Т�jS0�����6qa�lR������5�XSNGZQ%�>y���Te���P���G*9�m��#!���O�kL��M�K��v/4g��ܟf\��"$c�)m�Є.�wF���*���t�M�綽<ҥ��W h��{���u|X����5���c�&���;+�j���Ԟ��H�f¤�v�¤�C������r>:����HO��H��Bf�{��b��Y�3#v��4�XNɶI�"e� ��M�gQ:�����1��G�e�C�T�s:��Ω6�t�4[���|/T'��N��s��J��{��5�M^�k"�<^V�XV|���_�3��Q����*�鑷��;�Mc�F��p�d��b�+��O���h�����nt��A3ѧ6���&��E%-ɌT�jI��^�ĸ�2���S��ls���ڹ�nr���nI�6[=6�~�ڬ*Sh�3;��^u����a��¤��/��y�\%���uhF�������Yf#�a���{�\rm�K�������}ˬ|�ǯj���|:eIO mٌ������jO{��Q�; h�;�zfF��Q�� �y]O.��2.[?��[�I��Z�Nake���\o���9���Y<x� m��lT|I���Oo��'W{��[풰����q�*����~��w�gҿJ�����T��g���#�᪈?������0�l��ϣ��'T����ޏx�k<�l��b -�PH�������:��}9 S�+�q��LZ�~��5���E�p[-|��A���T��4�L?��u�9XY1q2;{u���Zz�E��d���U�:IW���,�� (�Iꭞ_#�r����qe��"�?�||��2�+u�'Ŵ������U3����8YY�̘��=|m�u�2��=��Q��N�lCu�������K�� t\���KN�7iy��?���[>��!�p��P���A\�|@�P�UgGY��3�8&\��&�%K���qu�*��7ֱ��Q\B����[�-�8�����}�y����!�$[��V��2 ��r���,�ҵB����:�h�4�,��C�ë��.��EqS��*g}��L��wկ �����_!��BJ���R.�w���̫�iwv�Y<��ke���Q�^nzLkݵ|�"���XR@�QvG�s邊����jE����8�n���|P-�<q�[�g��^�w�U��cg� �3�'^��H5����{��Z/ҀTLy=��h���� T��'y��Y:������+�����g�W:���,�@�TK7��C'�P ��;�x�5[g����:0��2�ɋ&8��d�8�;�;��MM�[Z�������PmW������回vv�y�T��Tj<�Wg�"���1Q& �]�}��s�~R�[X�g�r��c�Cw���� -]��'�����H���p:�w�owK?a�ʏ����<o�Ӿƿp�a�M$�6���"~\�w�{�p�/�Bَ��+K���+��X���X"�y�̓ N(�8 �T��-�!+O��Z��s��C%���e�r�6�,�O�|�'���> ��-����R��B��?w��oT]�>��ڃsH�d�:��ezI��?��B���^b��A�j��<|`a��2~�$�l�J���H�1)q�kN�� -DӨ��GT�+�HIT���U,o�C�z�E=��Z�����?i�����S#�4�NuF�,ڜ�<z)��H��]�W�aљm�ӑ�O<�d�u�E/�ґ���V���`�D⢅�NMھz�@��̷�e�b7�U����,�{˔}ruj;�xTQ����P�������{����;�Wal�!-�+w f:���F_�%K�q+ -ϙu��j�;�}?|3/�BK�џE�6��'�d)���V�N����+�(ఈ��zn �Ȝi��>0gxLӷ�����ʩ~?�\+��\()�X#Q����=��;fn�;z�('%�Ғ:�ʊ>>���2Q����'����*,��ۊ�?�YGl�Nfy5�Ջ�M^��� �X� /��Q�-'�;-.��"�[�kL��P>]���<t8wdž�8A�A}D����w� �����(P1k��5�r/������@<a�-�=�%'��;ݜ�L6���Q�?�|��vr���ɑn���&\��~fu�`�ͯ�W��;�/a��H��K��|��WT�rA�\J -�Z�ɟ�ք��S#6�9 �Jv�;�;T��vO�Ӥ{|��:Y��G���/a�O"�D��_~�ػ�wpF��_\k�yQ�^�"y�(����h��c�),2��&Gݟd��J���c��(c�|�p.ej�������O����V{�5���B����fg�/K�9+@�n ��1�riz/���ϝ���an�Q���~�v?���? v��pl�T-x7�TtG��p��VO��vvޑ�tw��\��fI���z;Qz��W�-�a%*�vdUl�b�<�����4T}�?�h������]~�y*��:���N�g�����.c���/�W5鮪��c��������j�w�*=�C�Sk��j�{"�<^Ή[��l�C��_��A�v�;�so�d:���ki�:u'��j�M'C-cS�Y���y�9>�E������Q�����]�f�ڢLsϗ�3p;9����Dҧ�b��ݥ�zl��%|U�3K�ʗ=�oͥN�?�Et<��ٌ�X������N:�����EO�O��x�K��3Ӵ͇��t��..�x�����g�/�9G��:��w_XFM\[��xk�|eo���j�m���ZL�µ��q_�q�q��=��9j<-�_ ��s�÷�~�p,�T��q4?I�;x���_��<�f�p��h -�JH��������s��R���~�3L�?��?��+�u*�y6�GnC����*� ��WT�<j? �4*���T�ʲ��;)ۿ�����3��㇂���[����E�Pe�;s�׃h���%l�Z����y��/Y���(ۊ���Rd��wU�2Rk�OB���t��k;�~Zp��s��+s�o6��`3y|KM�kkq�w��03�wOW�1��8�T��ʉR�~������D�]�/c�1���.�+�C���ᙞ���n�`��]v�J�p��P�Eǫ�|�����R�;k�'���Rf�0�Z9^ -�YcTS�E���~z���.�O����.���&���*#�'!�ͤ#����~��+����E��X��`.�����`-S��y�ǘ�:q�2��c��ϔ��ejl{�i-�����zʳi�o�>����? U�����e�{ă�V�����2[���d)r��y�d-�#���1�zoi��ɨ%ҚN�܀�M9��_*�Y'����]���P������7�����L�bA�3xpM�=�$&�����캭��p�bi}K �Z�G�ܜj�uߺI�?g>Y���'��=x���(�X\Z����:���p���gt���^��(3#�t�|*�QmUW��ʢ�8s�O���T�4�\u�9�����c5���#!�W,_��S��zf�-pg֩�>��}r���y��#�^?{� K�V~qKSV�`�Z�Ws[���oF�p��Pb�i�G5�b[���]Ft�����Ι!*Ǘ�3�,��w@x4���Ls��%���Ҧ�)ף���}�>?);����͑��Z�`����Z�M-��,x?2cZoh���>ej8;sh�������N[�ŕ*��������W?<�.��.��P�/9��0�Z��֊FsC�kUT��l�:�n�epd�|�� s�f�f���2�{�3�v�� �����{�V��eo�s��T���L\�x����;��1��{`oY����n�6�f��T�p�/��G>T�1��|��D�D�ѽ�2���L���g��P��OTo��" -o��{P���r?q[�� -�Y[aJ�ۜ'�Vͳƫ�j�¬��'-9_[�bQ-��'���4�����cf��M�^L&2�1��OB��G<=_w�@�Bˈ�>1Sb�7G:�v��S�7�������L+3]^Ê!t��,p��,��U�0i-}�W�O�ޏ��� -U�e�/�������L;� ����� �-;�l}3��ۍ��r˜�JX�����F�j���b]I�6%Q��Fk7���[,�I@g:�%�����U��i/��>ې~B��/��6�Y�\���~Ȑ��4��Ɂ|3Z�����&2ZK��Kv��rv� -MD��=?~d�����e�@�;ʑ* �0�N�������nO���'�! -��u)z9���Ӆ����b8m��_2�+-y������)��5vgaW�p�M\34���Z:�����4�;A��8�����k��ol�<3�r�f*���T{���HF�eY{v����1"o|�!���\` x}�f���Ym�,z� �n<&T��NN��|~�K��3�C���`�����8�zZ���4�u�N��b�; -c^���M�*��|t��s[�&0�҄��vS.�����Ӆ�ۣ�e*��r�?��������-��.��"�٣n��Ixf�f�/�s�݁3���Yn�U�Fa�K>Q�g��}�|�t9Zx�d�A�.�H�*��橶���K��U��|�:�Kx��h�,���_~�ޱ6N{���q�yq��]N��e5Jե���\�gB�`�J�ũC/�(�Ď���|�v�< I"����{�݅6 -���X�j{������m���? �s��}�5�-[�r#�\#�s�Y�1�ׅN��pZ������<���8��gs'o[[�)9����7�6�7����##���fl�m4�g��S�V�C�$�N_u~^5w�dI�=�q@Q� ���ȥ\(�{g�:Aﰿ�c�]ʮ�6��}X�j]� ��Q��W�F^X>����w��l�`m �����h��������;h]��2�#�̮D�Rә/d�z���#r���$w)lsJ��>d�֊*(�廻�.[�vxX���j��$�s����Y�0�n5�.;C�y���O����Х��lO���'���Ʈ���{��������ْ�{@��\�h|�}{�\j;�9.���k3�"��s��N�:7��sn<Ye��$g��C��=�? �y� -q@��4�P���N-k�p*�ۚ�M+�!>�Y�ej�@�=S���"�iZ{ړU�����z���<.:wl���ѡ|O�*��>�XyH5��C�̿��L>u���gj��̶���?3�G�����c5���!/R?�+6~\�,�Fҳ��R��ڼ�p�y��'s������gs1}�f�43�����γ�U[]��Ž��{�0�`ܰ1����Wx�]����<��/� +O�H ��V͇�vo���-���ۓ�/U�'S�[�x�ɔ�V�������?��������z~������z�I��?���g�����R��3���X5J�xq�b� (5뷨ڭ�|�Ɨ�I7��چ�Y��KYk�ZD�C�����mB�z�9�<r.f��z�e���Z��<�e驝w��6��翅��D'��h�7( -3��#�^���kn���۫���tz��ݙF�a����Ͳk {��w���⿌�H�z�Cُt���H&�"n��k��xɁ��ĀR}+�a9p�()����+�eT��炖�kcۜ���R���-�܂����|Ѱ�4�k�&h���8z�]��hm��Y���\_�E��y���x;*��y>�Ruh��' J��*�'#<"T�b]�&�g6��7e�;��[��+���L�AW�zF�^��;k�u�D�3Y�Y�{b�����t�SK��z��_��]*�\�ƃŠ�����r1��V��>��;�h��q<��6�;cZ��`Vs�W��/q���j~4����Bmz=�n��}����r�J�|� -�qJ�_; �O>0��U��/�*�����D Tbπ9�֍��C�x߷��dy���?2�\��oͮ�.0Z�Ȝ\����k�(��ޤ.�a/�T!a�?�7� -�����\��В�!� ���/V�-\q��S�{ 6�1����sv75���W;�?u�p������1�3������'5�}��gh��[��Q�7�w*�s�]=������[�Q�1��U���&���6;����q�ꋯ�w]4L�)�d��N���|���E���t$U^9�&t����M������)iq�D�P��?T��i�����N�<��qn�!X[e#����ӌ�~��:��KO\"��R�FI����H���b�������-����¨��������!M E��ġ[�{[{���Z��d67�� �����33�����1�*�S���༒��Ey&WrK�bl��N}��?������3dIv�˅S���ԆX�g����ֹ"g��t1<����|��>f����V{�1�3�ڿ2��q�Gtbm�qJ���������/�� [��Ɨ+��7I�2j:�e{a�R���/5#��u}���ޚ���ޕ�W�x���2�h �y\i��a%I��+Q������'��T��Ku�AYܢ�)?%�w/P G{+6�V-�X]㟳������ʼ�O��N�J��G��&㴩!�ӽ�hMsq>M�r'�����ϩ�/ϻ����`]�!0�,�M�[,�┪�I���נ)��,������d��3E�ޑ�#|���-�v���]Tw� -��(��"��ٿ\�d�LP�A��J��@Ք�\��i�h��x-l��J�������vL���g�'��4���l���T�b��<� �|�a�|Hc*"��,�@H��������eGXT�=1�2�7��Sgu/����rhAK���r�K��f�Lq����V���7'ɃƐ�������d�}��'79�7!q1ؿ���E�Mᡷ�$�3a4]6@���A�G����jK��u?���n�Ց����-���]E[ -�τ�<���Ϟ�;Q����*�4o�_#��?8�4 -��IV�B���[��U+k��+at�ʳ0x�ws�p�~T�Ο3�Q�kw�,3�_�s���?&K��G�$7*�{�6��Í����ջ��[Za�"������U -endstream endobj 29 0 obj <</Length 65536>>stream -k#�Cğc�˓��$L���ْ/i��8��w�))\�5=Ԫ�-�6DOJ��:��4���J��L L�<�nOw}@J �yXm�y,z�;��7�Qe�Ö�b"G{ -�Ƿ�9^_�q�yhT�_��w�i��N�b<KU~Uy�Yn3b�*�OSD�T��s� ]f�֣zX"R�y��-f��.*Sz�xPt�� %�O�#~�BP�(%dS��;o��ÈkM/�s�4.R%����h�F�J�r� �[��Tc�W���|��b�$/�M�-2OWv#t��Ğ~,�;?3m�؋4�Pf��x���@�8ek����Џ%k�n�8��l�S�����Q�2�+j���-����T�d������l��:�)��p2��V���m�[.ld�L@���G�۟��N����e/˖��Y��ݫ i%W�o`���һb�(��,�_����l�J��콛��7=��Ca{,��m_���&Λ�&�n2yW����EM��rA:Z߸ ��i�������/�{� -�������P�{;����䲔�f �?�J�u��k��VnM:۔8m&����6�jf�ȵVq�v����5:�RO�<���ɝ�K��x�a���� ��ɤϙ��Q��J�~�����՚�eQ����RPvў�m��o�٥��]�֦dr�ާY�B;����ʤ��U�j֗ξ�Z2�NoY�F�ϒ+� �<Ϳw��=W�.m��(��92i��� -�k+�a�i����� V}h�����E���w�H�W;B:/��A[�ݍ��k� -�<�h.v�ϼ��a�U�3yx�̆�c�����n/����'ڐ�yjs�@Xu���+��J�m��P��!)$�_U�����߂k\�?�������v3�f�Nhn:ɱ�@�4{c��uq�N���5)<=o�����z��%�x���<���� 7��1��G��� �#��m��xH2��'�I[��u���c����D��� 7f�\i&�W�V��⢛�Y����������q���<���)�Z��q��U�����?������<�Q�/��ϊ�����Y��ۻ=�����-��'@d�9� -�D�р��Q�>�J� -����z�م� �厦��s�[3'�WH��&v�ܪpvq����fa�Y�ɯQe��,��\�K�K�0��n�[K�� mj�����-@�u��)�?F����L�;Ξ��`��%���ח�bR��E+�X�*Yd�r�V��0�Ŭ�t��H�����b�w���p��!�?U���8 &�!� �]PJx@��:K+�G�r�̌��y-�N�ؚ���7�#��-r��-<YP-�Y��ҵb�|g�|�����Fv�7�ls7�~��<����\hY"�A�@�*(5�>(�*(5�^�0��/�G���� ������;��X��:Xx5��)u6_l�x��c>��{�r�Q��?�4�k�_2������g�_�T��ʂ6���!()�#���?����Ei_�R�d˙�衽�[k�Nm-\�X�(~ds�N�C]�)-�OmZݜo�-]7���r�����C� -��j��rX��s�4��41�{�,���B|)�������9v�(�]Yk�؛�.�.��q��Yne�W����Y�o��%�\7KA��J�ùY�vj��۟`��{݅�'���7_U��A���@�8�Fv�t�j�K��1_ڤ�5�9��}b�� -ݛ[�$1lҿ{��<�'x�9_���xvzPÄ7nO�G�-�NFs^��@I��\/;�j��)d3� gj���n�;^&y:�"2���vm~�1+�d�3��������߯���2�U�5��dP�)Y��������7��t>I�wM��(*�{^�B��4l���ct*�e��ll�,����^�ƴR����{q����Tڻn���͡��y��Ym�S�G)���[���7�2�ʠ\m�C�M�n�V����~�!��);�x��9w�]����5�K�{�I]y���q{>�v<[=��+���rF��Y:M�y'�}�?���Zf�*�����꠴�)�]��2��G�V<�=/�Sy�ڡ�6h�lB�-��6�֏��V+Z��jj�����48\e%[��l���*����kuD����S�����j)%7@I�a R���;[O��L����v"Ρ�6�am�x�^��1ӖZoq�m��*�S�rND�r�r����t�pD�`'=�m-���RZ/�����1����h �<�K���7����`-�[x�5T˚���jT_�ѭ�@���ձ^c�x^��{q�>��I*�1Y4�ˊ�tۉ;��ĭ�Y���f!nO�8h���U����'(��&(}�s���6Cwީ��W�X���e�O�J�����ƍ���s?;�)�1���R`7iQhs'�зx����J`��Zp���O�T�-�nc�:��1^�T7۰.%/��@d���8X����&ct���B�y,�Q�^��vO��w,EQ_h3�'�1���囄��C�'�;���o����O=�7�! ֻ��3ڻ���Z�����V-k�v��+N�le�?P>g����.�ت��z�-�%6Z�O��6��v��d�`�W��%E���s��^��@�Cr�Z[}_Ϳv���f0��UY���A� Y�;�����/D�a���l���\ BŃ}m��szc��(Y?t�>���O��\>���2gVQ`5L=�ܳ�f���=%{.�hR�64XBճ7�I6a�'zA�cȎv�ۦ+v}�M�(���B��e�<����ܢ�,� &�"w�K8���j̳u��FUYћ'���MˌFބÌ��r��[�xn =_�1 �W֥��O N�M=��W�F��]:�rfY9�҆�<�^�X5�� ޒ�O]�T�S=�Z�Q�ȒU�� {{zt^��٪�� ����0�l�e�ng�o�W��#��K[��L~JIljj�q�-�5c�F=����E����?)�����u�)�+�������ߞl���]�m�����u -���Sڱƨ��*��6����A��Ŧ���OX�m@5�SX�R^ �?u�D:��l����}�q �$-��Bw�#=����h]�K"��a�n❮�sc�\��S�h�Hj۫���u��$��0�3�������sGR�3%�x���O����$y �qIh�xF� 9Vz��\^ r��p3����5�k��'%�x�z��t}<��Kuڛ4�Թ�OQ�W Y����z~��\K��C����_��jⱘ��f,T�B���;��Ky.����w�0�o � ��q�t�E,vQM%���*��&���9��L�D���i}�������$�H���� -����@���įr��}�����ϙ�=W�5�(qE�ū��î`*Z6�oYL~�a{x��|}�Urj�I�����=X�T�Y��3��Z7~U�[q7jI?�۲>�9E��x����:��UΛ�QQ�N���+M�!��j�g���@���H!]�g��F�F�rw�:\Э�A�6 I�v�LfU�d��2���!��ʷ��<�Gb1�%;r�G��u��a=�B�[7�ә���N��h�z -�}/d���a*wى��� ��*=R_��x��U��l�L��.�C9�X�BD���2�~���GF��<ĭY��Su�JL��A�o�7FN�tЎ�S�;�6�K�r�����)~~�� �$c�l���g��Ev�b�E�>�zm6L��:)���:�\��d������+�8�Q�g�F�n}Q�ُ�~J�N��斛�hD3��g+4u�=��*c7IB3�Kgs"-��G��ԉP��,�M�s�7^��h�oӉU�e�&�.w�>�Xu�t�֠�n���`���� -x����xİ��*#�5�W� �y��FS�)�`�Z`���f�H�jO�J�0"�d:4�4�����v�x��&�QP�tH��N^(OE7iy�=���fw/��"�?����K�x����[�x~�����~s5�'w?��Ǥ�CUv��P]3��o��҆�a���CE�02S����8��!���JJ����$��rFD�� �5�\��l<���W��g23��-�<�i -�`����z͙z"˼��n��^q���`4�@��@��`��`x�E|�{�|���_A�-� d�Q�x`%��;��^!`�F��������u�'88�~��ɾ&�ԫ(�r���JS������0��J���?����5x��(tK�t�e�-���{n�p���-��T ��w"`����g�oc����� ��3��Qs�j�b]2*/�{�c��Tr��l{�S��'�Z��&+��}*6��ӈ\܀�` ʽ1����Nnц*9a-�L����̱�g���ʧ��9>�� Ȩv�=��.=�5`4�0�܋�p}�Pa�E�R��Ο��� �V1�KU�=^ݗ�&4 &����n�9�o�m��x�d�)�xp����ㅑ+�K7�Yg��n&�CU�=h��� �{�n��K^�\k�S|68U�TEi��w^:��Jm�ī�"�*�gS���㑉 �����_Vǖ���c7]_R�d�v��_�)��ߤ{/Β��e���W�C"����Y��>.,�W���@ ���( �:(� -d<��B����I��&�$��Sg��w�g�*���&B�pݤ���zC�s���j;��F<��G~�6��*|( NKX�&���fJ�����J��&�g���A;��]Z~��Iy��)���w��5��2��{>Z.O -���%�]K��>��QE_��e7?�+��O=w�� ����`�Y�(U�0�u ���w��� �TجTd���Sj=ϝ��o�������mb�{�Nn���ZNч�a����jW'i�����L ��O�g��ɰ�9;�r�H�^�R�NN��I�=,H�]�<��6�؇Vi���G��<r��vɜZwc�����s�'�#Ek̕Ѫ4��:�S��q���K����6ࣾ�ٰ�RmPbh9�5�� �5���V~2 DwVSo���3L{Jz��d�C՛���1�]��+q��\_ �sJv'k%3,.�L�����踸��?@I�����U�U�\����Q�C�вP�N/t�^�(v�*J�i��5�qɫ�{H���/5j�A�UW2Ni-��`u\��#"Iq[@z�#~(���)~�>�0�Go �]�����V�p���-��wv��֢�e��`ipzN���}=i</��T �ʤA&�Z��Kk��HD�_��s1��bAB�Ӂh�G�?B�R����=��/����8tA uzў1d�m����dqg��;vi|�����^ �<h+!+_��{S���LgZ���ۃH܉���h%�B=�/���6h�9�km$�Qg(�fs�S���湯�w@Ҥ<X�R��Þ���{�[���M��%�A�8},@r{��U�, -{��]l����bm|��>�p Mhq�GyŁ>�ۉ����Q�[�{������OPE6��`s��l���|X��~n.���l�ֽ#0�U�4��h���'����^�=��ͣ�����{O��;8lFC��}:���e2s.�LN�^#q�0r�*��)��o����ƪ+��D������/M:=�}v�| ��,�D#����om未1����$�k/��;��� ���M�gU�`������{�ù��Í1`�ͽ�S��Dvy��;@��x>tdi��w�;x����{���R3�5P�3�.���NW�*+Ǧ$tGu��L�C -���oF�]��F���+���}cJ?S�!�@�x��!��?@]ب�jlV�{@<?axx�Mw����CW'������s�Ƭ��Et��Kz�^�2�y�=�ܤ�*��i{g��v����|V��fpI�|g���A8���2�m�G���k�9����hZ�[�E�Ϧ�\t�^����37����ws�� ����z��s�Ț;1x�i>�^����e��]��ַ��w��&�e��dJ�)5�n7�}�ܶid�����^���`UzJ:�\,(��7f��d��;�LkzV��:B����~�g����K~\�P��@��uev|ԓ��Q<E��X�r���SF�E���u���u~�I����A�n���?6�,D�Ɔ^2���~�k�p����9T�U���#���:LZ�Z?�/�>2&4������A��ߌ3i��ڮ�^��'�� $I�S\��[�ĸ`�.P�x}i�?@�R��0�Y��J ��� r�=�������T@��t�d�=�RKi$��?�CE���xr'����`�=`s�0����rM=⡈�q�n�J*�,m��Zҥ[m����+J�� -�{G�sS'��e4����{Bc��79N�۸+W��^1S�\�q�2���'o�a�F��i�:| kmz����o�Lj?\U��b�Ԕ��g_�WJ?-��ϣ� �j��E����Ο"8\�s�'��8��8�N��dC�Ǻ�V�Ϧ��f�~Θ�'��Jf;9�5���`I��ice�o��^�e�52�i �H����ݝ�#d6fF�Y�(��u���u��i��H�#f'���D�Z�3�����Y6���|J���s�Ly3~��z�۴I�ف�3�0rU|ŒK����}�Ƀ�m�R���$����{%1����X��u�p�S<�ޜ����� ���!+���ցP�%� h���.�F�6�3w��b/�x(�����ר'�b��>�:K���t�uj���lM -�?^��d�>9�*q��-�]�~VB��7�i~w�w����T�Ĕ���<�k�o�����z樂�,����^U^�p�ӳ�� -�l��짓��M�k\o��JUG�fw6� Mzg+>���*�������L�����8>W����S��D9N����v~@By��Zu� �%C��y7l�g)�aM��^4)�G�$:�J����/ZÊ����t������NtV=qT���Ky�p�����j:�����Σ]�GvK���O���a��ڤR�����d��� �v��}��WK@�@�D���B&�ڬm��!��ޞ�h&P�63�(������"|5�eP�63���O�=��|з�Ƨ�o���G������3���V����0���P�Pu������UŪ[����Ͽ �U����w2���f~�����U� !��,@z� {���4@/�-0�@��7hh����(�L�r1�T(m����ޠ��(Z�ID"%*�w��7��QN�����#�<k=�L�;�C<K��"MBU����s��;���y���xg���˼`��bC�nXh�6�K��Q�+�p�d\�~����Gd,��l�f�O���������|6������p�h���xܟ`4�(:���4�h -�����W���`��[�<�H�b��^s}7<��i�9���x��}%"�� |���pۻ�r��/�zi>�������g7qh=�F���RՊ6(~^yx�S3����Fy`e�$vس��!��o�:]tÍV�'@�EX��S$"�E=4��4,���6�{g�(q���>�'�nU�i�n�y�-��!�r��(�y�Rp��`���-G8��`�~���(���;I�eCH���#@w��h�����&��"nr���j�º}�e�X�l�Z<>��@�V�����W=7����hK��Su��)��֗ߠa����2���5�o��J�ݠ�Z0�^�3�z��������v���]��Y~� �>g�Z뽱����S�&��;�ߖ�?:�ľT�O���*Rh���K#��uW�[�M��� �`o��<�Q.���oԢ�3�;�a�G�A#,s>��^d\��Φ]9�%�58Y�ޛ���=>=iv����Oֽ�ɶ�L�l�� ��qo�_;��>�a@{Pu^Da]Z����zK��іa��'t@�W�Щ+{=|a�_y}�I� -u<9gdi�W��|SZ�0�� 6 �~�D@�����J�ƃ���տ��S�Lj�^�Z������sW��J��9�C� 8gJ��+��穀�%�l:����:��vk/��h��nX���r�NRo#8��{����E�Rn0:3����T��[5����3S�!�?�/�����������\�!� -��J�zz �w�����SLq7ʂ���Y�h�yL�τ۹i�JҾ�9'y[-��k�=��~�Dg�Q�t���'A]��n9��J�8�oR~]�2�C�k������2���A���0����>�[��$ܢS���֦t�n�0��ȸsG�K����e��sC��Oh���)��v�4Z�<�[���}�X<��� 럞�0��\����]�:N�2A`/�߹7���ˋ�Y[�u]jP� -�z7=Oo.���0?�$>�T�e���u�%�[�����ǢG֤;�Hd�_���/@���5~�n)��j�M�d|PJ�hߠi�]_�˂p��&M'=-��Y�x�+��۫�]�Ia� -Fo{J�/^�>��q!�D��e ���+��ō�W!��\�������Z�� -|Fs���3���(��f��[?ɵd��/˜}�����g����d��^�;�SA�:U�e@ʅ�����"�ü%Еc�Zjλ.Դ�*�d�"��$�V6����������@*�*���8^����ꂭ�+���WK�=��V�^�j��ҹ�vۧ�1�ʼn���(���Q�:�ປl��N�'l� -�Oe��K|�u�����}�T���G��ƅ�.�(�T���U�zϘl�o��p���2#T1:����(����>�K�Da��~�u.4�'���Th�'���M�����Xp���*�Z.��Ѧ!�K�}���yo�� <�|��!��m�U�}�, X'�j�O�O�ٱ�+�l|���҇��Wp�]�5T��cN)��d�-�Dm��7���̡4S||N4v�4&U�G'S�UJ��:(���?��ʦR����!����$�x*�mwg�i�]���37w��EI���ǹ�\�6Y����|x�����r_���>�ۗ/�qW{��]�Ԫ��I�Lc���[g�:�\K��_�o����'`�URV�|��`]*���l���^�I3�@[���!y�"��t�P����x`�B�pH%�יe:���&�j�l��Gڈ͙��e}ی�ʮ���;k�B���)��-��0�iظ��|��4�*\Te5˽\v�H/��a�^y��+��O��4�*��I�G'�OZ.�3�*3������0�vZ��&�v�T�8�T�z�S�[���^ބ��u���~��h��������;�;[;�)>P�y�9�;��Sତ���"y��V� �8%�\^�1��.�ʴ��6�x3xz*�Ϝ��H-��Lk}N�Uj����O����C��;�W��a@KP���_�t��3��9��s��'_�S?�U�-����<-��z���.v%���j�y����0z��pZ������6�/I���k���5�W��!�Z�I.�{y�k.��$SG~����7�� -�o�i|p��g�\�SctV���Nۭ��o�#�CE�}^���l���۳�J�=��������6�u�?^U����N(����Eӳ�����V�w����Zy����Pe~X���R����s]��<ه:mb� �Wl� u\�>�H���8���"Kb�r�s��c+���&+k��|&�"Y:�Zm���\�M�������!9�b}�1���UJ��r���,��X����^���x���.��FV�J��J~�j�"��y�����70��}�oY��2C�� q�����\[�%SU�Ѩה���as�a�T�N��h��F��J�_|j:I���v���ͯ���\��{��=��D�E�䴩� ���{�+g�@�7����v]w�oƟ\�C���4Ρv����#���>GO����yr]< ��cԪ�#}�tz�:GTݺ��F����B&o%��TS�%Y$B�c�DT��3�������uS�������SC$�xyKg{��Yg�����P0�O�����Fq�t9"7�����v_}_>}_����4�S|�u�s��S��t��\p�\}�T.�p�ٿ>��x/���Q#�Y�_�0c��i�����~&�[�T�4{E������+,2FTY�fnwu58�-���s��N>�^/k���"=���ޡ3!��p����Qo�|B4�t%��nԦO�&�i嶬��ǼR�,%S�4{ާ&��&?FuQ%�:4�d�C%�9�Q�G�^���^����3|�Mi8�ɾ�R{^�/-���-ԟ��M�M���.c^4�>�Q� �L�ծ�n�_M�jf7��#3�83��u��K~�n���ƣ3)���ԟ�9�-mk:-�����8�IV�a�A�Qy��#�J�F:W�q�b����&Uq9!��4�-ț� �_����� -S�������E?���>�"�2��~�@1�@�A�xj���=�a�� -���� -�4�nP�((ܱJ�0NT���/��G=na�O4�:�y��_x���7��G�����T=6A�I�Suhi���U��a�g���m�^pd����#�h�������o6��%�|~�e��� �.�l��)4�p�����i�A��D�A{�;�� +P��<���C^���:z�h�Ӎ��͠�+�q7AШ=Q -�vr��i�k�y2�l� o�)Pć#h�Vr�?���u��s �= i���q��8(v�(V�kP��"4~=@������s6�6NwS>4=����DK(ٓ����Z4�#��k�A�l=�� -�<��n -��6[߃oIC�&�y���3@V �^Rp)�"A6�@��?BiW��^HB�7�K�C�bu}��o��ӽf����U<;�Q�{ȫ -ߡ�x�������RR�����2�EW��2`k�B�ͻ�i�M��?D�Z�a�Y�4�@2�ɷ@�=( -� ����E:Z)|)��['�]���}���c��_�ZŞ�&�{HB3�׀����)��'�KBU~ -���� -�X�З�hX�Al���8��Ĺ��$a���.|�D�l�F���k>�5+�;��<>�]"�I�۶�8��מ��o|Q�2u#{�rB��K����Bٽ �i �T!z��k��#��o��6��[w�A�����c���.���1�M��ڣ�tu���pF-�o>��{�\��j�}���7Dd��~��S����@��CU�y� o�����)���/�)#؊qc�y9`��3� -� ���lP�Ϫ>�^hu�y�nr ������E�u7*�S1[��Q��{�\C;U���>���u �ˡ�9��5���������������J��6���l�<�G�OO"i��^����~�XIz�I�p� ��ua_����X˼�2��\���%ݣ��6.d�%�n���Y(�魚SY�ʨ��$���������}@� -@��`��t<��7 �V1n�!s>{���tGc89eW���F��ҹ�IH��Q+����)l�1h�y-�IV�OkV:;UY�*����U>�\Z9��7��o�|Um�?U{=�l�=&N�B�+��J�1Z���;��fњn/��o�Fְ�[��`��g�q�gB>R���~�չ{���nh��\�E�t�oe� {�)���������� ��aH���Z�?F��5��o��k�axX?���G~�n�&y{�}_L_�/O�~��a��^�ϟf��w��)L���������-'��sE%u��K�M)s���Ա�?�(&�PLmn�ׅϞ�h����Ў��"�Gf�����~����(��O*/_�>��&p������'�x��R�̼]�MI�S�������RZ�%��q�&��fv��jWKIXO������@�o�@�69�=lai ���Sf፟�!_�~�b՝�TY/uD�}xʀR���7nD߈.t��Uò����Z㛼��c97{o�s��J�xK�ϊ�b&-R�;)�V^B�����_���_�C,��]�yhLkBՈ�_ye�4���}�{�O�w���n���F�Dž!��*��SRm�r��º+a�z)��(챾'�vV��.)�{�����)��S��5@�%�X�a+���tl�~e���A�O������f�����2�㱵;�}�w�]U��ا��l��v����m -�ej�7'u��o����u���$����/G����Ow���-30�EEm��[��L٨2o�<�S -�g��6iA���^֖���j�̙g/�����9U $�<��ʊ@xv���\7+�A��N��~8��$�� �ϕ߬�7!�Z�J\9�k��\���g����+L��Ma��������-��H<����Vqpiu:[5�[�J>}�[�����&N�����a�4:�LX�i-Q��w�L0�^�9dn��ade�1�[&:( �5`4�T�.�X*U�:Hit7|y<��Ӹ�`��6 -���Y����ry��:E]Ž�2y~� \��1Z3�֞ZU�;�*;S`�{����˃��1��0����)�j���8����������wS��������T�v�>��YJ���ؘ��p�\���s�M�?��+�&�Rqg�8��u��l�x�����>�]W���p�������T�ϣ��&���a�z3���ʹ�����W��}��.n��v�t�z���B~�OcigvB��8r���f��C�E���/Xn�J� -�>��� � T?�#Ꝗ�`u|oY�v��mWqX����&��@�վ�x�U��ɠk��!�_s���b�.���'F -��p�&����z\�Q��`s�)=i�el"���� V^��dL^)�_g�F�Ζ���\>��Ѧ��� l��Ʒ�n�x�r/X�P�oV��9�B#�ߪ6�a�f�ë�u^ɣ9�T���>�S�,y���6�a*��aA��dΘ# -(�(I0 (a���d��U_յ��?7µ��a0fc��!�W��_߫��J�X�.3�������~x��s;��o/���7Ŝ��{J�j9�;@c`Ƿ���������˥p4}�f���M��G� �UoJ�V��S��ǵ��_�[jSXn��t�nղ*߯H��`�/�"�i��(��U�/] ��B�,kB _r*���b�K�t:����$S~�����g�T~�Wg�ӛӒ��E� 8�p���z�������l�ذ�y��L�����Tp߮fIg���Fߝq�|>����+�E��g)]ݩ}}�Y��d�y�����jq� ���l�X$��UZ�K������2���rߪ�!r9����{�HWn��l&qC��U������>y�z��4/p�r�I�9_�G�����u�Lﴝ�Z�9�';?24�:L+�vL1�;@>|��F�^X�ω2>k�Vi���Q[�ΕC}kY<�3��;�Vz�we;ū�)�!j����͔��^w��P��c��\�O3c�p��r��R�Okbs�zB�Z6�Lϲ��r2�� �������@yǘ�i���TSr�a���~�������1� -��Rn�,֞�L�y% ��w�r��b��l�3Ƥ7C.���NV�������6�BjT����?.x��2����h���ܢ>I��k���6'�%��]���3��wE��<K�-f�G�rwo�lzW��w�n�=��|�����_��{N�R-��Vj�ul��[�A���Hz�ɨl�0C�~�����{*�l�f���z�'�bn�s�#l\k5����Zeg�K�"#�����=3�f�����剽|6'�۫7^U^㑻~Q������E���m�0֎���r���;-�Q��X��I��M�������ϕ�ڜ�k�o;��-A�b�hy�e��h՝ SsZݍo�w 5K{������{o���^��G�i����tL�Nwp�ʑ� d�%�ګ�Xo��۪}�iS�L#8�b����zlO��C)}2�ݨ]l��t�5�}2M������U��ؤ�à2�e��cҗ%IW�L{Մ�:�u�?v�F�緍�����'�T�z�e�VE<�T���U�wJ���r���j`EEW�3Ea @G��E���V>�\���:��Gk��|�����C�'/@|'�{�k�����ت���6���6/�=�� -�Z�����n�vX��e��A����+��wPj+�����5M�ng_�y���]M� -!����6��(�ޘ��;�����[/0c[lؒ�����2���#��S�]��#L>��~�g��7�̿��:�v��Ψ�uʓ����e�RC�G���� -�[��4)�����i�X���xF�,L �<���#�`��vz���>��dl-h��Kv�W?3R�����/�h�v�zy�w -p�2�9���=@Ln( :��q��,��9����.1�+�y+?xq-@��s�첋a��N�\,��y�H��g���IaC�OH���Զ)�J������3t#��"G����* ��N���u�ڰɠ�,w���������I�������']��0'`gO��!BA�W��u(�~F��'U�����u���7�Sá��P����~���d.A��1@~��]���~�$��$ٯ��>�]��h�ۊ�Vn��:ZU��P(h��_��w�~�^�.}=C|G�m ���ݍ�ǵç���_�7�e氌�O�ߵ2ȧb�W�?��{r�-o�CO��b>)�1�-y�}YNE��,��ʰ -��4�,>w9��[�n�ö��9I{����␌n���w���f�:c�W${r@���*-�A �D+����̂����L�&�(��z9k��}~�XA�n���ج�;Q0|���k=�g�漽z(�}=���~ܛ��u_��8st-�ٽ�������RM���@ﯜ`�� ���|=��8��3��\jDB���#-8�z�y)�B�Jϋ9,+��T�x�C=/���2���QM���[��u���\���ZZ��XC�B���G�:MP�l(�I&v�3���Ղ���}H�FDf�#��$�����=�����x����GtG�F{���UIs��c����ry�#��Ok�u�m�y�e4������B���\����C4�>���"��Hs��|��v.��%韨�̣���Y,�ՇK��;����(��e�%l���vn]�Z;�~�Ђ~1K��?WϏ��{����)��]K_w��0Ot��{�/�A[���6(ܮkPT��iBɺDo�b��\�=�Es�V-�.�ݵ?|k��`��Y&���B����*�*�9W������������"mذ}U�OA��2���m~���_R�%l3�*�*N� ��<��y�̠{[x/$�Ky�ڷ��0�u������N\?��B��٬dE�\�F�k��h�tY�&�b���ɏu�=��0�6Qw�K��L��;�v(�L��J��m@�R�6�:���7���iP�yv9�.}���-(;�ַ��Ҽ?|�.R�6�����}%�ed��"��S�v�(8��T�u��S�a�}=���.����w��,C�6[��+��P'�C�*��|t���#�n��Y�����-�`��CGf�*�^I��7�,�u�OGGۈ�c�SkV�--���If'O�cGw�[�bߋÅ��$������& �n���x��% -zLƕ�b��d+y��W�:=1o���ٔ��]V�M�����!suVO�Z�۵:���B|αhȯCe���u֨��2݅����of�nw�w;�{�8�Y^���i=�;�˯,(������� U㞫9"������꣫UT����.��Xzf�Z���x��O��(Ȗ�^��})|w�T��=�w�m?�${ m����3w��n���n�ǜ�w�,�A����g�Y�����6����}�6��P��c��x��o�y;�Ӿ�-��}�QM�4W -L�?�E��w��n�}{�����_�x��]qF�[q�(W�]oR��]���q*�?�I��ו�iB��vBՔ -HJ]"��G2;��iԿ����V����c�%��Ơg���Si+�)M�9���7>y7;��h�aϰ�\(�7֒�m�Ƴ������:�ۛ<��n.��l.�����������O�9)fc.�W^�y��֏Ǣs.U�V)�-�f�S�ϵ��9QE�֎��b(�k�;\��6�Es�)K��\�WݴP[�k�_��BmTK��OO�u�g�5�����/��$�:� _�'�����+�b�=\�oG�{��&�7��hr:�����'��.9*}��NI��jgw4��m�]�D�k�7E�:�������v�E��t:,ש|O\�����>�y/Zߚ�������+�'�K��J -��#��(tm#��2C�^<'���dr�^���t]���1���A_��x���=���Dpf�h�I�)�-J�B@8���V��)��Fq�j���J-{��A!���t���pvI�9�@!'��x�%+7yk -6!�$�;��=c�MթM��Kj��.�����l��tU��lo��Jh��5'�F�j�hY�d��y_�Ӗާ�k��'(�d�6;��U*���<��{< ai�m�1�>��nj�Q7�U�a�xlt�5�d��}�n߶�9��NTX���&2��;b.��vVsI�gڎ���z���\,o�.W��.T�muzgC�OH���'���,�wOZܘ�����2�����p�y͜49:ܱ�7���`{����-���a5��Qlչa{9X��&�,���h��9-�N���Fa�担��+�F��,���Z�G�X����w�"�@!��V��\n������L�����:ֹ�W��m7��f����X����5����-�ᲄU`͇�fh��Е�[և��B4�;b�,�Y^T������Ӛ��$�ڟ��������WZ?�)=�1��?��7�ν��X�'�Գ@I���U�T6O��_�T��i҇�Ӆ�����pGr�������w��N�}���#P�yz��fzA/��'n9�a@��kYm6���L].�"I�&G��w�ԩ�=���������{��>�?�E����S����.��t1���}��O��P?�i��ݗ{.�!���ϼ0W��<��$C3�sS\�h�?)x���6<-ǷbE��p;Y�����ޥ뤑◽�|ھ_�;b)s���0��!���.5�Y��&�������E����xw��ߣn�GiݙMٛR�&���!;��ji�vgT�pz(�xu�J��A�E��n2K����k��[�A�ʵ����Q[,�U��[%��f�t�;�]���_ٌ��/@g�\�45�?�@��v�7�������W�GB�����q[f?�;�~��q)�2�θk��;��n� -�ߑ���T���Ia��ݠ́����!� }n9~iJ���p��F���C������c%����7���p�7�p�k,�R���RKɒ������i�Hy�ro�UO�����xS�rӿCC �%��QSA�j ���(5?�)lJ!�����m0l�Y�0�.{#���"�����D�Pn��/���z�?�N�@��F��:,����p~-��괦g+��(T�(�,ӽƨ\�j�OJ��c��Q�2���r4c"��۬���ll�\��r]�� -$]Q*-w;�7�~��؍q�~��lcP�Z٣*�܍+����i��J�~kQ�E� �Q>�������zY5�6f����r�I�ʃ�:��\&˂lБ@N͘��?���t���`�}�t�>�*@��������!�&@��'������TI�uRn��<1����#���eXM�� �rG�uP����@�f -Jl����OP8�5�X����$�������g�1��{���,� �P���A�����p��xQ8q1h\T�U�|c��YI��s���{���,�@n#��2�;���Y����L,r���3���`�c�g��#{kE�y��F<�v0t����a�!@?6�(@���&bV�uT�p�����[z\W��w�� -�T^Ф��nr� -��P�C����{)��qj ���D3xJ$����3\��t�c۳�U�1g`�*[�b��*b-��ܓ��ꑑ�ѕ���c�+� ����W��B����ux:��;GO� �T�\6$���~`?�n%��>�C�yy�����}#�S��:���CFl�4 �J�6��������vRڿ"�����\|����ZW����N��p�>@� -�j��iDj�}7�K��E���S@��0x!�w@ �7��W��~�x���ѭ,�kZɸgT^�?��n!���`s8��[ �_��m�o�}�|�&\Ov�������pۄ�կ�Ȟ��ۡ�R~��(�9l����@��y�A?⪻��7�����(��8 -By�Z��Q���y���蛴���(O��Xf�iv�}��I�s���ʃ�E���{�rv��0� �"�:�Z`o�K=�;���Cw �YK��~�@�d�e���R ���;�Żs����^����G+��g�0�M�h�i�Х�X�;�.P�r����7�2.i�B�7�I�C�?H������+P*7��Hև�x���5���A ��� �j?��[c�[��𗒭�|�C��I9u_�sA�����ܢ���S%��^�3w��Հ9X(�/�s����P8�3{������5�֤�NO�ퟭY�īՄ*I6r��1������o_���K�w}��u�����^��x�/d�9N���:�����2_�fe:�%��<���C��h�����"�����А(�.��} w��0�܌��\o|�4��6��7� :�O�xH&\�e���o���UޑCg�����q�����Kh:��o�T�M7�vN�A� �펻��� ؎��ٕ���� It������vG���nh�ܨk(� ����������g�*\BL���I�9��d��y����y=�+X9���Ǿ���S����&�zD�kڧ��i�%j�I��TTf�S8Z�w\f���hk:�vU�ΰ|g�O@|�����x��F'z�t���������>���"�n�l�y*�᩹�-�j�,U���Wm-m�Cu�U�<o��>Ba�sẇ�dy��_�el�%X� -��((��B��W(��z�|������ -�I��e��UṼɵ���-���'~�Z�\�ȝ^�c���ݮ���mZ+ub�;����Խc!���nz�P��W���ɼ2�ڰ����<��i�OH���/�� 65� -1��$&���^#C�>�횼oۃ�3�9kY�u��j��;㤍n~P�S|�R�p;RLkN�Uys�-�I_@v�|��*������+̾i�w�`�w;�-$��~�[iT���)���wO>Ne��tJ&���s�xT�|.Ϥ�����g�s�ލ����0u����j��E�;?T�7N��y��m{�g�m|>Է�\l������O��OJ5%s �_PQ�R�J5:�ъ��U�֬��z�oQ8��feظ����Y��;CTKO�+�O�{�)�ٰ|�_㶼��T����;��BʔTO|K#�X��E.eۛmQʖXA���+i4��O�Hň��}?��S����[=W=�H�Ux��^/LN����Վ`�&�z)�"�><6墼A�־���-�V��=����,�Q���P��m��0�,pR�\zCqs�����{����쯀/^�:�� -��)S~$�v��Ƌ�}��%��l��MA������颯+t�qnw�7���^Bm���=i�3qvq��B��k����=� -ܮ2��w�.�͚��C����(�� -��;(�r�X����g�j�{�9�HQ�e%��SHWPc��H��Z��3A�� -}��⦚lr��M٪�iY��()�g���kFx�{ޓ4�o��4�@�o<M�ߠ���*>'T;.+p+���A�����-�#�� ګ��z������EW_�Y¦�1�\��)(6TX֩#'�{�u�}'��>]ꐖ�Z=���Wc�j�B�],��Hy��<�������=;��vŮ�!pd�Xq��r�A� -�~� �Y} �Փ�Ъ|=���Z.c��iO��$��6����Ji�#�~M䷨m�]k`l�8r%�ă 3"�u��T�+u�}j0[����ı��3d���b�f�L��J�+�K���7?O��j&�������sߞ)X�gj�ql��&U�J�x?����w�����Ճ�"���ٰs�g��G3���,r]Sj���٘9}�,me9��w����^ :OK:�R9�R߱̔��!��_��m4��#gH;Z5��'OrlW�Z���7Å����}� �w�Vߢ��\��H�TJ� ������J%���� -3����?2K��n����X�D��x��łg�����̒�v��#��`Q<y�*�������y��Ȗv��o����z�2If�J�A:�-_��]o�n��+>�7�� �ոPx��R��8�坿�O�9�^|qOI5ۣ$®SR�7�Z,�P-�@S��6��Ș� I����?��u8})Uc�dJ��a� }$fZJi���q�Ք� 7�I�� �� [)��qn��5�Z��Fd���`��<Y*O�d���l��uj�&��K��a��}��7x�g����)�3=�p>2���D콩���#UN֭�_ů+�Em�d�X�Le_]��:/�č0�]�!�B��:Kt�C��l2�g��]֏��c�`~$��<%���(���k�Y~�,��No9���)کN'��NL�:���KjYTLV�s�*vu�-k?���:�.piqSb�k]f��!���Dj�֧�Ż|h/ڇۄ�j7h�R����i�Ӑ�t^j�����U=4�츰=g}�Jv�7ڝ�Γ�F�?����+���+��=�hӝQ?��zmk`Ry-h:�O�,}�K�e�T�h]����P�ΐ�S�ν�1���`Bg;���Q�Ov�[�GBSԇ�θ � ����c���f�����w�k���ק����ү09�[�3�C�X%��]c� 3��AWcTM-�zP�}z:3����t��m[#Wh?���~��/�������l����Fo�齮��N��-Z%�Ȥ��$�J';*����j�dJh��}� -]���R|��;�[7嗌�F�%XV��{�!_� �ߚ#��'�f`���h$���:��A ����Z�g������5���n�m�J��le���A��a�j֪߫��`Z�<i����jl�o�������?'�3�V�r�T���:�R�p�Mu�xq�3�q;�S�%���X��+^�]~���Z����f�؍7T�0�/k*M2U�+'9����<�H�B�=�*�X/ -�.�T���懈+�8�~�8��,.M��� 4dv����D��ڍ?���Y�����.�z���+���)j�♪�%[1�#Z�%/��q���B�?����╉�j��41CbIԔ�}�V�Ӭk��3[/4�L�=l���d�� -�<��Ԣ>Ș�9Ȓ` �7�r�y�r�^ ��� I��@��� h���M�!�&� �\a �[��mׇ�S�a#��VX��<��1~x������$��#�2��� �O@YS k�%�k�ȩrU���c�\N�x�@����݂b�$�?�Sf!�=@�i��i��7�9�[���j=��O%���r�jTx)�p�^���� �MW���y( Av�-������O2�E�h�U(�����AL/��/���,U�0���#�d qV���_!���V��c��S�z��ε�2���~a����y��+@V�A��< �چR��%�Y7 =�%������5.��[2���)��z��l@�ʻ:D0� ;����6�zl?�Ez���L���U �̵�#��~�l�-�f�֫� ������&�.A��FdYnrC:Y�y>@'���X+��l�R�L�`��B,�tk������yb"E�#��x���V� ���5�߯h^}��%�ꎚm��-t��.&�A�%����+@.]�:�6��>B����g�n���}n����������{�x�+?*D%�j��0�'���/W��=���Y�d�F���.{z7��rk��<7��sv]�܅[!���|Qk� -�˷���]�[�`y��D�J� �����CG��aC��ozT�ۏ���O��=�w����M.S�W�)R�%�@��@4�L:~{ta�zw�n�<�>X�>����w�N�Εl�Z���V� ƍ�.�q���%|m�NvE�|������'���P��Ǡ5��w�C��~c�����Ԙ�m6y��5��em�ڵ��-H���퍓D�M̎�WRi������*���J'/f�l��OP�А����Ȭ3�69���N�t�S���*ѽVI���}�yw3��+uI��i�������:E�p�]���4��{���o�b+\w���9Pg�V����_Liظ�ѰkҽB�,|3��5v C\���?�?��(u�A���X]V�^�\������62N��i\f��Ԅ�UZ�.������n���8@�~�Þ��u{d���)K--���\����v����g����v�Þ�!^���Zt�zg�mh��S� w䠫>����fd'�ɇ��Dk��*&�� ?�v�B.O=x:���i�p/�n�f��rɣ\�d\�|g�ѹ�W�5���$e-s������YՎd������j�SK��RS�ǵ�?�.k�V���j�8�3��yz�3�����:�����8��n�����&[8&X�u��Ϗ�:y�7n�kך�Y=}�,4�}Dm�j�U�;�j�[-)��S��s����1_-5�:�� I����?�ݷ�¡�e���:^�qD������%���W(\��/i#���BWG�Y6��O8c+s�.ӽ��h�S���7�:�5��� -6بDž��+��+�9�6���hDm�{?�ru{��=R�� ���y0����E:���ac}�� Sz���9�L*kO�t�B����3�N����<��/]zk�N�R��M(8�֏˺:8�%��s/������/���%��˝}�����7w����+���ڰ,U��A���}ėp��vp�pNq�A�n�Z����I���TR�S�=��17���6�i��)T^��1�X�S��s����}j�l �ݖs�Y��q����Z���� ��jn{�Z=A�IJ��'?�G*��C�ƛ ��)v�������] q��j��!>bhV;�$駎�IqKm�c������[܋d�!������NkG��V��k�;\]�ᩘ��5���M@J�ٷ���+`Y�@�� -�Ae�\rF�|4O�N���bl4L�&g��{�@��˵4=ט�X�[��V;p�#�S��^��S�����n]���z(e�^����1ؘK*)7s�v���!"ʭ�+�ҹ���E(��D@X�n:��ݟ��Ԭ=�0Gin�χ�Cri=�j$�F���c%o\���+r�����ś�� -lS�1*e���N� u�1k�uR�Er���bB ״a@�͵m�j��$��� @�O�O�U(U�B�~������گDm}?:goR���9z��|T3�.+��cpx<���Dj�]$���g����B�t)%.�Y�?ŵ�H����B���u��k�^���)�?�t[����zҵ_���ك!�~��X½I璪Ӟ�|�R�'�f��L;��p*t�Z8�+�+���b�V9��4i�q^��7y��]��!T�k�opޔ���v��U��\{RO63I�-Nb+u�������U�`����;�@��ӪkmG,������DR*U����J{�gf���e���CDž�VA�b����:������枀ߨ -�m�rm%o����;���_��BmV�g�G@뫽�V��$��a��6aq*L�����f��Z���^�-��]T��9<�.��P���><"��a�1� ݛ��)P���R1�z�y�[�K���d���Z|����\�m9�6[�$.L54��jl��_��b�[2�� 5�+�w�V0�!�,�Ӑc�p�ٺ����C4�]��������⭳a��|]����F�ܞ���Aq�d�����2<A/P�\2i�^ 鼤�}nY�S�eq&�fIs�*���_�R��<?T�wŢ�A�$Y�頒?�"���.��[�B�Ww���os �ܚW~*a� xrP�W�8%��xQ�3wthk)��JA��������8�͜4���5jmg+�Z�E�{�O��P�wǪw�]Λ���Е]'���'!K#�����nw����`d��Վ`L�¹T:�\e�ԘEq����e��Y���54��/�$��Z�ty�[-&�N���w�VjN�By�:��9�" -s���� �fj��u-]l�� 9��U*�C�^��2�4�V�d�NǸ�,��F_���Z��b9��,x�@3⩽������q8�q�y�ޭyw��c0�g��}0�1S�j�<�jj����*1gwI�v��Fįx�N�ɴc�����69�ı���ˣ��3L�XT��N�%�^�3cs�\�^T��'U'�Ø�<^��^p����S�e,T�N�������[�O,X�m�z��1�/�2:.�6.p_�Wܽ�������0}X!p��~���Y�}6��:`.��X߅������p������Yv��MQ�L.�mv� ޥ �cg;��KȞq��0�6*?����O �Nk�ֶ��7�^j $����wXn`F�(b�����..p s�,,π1#�_4�D���� -4��F�p��!ܸ$�#>˛�Mu{�+��Ƿ�q�/�'�~o�t{�u���bu�5�ҩ;y�."?�� -6���v�o���M�����'��MS���_�DH牕��U>�z������� �¨?ء²�bO����Az�d�d��?�I�L�}��=Z�.���J$۳�����l������W�C\��N^c�����<<�ġ���|��2gz����ɹ�9�����t��9W��.�H���۫fn����o�ho������ַ�Q�|�N��Q17�]�w�g2�'�-U2�|�5�nQ��9��\��������=�(����;m'F��5�����)|���#�I��$s�Z�4��bo�mt��dnl�enD��<S� -c�r!:�K��U�g�N�����Na<!���5�<6%%R���w)"�LnE��Ǥ��s��� �4S���z���bOf{E��M&M#ܴj����d:zM�.�*�`��-�r����0���4��w��x�/�>�-} �o��a��Q�w�O:x�s����S�֩�귋ca�=��dF�@���@��ɃT��|�"M/� m{C����YWt�9�.�x��bnd{��ν� -]d� ���'��8a �!jt"`@�KnA�� �J�|�;G��+>��~���;@��# =�Po�%�I·:�)�,J�A�� ;�=7[?���>ٸ$�") W�uF�1 r�T�߄���ܼ���9@|��\o�"��������]�^�|��IJ����^�R�� -{� S�&�Ti ���dw���^rـ���rlnr[F9�e��Q���mC�����ϻɅ" �@.�l�������}� -�����;xx�W�o�wPk�����?dz$%6�P��#��h���p�Q�e�rN����Hc�d�2�[��e�]' V��,�xe��{����zX>�נf)�[l��I�`LJ�]e���E>��d�=(�&�T��d<�9�C{}�.@�4@�� ���-�h��Pט��Ⱦ��ͥ����p��t�JS�n`sA�[�߭!y�=͞�����O5�o�ı�-�?�O�a��A2K��'P���-�l.J�~� �}@�� �� �$����8�������P �J��������]�o�<Vޭ�����~�������?NT�����ݗ{�ݯK������x�ݝ;Ww�����9�|�2Թ�C�Ad��i%�(ۈ��[x���Q��^y�z���nK{�N���9�?O�yo,��^�7>g�q�\��`|ºsa�ukl�'"��ќ�a۟z#���|h�t�'�̃'�5�h�A 3�h�<ԉn��s ^�W,�G��>{��,FK�}'s��0Ҟ�S�yb��.���㺫���U������Z����{��m��zε&���r����ȴ6|h.��������l�#�D��?CR���H��P%��l�x����Z}�}�����y��˩k���㶘�w��nb�p����wz���� D�ᾒ����ܱ|��̥�{����\�N5*��pCc㘁�w�?dKzd�X �(���5����g���~�����E�D�V~�\�w���>o�VQ�#{�`�{�P 44m˘�Q<��A��2��TsQ�����k�"-�W@;��d�F���b�k�BU���?d�gXMP�2X����i���nA�{���B��:�&,������q�2���>�v}!LN4��L=W�u��g��]`�fL�)lM�c ��-�i�:���bܳ_e�O}��&p�#uԂ?��_� w�0�8C���٬;*�>��5�O´�{�&z�@�~�seQ���6y~����҂�������E��v衖ښYu�IC��x_�^���RI��đ �@O���9u&���"��� ���>�N��:���_��UN �3�x����,~T�ţ3x�ms�y�b���{ �}:��h��j��҃����MG� �xq*�c>��Y ů����Ґ���{�4��~-4��F=��@r�v�W:�@։��G�P��B�1��P�ʻc#���յ���|2>���x'�'g�m���ͤ� �?�CTa�#(�<.q�r���ǡ���Y�����1��w��+�6���Q+HC���}x �mw�ȥ�?�4�ۗa�oONn���ꢯM��G�.u�I����zZ=�|�at�����j"Ŭ����3���+���}d�5lt�o��p����h�%y�m�}K���*�J��. ��JCL�HikKi� ��k`Ф�S`sw��v�Z���y۔��4Y��%�J�Z������S�O���W(�n�:>�j+V�i����6H����ME)iHb%Q?��d�E$m�7g��ڜo�o�O��!����O��u��«0���e�T�._�t�'s�0�~fdR���5U�-P�W�/�d���*���0�z��[a������ U����h:�iP�.�/�f[k̉�ڞ�Y)���y^Fˑ!�b� N��'�jև?�%��B�[��”S���¯��d���1�EZ���8'I�'����r7�m�V�r\�P�dyhc�g:tkgЃ���(֚Ȳ�<�*G$0����I@]WD��v�<B^��w��.{\as]��$���8O�O)�dS��?�ޛ��{�Ez�wJI�,���8K��Z�2\�ЩXm�{OO@;�{=��l�K�^=�F�oZs5�*a�Q��K������Pk���Ms{n5ϛ[�r~�s��Y��^�--���8��d��'�&�Y�"���$��u�"S����(ݛ# -<0�뻋r�}dV;7��Q�}�Q�@�/�4��eu -��n���> -�u�����f[k!ڦYy�N>��}�� -�m2�n�Lb�;&1�W -�a��&ΈX�(��"ɱ�Bz-Q��c��Wq#?;�y\̏�={�q��}���n�!(R>�#�JPÁ�\Y���n/V�����㻁������+c��1b��jf3�8��lG[����`�ʊ����ŀ0�E�{���l�_*0�38'�C]�~v����:ykw��*� -9�G�c�J�B [$�]�:�_����n7�}a�u����Q{�|;��@�f�rm�������ñ�ɟ�� '��^>XJ�KR@�������Cz9Y��؏��Oӽ �� ik6k':S�"E����I��b�ާ��(���[6��6[���w��F�{���K6�n fE0���(�Y,W�O_�q�n�p�n�g -݀`���z]�6�`^��_�'h��y��Y�/ӽy|d��l�>�i'*�" z>nA���'�7[�p�/ms����� ;�/Ef&��+r����$Q��'m�ۙ7(�n\�N��E���Ei�͙2��u.��p.6��\̷�������d��ٛ��.|�=��htKdM�G�y8�0_y��Ϋ��$��`rW|Q;���q�*�|��s�cK]�Ә1��������|0�H��|�ھI|��-�I(�0h�#��4��z�8l��K�R�ϗ�;��&��M��A���G���[1��=�t��G�S6$-������V�w��F4N� J8T�y�U樈g��I�����3<��S�x��x~08��;<_>\f[�̨���/��1�O��w��:`��>� -�-;$X�����(`'5⼸�a �W2��%r��G$Mb�j -���#�o�8���x��ʳ�0��*�DN/Λ��[��� -���K��~�zڄC�ӄ�=N��F;f���<�#ػ���Ѡ��@�V����=��Ӽ-+��0��������^�ϔ97��Vo������?44-R��q�+]}�V>�,�me�(A��H/��#��)���a��=�����i����h����,:[�rB��ߣ��������|0P���b�pah�F��6Z�!=�?� ꜎Z�>�-'�A��)l��` �k�_�ݧ�a��^�^���{����Bw��t��/u��Y�;�,�X���ߐ@[H�~{_�x�Y�v��CsFws��ġ���螭C��>��/!��y9u�kmNT��َ���v�}����Z欙�w:l-j�O�A��i��享=���]=�֗�I�sa�"� -Q����ID%X����O��0�0�?��k����u�}��G�e`;v�����_`ZmAW�����n����:��X�nf0]y��� ��0�� 6~���8���"��E(.����|�"뙲1g9 ���M�<ӫ;��7��[�n��l�y���6�K�t��Ɓ��+��;\yU�X����2]�K~�&C�w�^������Dn����M1 ~ -K@�*� �Cs@ -W�mp����c��+F�B�_����і��6}���tm����eE�]�2Z�V%�j��iS� -�&�\�ao/J���) �,��t3e|J���CҘz�4�^2�>�Unӥ'�A�w��� ���+�X�&@�B���$��^����J�;���N ���N�|�����0���'AL�\�j*o'��5�J��I���j'���$��1��� ��{���(����&h���8N��i�Z> ��L��i����:W���L#]5#�� - �c; x}L��RT!��o#���S��'�,�$>.&���uBl��q����p#G�`*E�&E�^�MC��$�O�oR����tWA�b -\/���<�Jz��N��YMGH����,� �xH ��:�J=gN�-�A m��gP�S� @߇��3Hm^��Ry56���T �@O ��v��˖�_$�u��;�����(�����c����lҽ��a�sn�Z <��x�_$iELYe#�c�������G ��4>���X�����]�T�{m�)Z=��_a�Cy7��P���g�h����$^�$ݑ&�s�q���|=���,I��M_+%�&p�������1���T���e�ˬoD��x��s��ŵ�� `�Ϛ尋�{�A���'�`2/i�����LouLo�*�Wl��$+�'.�@�z.�k�N��y�L(1A��� &�\��qᝫ�+�V7��&�W�\�!���k�S���ּ>�f���O`N��` -#�(�H�����U�;^Y� ^3.����_����%��?�_��\�&��%�~��� bTI�p���hpLrs��(|��Z>��D;W��� "0�"����;��gR�<��c���uߗ(w�2�fZP�s�q+7Yr����i��������G"|(���Cz��+�?3�?O<��(� ��?�u�ɍ���>+����U0����l��w�6��y�Y�n.�ݚUA�<ʼ����Y�5�M���O����-�G�?�L�p=�P<��J3 �,5g-����7�H��#=��� �B-�5�I��vl�� ���8pBq���������~��ȍ+X��Ԡ{g�CO\c@R.���l�㾿��U?N�gx([5d� ��s���.��F-[� �m�z�f�]K�!K������ � ܟC�$&sN;u�Z�Gq��:��>Ƈ����p��;�9���}F�87�������2��{�8eo�r���� ��u'��vF݊5�-ւi\ۄkӰ}�0��P3r��� �U�8-Jr ���@�y���n漓Co��FGs���p"j��X�ց�-��T�8m���4=�_z��aE��Akg~7��i���W����T?`��1�^�k��∥-���U���_��&���-�����X�yu!�r�1~k���9R��:T�ό�>j�w"(�l%'ԭ& -�i�7K.nx�Xʆ~pLW��o��.ZЪ�n��\�S-�R���S6�d)�)of��%Kt��K# A�=���o���*Ob����#Jݯ�uƦ�ё�>V�u7�9�|�dG��i%1Y��W5�{�c���D?N�^r��NӖ�����ڐ����Xi�՞,�v��s���]�-)�����Ez��i��J�ݨ�F� -�I����r�!x��Xg����]��I�к:�x����~ �yu�8��2�8�j �T�e�Q��%�σ�����.Ev��9�-&��J������qrD ��V�� ,��$�G�����I�ӆ���|/�}.r��.:�����>�[+m�Z�^��S@?)�[��bN�]ņ�<��əfK�����p54E����`�J�ܬH�_H��x'v��]��a�"Kte�~�I�#��e�h�_|���6��u*Bp��'h�e[�1�V"��?P]_8��1'�r��ɬ&�a�'�=d,�J-3$;DP-���<�d4���Զ��v�]�m-E�g�ekif�� �U��v�\R(�[���Lz���Y�C���v�w-gAUz`�O�R_D�����Qm]��"䛀,������m�x�P�� '�,?_��iw?l�$�(�(m�p����M���^VO˖6w�cV�� �m�$���9�����>]98����������ơY��+9W��.��.�(=ζFF�I"a�� N���@�|�?����Η� -�7��:w�\��ּ��|�Kk�Qr2�s�h������?Y����˷ֹ��١���v�߷��į�[_����%5\.��,Ǣ!%��&�c$�#�i�/5��v��L6,Ѣ� ���dn푵��̝��¢�����!�^�]��af�� ���i<�^@>���>��sPh�Wkb�9�twx�������2�Ѹ�J��Xg��I [�? -�=�����8�(�O,&�=�fQ-�3ڳ{a�����L��Mqv�4и��?������O��[Hv�N��`�~?�z��Ɓ����l�ʑ��I�:����'����X���e?Ktmk�N�i.�����Z��>��� ������� -~�K�������^�C�[�/[+�\�e=��Hrr3�C�W��^����x<N�]�p�t�_�*e���结���#�q�?�#|{�yn[�����x�O�k�y�J�EƘi ��F+���˂������ї� ��۠W�`E�<��0�Kn�gh���J1�fg-�>�w<�ۣ��nP��UݮxWQ9;6�Iz=����l��u��Iq�~Ѹ��Ҏ���Io�/5x�S��(����L ������t��cW�V@���1��6��,��-��H��Up#>�(�ؽ��o���n��MV[�_� cӲJ��<�@V��m�mpf�z��)�M����ͨ���oqnM�EU�rtc�ըM�Ο]�&#ԕH�3���'Q�3'��Z!��X����/?,�/R�~��K�ac�c`�!2PB�~���n�M�b�E_^9D�\y��8_σEmX�ӷ��ק�Ai�Am�b0��ϼo�R��6a�� �n6T�+�>�A�>ۃ|��"�I����Mz�|N$!<w/*�4ɂ�Ԛ~�!}G�_������%�]� �`���.�M�ZP!��xH�U����-a�=����#1���=/�ƥҌ����w�t���J�Dӊ[@�Aa'�ꚛ\a��0J?{���ŵ7���{Z�uR^��.?�l]9���W�u�S����[�l�0+�O�� �iu" &�ʛ��Q�g5 e��}�茷��}�~Lc�i�K��B�X���FQl\F�����ӘE<�E��r�@��_Oy!��'�霝jw;{�;�6ۺ�23^�h����/��L�Tc��>Gf�����?�!�b�!xtQ{S��8Ƭ�\�$�a��m�6(2���6����&�����pP����WDu�_\����3���t��럇N-֊%�+���0���0r��ė�nBL7���(B�dY���u7�z �t�B��m/��γ�br�vD�����jm���-}�~��=���ڤ�Kz3��皖Q�6����"]ͯ�f��[�O���]Ș�~&>k ��^.��w���vu5BCrP��nk9�t��������z;j� 5�Xn�Wz� �Qx���<�֘eDU��ƶښ/� -���*]���/���e����)>Q�<��_��(���Rd<�C���f�7��R&7��&�>�/�I���F- �͜�WŇx�W"�^���jZ�+�<�-��ٌ�����녝�w�u1�-��n��u��7K����n�����H�փ�P{��W�?>���$�+�E]qr+5��-=f4��b�D�7�����T�m �˯��*�n�h��*�/h���4s�qφ�}�����������z�`����X??I1��6S���}���T��E����}y�Z�M�����C0_ ���d���젶t�V9�(�V�E���A�us�ѽ{ׅ���h�2�4�YM�E�,V)6������=�i��\ʱ�Ƌ�S¿���?V#�?Հ�wKA�)�� {� ���^�������ǟ[픲~ğ���_�SLY7�o���z~�*���/�3S��w�I16F罫���,ѡ1�Eb1�,�FTc>�(�A��%��}���u������u���MWm̩�����"�R��O��f��O�Q/���I -K%�x��** Pa�)w/z8��\���Jݨ�J����R�z��z��!�m���,��-���i��e�vR�G4�H�s��$%`�� 0 @��O��76�R��w�%��# �ؔ�[��R�MNn�T�7��#�K�W ��a�6�Ϡ�N�<(M��?}��'�Lw�f�\�>�����-'�RB�����'���pv��^S6��g.Y���#Gm�w\�]_lt�U�hAL̨�P����z��<�dTH�������=a�������%�O�/%��(_A��GN`~�fQ�iʚIy* Dc�kA����{�E�h � ٨rA�/v@�C/����d�v����h<{�������R/8��P݇������N##�v��ĭij����u�0��ϙ{i��Jew��'���n�WD��&�Q̈ �=E����O�n�-�|r���}v�4�=?ƭ*�_b�[���H��ڞ�s/z^��o+gS�����}뛩;����K�i�������@ -Q� l��}*A*hVU������.쬀�C ����濾=l|�4kx��{?�[�^�[䍡��+�Ӧ��k��?��tGڭx�VQ����a�����y���%���g��E\+A� ��H���O-A��(A��>ɍͫ�X��}�)�d2s�}B����յpj��O�{4���˴]�ʣ�ot�O�a�:�܂x$����pӽCi�����U�i���n�p���Om�U��=�E�7����+E��CEKrC�~m��~hUd��l��W[�f�N�&�މ|x�e�{$�]�p����tyt�g67�W��ܹ鋭��,g����e��3���\���f� Ncw���̰,ujX"?�ſ��"���B ���$�F�U��i�~y�K%�%���^s7���i�9h�B�;�X|��(r�^ ��Fi��M;���W�����54=��̺'s�Wc�n�"�S�|�s��.�4�d�j��;˘�"?������ � LrĶ��<>KH�wP��X���H^�����7#�ϱg;|�O{�3�?�n�ɛ�ϹnN&�� ����ᅁ|0Q�O��^��P[�rM�<r�ޫS�=��N��\I˅�/h�{ �|�3b�{g��(��9/�z�ݫ7/r p�?�JϹ?b��n{`<5K����9l�&d�u`���+���5�C���������W�$��S�:�Ci�@Y�,&w�ׅ��w�$�e2����x���Z���2��+V��Fx[��{V���8}��k�&�{��cK[Ќiʊl����w�w�T?�����чE�]�����v !��y��{�ʐ���.��mI�`(b��B��S*c����'�Z�EE�5A��3������ɽS��Zw�����L�}ۯ�]�0:�0Q[��X�K���V��Y�Q�S�g���[D��ϑZ�{�ԓ�Ȟ�I�1TE��� S� �HGy�.y�\�3��/��O�Զ�$�"���w��%����m�����ǩ��0}R0�������V�������Q��,r���]XVQ'8M��Z,y�0d>��nj{\��������_�<�6���� ~����%A�cS�8w?�����,�X� -��ɼj���n"���(�Σ�f�Lmr'V�P�%�K�x��&��Ep�+ H���I&n�~�ږ�as���M���p7���n�Hsk��dFV�Y���$7H���|��7�|�[g5��G���pW?�]@��Ŧ�c9��<���4*�-�X\."�;=<�J~��� �շe�0ج�W���-����:���z�ug7�Rh:��� �~nI�-��oiR���s����i��a%R]1����j�<Vŭʒ� %��R��lE��.K'~��>_�H�R_���*����q-D����ݚ ���>�ļx�y2�Cg�3~��H�R�OK�1����~����j��H� s6�-֡�)��ʃU�F�.�*N�/!�"������~�'� �������%����n��3q�e��r5V���2��FJ@���@g�W�$2��^�������3��&|_��ۉ���O���_�Q0=EmUڢ<��h�0,��i�?H�/��v�͉V�L���n炵���R��� ��.Pg�ju���r�`�%Q-z��-�-s{i�r���/�w@d������|]���r,�և]�q�Jhs�Y˲,��F��{��/i������n�r���ǵ��U��I�^�`�������������D.�+%��w���])] �4�1�~Os�}�D�?�R�_F��$���8>Xz�3�햩�Y��z�S�em��F��溰~ގYw��S6��Kv�\ -�~ۘؔ��l!�K��ċ����դk��Zo���Ns�*���RM� �F$����M�K��˖���e -�Ny�xڅp�`ڗ�^c�EK����S�&�ia-^�WV-� ct�5�m��w���r�-��«g/(v�/:�]7��&A��*s�P2��)�ڎ�7��td?�p�Z�Ⱦ�O�"||kѵ;n��~{�G�h=����b�EQ�課Y��U�og��:�e�����ݢ��&F��E�Gm�7�j��~.J7����9��L���}D�&%����|�ɫ����亘��>�'����J��#���낮Ʀ�Y1���:<������u��Rލ?�� ���������:�1����[� �=V*��������wF����tFS�O Ӓ7xL�|�>-m'�i���[�_�����PpX������s.8+!¸"�88�=��9�9��(=?goT|F}�3'�$3�>���zܞ�~��u��\�67a�uR�v�����9�a��lc�^b����������A��]��A)\���;%z��'Of�R�jM,�I����W$j���8{[鲴�����t>#]z<�5{@g���k(D����e�6�z���M�:�jr]��8����`w��ibBn��쁃��eCI�����]�Pk�bG��5m�w�֨yʠ�*�u�|6�G�����n�#�3s�p8ĀA���F�sO��V��i�Ꝿwo�1B��,���;[�:����۷�<�j�?�f����~����նhE)�_���U�����B�vj���7���9+��)Tg�8�$�;!�+M��se�l?C�ޜ��n�\��+��Jn#'��Q�T��ԋɽWsӃ����\(�Z�Wr�7��w�Se�Rm�����U6-���̟ć��P�m��3�� *��8>(Qc�����i ��$�ҭ�j |��Ԗ7�W�ޅUu=�J��P�*�~�-��IPz���4�\р���*���* 皙���a��SZ���s� �nk�E� T�~��wJ$c�B��M�+�6��3n_���9��Sq4�Q����zq�,U6;_��AI��D�KN��7�]�-���\�g#�q�ۥ� ��!���Y�S���>�2`�� ��n���F�!e�������d7�R����I&�Άo��« ����i�V��R���E��(ra��s�{i�|z�((��:�iʄN��p:a�U��I���Q��~J���>�i�;��A��4hd�1/���7@��mM'��ʍ��>Ga�u� �i��R�x�%�䴨��P�n�bx��^z���׃�� ���OYVS��O�7e0�w�?����ĺQ�^��Lް��`������_ı���8@�8�d3E���G�cc{�c��X��x��x����]�\�8f�fK7��z)A��(��b�W�$�#Vm��8^w�e�\ɌG.vyC�^����u�ۯ��@���4}��"�O�'Î'd'�'Ͳ�~�:q��)[(N�j�`�A��7dʃ���@K�qB��8Y�^)s �(���8Y�z)�$弌�,�����""�n/�C��Ue��������5�?�3�4�����G+���������}�O�V��E����y��ɪ�v�G -x�?��K��)!��R<)�j1�;o[�bo��o$J��|�V_'ssRx>[]���������{.2zL!n�M�g�_��Ncs�]��l���C��d�O�Z�7h���_�_#�����tu����|M+~����nP�Jp!Y�r�N�Umΰ�lɐ�����M�� ���|=o!>��Ϣo��ݱ��ķ�_�3q�/���_��K���3?4�Ҡ�s��&������&�;�;k�R�t_��,�,�� |n�R��_��}o?�la(~���z���� Q��ɰ��зE}������9x��.�.���=��>�⯇�Q� ;��g�P.�\+%'ೳx���Š1l��,m����1h�`4���!�i!����lҸj��^���m�}��,R�.����nbx�:ݰ|��v����8.�<=���=��ϗ4J��8�K@�;&�g�N /&��Ҏw��$��,l%s���Zk�����n?���Ľ���~�&�e�$�'A����g���t��)���������cX�-?��>���eQ������ -��V�����E�#�}�Z�7��%���I��3�1̔�QUa������NƼ���o5�Z_:����y�`��i|foG��y�L��9ܤ�?��ξ�,�37N�]Ww��z��\�u���g%-�z���Fׂ9Rvy�;�s�H�Ivp,k �� �� ���rK�>E�-��9P���N�,�W��羝mW�}tT�n�l�+|trX��u��4����0��v� --yݞ[hSM����a>���:�ke=�ݔ��uZ�E��ɖ�1ߥ�x��z�'�J҆���y����}�o��-��#�;{� '��T^�W�����j���cEc�b��� %&�}��L`۬3�����CT=4�ݭ|Գ���j�U��ޮ(�\�(�"郙@d�J`���8�}�a�k�3����:�������rn�yc0C���5,�YSC�s�i�6͓��ǞN���^��v|�KZy��М����Q���Iin̏� �lD�ܑ����$�o��:�_�{�j$P4�'H���o$�欮\��Gp���?�u�ۢ����3�Ykj��L6NZߏ$^/�{M[T/{��o*3BB��m~��-(͈i���K��a$i������*gU��C�g�C�Vc�*���^rM��vd� -�/��n�p,��z�>:�v�xű��$4���7�ꡥ���X[EJ��N��Y����Wɗ��>�$�� -i�����/*��S�h�D��y�!|��2��C�Y�E�YWY�s���ӛ�"�,?����� -c��v8����A���a���u�:� ��H��nj,L��܃��$��GwQ����YH��*����(w��j[�A��E�]ڸ|�g�e�g�2��H�����Eyvp�]���7C�_rv�1�U)��M̫��^�Ԩ(�ї]�IQ���H(�mE�{i"Ў��7TB��4������lo�Ԧ��T�"O���"�6k����ڋ��_$�3��<��Ə�a�E� �#i�y�îk{Ъ҆�$m�����b�YOJR4�vĄA�"/)�4N�;�wf���-�=�ے��lVH>�p�Q��5�%���q��>������Ȩ�bI��"�a��<�s.�s�'���A�uW��4+ �Se+{��Î,榡��������`i�P@��;g��5V���&�.��qS�n��z-����B֝��d�uu�����D���(b1�·e�d5�lY˨���Zz�V��ݍ�����}� -V�n���h�)��:[Ұ] -D��ZWy�<���j2ۖ98�eds�¦&�4n��u0L�K� ^��ZA����P�ƫ�d�]���u5V<d5V'͌FF}5��_�\K?'hVt6�O�8/���A(��^�-Zٖ@\�HU���߷���4헶��:mp �1w7ns���'"i�-�mVZ�\�}m<�Q�����R]�g������9i�q[�%bV����/~���D���?ؚ�J��mY���M9�f+)��S���Y�!��N� -dE�z#�q��l����B�;Џ��G}������ߗ;H��A���kd�N��%��7��Q����FF�^��lk�e�?��O�6w|n��2JZ��:�F7Jg3�E�_L�'�a6���q-���� -���Tab-�0����~��ke2�zy+iI���� *�E���r��QW�֦ؾ>�{ZϘ{z�3��n}�ѻjF��������y?����~fc<v�q���ݯ��[I]5i��/b����0WY��0��W�s�_�}��t��`���ű]X,JbmK��c����b��`�O������N��Mɞ�Wɞ EdOҪd��+dߔ��9���KX~:��)4��2���� +�ʋ��9o�X�$�@z���Kg�r��,��g*ѫ�M�n8J����;3��0�L�):�3�.�1\������/���c�(���(����g�_�����|2M�>24�9�R��[����u�3�||��Lҵ��RM����F=�w�%DJc�B@�M�������#�587t[�'��~f�j�,�_@���ӝ�;�O��t�G�i���O�jnZP��/���=�\Ӷ��j����瞏`��S��o�y�ftAӵͬ=O�t]".:���:>�O�8��^3�A��U-O+�3�j�hZ�_ɉ�ד%�����v�,Sx�u5�@a^BR؆ǎ�g����0��D��|����?8�*�3mI�i쪻���k��|˯�N/�Mb2���َ�צ�2��ŝT ���L7���m+�k_���S^G"�4G�0F�9Txy3v_�D�:&=��K���;l�������B������<ֱ�~��]M:(��� � ����@�ւf+،������ԇQ4-�c��ಾ�"F �����[�"���C��н��R�U�۽��Y�Qo\�����Wz�.4�k�N���ŝ��;w��bV;G3mR�Tߒ\�c�N|G�.��M����� Q�8�� �3 -��������Wk��^g`�B�(ӌ߂�����Q�k���an�1�s��p����\�H�}="�7_ԎK��t�$���Y'���kr�����~��*w���_�n���,|MFz��z�!�9T�|�.u� 7ӪMvl�y���������z�GN�r'n.J�a��+6�]Iҧ^i0�}�q�Z-�/s\9�m��!i+��f-*L� -�n%(����di�l����M ���[��=����ka.�zVX�1���t�V��H�9�=@�2s�·R -0d�yB)�r��h���s���,5���Ӷ<ԁ����9PT��?os�������C�#C�q�a^<�B ��@�<���ϱ�5�3y0�yQ�FHb]�����~"t�1�+�����K�¿��Ç�w� -7/���ayp�^6�Q�F3o��.�p^F,rL#`���)Q����lp�HS��m��������~lo��e��hPA����{Db]�F�Ch�ŚR�;� VZ%0)�N��Z�;����'�Zm��V���N��s)l-��X���0̿ӽ3�ߙS�����n�����6Ȫ�B��&�dG�����6�����>n��G�R�p�#����wr=���@�mzX����� �'�����'����6 ����Wo�K���+���醠��q<���Ma�~�r��x�٧X������l�)�(�g��mR^j��]�����)�(���2*�Z����4�}O.�I�{�2�m;�k������Cw`e��p���I�ĥ̗):_��4�Z������&M�Jʭ�F�&R6��ͮ�&ṟΰ1>8�p���3�rA��R�=9DžhW�jQn����9.���y��n���7C�����_�1�1?�9J�& �������!d�WON�YJC]Ίi�N�_��n�������#���M��6ۃ�Z��hW�DQNe���+gͨ�홍�u�oZ\�%ݏ����>�(o��db�����/q���q�fs쳗e��j��3���3�y�/��~��Ј� 6��҆|[}�yê�Gd˫����1<��5\����l���)=|�-=:�����6��ؐ�;����ܯ'�en���t�\A�zal��_��)�� +��c�<��v�[0�,A �s�� �#�����3o�=g�����EQU멧��������2�K����ͧ���Jǜ%��4�73��^�p=�$��!뢜�E��q�[���x8���z�J���E�r��~X�<D��}��VA�o�e���Ƭ���6k��0�ǻ_�OW���K���^Ԗ���|�cEf�����LwR3f�`�z*��`g������U7"Tw����B7�o��qw�����B�^G(����|��&��F�g������U��*�ӣO���_�zo����:k=}�2�����d����oى�$��K��?��i$�:Ѫ��S�+���rf�U&U>ԅ��I����;�r�����G�Yg��=����*�C�U��)��˥ć�����H:�������A�������;J�|d6��#[rn�j/��NLj��8�D���9��4��X�1�ve��BE�W������3���;�՝'F��I^K���,����p�vM��:����L��؋��lcr�����<��A|����:���^a����{!��O/Y�$ � ��7�n�Q�j~���� ��<7��7��� ��(n�nBw&'繪��3�KN���l�ͬ�jmY�n�0�5�:�A���K�g�{?��`Wᑩv�G�(��\N�ļ�.Ώ,�BC�x����k��j�Oٜwv -������M v�c�ʑv��"8c���^����y���~���*�ʇr~δ������&?aM��_�F�S����~�U�p����)F����,�ܻA-�=����X|��w'��B�O�Uc��ͺ�^*����!��G�����=E���'S�� n��|��Ι�q>���<��[�%�W�4 -�Fw�j��)7��s��̾�Ӑ���6���Óp�_x�ͺ�G��,�֪~���0�<��h�w�I��E�T��A�N��<�u�,[k۞��b� -�^ȁ�q -��!��n���B״�F���E3��K�nC�w�c>�[�����*��p��p� �w���h�|��e�vD]ߏ�|A�F��=��`Q`\�_�6�wV%���]�h�1�iȑ 귎?Ե�c�C� -����j�hb�N� ���=*K��P�<R�W3�g��*m�(���(����]ܑ���/>������ȼ��Օ�y���(����I���oG�K\��J�Đ2滼���t3�F5��h��%!���:;P���:�V����p��+�R��|yu��r[M$Z9����"g����k�d��>�������G��n|���-|�0����݇um��;������o��%��KC�gYu�%��o��RP4R�Ŧ ���!я�R�9ŝ�g�X8N�� �����;�?��7��� ������G��~6�@ݼ�evU�gK��'�⺃��:��Ni#躟Yh��(Rݚ�-��!NA)���L��}�7�R�a�T/��S�"���+H�:�/��We(��7��F��A6`ӧ��"�?����M_��]��#kv�W k�ܮ���j�z�|k�> ab��dF~���}�����R���V)<ڱLR����5��o�⡐Ė�DŽS��]�����+LX<h�k�'��%�. �3f����"�ޘ��ߙ��ye�����9?�ws��;9��=���u���Oe�P6�Ӿ� �MR�e8C��Z�V��x�� f+�K�IR�B����Uیx {R�=�ܠR2��v�bGy��,f���m3 �gm�&��'a��V�JE8���fL�������xk���"�� �ɫB�Ao��R}i�Rm����ɴ�J�-D�y��F� -<�j\��{��RP��q��}�ql<�6�,h�R�҄J�����QԜ¨�t�j�r&����n������}�z��|� 8����`������iؔ+���z�� ��.�]���܍�^�uڗ -;�d[lF�@�CGL��(zVR>]�.m6Eג ��1�8+�Y���3�Y3��gb�p� �����q+���,�ץ~_Z� �w<`�-)�i��Q�d��k��s��7bJs�a�O/�̹|b�1�` -Y>O�x�F�[�.�9��� r�Ey��Dcv,.W��� e��>��q{kL�2s�����w��.�������_\�y�فR^_팾��f�`��;k\�'�b��ͺ�C'��:��Y>�2T�]�-E���������W���M��n�3 ޏ����[�Q��w�Hs���S-�{�>>�� ->�u���$/ LN���퀟fr;���5՝��d5Fw^�+L�=���X��B�Mm��%E��ݘI��?�bӈP#�N��:3�w��)�(��B��g&�0 -?���6���-��4E�����'E�L�4r���Ӥد�A��b]����Xf5�T�S����x���V��3� -3i� p�N�A��v���~�-��È�˗���+BYw&��c0��1��8ڕ���a�塐�fC!ܡ`�?k� �C������~�l�8��B��7`4}~e99DƘ� !f�����_�m���<viMJ<�����qp��5�aZ<�tW�z�&eT�m���� .l���n�H�36�<5%���2�-2��?X���Z�U�# 9�0��Xͳ �K�Q���Q�ڜ�q�8)%C`ؙ��AL�[8�GH�]XdX*h��<^�����O]�\.��<.BŶ��s�Q�A�p`e�v��s�3G��glX�3�y�g�ݺ���&���/�i�ϥ��x��T�ߣΔh����I[� -���d��b����>�i�z��z�d;���{J�fW�^���wm��u v�m'P�j�im�K��������R˩Oٖ�d�ֳQ�ܛ�3����U����.���i��g�)�K�B����_�[w\����� 5�=#�� ��,��@cu������el���f���G̨�_�S�c��C�գ�mT��dR햲F�rN�ʖF�� 0�R��V�L��NPP�� ���>�K�֬;�UMd��tgS+"�1��7��*��J���g�V��=�z%�y��X��sѶP�b�P�x1)� ��F��)������or jV�F��8+��;�^�2Y!E�l��i��Q��u.C#�:����^��L�p�=�'��?0�L�Hl����Q���-�5ވò����kNV,� ��9�����-�i�Q-�g��|����4�i��7�'���.�L5@i b�i#�����/��9h�_�K�p�"��_���2�� Q�HKZ�c���DΩ*��]�� ��1'�'�s��n��Ry:�2?o���3��f��2���H�r��6k\�0�*/���C�������8���8�����L6�`8H.�6H���W�oT�KE��}�N¬��ޟ���ت�W���#��!0�v����9J����� �����G������Q��zd��_������;w� -e��WY�V�C�`��Xm�����9.���)OG�M,��F�!@Nl����Q���� -���j��u������j�_?���}��Ƚq��|�O=�����c�M_��v���|�Q�C>�o��7N�_�����4ų��Uj�� �c�"����|&��1�d�l��5��{A���{�٭g�B�X���~���<��~�\4�����³7:��������TǙwcv�����&��-��+�Hܲ�oy�^p/�]��S<��qY3�䫷�����+;�6]���H9�=_�UpVK��)�g���;,�|�<y#;z�����o�w�����;��GC�Zo���r�s�8��W��qкKqY~�7�v��jFnq���ڴ���q4�^:K�yV~���� �MgG�iH�K -�����QVc��M�_|���؆���=�'^YdӔ�>b��<�t+7�&n ���=ųSo���]�4O\��s�L��Y͗�ws�>'VNFZSf�>���m;��ʳr(���|�#�.�d���vG=�i����Q��ѝ�p���L��az�8h��M�a�h?.I�h��`�dnɕ�-�n\�3�wVCep�ו� 1�w+=&Zdu!�Mu�0}��u/�wL��nT>O\�h�Yϫ��r0p8A2)�����p��� -�����a8D���p��/��l*�k�s�Ww�c9�K����X|����i�)��\������l\���B���ۊ�,^�5>�KMec��|������M���#�Tf�U1��ʰ'zuvd.��m,[���8���B.%�/�&�q2{C��W�M���<��d�������\T8�9�w���e�_��mwX�m�be������L�5�2������TzEB�_o��ޯ�������^��'Ka�,��2v��Mt�w�p�z��dn;|d�����M��s7x�y�z�˖��v�o��u�c���D�#�vB�tB�3 �����Y}�f5���Ⱥ��֧���=�c��-;�����¢oK��<ׅw��Iy8vЇW�]Y���b���is�:;�Juc��α�p����J�a������Gf4U�UF�/��Ѫ��=է֎��W��u��<�<��d٭B���_�;��."ٰc��.N�pEΞ늝�H�"*��*�S�9O���#���*-�s�S�Z貤8z�K�� }������)R�EB�:�$�oV�fxs�F����� i)1w^��;��7g��{�͗�<�hZ~2�[���7 �WU�6w��dr��g�nD"�2Ďu�ϋ\NW�I�v;�y ��]u�)K���`����G�Nf��NY�<�Iң���R=�d���%�Sbbq�Μ;�J���4�֊��V�=�4�*���n6���q̕`C<&��=���S���54�x|�Uk�Ψ��*�$f�� -��i�L�r!�,�[s�`�u~���&/0�$�R<�<�E?(wu��f�-ꀎ3�(�Pzw�0{����z4�t�ntb<��S�����NK3�U_CzgT}d��jy�-JU>�#����~�|���%��.-UԲ%n�O��X�Ʈp(�_<���>2'��x�\I�m���p�we�ofV��M=Gٷ���!��X6�ke�_3��vo��b�_�BK�p7������I�#J.�a�O>i�xk��Z'>;R%5Cq���b��5��~H �V8�O���ej���Q�����{���ǢƑ����V�OAK�0o9��U�dR1����ӤSM����ѐ�ۨ��~R�&U��8'�A�"���wS]QP��X�b+Rd�� 6��%Wl#���W -�:�[IN�0-���N!������ѵ��VD���X��'/��Ҷ�}�0g�zѐ*�ݷGN���\Y\���ʏ#�$�wi����K_���6�FT� �F����U8^�D&w��kN�M.�*;�����y �q$~�8$�`ƕ�������mp;�����A0,���Zv*��h��SN�_��:�3��W=M.���Dw�;q��W�Y�=Q��Bg������ᕰ�pq�6��ØaӁ�����s��yMzW��m�LvFa���̊�5d����ٮ���x~�*&��X10Y�=h`�s�m�Ò�2���CGɇ�O�i�3��{z�p�C�ç'�2�|�3\L3e�KM65���xĢ�ŸcXf�{}I{B�L�t� -�ބ"�[�"�U^Ty^�L�����,����ƭZ�/���l�P� -kmC��J�!�%���'bS��Bg?��*b\l{�^b6�g_�����@j���z̄�P���w'(KTpB��W����$c$C�>0�ީ��������"n�'�����tc��ڇ��f��pވ_m.�J ->��$z�( GY�*����Q��G\�Cn�eغ0�K�y��~�X��z�M��)L�����a��l�i��I�fB+�!N�"N4%r��R�]���w~q������G�\B��W�'j\�Ƕ�N%)�^����dyo��ǐ��a֖�Ir�i���i��D�Z#c���UI�5;;��F�؟��� �4qJ�&�67��|N��?���0�Z�����e���gL�^�8?A��1�jTj���EG�=��L�~Il�'w�);|�ZL.Ɍib6�(J�:�z$�_�dC�_f¥���*�)�K�'f�Pr���F\���gS袪���� ��f[�ӽq�Se]l�ľp��>�q�<Vݸ���i����3�~gOm��mi[�K��,p~��ҠV�����}4����L��"�5j5����T��)<����)�qt<�c��b���a�v��,��;)L���V� �ƫdN�WA�φ%����nf����Gںg�%<C;8�)C�����m���J�"�$Ԧ��fђ{�uiN�[�����h�(�KJl|�XVo�'N�&ӂ����4&{�ָ|F�nS�z1g�kc?��aq����a�1��Cd ��f>l�K�l�����F^5���B���;���1*W�n���=�g�ܼ�)�S��0�\�;)փ�4_�y�#&��Q�QV��;���I�'��B��v�eope���L( �[@��C@��1B�}_A5����`��i��/��C䆢�#���QD+����+S*S5!�"��u2u&Ѩ�Ν��\De��\��@�'�T% 1w��/~"�<�����ZV���a\!Ϟ ������7��o*������~v!��@�v�����˴����Lau.Ϻ��-�W<j�*������#�:_3�*̷�r� �7w�������*�N�� ��4�]�~:�U�eo��oa��}�h�m��&�q�.��RQjߞ����..X-k����Lu�C�x\��w�萇ɲ24��xS:���)�p��5��El�¶���֣Zֵ�j �1������G\srl|2���(�?6��*P_sv�^9U����5v|�ԶL��+ڈ�WX���[����������LAC�0�7;p�KhO�xLV��vx+�'e��;d6�6rػ bZ7�QckW��⮢T�{��]��7h�u��p�����sPB{M���黢�5���� -�3� -��g+���z`1s��q&��S��C���v��6b��bı�]���.��Xi�Z���8����^�6p�(S����z 5��&�56s\��$r~���D�(������u{� ����4�5���������w�J -�����s)�|Tu�9~��E���m'ҵ�AL��4:@i�+:���\�+����*J��'팒=Tj,�� �J!@����&�wg ��Щ�y<�������`q�ga`����!O��}�ޢ��y��0ݨ3��X���/0�f���B�s��oN/G�U�Du&{Y��p�sUϜM����<,0�i��9&���d7�����26[�����r�������Ao��L�,0`���v��E`�ͧ���*��p3�/��f�]Q$�roO�>���~��Zn79�x��)=��2��%:�7�ҟ����_I�W���U�0����<U�AD"��,���E������&u�Sx�b`���{�B:Z�%�ß�av�Tj��5��5�M ˶&������R�R���x�4��T{ kb��b������և��0�p�� ��Un�O̥���S�.d(��h�����W>���M�W>M�q��/���X{��4�*������{1S+�rS4(�ɋl��[��d�ůI�w����x���U�#n!�Snt��n5�=�ʼ��J��a������O����Ua�_�)��'���b�����q���-W�$�T��W�*w�0�L���0u���qY��[ط���[��e�ܳ[��d����Y-��s��'��.O�8�|��C���~m���_��}��OQ�Z�y/� �G> {=�F�F�u��+�R��8���;�.���<�ys�g��ɨ?&������hC��r��(�gϻ�c����y��O ��t(5߸ ��\���[doq��=���k3w�Z�"�ʍ�eR��~ :�Dx�7}�����Q&r[c1��3���Dw?K�����O��� ;�f{ӏ��:i| _�� Uk���{ ,0;��[�A�ޘT���h���H��ً��,������Opp��������5w�:�ҡ�j���b�j����Y�S ;��Q�[zse ��������s�'#�Xe/���G�Cث�� M-����V.M�F-}LQ��C�K2}�v�(5�[͑���G��6�X�ɱ���O�><e�K���d�����>���6�=��i -�`4Ya]����V�Kp����Q��sٸ!�E�`�]Ųp�?�0'ȉ�/��_|����b������D��鶙��7g������|�Mn˛���O~�N�P<S�d+�u�k���z�/�N�Ѩ��¤��k t�L��B�Qܢ�9�]U�l�{^M�ǰձ�ǬI��Vn���>:!���Iؿ�/�T�Q�;�tF�4N��f|U::�Qgǵ��FO9,p1�[Mg+ӯ0g߫���R�T/���K��q5a\t���9�$�U���~���*��qv��M;_7��$�����7� ��܄�#�t����� �H�ɸ�)�GW|���P>��x��k+J��b����&�8`ս)��I<e���d;����V�Ok����yPD���I��e{��L�Ƙ���0���G'��2�wk~[S�:���)�"� -��������`�����:{���s���r���<��k �\}��T�����i?�_z*��u���Ӧ#4�;.�W`�f2��i��}��q�h�٦~�aH�gc�*9���ޗ��:hncŖ�����q��9��8ɷ��D����s�#�C6u�_�@Ç����w�8O�]�?-��=�X2سs0���CW0���4��o����./�g��ቦE|F�D��&��s�T�i_y���2JƤ���e�d<9��ҬY��k����]vځ8���a����f2�@�)sp�-�'��t͕<�"~�-��zr�����ƽmn���!DCVWʒ��To�&~�Q���<_�Xq��K�ևyyy`�2nz]i�i� 1���H�7a�6N��>�4Ĉ1�Y�<���S.���fWY���l���oote�������"�Vl�/������n�R燖�����^Y�_��Fw/��"��!�Ϲ�:`Ed��ۺ�xH��=�O/�w��f���\����C�M�E&��<r�Xf���߭������t��� �y����͗5{�'M�}:��5ؑ!��fJ���b�hJ$zb���"c�G�P5b��/��?��������!�])í�=�ލΜE���|�/3ʞ�<��zrnp��+��^]x��29x���E܅k6�U�y �~,#��&�c_�W\oԗ���I�n��L�Մ�p�֭��O:x�{�ɕ����$?�ޯ�*k.s]v�#C�x�I�A/n�������)oHJ��J���^��_$�E=��m���M��ZX�f����:���������^JZ�k�C@���*+|�?��w�_W$;6����`&�#2�����(:��e��80!�f�ԬP[��y�t����2af;8��7�/n�z�����U _/gē���3!�y��ҏ� -�z��e��О����f������&;����7�8�=0���K���q�*Q+W���l���M����H�d��f�F��&�Dt�ЄH�xB�8���_\�QD��4X]�����A{z�Ԍ�ͨ�p����u��z��g-s7u������x�Иl�pi\�Vt�����s{���SJ��j����lߦz3�y�fM�+R�X�U�>����T�O1�i_s�i�չi�_\`}3��x[1u]��dUb��ks;�,�TEo�L�Ng�y%�3v�1��64]�o25{��_���y$S�3�={��y3�5�8��2!�{��e/��*�`{��_q;X�3,]�<��i�:0��>�ᜒ>:�ϛ��;�!3*JUm -y�1cA�;{wB�-�/�� *����d��0��ڬGqL�+B��;�Kaש��SP_�;������=���`v�nc��'�ߚ��1��07�0v��e�_���d� �u���ɳ+�PzӪ��cqw�n���w�8�?).,W2L���P z���r�CO�$*M��2��Mh����{�;6ꎳp�k��v�&��M��`k�K�a4Z{��hm�؈j�>C�*�̈|�_vy� �`2]$5ylm�:��8�����Re3�H�Z���'d�j"���/ ���Ñ�Fb�s0ˠ}k���7�O��xU_]�%!z�6�rqD���p7�aC.?P�hvQ��Q�6���cPaiѨ���/��{��l}�:����:�TwX�J�l>���"�a8k]��BK�9FnX�0�xm�WJ�bp:�$;�Ue�[�.ް�o�Qр�hQ3�K1���>� qm�!�0�����������d�%�dQ��b��1��教�?L�P�>��kU��s�>��l4��2s�ɒc?�D�r����m2lZ�*P�l��@m�ؠ�\$��2��uN��[�{�^�>�\,B����e������E!�Q,l3��?;`AYLT���_��, B��)(�m��ݯ���F�Y;�Dؘ�#��p*͙2�mᡒ�a`���^o@9~���E�A���Чj����k�Ƕs�^�<}t���m�@�#{��+ϣN��:�]�ș֑n��/��&��w(��L� �?��¹o��Q�g��z~����јl�S�.qs��o��e^�F�p���sFg����J��$h��FnCQg�J�"ۚ���t�E�a��9n�>e�K��n��;�(d �߇�Fޟ ؝5����_�X�햨�\�Z#B���t�Pa�����q(T� �̮�M��Qn; -Q��Α�ִ����U?�B�|֘�]�ն�Vu���j3�L+� �*Uv˗�x,�F!_��dld�d��V$�*\V���s��~�I�w�-Eŝ!Jya'U�A�8��]4m��ID"g#zѹ���t������LY+v��(��il���� l��l�|r˭�h.W�r���!Yv Q��`a]�e��)ȱ4���ϴ�].#,�V�p�������ro(q�]9B�z�Ԛ�I)��>^�i�u%�Z�=|w��T� -Xb��>!@>�@AD�?�l�/��7�+�7�П��@�Ā~ �@�����������{�m��O��;o�0�M�hX�b��Ӓa��p8;o=�k���Ĵ�p�^�'n�2�`Qkh��x�ѡ7.40�0�q@��P-X��r�������T����b�4.*�D�!��R�4�%���С� ?�}��4����ǾB����V]�HS����G�yP�����=gKQs9x����>/�~^��^)��������m� ��Ъ �p�3z��:4�^x��p�n* �ab@Xe :��4���C��� �W�T!�Щ/��zBL`:S�������Ao6��n�)��=�إ���������hh��$e��f��wD3����G����?�����Ai���+u�TP$�4�l��`��*N%����7�,����¬-�X��N��v=�!�s����IO�C�rz��m��|�Xc���1�/�c�����7��Ƚ ��'���;c���5O���o����m��l(�_|E�]齃}ݧ_�W>M���O��v�o����� �]f�}��|�q��$Y�|�{��'��������g����z�����7�������C���U"����_;���..�L���~�h��}���N��?�Xeg����u���ft�+�@�צ�\/R���T=����S{�w��@���Z͗������u�_��O��|�#����bi�%��yO��2{/p�r\���}]��{h��֮\r_�h�_�<_&7����)�e�正Uo��K��Y��xY�Ϟ%h�d��/��H��P�<���=�����?:���R��t����o97.���VS�ӵi�ɥ�������Q�*ǔ�Gt'u"�����������d2����gɱ�sJ����В��5|�f��7V/c���Yem�����_���{,���?�<�gw<<2צ��xnϗQW;�8�A�n����PP��.D���~d��#.'a'�_�"f�1+�Fc^Rq� ��^�� �=4��w`z�;=\��aY�Ͼ炯���W��Z�&�o3o��vMܼ5�a -<�� �����n:���;����Mc �v`�k����8.o})G>��\�`�O=23�.C��,k��Y��t��c��6��Q�lb���b#��j=�UŲ���o���Q�8-�iʿ�@`5nakT�^��Ϯ|1�r@u;�^`��j"�f�t�����^��Kvhy�Fe.��q������f�^����~&i�dQ{i�y\ͭ�;�q=*�A��3�r�k�ݤc�ר��?�~��2m܉|.V�^�ֵՎ��F�Oo��*|$U�H]��F�(-���4���b�;1+��X�mͼ�=�\�6��w�R�\cR�O�R�\�C�$�5��B�ܨ[Փ:%M�3�z]�>�NUCW=|ͻ�x7�0�>�r��z��\{<��Gt -���癓�*T���]Hn;����if+@��N]��.��K��g��\ߥ�I���ؕ��`�w@�&Œ.搖v����cZ����¹�A��\Iv��Ǔ���,-M����ѡs�,��t7��n)��ߟ^x�ũl�{���#�ݰ�&��Kg�0k�gp�Aգ����M��.�n�)�0����F�z���PT�1�T�Y�3��.o�r�����{1�k�ҞCB����K����1�E���xsݷ���_ oc� ~[Frk��Б��Ah�}�a���焤�g�.!�]N��#[Q�wr�<�ۅb���PŃ���.��F>%�Q�6�5���� !��hB�oy�J�i���̋C�W>�뺏�W� �>�+�r���æi�ׯ)���j'v ����~յ -��7k��]��JI� -���\��bsmJґ(c$kH�Vp$B[|�5�v"����*YL��� - ���b~���A�.��7-^�:���n���1�[��\��w��lN����oW�b������Wl/��ݜt�Kc����寪)�ye��-[9i�&3��-�"�h��nY7�<> -� /��#�1���ן�*�]V��Af��$��Z+������L�C -��P -��/� -�2��r��o�~��.w�T?��2��^ef�qyT2Z6#���gO܄4,�� -&4�%͋�H�;n���\���(`c�س�S�2��?�a-)ѯC�CO��ξ���N�*�A�$Q�C�_��,T��,d�O�^���z�V�)��R��͒�����#�]NϾ:��myɠ�T,�bMyU������x̩c�d�³��ј���k���̐nli7�����������"J��7����ʾfuU���qD�-B��*7��u���3�eȖ�U�~����5�.}%�*�RYȮ���%��{%N�/�ؽ�iN��e��ez�s�tN_.(�m(���ȵܻ�T��Ͷ�S���Ҁ8���� !Ѐ;=��t*Hk�}�/�B~q���`����M��r���4+j��=�����J��[�G�{ -�֮�}4�1��tfT8"�B��t�V�)�T�0���Qq�jW_Ͷ��n�.37"*4B�����@S�֣���p���qH�4���1�C`t?c��zF�,�к�sԭ��m�h��hH���)ʭ�z1����ج9<��\h�����T sG$�gdU|�3��Uf�EgND�YbC�N���<U:��-:e\ǔ>��n6'Ys���/���ޘ8�p8���h]D~q/j㍽�AO��;��X%CZMS ���^���Q�Q�u��4��*� ��<Jd5�vf\g����8�h�S?JSE���~'����� ai}�b�ë8yu���sY�,��`���z�]�Et\����J;���3��� -U<�Z�Gu0l%��5"���{?-&3y�Ԭ���Znz�5�f���G��^�9p������\SUi���(s@�%'A���9a���J���z����c�t�u��i,�jԨQu�ʊ�L�JCz���4�V��&b�7��.�Dh�z��� �u���Wm�[U��[�#˛��C�����Y �lykP��q"��#�o?o��gR{�?As+����khv����W�m�)�Q��"���ߗ�$���Lk -�����dn�>�̗u��>�'��E�q���j��d�Hu�z�W��� ����b�b�K&Xwz}� &�1�R�|k�?����o�7�y"�= ԮB�Rm�3�[H��EqtOS� - y������K#_�����՛���L�kbؘ=�� �K�s�g�LGwH��~�t��i4�w�4�*4s�R4�,��X -t�>��7Tg&ޥ�ϼ_LM�Nk:�[�|5�hؗo�BGZ�j�߯\j�7,���n�w��5�� -{*l0�_�X�B$�s�L���{u�4�<ϩ�R:R�]�c�\#���3 -�]x�\!��AL!������k�F��Q��"����4���nӯ��̃�gx����_:��N��%n2�1tJ��}��^�m� -��%�Q�o��mW$s�� ���5D��!n��%ٻf�"�ku�zuw� u�� -�0��Z�Ŵ��II�L��?�Vn��;�Y�[��<���[ȷE���5w8�H�tĘ��6�g�!�jV8tZ��m�~bΧ<��S*��CV<�n]5�i�v�t����� f�0߾`0�FN5:߇M����� ��©&S��:,^�rg0�P��R�v���f�)U?�R�kD��luz�rI͘�3d��M{�5�]x(u�K��z�����g\�x�]�~�~U���BvGC�CM�ʘ�����\��Si����O�L*��ά�l7S���.�v����� JO�F�/��=������F��^zu�~�\ E��mA����Y��^�"Uf�-W+�����b�&�E����\���)�H�����.�9{�]��e);I՛ٜ<e�#s�[�Zz�NF���9�ds��?0,�vz�m$w�'[2���[%�XT������1 �W�����]ì�2c�t�l��/�ݐД3�s�Xj��[O��k����`�� `�� ��C�MO-�������������E������%A�J~[6����-�����a���(s8��mK����(�}�{�����J�� ��6�^�(��S���}�C��L����j�z�`���}0Q�ڀ(g��3���: �q�j��\��Et�[�_mǺ�X��E`��TE��Y���YZ:��b�@I�V�úU�͍�OQ)]]���/�p@؝�W�! ��S�Vo��2��I�c@Jp��eb�%ǒ����m������ �|P!���� -�B�NБʙ�=.�f#˒�֒ʨ$/��$��[����y�P+�J������d:��P��U�� ���#r���|��.�yP��;�j�Ā�O9@;M�;���&ۏk`J�Z}�N�m�F��-���8t~�h����ԫ���<\W+*^�R;[���I�?�%5}������%�/�}���N�S���`�R�E�V�耝�!`�x�W -rV����������'p���W��6���.~��)�KP>V��vV��f���p�P�h�Im�V�O�D�C�f9��LL3�?Z/����a�]�ҏ�w���߀�>�}�&�iz�7[��J�W>M�ɯ�o��]|0=?b5�~�oi���+�A�i�ߚ�ߣ��`�0���7y�����k�m.�m�]�>���w�s����G�E���/>����țȧ�7i �O��3���J��[�jwai�� ��n9�!^�cC�}��s��������Gݳ�X�����9�2v9s#}Jk��I��<�G��O(¿�>�E�_�4�$�h�?�z�����Jaq�閳������y�g�NW�G#1���!�1��cP��#��������g(���Kk���z�f߅1kx^,���I-��L]����$�-��7���2�O$���1R�~�WW�Zl>�����<������s"w�����ؾ�iw����(��n~ac����I���Aa����e�l�W���o�ι�-�E�x����TI�k����'z��M��#���R���G��+�Ή��}g�;dOm�Q���c��V���O���|�kx����l�$V��"��7z7>�H�ޭ�넜 ��5�0��47\�&zꊎWߵ��B0FNy���c������q>$.��/�����=�<�1�陸��ʥݤ���[�CW�T�W����"�b���Zs*?j�ɜ���2�$r<�f�|���^)�ȫX�0n��!qZCÀ�ӃGO��y�;=�#��3�G?=ui��~��R����y�q�6�O[�9�e;_:���s&ė�4��C�b�G��dC%rw~�Gc/E�f��q��C2_���#�� ���b����3�~>�����e�����ɨ���[��Wgw�_�f@?�3��ϵ��Z��l/����~��"(~����h2Wf�q�s?EGʄg��������>�6�?vlT��\��~�bD=M���^^��;^��T!յ7M�s�w|�n� ���^�c������|Y���O�^-{��q/mO��#�y�{5��2�N3n��N�J�W���t�q8ן�D����TOǺ�`5�A����������l�9bx���Ҽ}��v����n�=�b^���_�+�'���e�tC��l���Xhz-���C/�.�4v?�����i��dFc���QL�����u�'/�J'��J�8TιJ��D���`���h*�NܷZwq���}�5/C?��3?!ol5.^v���^ �XT��i��f�,�ϽmwėmT�J��=S�E2�}��FzW";�E����y1o�Yg�{�q�n���M�{T�;�J�����W�Wr�/�`�O����K��r�V�����-N6��U��&jB�j�8�g�WK�˶�,0���� R$V0�?-��@��)��ֱ�������ǰ�_���N�ct�/f��s��-�C3�3�w6�sZ�F��xhq'�勐W�&���ŊvKx,4��֬8���j��Ԇr�>�ݑ�m�e�l�� �YxZ��Y���6�e���mb�Z��}cd�7H�:���|���Z���.�Z�=�'�����~q��w���^��z�����S&��[~�O�ag7n��|���{�����茮�eC]�ӡ�����ÃY_�f�F������2t-5iKE|h�s ie��SV7�f)�ٍ1hgԯQy���Ԩ=�Nej��`:d3�[�m��g�R�]�ު��VW�9��m�Q����Ya�{Ǔ,.��k6w��}t�T��13rY�k��g��]/�לfvL�����s5T�z馸c����NEAa�������+!���e"����KZ$:t�Z~��]�q�؝=V�٧�7.�)���hq@�p_�L����-��������tm7����{����Z�V�Uu*QF94߈�q]�� -��6u���!Y�%]=�"%�,�ZJpbvdmu�R�i�e�TΊ{U��l�%!�GW���i��k�c��Ak���E��v~Θ�Ӫ�[ }n���x+�ڪ���ꮺ]:]�i�G��zX*^��s�J���&�*�,�{O|;�8XW�"/U!�� -�_�M[Z���ߩ�a����S�,[���^�ȴ���ж 7��: -�H8o�I�r��K*������=T�(F�参Z�r$J��Ŏ|V������͏��rK�@|���?߂�~G��Xo���qS��Q����,&8dC[��4ƣm���`q��,,�����2ֶ[+�a جi���:�<�[�%ck�5ԝ���Q�)>� -����y�Pd*�6��xR����+�L��8ڋ���)HY��_W�U/ ȑ_,�9~���^�A�K��� v椄�Uf_��(��fk���t_zz�:�o[�)�0E�b���i�2>Q�r�Ӈ������ �W1�w`���Q������Sҹ+H��H�z⦮����A�7s�m���٩˞�Z��:�2�ϛEv�� 6��feY Ó� ���]H+�\Vn�� mR��M�Y����nm7������u�ؽ Đ���{��u��j��{D�k\ݪ�M�_�v!U���]�۹�`t�c�N�dN��fZ�ֆiP�!Rt��-$X��=l��,kC%�g�$��}e�\9�zZ������R��4|iR_���ib�gb�p9r����nh��M�2�*�5�\��%�uξ�5��+�&6�� ����:@�h�&5��%��o���^����1�2Ul���ޠ�ճ�i?�Q�����t�����y�M���4�+�<�4�zy��-��o9�>{���6�zN�����wY)�� �!�H�A3�ާ�BH��&���If�JL�g�34�L�|Ex�L�'r� ��y�&#�c����n���s��+����܊�Ͻ�Q�n�o���2/q�y��Ϝ�(b��uM�J����jH� -��"9�)�41��"�RD_��.np����- �V�"��Su�i�B�Y�^�&Df�f�����AT:����̦i��5��%D��x�D�6��JW��Y��.W��[��Q���w�)9���@H��_B�/;��0��x�����Q7]�����H�6o�.�{P� -��O)�+�d��[0{�O0�^�av4J�l���r$�q���r���C��va�7�Whehwl��O�+F������t>M*%P^^�꺡�����R9�u��F����1�E�G��J�lU�U�j�mR9�ʕ\��Az֛@�y�R�JQ�m\��>��˼ʫL�U^��3A�����\���Ӧ�g+�L�� -�}'��^ - ���sY��T���������0^�|�V����V+�Si�l�Ѡ\��I�9tץ�������*bc�T8�}�@��J��K�<s�}�й!㧳#�s|l�sO�z�3~��P��bg [���ҍ'{��zz��0%�p{3�ҜT3�b�JH<k10`b*�+_i��#j!�b(8;N�t6k7��z�Y���k��x>L���"���s�:ߥ�� k��@�D ����K �g �h��{�����X< -��M���X��~����Vz w�:D��L�?|b�|�(6.H0�T��x�ҜV�2�̦��h*t�'?����Q@���k ����}����i��}�����-��[X���F�������]V� -^c_A�_.���P���^���9����D�T��0��5�z��!�q�+���ܢZN�8;�H�`Շ�v�RH���x*���l -p�q��.��� -�l�68$��+ܙ��o.��w���B�v.��.�1D#mgK�W_�$��xWtJ鏰}�u�7�ې��j�VZ�2��!�Rpr��vw�G�SI@�L�������1�O����̎ @�(��s�Z�����4������i�1�%�W�����0"Fk�),�M�puc������q�к���ސP3e�I�a1@|7�$6k���GW�U}öȸ�T����R|6�|�t�g��2M@7�3@��/@�?�^.�O��q&���J�#FK=��N�**��w�k;�7���k@ShߖV`�-+m�����St�=N���ߛh�{������?� )d�����Y�����=�z����R�]J_��q����{ 80��K����2vL��V-������1wm�� ��m��ϭ,��ͮ�?5%嬗�&u��s�߲���#�2�� �������<_�'�8�)�<]}��)���NĔD���O�]�~3Z�T-�ɾ��zْ|k%�k���"�ۤB��d�[��G�9�ͮ�)�\��D���<8oY:�a���h������_���~��V��:�X$�ZO���ߌ�$e��MS���L�n�P��k)�Z�S�e���͊�-]������8��l�b��;����xզ���Z_ܾ��G�٧;���w����+�]~�d�m1�y��0u��|ZJ`U�*����a�.̗㛼ޯ���t�@����f��}���v:*�J�������;p���Gp���^$��s�l����@Q��i�_���bٮȯŵ��.���#��^�_�K;�iɥ]���ԯ���.P�s���vJ������ߍ�j�vK��86�stZ;)��:��� -�7_�v�N-� ;���vi>@[�ٻ=�I�v����N�M���y�Ҍ�Z��hk�$��/��|Z��rf3s���ʩ R�K6�ŭvq�D��e�>��ռ���̹�?��U8K�� }XLs��z���~��籕}?G[���k��j� c�j���j�|b�O��}'����3ܩes��<�:�{q5��|���,�Tt*�Yj��qn�)iM)m�ps/�� �"b�ݪAخ�Ð86'������8��g?������A�&J����(���!(n�Swe����W��3�}�7?�[f�[\��h��� -�kS�\GNn��6ƍ��km�W|�h��`@��w/���3J�o��MD��)��Uw�㯘ҵ����mr���܅��m��X�<k��̺՞�u�dw��U~.dy���:Գ��a��T�̱"o�&��] ���l�φ��~䬞����M��R/OT�`�扠�(lwʹ�n#?7:;?v;M"��z��k�u~�A�2m��gp����K����k'W&�_]��^��I�ע�8Ȝ�k�cg6"em��a��!�D��Շ��s�U������%(/n�k�bg�%���x;�;l�0������=�7���n��17��Cy��O�؏��{jXS�� g�N� -[:������1N����~��ǻyx� ���������C�0��@e{3�� �vᅾi�c��&�h�c�bϊϏ�l���/x����j�k����4�àY���ᙱ�;Ͽ36"�Xß�u�����>���az���>ϟ��lﺊ��nZ�GaxSdw �,��#>�p�����������K��3�њ7Z[//+ s��_@�%�F���I9�F�w�jM��}Ҵ]���U�o��u�CG�֜��_ň�5l���H�>�.wz�_��{�X��m?AG��P���S� Su��EF�"���>Rt��� �>�^v�����ۡ ���cGT�>T����|��C��.,�qz~�V �M��`�9h*=�GS# -�#�Aߺ���G9M�ۯ���K'�r��ji��Z)J���֚��y"�����M��n��.�4ݔ����z��Kq�"�nB=�����Ơ���e�u+ �iQ�I��9Lw(=��v���ώ>j����cm�L�Z��~� BI�v� ��&;��c��d��?~�����]��l���0��`���k�i=��^���+3�Y�m̤�9i�-���6��kz�`s�����g��O,����k�KSьnrǶ�XTF� -ҝ|�y� ~Flݴ9��i�$�Y�)�N��/>�9�x3��3���������(�i ��S:u�i�c�>�8���g��s�Cs�����"���?t�����T���/�V�ՆˬZ=P߉|e?hZ - -z��jC��kk��Y7�ne�?���D�B8���>���t�6�{�C�6 -����;�~�L�;�.��R�ni�|cs�ڵ_oP�m�`�2S�,0���L/ -;�䩫�Yk@�;�����!��/Q9.�h��N 2ѡ�{�\���}qP�-�w�¾W���#���w����?�m?�=f����(͕r.>if߫\^�6��^��9��]V��̛/��r)��d�5�?��j�V�����}ȱFgd"�Kҵ�֤@�Q�c��ŁzW�w\v�������7�+��T�y^o��[͢g�Ć����A�֝���-�f/���3}\̄`ND�O[)s�mn��m���j��7dn�h��\P��)K���]�d������Mb�tJ|f���7��` -�1Al����ߧ@]�l�z^-tx}Ɏ�51:r� ɲ;vD��Ho1G*�f�U���ڝ9܅[�5�������LϥC��&~��~?r�rь���%^ur���Id�8�2���i�q�6"pܳ0��O!�O��i�Z���\�_8(�5Y�־�s���gw�l�"yi���П�Nw���k�� -�%M��;M���&W�k��@��~{��/��1�˧յ[o�1��RӋіSk����<)1^�AK��UX���Z����������/�5���e��7˜`Ha��֤/�x@S�Ԛ���'�.�(�GY��s�<nd݉�d]�^,�ɦ<�v��k�g��C+ʶ�ͭ� ��:�d����29)Q�P_�$��z��7xc�w� 卹�l�f����"���y�f���ky�2�a�.z$շ�D�%���.�z�3b<�ވ\�U�5�p�^�~����R�9�l�Ca�n,���jS�k]�����&w����;�_Wg*�m,g�ӎʜN�&C��ݕ&���J/�Dqa�NF�a�&�"1�=k�|�|N( -^�L<l�BcZ�'ԩ�(��H�A�}w�FX��:�bZ�����ɀ��3�q�J�1m!Muw�u� D^Ȏv4g[�~S{F��EA'E���IW�����u���t��J�=���fu��}�E��(r,�y�3f�}���8��i� P��DR=�Q�{�aG�AF��6<F� �[�2�mi���=���3ЍI�����.��L�7�C���F� �La�bН%�hmg��l�A��>��W�E��Tw�mW��,b���צ\��2��bEjN=hW�P�������ˋ��./���?";(�(oD|G��}��!��;�����ցV}�\�Q���v� �'4�,o��Q"?��()���K�&�jj` �1��hބf�w��Qy�q�2�Si3Z�Kp�]*�:AQl�Z�j��<��|w�C�n�m�/+���.��ֺ���*�`�J��ȿ�j|?X��d�/_��/_���>�"�CX��Z�G�*|V -�֥�J��.��j�ЮQD�x��>t>��<��5s�d#??Φ/�&���5� -endstream endobj 30 0 obj <</Length 65536>>stream -{�i�^�S�q��>���.N�]��ŝ���SƩ�Ώ��cP�gK�r���`N��W�ҜOM�3���ǭɜW7���w�~����S���c�T+�@��[&���S��������H��>hi\<�d���-@���+ !�s �#������b�.@�g @}�^$�`��:��������&K����i�m�':�(��ض�gkL��U��Z�jS#�g�1Ha��#�9��}��.�Ec�-���:@Q���bP��_��Z�*@�~��q�$�����5���w�@�[y� 9go��$h|֚n{%�7���?� �) �RJ��u���O�bݪ��~��Zx4����^�ˤS���e����ڋQ`�a�q����O6ޞ�6����I�l�2���"�27�=����N�`q��������ӳ\*��B��V(��TsKk=b��M�3f�ʌ����D."zp���O%�9a�������6��{����'|�l������s@��Z��A7! ��1��>X����(@�Z-@��G@l -���0���^E����$L��%cAe n��vL�:�(�t��RnT�Q)$��$��G�&h���=O�?&�D��2_�ײ��z���r���+��eP�QT����T���4������b��xnE��O1���!�2����-Oj@ �r��ډ)�����oM?�$���8�U�w�k&���|�>�7� -0��0��w9Ol����K,���+�T�ZV�z�`[� �R��~��g�-�"K=�P6��t�rs���)�ǫ����%�&9������O~��^m?Z/\���V��: k/4�O6*��H�oLokxJ�f�d%2Q'�j���aIFk2��}p~� o��� 3%�ou��ޣ�Q��͝z�@g3IF�����d���"�� -���~:��3��&�S���ٷt�D,�鯮����a��?��X������6�]Φ�{�>o�r����n�Z����y�#�-�'��}��.�c�=�N�8�N����ju��{�H//�à����l��V��wx��$�a��d�5�@�I<���z��7p�=� _�mR���y՞^�X[����oEC�r"&���1p���������w� m��ެ��� -CbuPz� -]L�e�|sA�ԟ?|o;箥�f1���C����-��[ΪQ��c��SO�hm�ٮ��*��.ג��Y������@���r�lg���ݲ�>q�ι��6���{��٩��B=�&�+����"�GD�G�v�<$U��㥘��4��?t迾Na��/+��8�\7s��N>\+�-� +/��Kb���S�f�Y��i��'s��M�ѭ=�2�`�@8r -��^����Z��¸I^B�d��1��7�^N����y_�ؗ�%�/���S���AZ�m��`�BǷ��^��g��ש|s��j�14��#'��"�,�alsT؆5~x�dq`�:��-k0�Z�w���� ��h?�)�����^K�U#�t���k�l�Ҥ��M�Щ��[�0y�� -^��,��Z���9��8BW�2$v��0@����x���������2�O�y���3p/7���\�蠸������U���=hu��j�����w�k�x�K~PD�I]��@)D?�s�0ARb��}Ns"B{��2�=�1?��9�5��B����\�ݾ�XD=,��\�m�+mz�Z�㵳�v��S��ڇ�%���T����Z�M"D���a�4{��8-\��휽+��LsZ<ԛ���9F�;tJ��3{��W��M�|w>�Pilig<B_��0���;��ބs���`.��������#Y���)�iuX�Ԣ -���k��� -����d+^}�`�U9Wzu���\���-�sLU\8Pa{���Rm��a�h��"�Z�rf�p���4�� -�/:�Ea��s�TPE{���E��62�ASi]Z���eC�]��=����IZ=���6�L�n��}�Y���cY��N���8��8}���ׁ��V�73xU*�S8��ӆh����.t�d�uq�͒ ��䄛�Н~�=���X���G��AӪ�8�ͷ.�� M��Z�sc3h����Y� -[�2Db{gH�v1S��_hb�{̢�k�SU�dm�+wÃ�5R�y�Kc�g���7M]�@5��J-��n{��~co�ݴTj�O#g��JF��t�n�H�����I���_��Y�9��=ܞ���I�.�b�Nu������&W�r�k�� �r#uӺ��$=�U�֑��*;����9+�Ұ��6���!�˨�}���Q���2��N �+�'v�ڽ>H�t��<��֝{1@��:�W�e^�8�N,4N�bú�����ʑɹ���ý!�NW}B�_��}�e���4��մb�R�E�e;ݛ��U;�!n|�.���zQ��o�������Fx�߫?�_��=�lzu�� 1E�i�S}��sn�0�����V+��������[F:���"�3=_qw����իW��6�V�GA��U�J�G�_����l�R��|�'�[���? -C1��E�>���.����?����y��M_�]�U -��^����^h�\]r�V�፸�W��%��TpCl� I] 箦_�}u��j儮�]K8*H�ӝ:n@�kQN:?�D��xOÜ�:�&�]OH���>NK�zV9�xu����X�p��d�-n�o����L��!�cZ��a���9��h�Ӛ�ӌ�=�[�L���X��Ok7T[����'��쎃��Է#��<�2�������M@엕��^e!�'�P\���zv#4y ���0���z?�n&8��̳�VO���.��/~�}�i���%�j�yʺ�8S��.� G+��c��ڴ��˽�ZU@]A��%��%uio(���L��ۍ�:_OB8.ޅt���9_��0�g���V��y�f�%���a��l�`0�w!�<��֤2��Yѭ�[&Xޕ�s��_�OQ�Y:�kP�n�5�7�� �O'ϪV�wʊ�cK2z���<������Ȟ ��q}���uy?��s4:�Z�}��Q&�Y� -�n]g�]g�ɠ�\�nsڂ��^/*��8���ܦ���,ɇ��.�K��p�}GA��;:[u��~]oPC�n��mB3�����jY��BQ��T�tW$!��znwl�s73��ڌ[E���T��ݵ�3l ��X�c��D���i��n��Q�i�&�$<#�^x'�7�b��bc8&Di��j:�+� B~^�u}� ��������� �}I���~3�JW/���R�OZ� -?�Uy��J:g�M��AÀ���=ws>��٬�irK>��f)�٫����$�6/RMt�?���p��.�"�V1Y60�(�0�ݜczl��J�;=̜���9ۑ��k~�r��f�& j��tե����Ku��/p�P`��.����b?�g-��#>u�P=�����5�p>ړ)lw%���Md�rW��-K�:Vn���h�;���*!����\H��ϐf�:M�9ͤ@ߘ.��מ���8�[��E��zQPK��U�+�e����7i��e�4Y��������L��Vy��¾��L�j��&�n�-v��~��-П.���rׅ�}~S����j�����M�W��7��;��e?QM����֗�\���o����Y� ����-df̗��2�or9���!�d/����F�������@D�E�+�bE�j�|w _������G���SC*Q}&T�۪)��W��rK�]�[Lʍr(נ#H^�#HqQ(лsU��� -������džZ�Ţ���4�x�kl�閩�,�r�O�[9z�ⵟ����W��FT�_��Z��]�^������yyQ��%�x��\T*U�7Ulfe�p�N��n^�g� �Ut�6��������n�TaӬ W������}S����2��U���se2mE9����^��*���b�N���K� U+����Bᔒ���������;��4w��}��_ٔ>*e�M�I�`�N�Gz�.��LZ�Rbzw�L]�3��x{���g��M�Nh���.hYR_���bu�0���k"'#���K����T�\�^6�F\hq�m6\҇��p�L��eӚ��E|����MA���;��l`�|0}ت������dC'�?��,�ٲ���0"�گL��ܓ���J�Ci���F�#L����!K .K/�b��J�Z�7@�d��Bk�:f�aq����$�F�OP���^��+ j -�|�����-��H�5j������@�L}��U�?�z�&e��ߗ���|��B�#��k�c �4i���NYj�2' -��!-(����˕�[�{�L^�$�nH)Z{���o�ԣ@�4����5���.x�����3��@6 �p�i�d��2`�Lf�k>t �0�F�j�ү��"�,��)C�:������}S���\�f�ޙBa|����I��Z���e�@����\�=�t�t������U�b��1>�� 0NZ�f��^F��������˒��Xkj�RJ���ۇ�OEٚ�9FU��i�,7� -��T!��W_�u�7�n7O�����7�s�iVf`q���8f�����}&��p���R�����@��7l����Y����#@P��3`�Λ��d«B���U_aԈa�����]cݻ�D�-���^u6���C��?��$l�W��I�8*�� @B� E������y��*�k���!���P�(iiJ�L�0��F��4�����x�>'get�lů̥�Q$���)��p��Љ���w���L�+`� -���G���9��O0V��fd.���S�����o~b���7�����d����ҳ�D���hs�;�9�G�+��S#c�*���/�7)�����?����O~o��U�;�NGY�Qmpc��Yn����v����ڀ��5������k�M����O���4 ���]\���q�r�r� -M�.,;�mu���*�y��_��qY���ż,��-����q�M����W�i�F�����cP>������nb�M4�� ->��O��V���3�L�tc��&o��k�����<����g�OTc��"�X����D���o���i�k�r]-���z+]�kxn�+�tR�����=�VU�m��ok�G��P��d$�(&sY���"c�=�Yk�/�6���Jꮫ:�[�O"�O�äQ���4e�k:f��2b��F��=���v`��+5>l�u��b�w:}��$���Q�����$�{���-���� -#���^�SiK�����3�u�a��ۯ�+���-��\�gg��C�;�?ڙ�EqSHm��v�X��г��FU;MQ�9�Us�� �d���a��a���G��v��մ/W��Y��c�X��hϾ���/���d�����bcr���I�Z��;;�Y5;fc�G���?Hu�j�( - -��1������^��۰�Vy����ߢV�-}/��.�))Ν,��:���mٚF6ٜRII7�XoB�k��_�gc�� -��_F���)�>\���0�QF��:�}����V�T~������\��:�~!5����}��z�X��n8G��_�Sj��N蓛|�-q�E�����Ы��|�������Å��5�`�k-j��&� B�������6�E]�Q��ֽ�~��/�]�3\u��2�?���,�ɉ̄�|��\ȐR����Y��Ȭ�O=�Ԝ��Bo�^J�@x��#�]� ���9z��<��f�3H�����ž���}�T'z��=���z�X��ϛ���Ћ�毲����՞&�V��Z4�^K���ɟ��|z�H��H�t����x=+S��)�}|��mύ��6�@ӓ��˓7@�'��"�_����G���n�z?t����y��{�}�b/�/����\��Jޮ���Jni���&�z߅���TK��a��5\�0��k��Dc��|�[������2�VNz�I!���p��?� ���0%��>`{T4U��mjv�����Q��D�:jO{�i[��Ɍc-m$�[���G]3���;��F�K6��7ZT�r���Щ����7>Y���5��-����a+m?��w6W�#\�v�Y5��Aɷ�S�?�_L����{mw@��}b�8Wb�3C���o���{a��2߉[^��}��5��5Na�Ѡ��ǹ<�y�֯����f{j� -�����)_[??���a#�:���T��Y��%Wj�߄gmӛ��1�3�oo[�O�_��J��_���滭f�x�Q�EQo6��V#ҧ��|�%;=��t4���!ٜ.<�`+�-��˛3h��Y-��%IՍB�h�t�h;�i��TT[�PU�گ�G�����3G�^�����i��(�� }o��}�¡�eq"�Z�����y��F�x�D5�isM�{h]k�y�%��9�[�.]��r���������N�ڸ��/U \e��F���������IQ�]�u�ս����#��r����J�:��پ���g8�z�j��� �k��o�i���ӹ���=�ik�?*VF+8f�\�+IŽ9ӷMo���^ۯW /E/�t/@*��zw�U괙�۷!Ǭ0��`{��� )AZN�a7��H��7�T�58.��4f��IM�iiW[[v�6��g���~�ϗ5)}��\�(ctD��Lݞ���/����������-Uj���׆���(��(�,�A�@Hߗ"H��[R@��5�a!9�T�'3�r�ˇ���/4���9�"k�#}�h���>*��m���`�Rƒ�cl�k��{�#3���WCS=cܬ_��n�7}�3�"����k�e��I9�3)ۺ�v-q�F1M��\��L��� 3\��.��F�Y���l�~�ޮ����Ѻ���QkR;�c{Y��N����,��P��F�[D������T��[�<懾,wSifWR�{q9,\D�����V� -e��� �X㎟�Ƒ��e;�h�V�����_�q�?}�k�>9�Ke���d�|F>�x�δ"�݃O1����t1.���(Ո�3S{��E5��yE~?>�,�EC�m������#X� -�Ţ֙��l+�;�߸��<��2\� �ًժ����^p�Ǽ��� Ꝍ�KZ���3C���|��_��%<�'�e4��i4����J�\�k�[�pS;���s����q�F_�A 9����8Q����V=قM�<~�)���O��\Zqm��g/���ҕ�����Ic�Ō�����.-7�yu]�U�2Uն�UU��U��RY�Pp��j�{l�f�\�[a����%5`��k|�W�˓��ڮ�X�C�`�4��#C�q�kp�٨�Q�S��.#�r��[&n�"&�/�۞�h�{ǫ�:)T�N�b�� 6���N�r -�o���ws@�g#��o���t��i�Lj�o��W��5s�,:֔�z��d9��U�}v_~�x����������/�^� �b�P�-1������`LO��%�-^�U��ܪ�s�E��sb)T!-�]G��\o�%��6 ����5ߝ����?���3�J�K����ږVaͰoB�$,O��W4�ɋO�!w�������}3�"SbĮI�3��ie��%q���jW64<�X��T�����M�D���q|�:���*~�U�a}l�m��{�A0��w=t�!u1�v&�:�t*j�+�f��nUK�֬�W�D}p+�$���� b�B�w���~�4=��ת����&8�+�2�P�f��0VT�V�X$)zm����D�����b/��b��ɠ��C3&,"�]�]^��MY�>��pS�ˏ�CCH�4=$WT��R��z/� -Z���u�]7agʩWuF(b�SK�|�;c�wm�<�t���+e�| ����RE���>P, -{-[,6j-T�\v&*�g�<Dչyy�u�e�^?K�0[���d���W�e��"%T?ŶW勧��Y<�&��7w��4�nf���1tmh`��˛?�]����%O%�(~!��o&C+&�$��ѡ���;#�]���j�P��]���j �]�0(�0�͢'J�b�^�y�?��o���:����RV��@�,�@�l�٩�1��-c.i���b��R�4=V�*]�Sƥ>%n�m���Yf�����������Ŷ�CC�&n7�������e��!�ܘ3�90v��LZT��y$Bz>��6�Δ��4�����7�;�����]t���ϳ��˿�M�j��� [D�ٳb�5���5D�z�U0w��F�"[]]������т�>��u���N�U);{����}�ؔ�'������s `�J`�@�>�&��I>z��kɻ'�H$��l��۴E��/v�'3n�`L��g@�"Rni�2��D�~h��1z�y�����;�R�V�]�ՂV�!����[�%�GZ������ZD �D�u����D^S�_�箹B��e �7���/�X������/g~��4���Cc�=���-Y�p2�e�43 h��v�<>�{_D?lp��Dy�E��P�f)�ƲS@ԉ9 ��E�9�ID�b����Y@Bm=�W�Y}����\��H��&o� �+���d��?3lsХCk�*M���rD/)���a�eƉڙ("��05�9�7���A��I������0g��Cn�U�a@�B�sK�PF�T����]*��3H"P����u��"5�D��|��|��W�Bc��/m�o��O�*]\�X����4��pź�\�x�����q��q��*���B�/HWV* -��c����R���ɠr�5@����ju�UY�&�%A�Y�)�l4��:_��܁A���@ux߃j��t�~2�_q]���( �fųAU���e;� -|~�ʹ\r-�����п��*<)����v��ߔ���)�Tw2�}���u�}L����b3�����,90'\�L�$2;��\�Zֹ�sti,��R��o�WEg��2���RS4���_X��2��X�1�h�i��/����j���j�؇�.[R�FC��WW�5���� �%7O�q��)^����]���U���-�px2��l!f��t�?��_:Žic����r9��O�������/����@��������E�4���D&w ��������Ƨ���.g�7���W���״(�/Ȗ�z���cMԄ�q��{X���}��ہ��蝫�˵>^^���x�K�'�����U�?���v��fS���)������ӐIZIi�r���¢� �|z����&���\�7|x�^۟k�r���^�4!��u5�k�4Mn��Ee�o�˷�P�0ҧ������qQJZ��J+B~��/������}���V\O5����w��\C�.�\�F�I,N�Fis\���`<ƻ=2��;|^;�^�z�^M�w�kÞ��u@�շ�"+h(W��gT_��Lg/g���P����:��Kͳ��Taz�<�����W�BOWH� rm�������m��S7�A���ثi�i��A�������`�J�yX���Ȫ�J�e���\����Lz�?z(;f�A3a�S<�NO��TY���y`2��e��)څ�\�m���N��J�ξ��p�[��ni�,��ߨ��e�U��Ó�1{�� �V ����;�F�w�HUٯ��U�웰����U8����u+�������+�!�_s� ���������_����Y������e}:|���4A�.�Mn��/��Z�!>�����<W����M ��d�� -��Q�۫]��G�~:�1^<�6�@ˌS���G��]_�����|��|<�ҫ��T�3;(-rr��ܘ=W��w@Z#(����Yu����xҲp��~қ����G=�RK#��]�������� ��s]؋���-����R[)��6�i�-�|ݵ�}]�R�5�n�n:���G���zA��LCyCjv��C��A�S -bɌ�a���r� B�J�V,��f�w����ҷ���k߾�\����i�;��iٶ��o-��ʇ��];jҒ�[h��43��\\�e(Ӊ����h_���ݫ���ƺ�Xu�T�n���J%�{\��ѷ��$�����?��F�ս����ό/�~K��dõ訴��h�7g�5���ݦ�?�&r�ơSZ4�;�u��9Tl>�؇l���- �f/\S�������,F#��~ϭ�VFї�����F����_`�7'j�;'�2�n�a��'����[Z~�w��m���DURl��9g�n;�R�m���L���M`�r��5�@h�"s1�;�mf�بG٪�MM/����V~�e�i�DGr�_���i�\}_yrk5���k�{���������QK��U���l�RP�71��6��w�Q�fDR�^��i�f���:srZMH�M�E l�|#{��5�ë�x\R���I/1Z�3��Kn{��G<�e?�[���Tk�/6�k=�l��Ase�������]��6�~h�ۯ�s�U���dg���֤��� -���Ũ��t��U��%��k;t<���x���A%&�k��_�6�J -�W��V����K��K���Yߖ�zme��]ulb��|v\�Q�o���U�=en\����xsO:ٜr �`�:�ӫ�K���}T�K���5仦[m�����C��m�w�Q���B�?�;��-/2���� R,H_�����.��RG(!�m���MQ"_��_25�ͨ<L� 4��s]�H�M������xy���jd�������A���GkL�z�UJ%m��%F�r�Le ���k���#�<�>ol&����r#��,V�%/�R���}+u��g��;�m?�K��R-��Kt�ZCx�Ƿ�9h�%��)"�۸Eg�p_kR?�ܨ|ַ�h��99TO�U\���r����Z#��(}UC����!Aި-.ē/�g���n�7�GީH�0d9"w�Y�����6n��X3�w�0~'����W�Z�y�f˵����~�%y/ׅ�&���_���?���s֚n��R*V�ʱكdD�4m|�#���x]X�hS0�v�_v�wFi�w�W�pN;�{�n��@�3m"������7i�z\Р��Vg�X����L㵹p5�l{V��Ƅ�v� � `Kޔ����Ԛ�ӹ~����P(���$) ���, -�'�Ro]�wJ,���s-*�pĕ��C�j�9�����y^��g2_z<d�4�$o֕�BV��άR��W�X*1�"��J���#�����fu�JYt��r�� 6�j1����R+��Y�Xd/* -�%��V���w=4�cY�w�Z-�ʞ/�Ve_�ݡ�Y���˼�o��'Ș�@��$���ؾ+��X)�4MY�I�erL6���l<�U�q�D4�l�Dcڷ/���*vb�B3��-��a�sPJ˕�4�H3*ދ�co��ܚ;Aޅ��V�O�6@ -�rQ�q�DƊJ�eǦ3����l�[�bHȞ��'���<�W��A���7��mQx�*�U�:~��Ƅ_���Wl/�����lJYYwY���)S�-��,��B -��t�c6�a/X���ҁy����u���~��*;�����j���*ƺdPa mP�M둇�;&��tCxp|ů�J�O��T��q��;i�c*�s�ut��hh̝�q�<�{�`��xg��p�4��C��R���)\xl��خV�01X.�ɫ����ͥ�V�� -|*��o(�����+��X,Id�2ᵖ&~��\�7- �g�Y`���`�~#�g��dyG(k��+���\�F岦�R"�Z^���7�������L$[܊��4J��2�Rd.l����d�Pc��- ��E���L��'�!�H�Bx���q��l�e�i](��Xt��dT|�,dfY)���AYǏ�ҦS��ʙNn��Z�q�E��)��[�+�xT^�X���v��6Ds�2Tj1WF�6$��}�7O��p��E����ۊ٫-H����jxs��g��`�32;�_HN���>��r���%�$��=e�p�:��'��-���Ѕ�cRe�>����ƪg��e�yg'���p8��}�����.�:v�b�:QQi�V�孝Bq�T��ʡZh϶�q�-��|4E3�wX�� �qb�*���_~Z���{���?Z����FAr��Y�Nb��Bb�C���e�y|����ػG �.W���+g���<��flO>�"�%��m�c�g���I���3�4�����S�ᵺl���4�� sZ�rI������HmJ�����B��g���2c��=�+@�[P��O�O��bJn����7���=T¡D�,@�Y�����ͭ�D�ٲ�y��,�&|�%�7xV�a�h�{koz� nԞ���������7X�B�+��g�_��)]rm�A�~"�<�q0\�>��ɟ�D49�dJ���`�+7����Q��P�K -�KW`eYXf`��;7H��u���!�fR�9���棰��v�މ�qyB��k[ -_�pӂq�.lz`_,����<�]�D6��0���vOd�^)�1�Du�W�v"��i�p%'��^F�g��sM����8Yk7�fv`n��V%CE��\u���,��REV��@��Tj`�m����*���(u���_n�#o��� r^,z�%��P=͘�+��) nI�G[� ���bLM�ͽ�+ -�p�-@4�ȞgϾ��f�.�3Y���^�"8%��De��ƽXT!�d�)�~��q虶����|�����HK��Y�9�'�xk��|l@~-?�w(���������P�*��0��T�$r][F���M��|�\�(����B/�2��o���{�*���%Ƌ v����l�P����85����}�C��A%K@�YUA��ɠ2���rg&�"PI�JPy�l"T^����EP-8*�|�Y"���*�t��F���F�ڥ�o~��\+��N�0̷ޙ݁��j[ʡ-��{{��v�������?v_�Nf��:�5��%��q�s �$ ��su��ÿc�{8`�+L�y��ʬ�Y� -���b��F'�����Y�_t���� �_8t�TS�����l��P��%�P���eW��n��Rq{�7���Y��pĦ8���7� -@c�b(�%r�����?:xƙ(%Jt���[ M��?~�Y{Ӗ���'�Z{����w~�y�<��~e��. -�#�\ �MB{:B�F�\3��$Sko�Ni���7��,7M��'�{ıR�ba�_�"���O��Uk�\{��"��9�����k$b�+5�v/��::����tv;M�L�����K��:rnh�J!oj�M�����1��7���k�\�.��8mOw�:�ޚ��oFpm��KR�燦@gv#棠���o�A�:ŏ�~�ڇ����z�db�C -��Oh�u܅>���UVSn���0� ������/��MM$�����0�(G��v�}�q�J�Ι]�����Ջ�C�8�2 ��j���՟M��c{���X}��� -���R}�23���P��sd�3��M#���W -��l���dS�v��������((�������~��>U�m����y����NX.j}i�u�x�ks'[7g��gn��N#��?���d��Vc�>;{�᭣��t��#h���:��������K�瘼���ퟭ��qg��K��jJ��K�q�/���2w2��_�>Sj7�&������>�����k�[��U6�X��Y8�����a���;�&�o����}�G=-�{7�����{)�(r����VjA]��0;�Kڔھ��ýyAܹ��pOGr�\ �{���o���Ah<$�����}�����v�أ�$E�=�ҥ�5����œ~�0n�ڱ=5 -�6�Jk���r��$����7�AXA�����n>nBy��R� B]P��p�.��za��1��wu��Yu�acc��z"h���Y��Vr�WZ�3- >ݵ���|3l��&r}��~���_�e�;)��:�T�SI�%RS�jJL�i��AP��P���A���>�;gz��Y���'�I�� ��Ӭ�V���г�h��VD=wm�z���f��C0�&rV� �j���8�����Y�{��Z��Բ�>6���iv3E�%��]L%5oo{������-<m�of���*�n���;�cx�����V2[���ۂ�6/�����ơ����8Q?c8^����Qε{ֵSrh���[a7㥵5}v1����PP��/Z-Nר���{�J3kfF�_!����J�4-�F��{�����W+������� -ZZ�ܵ1�-E4Cg~jZ�{���sv�;�l��/=*A��y�9Z3A�p����lu>L� ���!����j#3)֏Z����YS9wşV�B�W��=B��J*i�������Q�?;<�=�����-�&��Q� ����3�7�w�����ے�����Y3@�:����+@��w�^쨖fg�-����&{��O�kS���E��@��[Tf��$��ˎ�%��8�T`Q��D��(w���a��[�İ��7��@�?m�an�C�qp.�������ꄾų�М�cn O}5w ���fBC�+�6o���sD�{yĬS��Rz�ϵ6k�e[�ǖ��%�@R��J����K -���AaǗؤ��J-�Me5�cy<�HԠiP%��,���Kw�IG+>����Ff�B]F�tS\ݸE;���w��o=�)�z��`� �� -Ӟr�S��K�^i�� I|�]q&��� 할3��_����\Ù��D�Y��mk���w�j�A��u�(�ʲ0��@Ft�wu�r{�&��>l�V=��g�5 ->��v��u�W C[ֽoxQ���-?W���SDO9R���&-Ύ�(֓GX~�P�#�����Ȃ�&m#��vqp�zXf@��k���P�&r��QR��#�����6�k�����2�������崙���(��Pkx��Jb��1СRS��<<z�4��O)�=B� -�K�J�`��o'�#�p��kq8�k���bP�f�Y�֠��q��,?�h������v���-+蓩̟�J"K�2?�k3 -OF�p\�u��u1t����L9����P%w�R��D!ϡ4ў��"�y�؋yH�0a~x{��s����3�F��z��\�Z�^��L��Y�˰�?�3 -����V�"1W�*�*�P������/��g��vX -�6j��8��\��R�6��&kM��K�N���Сr-��W�HPa3��xć�r�#� s����<�V�:\�y-��T�����ϖ<��m:��M�Jɫ&}fTY��u�`�/��+@�s(m���S�X�ps$�.�Z*���i>K�wҨ~�Ys���L��������� �=�L�#1?���>c+�ٱ�;�̓�}n����F��S��΄z��|[Re���J�q)�� ��]�X���@-}���a ǫh�a�6`�R��������:9&PF�ޫ/�U�|Ԏ�V6�n���=+~�K -c.Sڍ�C�Y�VԘS.����ֶ�ر�4x�_�:?�Ts�\��Jm� -e 1�����ۺC��.~�vS�:)�ArЗ�CQ~�)�yi�z �\CƯկ�@>ؖBF���uq��%�X#�;���_߫�,��Xй%�ƭ�v�}f�:�i���u;�������+eC���k2Dbg&N��oa� ��{���:�V�-t4�F�'nY|>������P2,�W��Z2�\Z��ji��%���ri4��ײ��2,.��~z��.�d�k�𭗟���^���= -ɰC���FG�ZZ�p��c,�bl�/�@Tة,2Ś -�'NY��%3W\����p7���\�ŤZ!j���t8��X�;1Yl^�Ԁ���m\ڬ�8�ؔ�9���A�b �� *��W�ә�ޯZ�O6g_����Ÿ�z����^�Da��� -qY�5���:p��)8|D��Vt�BҜ��o�~�{��w�͵��8)�Y@?W� <|����d� ]������2�F�� ˧.s��6����l���K�VV�Ր<HB���G�-k�.�6�������]��:Q���g�#.�z������%��`�R��A����,��I��˟�e�r�\bPΔ}`��+�t��La�+��7�ʙ���1��ꞓ'�O,o�;m�C����zŦr��k��q�m���cb��`����rݬt)���.��T��eVZ�)&#'���,(rX"����3�S�7)L^�<(G �7�����'�����ko ��1k�7g�>pNyi~�|�N'��{GzZ_������<D��C��w�6��F~`�9�:n���( ��Id>�8�H�vH��H��D'�����r�D� @f� sN�S��d~�Ү �1��ۺb��X�v�i��W�� ����'���o�X���5q�foR���0^��<ۮِVh��q�l�P���V����7��"��qʡ�z!��QENd�h�����: ���:@{��}.�FXh�Hs#T���g���{X�P�e��;��WN�)k�846b'���� ���r"Uof�����i�,3��V���Fc�ɝ���`=�����ۂ�ЉLU���N"���`�\���<'hu��,8���B"{ӆ�C't_P�␒�����C���Ǔj��C�My]��E��r�=������uo�Bz)�M�*����G��o����W����k( ��k&���(���������(�. 2������%Eϳ㙥u�To[��I<2�ڣ��b�'���HA��Ŏ�_d룷�W��F���c<��f�h0@��_;1s6���'@f��e/H�����6D@v�oJ��1 � �T���9@�x�����H������2p����J�Ǝ�hk���c/O�nf�,�k�wwY���ӓ�O�~�Oƿl�?�o -~S�j��PK���$�Q0LI����ZY$r����"����F�g�����|ޚ�AE�:%^����4L����$E��� �T�v�j2��w�~m�Ӝ��v㿁oz�t��4����/��3�ο-@s���Cz����v� -h������I� z��'��v�^}�3�4֚}>g?sR�$.���k�\={��9i������S��6�g���4�c2��#�����ŷSk/%�A-(�Am�/��{,��5�dd�d�c��%�HĿ)������<b�Y!�"���믓�7���C�������#]�?�������)�d��7� �vh~=_������Rk/%v�n� ��f���aiLo�O� o�7��/��?�9�&����B�����O�{/&5\����P��7|t�_��f�����k|����>��>�.��U�0��vH�.fJx�Z�c��!����t�3ي�̤�����sQZ��i��#=�x�x�|�����РǙ]�H��'�4��k's0�Vn�Lj�0Ҋ���*��G���u���շ?��X�.5p.��C��m��cb�����oM}}�g��no�=$���y�g�.�Hu��T�K��G�v+�ꂷ�xw�p���=|�뀈ǫi�0�������Z���CV���婇y����� };��A������ �FJv��W��<����.&� j����|ġ����t��i�W� C���X��Ke�@'fv����lũW^�<P�ks�M�hb��ʼ��U'^��h*�#h��Z�����|���a�>���N����3�]��&���p�^ɗdX4����}'37���I'=:�}2��F_����]�UV��?VY3#�y�����]��x�4z^��H���tL=;��Av�̪��r�G�'X��m�R��/�������A�ɉ�����j����՟f�d��آ�ئ���]����k�|�ҏ��H�N�}S���r�3ڞ�]�O����Z���3sM�pw�K�i�]�I��.������<��, lH}�)�M��{��߁S��[6����L��b�����_�Y^<�]O����T�zm%�[�7ni��7��k;X�&��7C7<7�s�l��lp��q�Pȱ{rwf3YX�U��� ���ʿ��?�7f#J�d�-���GΣ��x�|���\� -2��V(���ʸf��7�fUn"�6^�l�ZÉ�Rۡ^|���M3�1o-.�;���1EX��Sg� -ƹt�nt�JW���eRI�Rd�/|5J��O��颞�ϵ��5Cg{nZ�g�D����t9�B��ŭ��[�|9_�8�@CS,І1�h�Pʪ�/�͡�����G �c�ey���a<��]ٍ�Dd�hYARI���S�^�C{���ZZN4C;��k;Q�8�}���x��|���,��b��6�k�J hW(�V��� �h&�F�i�T�5yzͺ�O�J4�ΕJ9��e�+30DJ��5$��X��<x�ATϥ����f_�����l��e�>k������ίC�L�f[�з�x��b�<2��y�/��V/dѳf�*��k_��6r*����c�ՉőV:�SP*��&�e�)=�@�p)�'���A���qBݍi~y�6���o����p[�,p�\9���S��'����@�}��{��ꓝXB&���6�J�2�ú��zW���joh�m�x~�V<�m�\l+�T��|_,1�-,)�u��� gq�(�"DpmA�#~�Yox���8�����P�E�ѪyE���'[��j���b����WXf����\_-C;GΕ�o,a�L 5r}n����ڴ���=$�Né�ݜCyp�E2�@_R@T���Ts�T��"�o����h!o~�:o�7����P��n�j�nqanx)�0^���;�Ҝ��W�� �ȥ���fC ��V����y�6g�wN[cq�fZxӆ�ۺ�&u�n�����Ǹؒ��5�D�����Q�V67A����{A�c%n'b�(�i���y�x��ZG�5�[�05]м`�����fȑV�FԨ�J�+'�u.3)�2a.5b�}�b{!,� o0<[�r�V>��-L)��'?5�%�ŗ#�ƾ!*Qq(�ڒ���=_ε~h�k��/�x~���T�]�;R��^$sH��Mk -=BN������nVQ��Z�-@CP!m�g�哽%���'��J�J�Jǿ�[�p�q�����^F8t�j0�<\�{�$a-�^�:B�jZ�5]u���>eOBq˒!y�u��܇қ��,��vaZ�'x��81UY���@e�-��<]�P0ך��x�'�!pmL��-c��pJY�x{Ɩ�v��Gs��t}n]twL�^�j��N%g�B��h I�~_� �������D�m�[���s̗��w��|?�¤��N�d��t�� ���4+���d�n59gZ�[���Z[�_��0�Wo�M�P�.P�c<Q���� ��2���l��ǒ��E//��:��.��b>S��qjq8j8�k����a�A �߱:�*{SY4�?fSIn�7jlc�4�Y2�C0��0N�[Kf���W����u�k�*���%�$QQ0��0������s�u�>F?O�-eE����k��\�9�R�+i��1�a��aw�+1�F �9��DT -�k���e���:��9� ��V��Ą�K�c��1ḦHK2��W���0YOj�<{�;�&�gV�wq��Nq�,;���cq3�yK�-�&R���@QYL/�&Un��rB���/�=[�D#�����:c�� ���C�l�!�N�{�M��E��� kc��`��K�H�F\K9�zu�pߍ�b���B��ܺ����0h�\��g�ɔ�Ún �G�}nTw'��7g���tbE*���8Z����my@� -#\g���l�k�E��}@j�;��a$���T#"�(�y�0s�oc~cBwc�]m��ߡ�yW�b:Z��E��r4���v��m�U�zd�E�;T�h���G��p�Hd6�/�k[F# JD+�D�AՃO�m7��}����zB�̍�Qσޯ�2�s�#�+؟f6˕�/�I4K�M���d��m�B�-�ђLҪ(>�8f�L���^�d��+�sP��_�=�cd}:A+��9��#d�|���LGV�t��}�N�3��4�USy��xvx�N�6��X�CP|��\���+J�4��$�%waO��b����ߗ��k[�u��(�9 ��v�,�$T1ml� �p���p2�2��8H��Y;m�7h�<͠��ަ�)� -� -���X� -�K����(���^�����(��W�g�k�][I��X�To �nis5 -���+�u�/ �S�_6Ȁ���O�S�DBc��V�T<�W��{�R�b��%/*���VrK�a,� ��2�� �bY��`�l,����αh@�2 �~���T�+A���i~A8�� ��ڼ�Dw��C��*2�{�9ah����;_!�ұ���Aٱ�6���B{U�����ą��;��ƀL�ֱ䮱� @B4ˏ������vH�r$̳�D� - ��6�)�% ����A؏n%ê�5ˠ��*i�R�tqȓ�J���m��OT[G5���߽��T˂�>R�Y쨀��m@FL)� @� _�,��P��(TDb����m@aQP�4���Tj -j���'@>��;H>&r)���y�)���T��^�k�|� p_ -8�����w�Q���6̱_g��_ŸM����-@9|P���v�����-��Ti��4�:Z6ޱLq@+�KT�ʴcY��U @s��ˀ���X�<���],�<%�Lʜ���3�o��O"�[��_z5�qb0�6[� w2�Jjޯ��f���M�����|��or���h��F���)�R��:�����2K�F1��0a��h���S<�h/4e�6Z&c"V�t�#�L���Ӎ55��&8��F��_����A�zL2��?'�鎺��lj�7���9m�V��~�ZE�GĽ�Ie;�GpTi8�Ʋ��W�E:���:)�~ -9����Y��@F�������`:~��_���mqo_���F��"���/6�m��M�s�9N���&~Q��ګ ހ��$����=m ������H�ɩ���,1�_�Bܞ� �(Orf1ӕ�$���4s�i���c�|-#)һG���Y�������1�/��~���_xH�|$�:����F�&�p���v�b�D��Z���mUЙsK��y��!g!�F�9zNyQg�ˣ��C'��� 'H5a��%�$ -���$(�@)�@�Jo��i!����|�U�RׁJ��@��m��Cg��2��_�!!�%�4�����m8����Ӝ�%I1����+I�O����]��9���6��k�b����z�A������d/������J�MةSN��jѓ�U�ַ�>9~�b �'��Õ~+��u%�+���ʣ���z�t����V(��]����n"����q^Wa�ZxB���$�!�� CM��KS�VI��(U�?�p�sw���u�K�K���|��:I����Σ�J���~H{�݄7n������� -��k�ԢCW�yķ��P��Ӊ��'s�O�g&�Ӱ��7�@��ڕLR�ψ�4x���mN6�+�(�g>ℓ4��N&��[�ka�R���`� -���5~�yĺ�l(j�Ԗ�����x���crF}�lF̊Z J; B��[s��ջ[�"���>�ͯ�/�M��I��"�+�H6���s��f����\6����=�}|ά�L��Jz�A*<��U|x(���`� d`� ���?j�laf�?0�7��RJ�.<���L۫���&w����b�9߿t�VX��W�$n�?��j���7��Cc�EnaT��C~���Һ�[-?��Mcދ���ԧv�| -?v�NRӫ�������W�\��V��ć��x�"�<�Y�)�KV���{�sp�F:��� �d�֡u��/�Os���/�����FY �Nt���m^�`S�n{�㶶�b%<T� �V��V�qk�_��5��>�.럑����T�f�G-�EP�T�9^�v7���]!ϳ�_U���<�;D���\���E�$��>������O��w���lJ�Z���k�:��-6���>�z��s�<�(T�i���JeUY�+��m����e7������M94�KY~���M)�9� 7r' � *��g�|�?�au�'��_$98��hw�u� �mJ��,��̰n�Ǭ��3���g���O�+�f�m]�}��^ �|��U�r����ߔ�w�,u,'WҠ��F���H�Q�f��M<��I���c7�d�eMg�E�����O��Dl"��<Jɹ��N���xd�Q�[Ŗ�F��N�>s��<����C��)ݢ۱����}��k�T��Y�h��JZLa�kB�kJ~^�y��:�*�|����U���aȬ��n?�����8+X2ޚ�� u�������}�+���3�3?&:!����U=�r/���Œ�iynd/��g���ӊ6z���Sx����5��N雳jRo���ܶ#"9�����o����I�y�2-��fǽ�s��������t[$�YAvҹ~���|6{��p�`��th�`��^��n�A9l����S��W��(B��Q�7='?�\��*85����mt�c���tm��[a��N�,9#�����Ɨa#�)ð7�>ܧ,=�+^6?lt�E�����T��UvtPVj�4�ev��tI|�IBG��lͫ��/_��q��R�N��|I�Y���C�����m�|��ņ��]�n�F��ύW��5L�q2�*s�s2��3i�juF[�NJF��ZIg��K���H>5�[9�T)��=J�c� ���ZƲ��rz$CQk���(_Z��OsT��U�0[��ٜr^�^9�Q����ѬVm�5�g軒6��T7r;2R��Dw��*;��٢��j�]橑YR+KS�r�S�NS���qN��S5I�*��kkah�3Dh��<j��</ci���D�=n6@x�^��'�͉U&�<��4r�M�0(����'{Ӎ�r�5 �V��Y�OzW[YőFn+s���7*������n�ϛ�G��'"�^$-�^��c ����ƣ�#�����ͮŬ�R�{��:���G"�tz�i*�ɭJ��m�٭L�(�9l����;�UL�5p�4�����5��N�i�~`k����s�����L�VJ]I�����L�*���[4A�F����������Ǯg��<���Ԓ��JO�ѻ��(t˴(z3��$�t���;���}t�Nn[hy���nϾ��M�of*FzVw�s�5���44�X� E8T��[Y�;��mͻ�����s.�x+�Z�Q���_��)n�8G����m��c�K�<=���`F)�ԍ��M�x��"�} -S��l�d�~�8�7�9S���j���̩)�Ⱥ�F�1=*���q5Z;�TN���L��;�R��8�9*�C!3�"~f�c� �ܲ��s�;I"g+����w,��F��A�R��0O>�g@�e}LX���$����;�e����3��֪[[�6��G�nd���,��i���M���Txg�*��:rt){>����ʂ���|�|)s���Ε�l�ݢڐe�ڔ9H� �w�#"�u+�!J�� 2�X���M@㨆�3ݯ�s 눮*7%�M���]�#���k(U�pի���k����6�ۘ�7�f��-��Z/��|�tr��u�����,�\�fjl94�;5���ңe�2�: 0'�WdK�p!�s�M����[S�?^����>J����=��x��da!���`�������x�������[x��}f�����yT_����V ����\���r��ܪ\P�j��� ��t��T�ɹ��+��6�$��a?Zc|���8�Z��X����u=��Ǥ.��p��v������r��7W/�l2i'��Nj�ƥC0�z�/�ȣ8���i���V�eW����\����;t�C�u�����jGQ!xKTW$8Wd"�n[x�6s��iY�i�D7�L��b�Ԗ� ->I�S��2��^�����R�^O�6���t��Qh�:� ���>����0�m9hH���B�|:���vn8�K_� -��"�VE2ԕ+�}!�y˩�4جF��W�LaEGk�V����� �L1s�`^�q����B���HBP�Y��b��f��`�]Q��dʱ��c��8��������m�_�He��)���l8�qb��A�ZS��v4ty��:,�MZ1i�4g3/R��2��t��@��|:���)k�]�-��X&����2*���bY>vщX�Y�]�m^#�m���:"�f�&���`Q����M,��<�i9�k;�����ƍ� -Sw^N�����z�&ie^��2�4>|�c��Fw��� -�`��b���âX��X�G�G<��G�O�Lk��g�2��C���=Z��+�}���W��\�Jo��W�9苚�P��� 1��Z�\Xf��%O~R9/�fʜx)������O��'!�%h���< �� c�/7�_.Q�/�4!@TSl, 5��*�k=���:: J�u������$>ss4^��?0�Uh�w��I��,ϻ�/|����+��B�b@RE�XQ ζ�O��$�t)H5@� �����{O�m3���싀ӅX��U-}H��r�2�������ޘ��}���6"�Z]�WJۘND��tكr��N����cw�(&�,u��[d�ǪG��%�P0�c�������}@u -s@mwG@�O,}P��KXԜ�_2ߟ��o����Q9�g���h,7�''<�+�:�0�$�Efj�|�zt�m���#'E,�SG)�b��j�J� �~��>'NHڼV]�C@C@/�k@�+`�]0����!�[�I-'�!�;`�\�������р>�s����&<aR:��?��'�{��Q'"f\"�K�M�^����p�/���o�Zh���8�����'�U�[�V�`�U�7�;�v[.��nv�Xv&��^ -`�x��6���{`=��&2�aM�9O)}�l���4��64��J�U�8�o?�ko�8��3�/��Ŭ�K��|�0V���u�r�X�C�ϫ'��)�����x�qK��_�������y�f˘<��7��V�5z���t�_�~u�?9������v"&�`���X{�����/�k�x F�<�� ��m �w�ij$�q����^ρ��C@�&�V�/��d��f� �G��%����3���q���4�/��OoBX�x��¬?��Z -�'OO�E�@1f8PL���v�B~�ߥ���2P���/���H��~��ï���ÿ8tB�=G���K$I������U�-I�����ho��K�gUY�^�,Qɀ�U�@֓��nY��/�4�r��S�K/x>���wI���A���Zt�wgSO��L%sÂ2z-_�ĥ�P��A������o�3F=�K�<�[���Xe[|b�u5ïV�j��d�g���&��I�@�P�K;��q� U��,=��;k�{-� �0���t6���g�ז�!M�wl���b�ޚ����dp:u�G<�� e�:oF�ז�d;��f��m����s��e��0�����+��<�|j~���Y�}��vp��ו�<�<�*XuK���3�rt��c��?��xe7�19�UGU�������a���.���x���� �i�M�^�&钤$���l�R�ɡ�5ɔ�qˮ�]�<;�h���l�g3(��S�K\��xy1���w�0�wx��a�"�%{�n��Y��7��;�\�O<��"A��* ��me�m1Gr���<w�f��Za?V�o��_�;a�V= -��f�AH�V����(k�ާ� -{ֳu�1��s�i{U,-�䪰nU���'X6�r�湙{4�[���h�L��W��H����ٛuhA��<��n���[�Q���F��f;��$.�$�6�����0�oPk�y���U>����<�b6U1�0�I�)]�bC}���A��V�e�i�S�:���U��Ul;�V<v{���gn�Z^����-�V�_�ӕT�6���r�;�:��C�Q5��~�d����2�0�g�_�?��n�F���YƟ�'��xv]���w��U������#�J�T>�R���I}��*݆t����uNT0�ϲq*��T�e�¤v)��p\������Hd��i�'F�[άM}����?YxƷ*���K9�m �̉\z���=�f]��N�]�^U��1���{�9]�̫\q�RZ�Za����������.P��~䐏�6W�O�*���>�E�����]�P�'��ӄ���O'�b�BnÚ��r9Tn��O+�sxi�F�5(�`Z�1j]�xԾ���$�~^�y���Ϊ��O�!�m��sU�g�C�&ف92��x]�d�����m����`W��9-;�����a�{'��fϺ��?~��2�� �M�n�6�.�K�0)����.��>�vs�vf�����>D��ng�G�� -s�ͼ͔��):��\��0�A����)��]s�IHW����k�8hا����f���biobY���%�����G�I�����u%9��o���.���P�ݭe��s�:��%����Mɶ��ddDelh$?�?[}�۬��N�9�+/m������g�%e;&s -���a����WRX�0���ʒ̹}�v�̒��Ŀ��s/��C�����96�x2��G{(XN��� (�6�s�� ��pK�l��n3��CO;Hp�a�Y�F�� -���/Uo��);�vU��FFƤK=%����n���Ys,�8z�G���sL�ħ��.�c�Y�V��]�u�;(��B�0K���9�m�"�6W�7I����ID}J�G56;��,�;��*��V���9!Tv{���zՉ|�¥,���Ԧn��ÿ~hQ�lZ����wW>�!CK��l��~1�U[`*~��l֯!�����K� ��S7�z�@���r�U���q��=HӀ�@�a��Ec�5*kK�Dh��Ϫ��4��磣��WY>푺,�TKj��HR={"���J�`-�w~"!�I[�+���*; �C����m�¬�ؐ>h� -��� - �Lƥ�(��kR���Z�0�R��@�`����Ϋ��ybZT���x�P�����=+I�B͒�S�(Fִ"|��o�_aX�w� �|���7߲[���W��{fGLq���EZh�6^�م�c�e�W���}��Dt�.��*r�Z����+�y���F�^��`j^�P�cf-��O��e*CH����h���)�v'��S�P�F��-�n����>�Ë ��+����t�a_�e��ft��K�#���$L��Qu�g`�܈�P�R� -�Ǧ�ڢ٢�C��fW��<��V�� {_)f���R� ��-�/H]�d�>r�z9��9���Ӧx�W�����-�4C�L-��3��X��ԕ��)�7ړ���N�}4C�����~+���, -}dcw�=p��3_l���M�h�ѝ�'���+ז����A�|5Њ��Ղ�[tLET)M����BQ��n�x1v��8K��<S{����8���yS��r�)5� -�^���Y��ln�n\�ٲ0��(�%T�2�Kp}��f$+�I�'�tZ-�f�����&�����"���9�ti��T满,K�LA9USz�wU4�A�43,Wz� ��Hç?-�E�6#���jNw�����լ��v����l�a������"�>4��A&T�B�c,m(r2��gޚ���J�PZK �a%���ڵ���|~ي�n�ܣ��]� _+yS|w��Y�[����X-՞�Ð��&>�%�xJ��ŭ�����g�J��R նޅ���(#��*����Pt�} ��TZ'U�6=���@�� �Q�~��㟇NuU��%c�_;��dde'�vvY�UT��,H��f�Pz��Ŧ"�[x��a�'(�}Èa'�qD� t�PR��k[��kf䳜�.q��Q�^S��h�I�ʓ1�VWk�?W��C ����y��R���*��ŵX�u�Lɨ^W7��s��pl��ݶ�9����~�7�ǐ-.tU�|�E���Y�SgH�+�/�0��/n��t:��!��)Щѫ$�2���¸a��.��7b������w�v���ǣ"ZWL��-�R�-�u�z|�vsⵜ�5I3�U����V��JEzV&Q�]��vh�[M�4�����7�w���Q -zD��<�i�)<����Ʋ��D�X�!���`2�����hL�Ss �i�`���� kG��3�eT��ny�-ȍ���c���P����(�s�ͭN#=���W����F����M�C� �>��Oe���Ƈ>v�X�:��l!��o�˪�9��~�8�X�,� "�Դpȋ?�]����\���Ň�Ʌi�� uc��Hc �-wN��/_d��ƲۚNu['����n+nF2 �Z̫h|@M<_��^�3���Oe��9����bi.by_��,gb���ՒO����O�#��s���qw� �*�g�fEx����X����Է����<����4�:�`=ݼa>s�����#q��4��54��������a�y@����$���$pZ�ėc@�6����+ -H���2 ʱ��xXW@<UG.�Ē=ŲCs�B��������v��DQr\O��M��#��N�yR9#wu5әG\j�}����x����,��zU�W{�s��g3�|�%�V������U ���5 -Pz�����Ocy�E,e@A��@O@><�z��^ڭ�I����saV-���|rXZ1�>}��d��SN�E���6ޕ~y�3�WML��ߙh)�t�6���9�e ��}������-@���wzi@w}��|h�z}�w�Q���f�9�������9☫Ԣ��Q�|��M�.��\���M�_���&H5�2��0�L�����OO~;�����l����@<�=��;��<` xXJ6��~�D���e��\~�&�tV�;81���9���ⶵ����}����?v�?��+�=����"s�����^*�z-��^���/�� ��c �� �cW0�Q/��*�c��Ԍ����i�'����p��v|����~q�L��`֟P���z��pēt��qضϭ���A�_�/����i�AesG��:��$�,�Kg5L��~����س����C':��X�����8�?���d�*<e��%��M�$*@1�5���(i7�]F5���j{�ªI��W�����~��f����K"&<��R�7�����'N���� o�~����oi�, -. ��ł���dS�hׇ��� -��7��\��ٷ���iq�k�/W��=����nX���d��/LB��D�B��;Ȗ���eo��v�����j+~���G�JNbo���}3e�Ȁ���q��-�f��r!��Z>m����랤��nz�R��n&����4뭙����~���bѡz�x,ngC����� 9&�c�l*�!4�IFC���N -�{�P����1�P��Vh���ۣ:U3�O��wf6�Gĭ�)�/:�Q�뇾4�. u -��ڤ�Q���F�Qy��C�Q�+��@:\k}�rmE������ nݝ��g��|�%@3n���^UVDŽ�&I���U��;���A~�n'�x5z5���XѤ�*O�+��I�su7�ϡ������<�"��}����Ӄ���O��7w�x�O鶇/���?f[�!]�Uk��7h�z�sC&��q�P��]~����_������C�:��7s2W�∙rd�V|#[�b����=��q���T���G��*��6�<u[Ur���(��i3���ƭ=^7��`D�8�lG�G��lS�I���5#V]�����V������=�������K�\�x�~\��L��D��?����_ո�}x�k�\o���y�5:�i�����VZ;�MvN-����yiQ��ĮZYU�A�<,�V�c�g.���`Y>GԱ,��6Q����% ���F�����?I��K6)�xy��J'�K�?!�h߯Mr�Au^/��js�x��Z!��O�T�d�g��2I��]�-�YF,��R�X��>'�7i ~(}�_��b�0�4�|X.��q�c�:�:D�!_ǶC~�wn;�D�����g���˛p�$�6�u&Y���/��HP�EB��m�>G�ҍr��52�KqX>��*z�T7U�w#8�r���<vZЎ'-x��]�\U{Y�a�����7��$Y2�9��e1;�e����#|bɥI�%����&��|n��\��~eZ̬B�ػx���]m屣�wV s�Ws����u�y���!*���w�f�G��%g���-^H��d��;)�g�:��^��lt�<d� -{��ͣ������6p�D�������O��',����!����>t�/v:�|��Vh� KN��x��G���##*KS� ��>��[:Z��Qyd�k��\u�k��#��)��vܬ)�|f,����xnS��\h軡,����z�0��/�>̻$�m��v�E+4�yO S�q���0`�� -pt��K�I�W�y� �y� -5W�{ꪗqP=ݞ)ۡ�Q�v�,��,�wT -=Vo;�;�gCx�+K!jIi�PQ��<[A,�Aӑd_$�<9���Ɗ��ᐷ�e��a��b��I�Co�ѡ�)��soR��)NseBVW]"�z�x��ĂR��e�0�V� _�G�E�+���H�mFKQcvG!j���R&��N���M������O�3��,����Znj��d|�C��)�s�������o_���%�d����w����[��P�x�TKů�^>\�,8mU�03S��M^�ײ�mRu��mD܀�������<�o�d���9���e��>�h�Δ�yy�v,�j2%��XX�+w���S�}r�x/CY��VV -�J����m�)���<��~��哒ZO���ΉY���u��� Ϗ3�2�{��̥��(�d',�0�L�ظ����N�M^?b�T�~��ͨ1>XEw��=lH7�z3�����~��;�2Z��-���7W�V��K��Ӛ�,�VJ|�2o��o�{N ��$�<�����e����î��ǒ�T�6}(�G4�B��tOd'[���#�����R�~S��������+����E�&ר_#?���H�s�[�5�ff����V�)'�6JH2@D#�B��8���{�����+ޣ�*��X��Df;$��&-:�IE�|R}J��M�3�Gċ��q���$�Ħ����-�x5UB�F!��7p��)�57Ww�F}xDǞ��_�%Vs;���a��z��:�7!�������é⃥����4@ɀ��N���W$Q��`�]�����L����C��kq�!��-�;��������V�eu[�ʪ$�Y辵��4�E��׳�_Q���W�7��Z� �,��g��n��[�����7SC��?7��<_���"dVH�?6x"Ÿ*�<��"V�+hi� � � t�\�\<SL�)�=j*�zʼ[�w��g wn|�/(��{/VK����ǏI��]f��Y�v�����l:� -9qF��SGX�^��+��+?��ar% ��c:|���S�"v�ѥwвBe�M��3J0O���$��'�Az��IY�� ��8 -`����c*�F�s�c��AM���V��{�B�U���^�H���Ifɭ�Q����=�#aj�a������w"���l&���Y�#�]�_��p�Tc2Ҥ �;HI����雮������&Ʊ�12�%��x�#���fM�u�����T����pC�$�7��<�ly[�i�YR930��y!�%�rK�I��C�Sa�݂E)7�MX���J�����>Tw�t�ݸAQ��=8e���f@�����R��r���/c9�b�8@��y����H"[���� -y� Q��Q�E�a�|�rA��*�q��@��|�x��&(L=���5��I=�|pg=���}CX*}Iw6�Cj$���2I�od�C�4�&�O<@t6�R|�ú�#�5\��<c�S��86@.�V,�#@6S ���)�+��`�;�xFN��F���*A��x�U���ͱ�P&��B��������Y�DX����D���J�М��?�c�R�m�R,���h�j�$[��)�����c�8�� -=�6�@���PbiO�(�,K�^T�@9�a7����)Yy�L�R�(�t@ [s3��|���^�8��F�� �#4n�p���o`����U�x9�+�^,A+��`a�0��/���.����`�� �l�&��ov&�|Q��`�- ��)� slYS�mi�vhbx�cX)%d�_���y��2s��K� -����X�~��o��&0G���@x-_��B.|z�/:�X�W���;����Mn��� -l�AG��G���?�8����7�h�,'��0<�N!s�#1��8$��`�}�? ������o�e���m�mA�B$@�A���Hm��b����KR{���D'�<���0_���0�y@�� �X��]f����qCU��k�����A�33 G�0��GBC����T����$��ˣ!��_�!��r!4�|'���d���y����d��|uπ<� @.}��t��հ ȑ����!\@��nYL�F!�Yj<����T���V����ö��-��5�wþm����q3�N�h[��Co�e@� @K��������������m=@��!����Ȟ�{Q�y��zBg7hu���t,wD/1o3�͖�<�|� ���~h�(�i�ܛ �>'\5�t�ə �����`f�)`��!`9˄l��K��� XTY��}��f��WN��Vğ0�f�]��s����A���q�����p�@����s�m�"&��'b��2�&4����0��"�^���k'�]�8�6� �m�����\����L>ˑ�^�T�:��ᡅчc��M��4�ش�_�jNҒ5�_%��q���j�%����{8�@B��� a -�/Q>U��h,��,���t���wy�����L��r�MϹ꯳y�?: �H*�%!Ic�����Å&�� \�S�-q��1�&��;T� -��@���ZxJ#���@!�!�����s.������ ��WRt��N<����ߎ�g]����_Io�ߐz��6��h��d��dkkd�Ud������ڶ�ڎ���|���~���ʾ��$[����|���J�������J�`��to�3?X!'i2ď��]�?��M[ٔ?�o�����U��<���PYb�Mbd��P�?�+#��T�/R�V&�;�O�Z> g�/4���%<�Q{�J�m�v�[c�Yr�hbNѡ��<���l(�����7����\�G��- ���߿��߭$��7��I��C����m�����OjI�ξm����0�Ԧ�`�kf�`�[���#�@��v�Np��L������0�"=yu2��\�l9n�?�՞�����toܙ�K{�5�6�o�ZU��$ ʎ������|k��x��I��?�l�A��=���4&Ga}�ͭ�#fv ��d�WO�y�ߴUo�i�ԗ��|J�t�{�`s���p:���4�b��x���\������9����u;���&>�'p7��V�[�����}aZ�:a�I�Ą�l=�}���u�HB_�U�|����[m���*�za�2��AM銅�� -w�gE����C�3�C�ϲ���}u��ʪ&� -yr �*M���/Ⱦ���B�r�[�r�_(i��s�+�N�I.�O)���/a�?I_\ո5G� ���@_���H�m� j�<�T祌P�6)����o�@�<dL�*�r>sA�S�����:��w�Ss�h�X�O�hS�0��J��@ -��ټ��b{<����y�:�O�@�P�l�oP��3���$h��7�X>w�[YzY�R�4R�s�e�ȱ��gn�š�g��:/�t^-���λ|���;�p^s�������U��̞�Kem�j�ya\c�`?����� �K̒ �N�whP��|�l�5�t�8,$�E��"��F�Iy1)���"ﲋM;��'��y=�sUu��1�d���JY�É�m��M ��F�/���3}L?!���'H���}&�>��> �i�5��raR*� �t<~�;���� -��_�=�fz1���b� -����K�E����\D -�����.��.���_ޫ������̜����Cԯl�2�g��Aٚg�U=�]gG宕�]fZ�����I�u��ņS��G�ֈ�Ԫ�-*�ٵ�4r��|\a1��&�r��,��"�r��usD1,Z(�~ȧG����'>��QqL|ȧ��+��g2�+����γ�V���n�$f�5P�jU.�nRZ)Ի�����*s����V�Ѥ�gDIoLR�a�Ĩ�>$FZ��S�VB����Jfd1���HЯN�X.�����u�=$3���l�����*��p%�B�נ,����+��Ƙ�4RLV>�ɂ��%�I��I���J�&�Kz�9K���*�������C�$�`{��W� -o�FS��+�< -��7�x4o�FWЇ4R�#:(_-�6��;UJ՞jU�ߔ��|U(�p�ۙ�'s� '��K�NK�%R�`����»@Be���s%q�m5���� -��dě��/��G�2RO����(�d�k6�v�[3K S�)�OM2�Q5������W"Tn3��E�Mj�Vh�|��t�$�[ݖ�'r��K�pw�-1jH/�;%!q�ɠ���!�_b���N�樭���e9k��rD�벍>fNh.�ُ�j�+��u��<��mK��(�I��_ ����Jt��d��W�B��H�3�r�A��h����!Ʌ��H����o��8������1ͳ`��]��l�_��G[�ۨ�N`�5Uf�{5�t�z��XF�N���E���C��#��1o���V\��6mtsa��D�ρ�5��e�����%eb#�W�g�H^�>$)d�_�+���G�̯9}ϗk��� ���FOv�-�۴.(���"|1��+�Jݭ|�RD�AN�1_�f���8"KwO7�hݰHk-U�B�5��� ;�'�� ��輩���2����Z[>/��Xx6��ua�-_}a���|e]��o���=�g[-n˜m��t�ƃv���Rs�P��"���'�P�w�Є���w��Y���uFk��߅���:oX��4*��1o�cj��M�u$�-�Rk{�YzU/2�C1�z�J$�'�F�HLiH���_��.G]�+��'Fc��TA�� ��=�<69����a����&������FH�"�0������N��=�.��X�Z``A�*����K�-�SR<�e��G���BcP�_�y1� U967����hH@���kt���+#*�3���҇W2�XD~>>�+���h��cU� ��z�@�c4�@:j"O��4<(�eh�ڱ٠6���j ��� -⠃�xLL����쮏�>�q���r���r�^�r��/������y�'|t�����N�hL^l����84��#�<�~��'��/K��R;��K��ӽ�"|���D��f���.{��R,�XfA�t��x��06A\�"sR\�� �kd@�.@���x�x���fS�T��Yo�-���&oB�O������U�gz�{���~��Eۙ��/�;[�T��f�����O�;h���c����ak,�6⮃��C���j����,� U (J`�A�������MA|�*�Ҥ>#(��N;c��E�H*��\�}����XΧoץ�Z�J�ܙ���V;��ȹ�Y��<_A�F�&���^�0y�@�х(w�[b��,@�f��P��X@��1T -�<Hw|V����q���h��������RT�q�0��sLށ��6��t��R)mt# -k��m�#ʢ�����]�fYZ��[��ʿ^�$$� ��Uh�/0h4����!�����4Y�TN��ʒ@Q"8 �(![)��Ɂq��<Oj9��<�0}�)/�Y��dlѸ��˷Q��;�nṁ2�d J!� -7�/��]�\����x����w`H"@��C��1�>�B�� ��u``�轇���"�t�O����a�s�Q ˇd�ss��Cε��L�چ.x�"�[Pz�,�>�[�ч#���`��`�e`� �X9 ��8��w�07?�8t\�8�(��-`6������3����:?���46ٶd�4����;��߱*=��p�`��4�ݠ���^>����>���<l�\�vK\N4�Ϭ����Q�o�<��K -�/�@��3�;���(R��=���/�}l_��j��d2��:e��Hm��)zQD:��b.��H2'JY"��͇ǵ�-τ�~(�G::^<>_�{|$Y|�)���M$��2l�R���ZK@2�3 ��� ��������y ����=m����[턡�C�S�ƗߠRI&)�Yn8� ���/��i���;�>9}�С��|P}��(�X��V�jy��5�w����°�^Py����=@!]��`ȫ��v��οm�V�����b OVcT�ӇJ-��a���{�a;6��`�/��U�����[&.�I�`��xPp����mw����H ���� n肊( ��H���K�ɝ���j�T��q�`"ʿ3E��L��ϟ��DL|�����"5������m�'���8�V���ꀽ\g��7��m0$`�X�]y��V�G�j -�̻�(�K�U'p瞉��%~�eT��ۗ�����h�?�%��� -�����Q�����!8���~���LJ�/z�����5�H(���u�:]�K���*2��s�^iH�} -����H��Dro$�F>�s��8�or�gF����d��f� q٭Ab�~�D�a@n���L|�g��F��h���4xL��u�iĪ��-�#�j�\��e$Dt"&��u?h����&�� �j��ꪊ|]Ex@�w^����"���i�@���@�B��Mw�g���?J�Z��GRt�C����74@�&ʔ��x ����}z�d��y����-� վA�q�@j�H����@*�y*V��H��:���B9�vc�\���}�e������%֑�7��f���mj�xH=݊����-���N��|�L ��Z?�H��/���8�uj��F����{����,�G^���;���m���NK�J��{��~^ڸ/��3OJ�Q�`�����v�ܾ(�Y�l��� �Ǵ5�w�����Z�8Z k�2�4�`�]R��e��v�Ӆٙ����i�u�LƑ�7w# �+d "yx#�7ڙ+�m �n�pW���;��Ӡ�}�o�k��u�9�+d<AL|���#�Fp�#�w$x�t����{��&�ܭ����[j2lP��h�6��u[盺�xl���4�p_�H�\�N�q�FVُ��p/)���6�R �� a���NC�F<1������@��b\���*,�#|=;��486�{��u��}]�C=w4'4o��L����D�c�6f�j7c�j0�#bQ�l��FqA3Q��� 5tN~��Z�F��<��FD�#?B��4�w4=�s�bOy6��4]mt�F����y�c�3z�f��U*3o���U�����]�54e�;3��'c�RmTJ5����#]�j��(�Y���_��C�l�L��lC��v,���b%�v�H�[�f.ޢ�9���T���\Cx(b����J�j�����P�\����{��*yr>)�����\�R51�#Iu��)m_��m��(��:ڍ�nj�( -!��F�pE.�(�U�o͚;��|/PZW��v�f�Z�mWy�G�J=�fC��S/V���%;�� i����Xh��Tޞ����ݜc���쨮cY���2�ys�x�e��d�8�Lѡxm����U�ݏR"}�[�@])Y-6Y���L ����N[�� ���姑�'w3f�����9�cNvT!CgJ�� Aô���N�MCס�2��Z -yƪ5��j5}cU"F�x9_�%�FbJT��բ�"匸H�Ͱ������xyT�լ�����Ռ�ћ��w�FK���h�25}�*��R���Ve���&�M��ۅ$.s�i2�U�����-�-�.��NF�;��qA��d���E,C��˧)�z�S��TKg$���S��f7Nd�F*U��s�"�s�f�6ݤVj$gәJzm�N���1�r�C�.eT�$6��v3u!��K!��`�J��I��3�C��;锢�����U�,���_�����r:��v��90$�KjMjE6�O)NҫB"��Y91j*�;+q�-V��S_`}2����K��G�)�����[c -ۨl�,M�N,M�D��������It�DՌ��C�NO��O' V9%E�������HI��&Fu�P��E��C�v����iXx_EL�K"%@���%UM��_Mq�l#�1��6��s��3�#�m�-r8E9�uhK��~gF%�@%��U�N��� -�����V�}�!s��E��H���[b��-����y���c͛C�ȗ��#����U3I�#�i -�(d�,��q=��%i����"j���z��T RMZ�(b��"n,rM<_$7x�Db��M2�ȋ��1����O�V��|*�\i ��H���ů�`�m��S�����;�`�Ζ9�j�]�ش���h�?���(ݼ�䃏s��gB?41� -�-<�M��$�� W}�D�B"{��G'v����-~|+��<|�Sh�#>3c�x���iW7���PM�<�-��O���2Bm�t̘�al�p'lF������5����Wr\�= �Ĭ�ƉX���q�.c�SDkC����4�7�81y�����cOy>����R/�걧X�6|}�Qh�W�=X҅LW��$&ɷ�S��+_Q�{X�[漾���YP�?������{�1�L��dj���9�Dl�;��V����ceH�[k+ ��5�N��{6���<������ ̟< a��X�D*���pt�,��n*7(7�T�[�,@s8�.�=I�W�MO��[��[��ᩰ '���x��1�kM����O�41\�|^����"��vG`������<�¹�h,XU���G| ����Y&��~��d�&��M\��"����}��ʦ�%Q��TRjO�bn��x�����v��gjDV䔗�(�H<g��,�ie��{e�6��iWHK;X�l�}�Zo�bӣ|-�S -�٣��T`���Z8��У�$K��Ȣ�7��.[��^�p�s�LN�JJk]�$eS��>�"��є��r�ӆ���&a�17[�)fM�ct�W�W���Ch�ZF���VT���űl�]�o��/F@�����<��v:�o�3���^�YYx�Ȏ�Re�/W�W��KGI�p��)ҡ���3R�i}�<Rj��%V^0�-p���0"ӝ 'Y��ԡ�c�Xu�lc��q�}qUz�xi�e@��VA�s�E:tWpځx���-M��qi|�<���f��q��.ҹ��d2IR"�R�X垌v0��C�����ƫ��W�Ī�6DN���m�46�z,���Kե������n�@? ��(@`�,�` ��;c� �!� �`���K�@�al��_�+%�g�g6��~BR�1J@5�����e:�z�N$�,��/��M)�D0�÷74�R�9��f�>@b�@Hc���H�hG:t�d���b@v�D��<@��v����C�� ��@�} �� Ձ�5�U�L=mn�9Y{� ��R<�~��e+y�͚7�ߋgbQ^�p�Zhs�4�{Z�Bi���f4����8�1��v�M-���b��rH�&��P-[ -`�����{G��6��h�5����ʡ���5ȹ~��"FV6)�|&d���uɧ��u'g����^������(��x�Eo�#gp�����帾�i}X<v�\��y�x��e8��N�6�뤟����K���s���0H�����Y�87�NO�&�p'�o� �va'�!����K�����1.5�V���[�-�������7�7��t<��(Rn{ೕ��B��;�f�ו#���9�E2Lj>��GOq���&���&�C=�f�|!��]*ʌ�T;F���w�5��ɇ���XAъ�On���S���[��~��?����Zc' Nj�81���b]\pu�1�}xn���ޢ�zc�dSo霕�J�IiM� �yV�Z֒e�©�Y�p��=��������h�J�!FG����;:4 �5���TX?���2��cP�JP��PY������Q��9@>�@��3 ' �Z�%G[j�~�����S�U�MhV��^��ǖH��,Z����'�H�����/��Er�r;���Ƈ��f[����G������]�F��9�����ƽ(g"�h��h�J%�Z>e��xM����(o��Ç�U��-�~��#��72�EL|�U#�5��~i���pq�868c�8���<`gݏ�\|Xị��\f�{JYH�D[kr��&Ge��c�!v$����{�����q�å�,��eDۏE�!�za�c���f��N���o���'o�o�h��z�h��ܖK/l;ȁ���d6(��c�B�B��):���<�BGj*��:g�L���r�RX�m�hD��;�%���v���+[�8 1�a �/f@~��8���b �ڎY]�v� ۫�/UH}�{�#?�7:��~W��geJ|�ID����o^ڏx��d�(#ap_�x���S@���rր��<%H��$��,g�b5�S���e? ~�z��z�}�ϑ��L�f>���Rz#C��F� �ἳ� -����N� ҍҲ���Gu@�:l@�V~���z�5�3_�;�t -D,�ؒ��7ԃ�ہ��Ň�7mN��&u}��|w-�|�w�˻�ۗ�}M�U2���T���a�XylBk��.e��t�f,O[h뜗��J1���"%!����|�D��"5� �W��kѴ�VN�p��{�Z�_�̾fO�T��_��N����eQ� +��q�f��ڣ���khkhX�V&9Y6⢹h㙣!�J��۟�iJ,M����t�H���0K���ե�/odA�:��A�}{�b�?�cg��v+Y��ƭ���ݝ=V�G�3�9�ZR&[���~nI1��Sl�8mb�w�5R%~l��ɑU�g��Z�2��>`/��k +���C-�h蔘�0J��HC>v^����f�U6���H���s��33�ͨ�W(Δˠ9ykݱ���#��G����]l>hS��n��k]�ܷ}=q?�����S��C�f�X�(��l�1��Dٯ+m�槭F}5��d%��a��бl$�F -o�4��㊤�(���*{�VOv��t������HD� ~�|Oy�$m�����L�,�rd3.����ڵz�e��Ͷ��5��x���e]O_�5wA�55^C��*�����q����f��D�PRm�->ڍ+ځ-ҡ��d#��# -!���c?�eZԉZ6��ܾ�^�v��GCp�~]W�X͝��QVǫ�R���L� Ty[S��'�d�j�wvV���}������b��m���2AF�د��~��*��ޠ�h��j���zQɣ�R�&:4���r�Z�JV{Ze��⮷���}������z�Wޞ>�y���^���k%ԬJ��瞙is��f���4t�i�8��q�B^)7����H��l��V�� k����y{���Z�j���5���r�y�M��������Q��d�-5���I��) ]*�oA�SD�=�SY*�бa[n���x<i�����D.�&��K�i���������6�q$�g2�f"�E*evg�TIؔTK_UU��h(��&�<�����~,s�c����6)b�U���w�=nȄ���]w�8�'g1#?SbF�bFE��`�H/����b�CG�iƳPdT��P>�J���/ -��8=?�v��p����VL�c) I�攄��e�f� z�zU�v�m��,��?�d�9鄪:���D����r�br�h��ؤ�x����Nt������WxDĒ�B8H���JḬ�V_'F��.�RV���]W�N�K�v8Hx_iD�K4!@����(#�W8k��qDlTeE���(2cڵ��ȭG��K�ܳB��������h1�ԣ{Z�2��Q�G�2R�#}7��X�.���i&@��7�_�g��.��������K��-h���$`��D�E��+��w5��c�rN��w��:�)9�wG�{&c�t��D&�ՉL~�P�L��E���&��#A?2'����7#�; 0*��j�����c�C����9��1�.Ǘ���ձ-R̉r��C%9�A�� - G�!Fx�9CL�O�d�g��B�+��WEojm�ZmL�h�9A�j������|�ѡ�/���x��N%uţ�ٌ���>�{��wh1�6)玷(���� �d���ʄi9�T�ʊ�*["#wO��9�����ǡ��`�Π�:���/#��]��������9т�=`#ߢY�+)zL�B��Y�E�o��U 5���3cx�5�Eأ��!��I��]���f��F��I����ϯ���<+�-�6�PLA���%�#�� _�V/�<$�ظ4_����4�A� �b@ڸDW|t��x^!��%����b�m���a2#���D�:$�d�G�X�C�p������ l5.5�2_j��~CGk��%!s�4K����|��7�ξ_�ܣ��$V��e�@���A�����ՙ��4�%gc��!�=����^��|1��x���k��c[������|�#�l����t�`���*�nS�&J��0K9H\3���6|�qCX -��� -Rm�LG�&�>�5}@���.�H-�xY���>��� �u@�N�A�`@ѝ)/����6�j���%=1����v��n�����ĜLgcB�:�����B�e����V�|��pz�C��� )_�Mx�U������{���%����:�����R{���1�M -��1�Z�P�"��rɫȪ�'�H���y��c�C$���̉�0J�ËӴ�U������6rx���˴�}�C�~V�e��c @�����&�M�AIa��p�DpL8��p�Kv������ `ę�(X����J�"����JcXbr��"w9c+�=�6k�ݏ�q��0K�tt�t5��]��Z��<j� -�XFD�-,����,��ւ��ӌ���v�� Vp�Ť|���&��? �dx������<������_j�QJzYBy�;�I���_\u��3����<����aUB���G����%�������Y��f ��E�O��l~��� -�9�xIz�G6�x��gx�@������.�g�e��k�����Z!�hxJ��=��t<cBIX�l�v�~�3�ڊ����#�d��7)4� ǕӃ7ӂT�Q���* x�U�?�i�x1X�LJ���S ��s���0�$�"@�m r�f������;r�ů�}$J���V�>�PZ�{>��K���ׇd����C-7���������F��b;n���| -`w@��i�!��}�S�̡@��3@^�@�@� -`�(`J,t��D+�����9[��dO���2@j%-����MƼ�?���b�:��J��H,��_Q���3�Y\|��`���%�u?��2�o��5�T}yj �re���Ч���p��%ґ����C� @}��Ы -��}�`4�N@Kh��;N���z��RY�@�a�r�$N�^��J�*�[��P��b"��(��|o����U�Sw�����O�[�\��5��Б���N%�X���`�#�s7 0+;X/�����u� -ˤَ�s�E������̱\o�t�� -J�@z��{b_b���X�G:�z{�Ka�ho@t�\�Zew�6��>�!�)@�L�������y�W�mR�ª��Ÿ(��I\�-�H�(������6P��_�/��#�<2E�a|�|#q5� #3?��p39"�XFߞ���r&[����Ȕf�@�V �:&��#�oݸ�d~]9��\=6`�ڱ������7�q$����=�A� s&�3�FI���:�����s@��=�����& � -f -=@�� -��G�ճX��%$���,5͙(����Q��g�F�}T�H�8E�x$A��Q�ć�8��ZQ3�ɽ��6W��8�~�1�Z��,R,9��������w��D�!�����ds��?%�g'��*�%�G�"5�+��/��Oq���+<�˹:zǁ��ˑ����o��L-��x� -8������+qT���$��PB�M�j��>�WT�H�����b���ޯ��(��K��"!3IU�@2�3 ��H\ $��ӤĊ����#-ң�t�����+��$U.���t��4�CVo�)�������L��C_���^l�y6��LM� �����&��J*����@��'Hv�,+9Z� ��2�� �g�"vQ�qЇQ�B��C���E�ި@��8���\�axÏ2o��G8o�zyS�x�M��\��h.�!l��6H�^k�F{>H�1������i��=��ܻ &H+g]1�y$qst͓��cH=�Й�<�Z������ԾXj���T��(J_�vSlv�I��w�4�Hy�r����2KNxe;���F^��K�����ۚ ӑ�a�kg�^�n]utp�����̏R�]9MmMB�y*�@���r�V��t��]n�l�����{��~c�����8�e�i����U�rލ�%��f��= -���|Í���+S�+����?�ݯ.\(/�I�~u�f��v?[�����A�3�%�8�~�pA3�t����@e��o�Рs��Wq1EW;��I)L�$��a��o@�k�-}�'��-�k��Y~'s]�!3��_oҬ��> -��Uj���RG�<gp��}AAObn�2�����Yؕ��/�$�T����U�[��J�Wa]��4���9V�8�XSc��<(�i�7��K%1��re4Q����{a"�T0�Y�8g�{��+�[��ڜqw��� KmRA�G��)V7��*u9��L����tz�쪭h��E�z���,�ês�Z/���3��>c����G�Y8���Ҷ �g�����W���С8��k�g�U�_\�?+t�̯��W��MB�`�ia&@X�$�|u����l��}M��ӛ�0��9l�[X_�U�.�#�J���W�0�N���Ĉg�5-����m�v�!�:��O~S��ch�J��ܖ2�>�[ ���jm��L��"s��Y����Ϊj67� k&��v[���e�s|�M����Z�F��7�q_%k%K�w]K����^���/�*���Ǹ2�O����:��(j�лςzuvW���;�9%�qۗ�Ľ&��.&�ja�z�e�9{���&z� -�.��9z_��Յ��R�_]Ƞ�?��.d�̟��>+��>;M�=������=r�e���u�\7�͍�n�Ҽ�����羯��2�8e�yx8��olWߋ���_��"����g���A3?M���?K��ߝͯ���g�X�����o�����������wg�{?�����?-i8~o���C�ߗ4h�7��_�ͯ����n�UI�!�{{��:����A3�����l~K���!�<������{6?SҠ������l~_���!��<�f~s�����F�[~R���/ ����:o�L�����sZ2�\��u����r��jŇ���=��7�iz�$���b��j�&P_T�^@u�Nx}->zd�����~?\�k��Ho�}z'2���C�w��[nz�t�-/[�(��%R?��p%�'�+��B��ˤ���MIO �S~�j�04�^����"�\�V�H�Pq��y��[��9Z�Hb���lam�K����j�o��*A�Dž�S1����+�N+�/]�k{�)�9�gZe�^�5�����W�n��j�4�ؔ�� �����_ >�촢!�E5�S��yp���U�����N�M���Sf�����;U-�q{\Z�N���tL�f�mל<��ޟA��_��?��jp-eV>G�O�������N�ƞ��UrgPR�7��%�m/㔴��!�,�@]ԛ|j~�3����&��V��t�������7����T%���xQ���~�*p�{��B�X&\s�t/�FN\�\�6�6g�*%&υ�_��xFڏ쯆�g�����簡�7b$�leٲOy�N�m����N�زq���21�ssj��z<?�쨘$�!��K A3�_��.�m��U��M?�z�;P���A��Q���Ln���x��3������q�����A������EF�w��P� �3>����}v?�&4��^:��Y�;?�F�@`�~� X�Co������V����>yA��xQ�z�����|�=�y"St��!�|��xfu�K�����z �`�pa�y�e���Z����O�:�l'o<�7��p�b������ө~�_�ԭ/����!����G��>'^�>7�����<O9J;��Ta�2+z,�qQ��[���Z�p0���xH kƀ��K]Hn�~�3������'�9��!h�O|����<��/�ٵ�˘5���YIƧ����U�w�����XLu ��:�m��β�S���6��S-�NB�#����.h��t.�=��̬1\ ��e��2��ҞN��hlJ�z��<�v���u�7�Ӝ��1�C��5T$���T����?���zh�;qiV�n�ܖe�^��8ywPyT�j�����:u�������I8��漖Ӆ��G�*~Y4bm��F�ոN���0���/G�y�[�]͛X�̝BΛ����WX� -�yڎ�枺�)���v�;m|5�[��t�j`�y��f�X77�6e"�����g�ۼ�Lj}���`�2"`��I�h%m%���o�>��������\����m]���b?�tri��ur���u���|ķu �A3�����?���~��8����x����LI�f~s�����钆b����og�{�� ��3S~o�����5% ��ͽ��o��_1�iIC1�����y6��� -��ͽ��o������������w6>�9|f��}�w>kr��],���=x��������O�ͽ=�^|ص^M�,�Xٽ�9'����+���q��1���i�x��xr�HL9��, -�W��]8,��#�l{��~�牢-ܪ����Ըh1�;��� �CAp���~m�[��rs��A��_��_�T}�U�B������1mvY�8����pwo��.؝&�t.�R{i'�������}�m��+lo8���3�23�r�/!\x��O>���E�;����W&է�I ^g���mc�]�yz+�.����ƅ���z_E����-�tv���ΡM���p?����=O�I�|wY{*����OrL���sZ��y�v��}ww�ӫ��7��|�n���}�V��6���nW[w�&�翄P���~�oT{�'U����d[{x{�p_���>ׯ�����8�>�g�P�<T��q��ά[J��Uvl����\���ƨ'�뜙�������_>�>�AU�����#̻vu���m�^d��U�ѱ�g��.u4�ps���{����S:6D9�y�F�ɮg���7����:�|f�,~ϳo�^�=��n�s�n�Q�.N�շG��w�&��fi�����k�5�B��^�6?q�`&��l������S��C(���GEP�|6���R:���"�C�_������|�h��Vz��}�!!l/�]Oi3̠1�ʦ*��P�~)���)9���i���Ϝ^��^m�19K�51�j��٧�ک��=���֣5�W�6�,ꋔ�)�ΠH�-�g �6ʿ�6\Ih��i���lF>�j}�H������u�Y�J��)o{�Cm���D�G�`�3K��ps���gRI'��)����� -c�b�<�7������X@U�T���w�!���Y�~��|��x�����J��o� ��ed�����g2M@�87ѨB��`;ۋ��5Vu��~ Dߞq�䳅���H��J6��SC=�3�7�S{�n�y�-V��-��� ��k2C��1�ر�eT���C �1{!�]�C��&D�v��_B��d��yx��`2+!�|V�]��oE�_fY����*Z)}qL]w���x�̩�e���ΰ�>�M����/��k�ݻ!'��̬� -����A5���Ļ\��C� �P�tح�-z��s|1+ƄY�^��\������Qe63��o��~=�:����r� ���=Q���2��׀�R��QD����~8.[��V�H~=������-��g#��,�ܨ�/�6�����5�K�ä7��FO������n�o�~��7��������_���j��uþ�㽭��.�Yy�/��:�a<M�F��clJ��p�ePݮ��N���`%�y�\AK��j�=ʵ�9?�u�Yq�1������M���7�ˌ�w$�Tv���Zg��mY�(z.�w�$w�>m6��<�; r�S.��o�����.45Ў�1�p�N ���8Jm���ոW�5�%�f~�pV�u����)י[Q� ��!C�8S}��5O��]C�3���pg�*l/���ٴve��j`�Em߷汁x�6M� �T������{߭ӹ�k����m����e?h����&��嵭�&|~Ӣ6��&R�=�3 �u*44�Ri��^w�K+�_�� 0�Kw0��4�g����|�z����|gM3�`���e_���D��z%�L���S��$���RS.���ͬ�zNe�_�F<|y�J�g�Ҽ�狝s���3w损���3ZI�$��34Y�,��3S~��?��<�_���PR�����XI�������Y�pE�{{�?���~�3����ޏxFs�����v�LI�!�{{�������g�̯�������3xg�3V)�^��s�� S����˒��'��#Ր.�l,.�ә{�-�Z4�k��]�ִϳ��旕�� ��`�-1>��B�o��?�z���(C���v�]�Y�9,�GW��<��ݗ<G�Vw?�l��PL������Ν�0+Z��R:��U�����i�`s�⃕��@X4����ך����"z@5w�|�ryHx/�K�s'=�/O�{[o9ީ���i�4��?���FY����:���n_����P���D� ��\���@�SI��J*��>�N�u;�����T�~�#�ΥݜS�ޑ��tv*\��l��{�4��{��Av�l0�����H$v����$������=�*���T���ܠ�66yoY�}�+��iT�j'�_�G4a;�r��4+�C\PC����tڛ�Y���u�o�FP������rR�D<� TD5=z�hT��w���o�}�ƺ������i����R���Q%�?,oneWso�M�E�-��WL虞�O�_ ��Ԩ\8�~��МT3����:�>�P�'���#�$���N�o��jtŸ�^ ȾM��S����ߜg����+�q�����yߢ6���Z�7p��]D@QA�MAd'��p���>3�]�~����sy�HΜĵ8��;aBP����K�F���/ B��������+T��j��9m<��.Z��}�[�>˾��|:�k@��$q�/��I �2���"C��`�ժC���������8�� �����Y�����7���ic���R�(�5ݗ�f~$r�`,dN�W���s�C���9�q6��������.�Qm��=�� ��F�O6-��;��(�?��!NQ|��v�D2e��M��r�o��fV}ī&5��2�<�8e�t�a:#��}Wa��x����Hu�� �>��*�ѕt���4sp֛�g��xdz��jL:���PTZ�q}%�m� -]^�>W���@k����RƖ�OS/��P/�8���'s�?(���{��Rֆ����}������p_-'��H.s�#UqV9�.�ڡ�aY��:�ᐡ���.kjRz�Nb/w= ����8���/��k���������%�9u_�i��8���=��d=@��5�D�j;݉G�2%g���I/�[����Ş�z��/'C:��)�V��q&�8�] qH��%�U��G�bt%E�`�n��3�<Bt��o��A9�0�z�H�PD�,g>J%�ʵ`���*�@���0:����2&�,�><�>`B��o��}4�d��g�(�Z���*pő\3ݭ��簮�`N�����H#6/�5{�"X�&�D2���$g�>)�d�K��oe����_!P�S���T��.�<M��m! �{��j�'��U��q_�I�Sm&�Ӊ�+�x���y�,f4?{�"�"yx��+�>��3��e:�o'� -� �<ı�#��~��4��`����<NI&��4���s�H���%.I���za{�n)�� �ʻS�x�]7�aϵ���g�}'��7�!z�-����F���� �*e�[���������{o(��|.v��~�ቤYd���Ǥ��植��cEn2d���Z�tݟ��J�F'���s���'O�&t��!��콿�Tҏ��n{o<L��B����d��^Tux�?�ג+Wv`�=����H���ms an��j��`[�1�6��-i|pG��͓��e�����M� -*�`>�i\��@Ai�f�u:N%/�j���8cR�4J�#u�e���F��|���kfq��Cx���8���<����Z������f�]A�� @ũ��T��(���wH��bC��o虋����j��f ���Ԣ"��iA�N�f�]AJY���z�/)��(��K[!���O�T��?|���$+�����-���^f�ܠ���$�E��S��^f'��j�D���t�o>_lwPu���"�qD�j�F��6�����lε��� >o.ċ�KA���s����yY��^�b`�<6{!��r7�(_����H���#���ny� ��U�$�J�;(�W�wi��o�6�7�p������n�w����]˼&��8������DH`z/�U��t��{=?�w�~�S��<�t_{�uGG��\���m�y"��-���ޫ-�S�q���_P��gL���_��̤���<����S�qj�����.����xd�߶���������y�S�qj����m�[��w����K")F�[اؗk4�����>U�g�LN��y����{�YIyl��p�����dpp��u2˭`/��f)��y<��Ư�'-{ �����_�o���G����AT'��ڒ6ӑg'o���'�1o�P��c7e.����m)hv�]b�M�%=�9�0�n߇'㪮�����*��K���ʑ�8��M��r���YP%�y���Rp4��?h#o�w:"-g�����%߶��:��5�u�|XS3^�6���AZs�ܱ$�;o|����C��<W�oy��Gt�d?�̌Ǎ��+�䳳�#�]6�O�:&�ˎ �3Y胖�ܓʞ�e�)�-��U�_'�z��HwH�ġ�?��="U9j��2$�I����K�\���e�"w�g0/� s�HO��73$^a�ʷd6�V��m��{JĆ���L��RX)w���&��^pbJB����������#����lQo����'Ē�w�˺"�͝�Y,4DBӺ�%�6��=�-�X|�}�dܨL%������ճ18��Br9!x)����yxE�/�q��<�Y��㫠;qe���snV��6��ħ����IEw�3/k������Hg�Wo-ŰK����� ���x )��S�A��A��]��N��g�*57Pm��*V~؎<'�l�YQ��2��մ�7z+�Q�[��jyT��֭\���Yt��<��exs���mv��|?��_G<a�_S����;���bt.�+����w�)ҩ�U+y���$��H�) j�r�5}�����srU&�t�����=��9 ����f��C�xmW�dz�F��65<���Tsd*go1h�ܻ�b�+�n�#�V��>#����x~�EaZi����8��mspk?=�'����!����g���?�]�ã��J{P��uP=���%ѥ7�Wg=��,�Vߘ����SNq5�29��������Y��]��t������:Mv�k�g��1m#h�hn�!>wϵY�_����UU/]x�pEKl4 ����"!�6��Sy�/�f �h(y)T&��¹v�ls^�3�f�Oӧ~�1��{jx�o�!���~��w��*(���W��;sV�'�ʪ�SY�륖zϝ�J3��G��^�^d+���U#u�/�,��"��*�u*Id���m�?e2��O�T��?|WUO��>:"AE����U�����lz{/e�Cmv���I!3w�ʝeKXh����j������%���P*P��>e�j���e���;��z�����]�� �D���T���ѥܑs�l���x�(�+��@%��^b�� �ݹΣf��`h)���>}�6K��RIV�[!h�Lf~�I�xu�h�"R �*} 3Pf�'P�m���y��=0��b������~l�t'؊�|��u''������E���7�:-�J�%�"Y6G�C������p��@'���#��콨�SUP�Kq��BG�R�q�R;cv$�v/p9T}V�����u�l���s \�itz��'���O���$�I#s���ҾuêDW�v�芶����z6��?OW�� �ȶ�7�%��T�<o�~�x�f�n�r�]�n�>���9����X�0��֝�s -�}�tD�ݟ��Z��g�q\����j�R养��R��ƫ��H -�M5�$���F��������4w�wv��Cz�;oO�| ��=X�tݻ*��2���b�\ -��NcS� -c�Ƥ����Š���A��_�����lDW�"e�we`QJ K�Ln�3U���&�(���O�P�D�1=�f��Z8N�-<��o -���SO�Olv?�Xv��vKV��C�2�l��m�v��|�ګ�����\z����!Bmi��>R�&+fld���yb����z-<�zL���<Y@������ن�䓆j+�� N������������Ѵ�œ�Yq/����l��2%w�.m ����7���"AT������us'O+�n�i�wi�,�%)�j(��ݭ�������[�����D�5U�-XO���h�u5_��T�1�ȷ��)��q0���{�߭���-�( -�Cb�o��v��ۊ�ď+ �n'#D��#�'���{-ܐM'�q[�*���r�c�3��Oڙ2G}l���L�ߤ-cY��QH)>M�H��g7d�oZN4xi9���;Nk�3���t[�Bf�]��X ����>�/%�Uߝ.ʡ���:3��6>'����j�%6� �d�bn;�e��u�s�u������K~���)!p�,��<�^�s��"�2����nA�U�U�%D������fl@��@�E��h��@��c7��((�ҟ�C|��5��C��ͳ��n�)�G�������o� b ��xsŴl����Dɐ0Z��u����/U������<�xڿU�~�6�]�<�x�����X�?m�_���J�I����}���i3����꜏U�r$��X���\����G�(��]��wz��M� �n"��ț�k~��n������EB?Q���#R���B��v���� ��T�pTt�ۦs��֭I6h��z6��l+3k^ߜ���p�sG���M<֘s��D�ݬ��%?��L���fLf�#�C�������R�9�'5�W�h@0oW@.`!q�݃����)j8��Q�t߳�掗N��#~0��`R�^�����Z��*�-�a:)��&�U�[�ٯ#R��?]�$���4#Uvn�����*��g1��4��{�\c�������ݖ�Ǘ�Z�*��ֳ<G&6%W�}a�N{���;��2m��{Yg+�Q|O~���x`~�Dt瀊G��'2&�Nm�R�a����s��^ig�B��ij2�X�GK�����{sy�fG��Hl9Y�F�W'��EJ"F?ER��{�e;�RAoF���J�*Y��d��Mx���5ֽ�8)��<����Z���4MW��%���S����o�Эh��R��f�ܮ�J���G��z�ؓ��0��_���.�ϢZ��q�,���F�DŽ߷��+̇%gY�.����=k���h������-;T��fP�+h*,��SoIVҜH�yj+�S�gy��������n}�,���4T7v+8�c��K��V'������]���tu}��[���UHj�Li)�R���TiT�`����|.$�-������#����8��a�U�t���~�*����y�j8K0�,m�Mޟ f&�n���P��eS |����<1̀��o%a��5/B��R;������|OF��_F�ً�Ԍz��Y~P��IA ����3����[�nc��sn]Y+��w��L���.�� -�pN\��%uZ��m��̝��,�&,��J������^�)t ��k䩧�p?���?��y�m�I�������F�m�H�ʃ�5�y� �O��d�2�'�/p�4����Y�����z�b�Δ93�/ ���$��I��*tX�2~�DW�l6[��Y�����e(l�<��K}"�����l9�|eR��p9����&,N{�����.���J��XM�>����L�����u��g�yJ��-��� -�����jw�E����Z��$�˛�b��W�t -0Q��e!�w�vm�g,��,aC��,�g�*�[c��XSj`�����_�7U8�nx̭A���:��`�7\q������n�.IØi��&^���ބ����md�d���r��-ڿ� -0�5�����>�N�:;�^L���C��D��}W=���y����䴺�B߫�a�TyR��^�Xx���)�~���@<�0���VB���֤�L-���4����/����C*�:'�)|��<�Q�B��S�P����y@���=��e���#��E]ܦ3ZEIv��8\�B.�ދ�D�¶k�+�v �Jm�SvPs�8�"�|*@7���������@��s�O�&Ui�?w�w�ʁ�u_nVXA�)C�u�-�o04r�5�|�� -��wo>N�����f"Ri�P�+i��Y�;����B���e� I�h�O�_BR�_\�E��J�~^BA5�S�`W -��g���j��?)N�p�!_��>p&=]�T��Ӝ�fN������Z�[��!��������8-�;x�v'���7�<�7�8Uv���^�\���G_m�A�$l� -yےҨ�|�9 fx��4X\�0��iK�������z�=��u��&<d�o���?�v��T$v���q�������H��[���q ɪnn�ms�<��$[�b�Ԫ��J%8w�}�IV�j����qKvwd��>x&|��q�P|���<�X&���d���ݯ��Q��A5�ܻ�v=����^!�f(��B;�ķ|=_��m���R�UHX�]����N���{E0���/ӣ�q�-b�)�c�� ���$v��zUO����^�:���VO�Y�ܴ��ema *���]I-��ǻ�^N��x��{HxV�w-�J�T���� -�'�X�}Jũ�b>5Q�h'P�>��qH�g�}�ʹ�*���.H��b���:-���}��\�! ��%��n���.�\������ҕ�=�[�+�<1�;y�K�V�yqW�v5)]Ң^�#�v��j��W�LW�Q�~Ƃ�B�9�v3����sW�-&��������wk�ﻦY���������wk=�H�8��J]�;����Λ���]���/�n%���}h�6d��:�>0���>���}���u��7��;�˱"��n�O���}`Y�wQ$7mw�E1�m �����&�� �8�6-�^���y��:<����[�lQ^������:�}�Q� � -×��i��dQ�{�b�Wn�"�� 1���w�5ծi��n>Wl�/�\'^�\��qj9I�r��2%��8?�#��<�YŚ�:������S��?�]m�5�5lJu��B����;F+�͗�D�^��p��J�a.�R�����LCޗ��d�婎��鮞�#T�ӊ]���'&� ��\��(�V-PzV��2�<�/���F�GC���'9�\�S�A�Z!V�!aUٝ�g��⮃�1O�Fn��2���@=ݑn��N*�ƭ���L4��������T���T���<�J���_��I����<���������[���T���'�� �����N5���������G�O��.� ���=.h+�w���'�K�o���.�f5��ܐ(��q(K��y�����ƕ�y�*@ �y�U�]@�Y4�����6'���p�{g�,��Mֈ����rE�N�8{��x='�vA'_rY��t����yk{�����['��ַ�+���s�}���PbH��t������܋Qm�ޠқG���y������ ��1es_u�wG��ù,/g�nJN����yoO<��S���feb�� �o��h�.A��yQ����wO�X�H�V�. P��AHT�S�r����7�bw\�c���_�+x=�{�*�[��?�̮o�8�yi�����N��B� i(�U���'������$���*S�������-N�懁�4O~o&�ްA��8�I>�_��`�Х�,�Y��ҏ�����:��'�a�jb��*ژ���;��۰�Uĉc��wU�Z�� -���<�; -�T�Th��0h��wΊ�;��N��[���?Qa&�5M�n~���.��'�L��^�6'�� -���=�KU�J�0��3bW���/�|�����������tUs~��#��%B��,:X�.j��s�?���������$�kPsT��r��i>�y�{]�D?C�V�t.�5W�qH��%?�wհ��b�:��\�n#8=~�J�ѝʺe��=d����8uq=4v�GI��Y��ARV��]��-[ڣYpu�b��v5�.a�W����i�sŨ�>w��U��ُ���HU�ޢ>���'��Fw�t� �i�2��56����ǹ��mˬ�r�(Y�ru��!u�^�}B�ϥO������m�)�s�+ -X���J���g<���{�lj���=_�\�d���ОO����Y�ɶ��#T\�a�I��-�ڝ��8(�5ad��e�C�ꕳ|���s��~p��F ;�� ����|S����f� �B��Ԑ�z���zn�"�m覙Ϻ�(��0�-^��ZNLQ�q8��P_{E��� wt ���z�T��r%VJ��ī�>*�[[����T~ ��h�g)⣊�#U��v#l�z�K�:�Ҕ���]���QQO��V��S�ʔ��O� 1Q&ɋ��~��\8�;R�(1}��v -�z�+}��&���<��)}/�>�Q�@��RP.��O�ip�US^R@ڶF�����z�G��7���#T��)�X��s�\��=����3k�Jc�_:���}�9��<zC j�?��OH�'���:��T����u��GЎ��;��Z6�1� L�H(����bM�W���9�az�^����3M�����t�Z�7����-��'���Oco�R�e����� �si�Yȇ�z��ܞ@��?|��9;oZXQC�8ȸ�g\R�rHY���8�K>�\�N���iJ0�������Tb�+�t}ŧ�8�Z�8�q̥6MR�w˿�HU?�����y�%��q ���W3ݎ���J��5�@v�v�:d�����8�N.Y+�L'�k�Ng���X"���A�8l���a���ҭ�q���Ա�Z�&��E9��J� �5\a���ݭ���6 A���'��żp%�5q��s�=�1]����D9t�f�p�{�p߷�C�>��x��A��w��R�%�h��s~yr��g!�c�A;�ݝ� �m��u����,wU�) -�^J��z��P����1u�ҋS�Sޒ�z�v=������ +�;�L�w�w ��8x�w+��/@�w�z"ˠ�W�-P��o(֎��wII&#��P�5�:�o�G~/ξ���1a1ۢ��pxʳƒ�J}d���8�(��3��t�F�{�%���T�Ϟ:�8:���2��@E�X��I�q$����,ϫ�OȔ���Ӕp����#��̀�&M�|�rԨ^��܌ɞ,�WD�Z����{�:a��W�f缎��~�.B��ͯ^�_B�Xx�i�@%SZ�4��?�tf��V�A�a��u�:�/77���<²��s�3�Qx*�������9U{]w&��m إ;`i��w9��Pe��Т�嶷 <�";�?�_��F�V�*�ba���ݽvSՁ,^Of�Rm��&��K����f&�G� @]�w�T�`�熽6&L�#,#�ea�GUdǡX�m�X�o�:Yٖ5{�������B'IP�guk[�C �L-)�=-�|%����}>i�23V�U ��#䉃=��}W���E]<�.��<����骼+HH�T>��&���q[yԯ����h -���w)wl랶�mD�GOwa������5��d����D�[k����ھ�L����[�)�&�,�3��UP$��(�j�vi��I��rE���H�"F���6-b?�C���=��y��r�w���� y�V���?0c��Sf!+��=ޭՊ��>��Į �O�[�5����[�!*��M�oȆ�٣�]{'�����ۺ��U�^S+v�(��j�zO�&7��A�mrԙ�ez�̖�G}<�Z7#6u�f��j��e[�]�����M˦�5զ_�0���^_(�/��m.�\���T'��r��ϗ�kY���a1�Ͽ���� �����pJn�e���[S➆����"��1k�iY�zRK4���r���q6����:�W3u�*.���mcδ-�̐�!�A��;����]���a�D�A7�����R��f{ ����d - ����u�]���-�te�ʃ����T�=����Ӊ��'D�YOꮿ�'7&���B@��s�Q�4;��%BU�%�w��F����A�}̃�m�%e������S��ҽ�_��p���8KF�5�Y �un�=<��V\XA�{`fu7�ϹLPo6��aV�gҞ+7�^7Q��}ߟ��<����U|��O���_��S���<���U����������o}�|�,%�>(���������|��?]�M�<��"_��V�jXM8��<��m��$r�������5��~�s�D� �������<L��BV�� m��l��������0��s���Dc�� >�|e@9�D7]�} - t���(�mݷl����8����Q�D�u�|� ��;hO�θ֙�3��~����,3�ޣ����������ȵJ�<���w����rˁ�8� D�h�w.��~��.ϽޤvpY�|q���7���Sh�烙=1#k�=,��[%N�z�y~��z���u1�_�wU~U)���>s�Q�#���ʫK���4��6�ɇ�3i�_{�ܡ��p{g�VE[D9Þ��o� 6c-�Z��Y�#mˋG�E�����i���_+�qt�Š��:(js�%P -�S�����������wC��[�p_��q�-��A��;U�֊M]�iƧ��E�D�k���Ȱ^��~Bo��;qk�B����[�������o3�ja�v��s�#ը�2٘�]�J��i�tU���ܸm���{swm��&!d�1u����q�W]��VO]r�H�柉�d6g��ݭ�_(Z R��(�0���9��>��� - ]�{c� 9�1#^,�W�S�����G�v�:��i�|K�,Y˥$C]U�@-�ټ���\9��W{�n�T]�����?E�s (��F�]��(F�j����[�WVX?'�~�Ԟ��[�d+��Â����|�p����UF�[�c����[�����[����S"���j߈��Id[�U��6ڥˤ羊��\5*ѹ���u�o�5�� -�!(xð8r���n��xV����i��},l�ڲ-M�ҡ�� �ĥ瑢��ru��P{� -���Dʯ]&X����N<��sM'�U|S-�7(���'��3�ty��~��������<����D?Q��U���_�\������ �#$Ίx�����U_|n��9�y�rEt_8#�:���.?�,�����=�sp�B����ϼ����u��G���u�~��"IU��2����^b7�/g!��3z���e�b6�8�]k\ѡ���*��_f�F-�V�Z櫈sQ�F���U��b֟}E�!G]&m��1r�2��Y<G>\��݅�8�J�iN~�6�'�-q B*EOyi�\qj7����:��I�Ř����\6ږ�f;�1M��x|^���~|S��d�i�ٯ3ꠈ�%�v�7�g�I��{#��۲_p�G�����DCH�F��s�1���3:PQ��#sȟx��Y}JRe*�Ky*���/ �Ư�U�H�BE����A��z�S������2��s��hI֠�i��J^i�P.�I풃.miFݦ�HS�N�W����OE������F j���e�7Fs'�����H�a~�����y���u�v|�t6�*��xЩz�m�W+�hJ'�đl�.�A�n�m�\�=����U�e���ҧԨ.�$�gb�t����l>N�T?�)��.�$�Nz�� R��b�A����׳��U�Q����ȶ��]���ۆ/�t���:��j����5ܤ�tV�� �D�:�,�c�^�,�+9�������jԲ����k�:C"����,�oߺ��<Z���|�$QN[Sz��2�<?t���h�BӶТ�J�xE� �����a㬡�1�.��Sx�G9�� ��~���_(��g�[�[�q�[k@��,���l�@�g$�uAC����5�H�%W{r�����9K3f����q\B�.Yb�V�ڒ$�&�s�[.<�Ħq��q�Me�@R�s�_�J�܊��� c��K#��ig��S�x��� �H)�`���+�0a��/|��7X�4�b���P�b�t\���A�g[{�ц���/���c�gl�,�wA�`�F��N��U����>] LG�Pu��-��S���N�%n�^�&�a -��#��Q]aϭn�N��Ke���q=����T�7�J����}�%�:��0�x�{T|�utN�ѿ.�h�5C_(=v6��^p���uw�{ޒG -��i8��J�ݼ����L�1�W*�՟G�`��j�K=`�9�ظ;�f��]v��VeN��f�|nK7<�-��v=U�A��A��v�Gw��ޗz^�n2c� -��-��dE����̰z>�.Cf��T�>0G�*(-���|h��k9l�ʻ��j�+֚lo�$��gM1�� ٝk)�Z:֑�}��*�Tf�������ա�4vU�x��ɭ��oM�k�7`����J�yRanB>Zc��<�D0�T�����;)�ePZ�hi�oo���y4��y�x8��(������f�Ϋ_���ʔ��O���O�,��KI��5Q�Au���J&N�?��C�`C��K����c�xj��Xv" -��Ѝ�[[�%�m��e8��U{�l6-]>�=l�X{.����Q1��)[g��^���Hg�-6,�IZ,/'m��fH��s�j����6{K�U��F�|n��7������` -BT�G������P6-wb�����3��jkŎ�y��U<�Os����U'�I�p�����1��>�9V�1~h��x��3&� �氺�ܞR������G�5����ݠ�Y�Lm�b��j�^l�o~X��y9I�8�r!.n�}�� -Wg}er��G����[.v���v ��61��,{�&(��/"�Z��솬�ĺ�I��S����^��d4�J �s1�>�EVv�\^�S�U)���x�����,�3��S���?E��s���-V���0�x�:�����F���S -��V��aiM5��j��e�tYi>�� -��L��q��H���1��>�uN�Ê�-�<�{�8�)J�=�>n�9�F�!| �l{ܚݘ`��x�+�P�� -��c��{�|��y3��iŴ����f�dG-�<F�F��^��T��9x_���_��$/ )U���0�guiՓw���z��E|�� ΠP�@�I{�@c(^�)(��GPz�u�դ���ct�����ϓ^%�j�� �n�7�J< ��n��)�ʲ<��`���?�t�o�y�H���K��9X��O�:?���_�����V���J���_��S���Tg��T���RE�����@E�@��в?�Ѐ�#c� -�b;~�0�_���\�a�aLò���a�0gr jG���znA�x�ɵ���k��5��ͼ�L�����į�U{�;(�qX�Y����% Б����T�H2�P&�M�����v�z����|e���)y���S��L����;g�Vv�P�aO�W�Z\����� -� -�u�Y����(�%�z��'� ��p�S�p����<&��A�0��z��&�x^���NX��:��� �e������NFdg��TzA�O5�����Rx<ja<����=@�*�/�G�N�7�JJa :s0�D����u����i���m�Q�C�q�������y'O�á��g望$^�=�W�V~���k2���2�z<�����9��l����� -b9R��(\s/PJ����u7��k�I��r�.O���� v���HuAk�7�J�$���bƛ�ٺ�[�V��:HY��H*-_�*V��8�ߙQ�rKJǴ6��_�wO��<��������>`��7�-���8�hcqrG��͙�nh�/"d���SkW�O��ۙ��qz<���Q{¦�t�;S�go�G�tK>�UU�;����������q�#��|S-����9��0��s��~�� Ӊ;�wNN}�,�v�۠Z4w�/�=�61Zrm}�~{�\�6�&\˖*�IEݵ��f'�e�̔$\�������@���7��N',�[2�J�b�9m�?1��˧Gc';Γ�*�o�2�����>��އ$�^����+��� es{3 -4k�re����+~$�RM��-�]���BDz}�� �<�!j=�C�x��:E�c�xF��tdi���l��㰱��k�J�2�qQUFvU�eٮ�;�T2�!"����J«��-DS�r^��Ü0L�r~��.����⬡?��PL�~S����)�c��S�� g�<���J�3|Yk����fp�[����Z�2�!O⫀��W���E?u� !��°�З��V.)�������E.��d�~E�P�b${3?wςf^��n��G�]&�tD?��n\�g> 7z�P��3�]��eW~�DBj" '�ֱ,�q����2jk~N�>�X_���Oعb��抳K����w��]z>Ai���u�Z�k��� ^����������G4W��'`�j��*��kK�a��d���/]��W�_��.�.p���m�0��Y�>5�*yX�8J3�iDhAR�A1(���磌D!:��!�ߵ}p@�s�a�rA�Y�3-�/ 6�|�M)<�$[`b8 }a<�&.֍�|~˕�7{�8��Z+f�Z�Ló�X���su��qB����/�g˷@�-?K�V�����|����E��� a��A�.l z�&Hy_+ WJ��~�ۗ�8�&��w|Vd�t���^�v�Ԓ��+{�>/�ߴ�@E�\���A������MU���iߍ�OG{�{]sNN�nO|t�p�Un˝'�)��s/�$/��zU������2��d!Ǵrz��l�����e�4�W���=����&N����~�8q��%�U� R ۥo[������;�]���p�]��n �#C���k_PYa�I���>κ�x,1������4*lR��]5N��|t�u���!���AJJ�MJ��OJ�����(EW���-Q:��8�{���V������Uϛ�zY�<U���Ƕb�<Q��¼r�|g�e�a�d P�O�x)�j�\p���{�D���^.��ъ��<l�|���U��]�V��u>�:��u0�u�'��i��HȤ�h������`Z�?� s�E�H0������=�?w���j�� ������bu��H����̂�ﲮ��I�z��9IDr�=�A�E�h�~|@��w�E�4�j&E�BHٗOL��'�%�b� -endstream endobj 6 0 obj [5 0 R] endobj 31 0 obj <</CreationDate(D:20170330061653+10'00')/Creator(Adobe Illustrator CS6 \(Windows\))/ModDate(D:20170330061653+09'00')/Producer(Adobe PDF library 10.01)/Title(icon)>> endobj xref -0 32 -0000000000 65535 f -0000000016 00000 n -0000000144 00000 n -0000046818 00000 n -0000000000 00000 f -0000048375 00000 n -0001096028 00000 n -0000046869 00000 n -0000047218 00000 n -0000430377 00000 n -0000048681 00000 n -0000048568 00000 n -0000047550 00000 n -0000047813 00000 n -0000047861 00000 n -0000048452 00000 n -0000048483 00000 n -0000048716 00000 n -0000430450 00000 n -0000430825 00000 n -0000431895 00000 n -0000441724 00000 n -0000507313 00000 n -0000571316 00000 n -0000636905 00000 n -0000702494 00000 n -0000768083 00000 n -0000833672 00000 n -0000899261 00000 n -0000964850 00000 n -0001030439 00000 n -0001096051 00000 n -trailer -<</Size 32/Root 1 0 R/Info 31 0 R/ID[<C6590951C9F58C459C088039EAA0DABA><F6BA73647D5C6D43ADF87922D44B487B>]>> -startxref -1096232 -%%EOF diff --git a/assets/mi.svg b/assets/mi.svg new file mode 100644 index 0000000000..d4f7cf7e9e --- /dev/null +++ b/assets/mi.svg @@ -0,0 +1,150 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="512" + height="512" + viewBox="0 0 135.46667 135.46667" + version="1.1" + id="svg8" + inkscape:version="0.92.1 r15371" + sodipodi:docname="mi.svg" + inkscape:export-filename="C:\Users\syuilo\projects\misskey\assets\favicon\32.png" + inkscape:export-xdpi="6" + inkscape:export-ydpi="6"> + <defs + id="defs2"> + <inkscape:path-effect + effect="simplify" + id="path-effect5115" + is_visible="true" + steps="1" + threshold="0.000408163" + smooth_angles="360" + helper_size="0" + simplify_individual_paths="false" + simplify_just_coalesce="false" + simplifyindividualpaths="false" + simplifyJustCoalesce="false" /> + <inkscape:path-effect + effect="simplify" + id="path-effect5111" + is_visible="true" + steps="1" + threshold="0.000408163" + smooth_angles="360" + helper_size="0" + simplify_individual_paths="false" + simplify_just_coalesce="false" + simplifyindividualpaths="false" + simplifyJustCoalesce="false" /> + <inkscape:path-effect + effect="simplify" + id="path-effect5104" + is_visible="true" + steps="1" + threshold="0.000408163" + smooth_angles="360" + helper_size="0" + simplify_individual_paths="false" + simplify_just_coalesce="false" + simplifyindividualpaths="false" + simplifyJustCoalesce="false" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.4142136" + inkscape:cx="232.39583" + inkscape:cy="251.50613" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="true" + units="px" + inkscape:snap-bbox="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="false" + inkscape:snap-smooth-nodes="true" + inkscape:snap-center="true" + inkscape:snap-page="true" + inkscape:window-width="1920" + inkscape:window-height="1017" + inkscape:window-x="-8" + inkscape:window-y="1072" + inkscape:window-maximized="1" + inkscape:snap-object-midpoints="true" + inkscape:snap-midpoints="true" + inkscape:object-paths="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + objecttolerance="1" + guidetolerance="1" + inkscape:snap-nodes="false" + inkscape:snap-others="false"> + <inkscape:grid + type="xygrid" + id="grid4504" + spacingx="4.2333334" + spacingy="4.2333334" + empcolor="#ff3fff" + empopacity="0.25098039" + empspacing="4" /> + </sodipodi:namedview> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="レイヤー 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-30.809093,-111.78601)"> + <g + id="g4502" + transform="matrix(1.096096,0,0,1.096096,-2.960633,-44.023579)"> + <g + style="fill:#2fa1bb;fill-opacity:0.94117647" + transform="translate(-1.3333333e-6,-1.3439941e-6)" + id="g5125"> + <g + transform="matrix(0.91391326,0,0,0.91391326,7.9719907,17.595761)" + id="text4489" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:141.03404236px;line-height:476.69509888px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#2fa1bb;fill-opacity:0.94117647;stroke:none;stroke-width:0.28950602px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + aria-label="Mi"> + <path + sodipodi:nodetypes="zccssscssccscczzzccsccsscscsccz" + inkscape:connector-curvature="0" + id="path5210" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#2fa1bb;fill-opacity:0.94117647;stroke-width:0.28950602px" + d="m 75.196381,231.17126 c -5.855419,0.0202 -10.885068,-3.50766 -13.2572,-7.61584 -1.266603,-1.79454 -3.772419,-2.43291 -3.807919,0 v 11.2332 c 0,4.51309 -1.645397,8.41504 -4.936191,11.70583 -3.196772,3.19677 -7.098714,4.79516 -11.705826,4.79516 -4.513089,0 -8.415031,-1.59839 -11.705825,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -61.7729 c 0,-3.47884 0.987238,-6.6286 2.961715,-9.44928 2.068499,-2.91471 4.701135,-4.9362 7.897906,-6.06447 1.786431,-0.65816 3.666885,-0.98724 5.641362,-0.98724 5.077225,0 9.308247,1.97448 12.693064,5.92343 1.786431,1.97448 2.820681,3.00873 3.102749,3.10275 0,0 13.408119,16.21319 13.78421,16.49526 0.376091,0.28206 1.480789,2.43848 4.127113,2.43848 2.646324,0 3.89218,-2.15642 4.26827,-2.43848 0.376091,-0.28207 13.784088,-16.49526 13.784088,-16.49526 0.09402,0.094 1.081261,-0.94022 2.961715,-3.10275 3.478837,-3.94895 7.756866,-5.92343 12.834096,-5.92343 1.88045,0 3.76091,0.32908 5.64136,0.98724 3.19677,1.12827 5.7824,3.14976 7.75688,6.06447 2.06849,2.82068 3.10274,5.97044 3.10274,9.44928 v 61.7729 c 0,4.51309 -1.6454,8.41504 -4.93619,11.70583 -3.19677,3.19677 -7.09871,4.79516 -11.70582,4.79516 -4.51309,0 -8.41504,-1.59839 -11.705828,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -11.2332 c -0.277898,-3.06563 -2.987588,-1.13379 -3.948953,0 -2.538613,4.70114 -7.401781,7.59567 -13.2572,7.61584 z" /> + <path + inkscape:connector-curvature="0" + id="path5212" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#2fa1bb;fill-opacity:0.94117647;stroke-width:0.28950602px" + d="m 145.83461,185.00361 q -5.92343,0 -10.15445,-4.08999 -4.08999,-4.23102 -4.08999,-10.15445 0,-5.92343 4.08999,-10.01342 4.23102,-4.23102 10.15445,-4.23102 5.92343,0 10.15445,4.23102 4.23102,4.08999 4.23102,10.01342 0,5.92343 -4.23102,10.15445 -4.23102,4.08999 -10.15445,4.08999 z m 0.14103,2.82068 q 5.92343,0 10.01342,4.23102 4.23102,4.23102 4.23102,10.15445 v 34.83541 q 0,5.92343 -4.23102,10.15445 -4.08999,4.08999 -10.01342,4.08999 -5.92343,0 -10.15445,-4.08999 -4.23102,-4.23102 -4.23102,-10.15445 v -34.83541 q 0,-5.92343 4.23102,-10.15445 4.23102,-4.23102 10.15445,-4.23102 z" /> + </g> + </g> + </g> + </g> +</svg> diff --git a/assets/ss.jpg b/assets/ss.jpg deleted file mode 100644 index a337b3fa7c..0000000000 Binary files a/assets/ss.jpg and /dev/null differ diff --git a/assets/title.png b/assets/title.png index 05658c8779..cacbb248d3 100644 Binary files a/assets/title.png and b/assets/title.png differ diff --git a/assets/title.svg b/assets/title.svg index ad8290fe98..95ad11c399 100644 --- a/assets/title.svg +++ b/assets/title.svg @@ -1,55 +1,140 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="248px" - height="80px" viewBox="0 0 248 80" enable-background="new 0 0 248 80" xml:space="preserve"> -<g id="レイヤー_1"> - <g> - <path fill="#1D2022" d="M126.516,17.676v44.355h-1.152V19.26l-19.297,27.363h-0.936L85.833,19.26v42.771H84.68V17.676h1.44 - l19.514,27.723l19.441-27.723H126.516z"/> - <path fill="#1D2022" d="M136.448,40.142c-0.576,0-1.008-0.432-1.008-1.008s0.432-1.008,1.008-1.008s1.008,0.432,1.008,1.008 - S137.024,40.142,136.448,40.142z M135.872,62.031V44.895h1.152v17.137H135.872z"/> - <path fill="#1D2022" d="M149.769,54.471c-2.88-0.791-4.752-2.447-4.752-5.04c0-3.097,2.52-4.968,5.472-4.968 - c1.872,0,3.384,0.791,4.393,1.943l-0.792,0.721c-0.936-1.152-2.088-1.729-3.672-1.729c-2.521,0-4.249,1.584-4.249,3.816 - c0,2.376,1.44,3.6,4.249,4.32c3.096,0.791,4.608,2.305,4.608,4.752c0,2.664-2.592,4.32-5.473,4.32 - c-1.872,0-3.384-0.432-5.112-1.729l0.648-0.863c1.8,1.225,2.952,1.656,4.537,1.656c2.448,0,4.248-1.297,4.248-3.313 - S152.577,55.264,149.769,54.471z"/> - <path fill="#1D2022" d="M165.896,54.471c-2.88-0.791-4.752-2.447-4.752-5.04c0-3.097,2.52-4.968,5.472-4.968 - c1.872,0,3.384,0.791,4.393,1.943l-0.792,0.721c-0.936-1.152-2.088-1.729-3.672-1.729c-2.521,0-4.249,1.584-4.249,3.816 - c0,2.376,1.44,3.6,4.249,4.32c3.096,0.791,4.608,2.305,4.608,4.752c0,2.664-2.592,4.32-5.473,4.32 - c-1.872,0-3.384-0.432-5.112-1.729l0.648-0.863c1.8,1.225,2.952,1.656,4.537,1.656c2.448,0,4.248-1.297,4.248-3.313 - S168.705,55.264,165.896,54.471z"/> - <path fill="#1D2022" d="M178.64,62.031V5.291h1.152v49.324l10.513-9.721h1.656l-9.289,8.353l9.577,8.784h-1.728l-8.713-8.064 - l-2.016,1.873v6.191H178.64z"/> - <path fill="#1D2022" d="M196.927,53.823c0-6.265,3.889-9.36,8.713-9.36c5.185,0,7.777,4.031,7.777,7.848v1.08h-15.266 - c0,5.256,3.384,8.281,7.777,8.281c2.664,0,4.608-1.152,6.265-3.24l0.648,0.648c-1.656,2.232-3.888,3.527-7.272,3.527 - C200.672,62.607,196.927,59.223,196.927,53.823z M198.224,52.383h13.897c0-4.176-2.52-6.984-6.552-6.984 - C201.464,45.398,198.728,47.99,198.224,52.383z"/> - <path fill="#1D2022" d="M217.807,44.895h1.368l6.625,15.77l6.625-15.77h1.296l-7.489,17.137 - c-1.656,3.744-4.464,8.641-7.272,12.674l-0.936-0.721c2.736-3.816,5.977-9.145,7.201-12.096L217.807,44.895z"/> - </g> - <path fill="#1D2022" d="M72,20c0-6.627-5.373-12-12-12H20C13.373,8,8,13.373,8,20v40c0,6.627,5.373,12,12,12h40 - c6.627,0,12-5.373,12-12V20z"/> -</g> -<g id="レイヤー_2"> - <polyline fill="none" stroke="#FFFFFF" stroke-width="3" stroke-miterlimit="10" points="20.8,46.133 30.4,33.333 40,46.133 - 49.6,33.333 59.2,46.133 "/> - <circle fill="#FFFFFF" cx="49.6" cy="33.333" r="4.8"/> - <circle fill="#FFFFFF" cx="40" cy="46.133" r="4.8"/> - <circle fill="#FFFFFF" cx="20.8" cy="46.133" r="4.8"/> - <circle fill="#FFFFFF" cx="59.2" cy="46.133" r="4.8"/> - <circle fill="#FFFFFF" cx="30.4" cy="33.333" r="4.8"/> - <g> - </g> - <g> - </g> - <g> - </g> - <g> - </g> - <g> - </g> - <g> - </g> -</g> -</svg> +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="614.71039" + height="205.08009" + viewBox="0 0 162.64213 54.260776" + version="1.1" + id="svg8" + inkscape:version="0.92.1 r15371" + sodipodi:docname="misskey.svg" + inkscape:export-filename="C:\Users\Takumiya_Cho\Desktop\misskey.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96"> + <defs + id="defs2"> + <inkscape:path-effect + effect="simplify" + id="path-effect5115" + is_visible="true" + steps="1" + threshold="0.000408163" + smooth_angles="360" + helper_size="0" + simplify_individual_paths="false" + simplify_just_coalesce="false" + simplifyindividualpaths="false" + simplifyJustCoalesce="false" /> + <inkscape:path-effect + effect="simplify" + id="path-effect5104" + is_visible="true" + steps="1" + threshold="0.000408163" + smooth_angles="360" + helper_size="0" + simplify_individual_paths="false" + simplify_just_coalesce="false" + simplifyindividualpaths="false" + simplifyJustCoalesce="false" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="0.9899495" + inkscape:cx="370.82839" + inkscape:cy="79.043895" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + units="px" + inkscape:snap-bbox="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="false" + inkscape:snap-smooth-nodes="true" + inkscape:snap-center="true" + inkscape:snap-page="true" + inkscape:window-width="1920" + inkscape:window-height="1017" + inkscape:window-x="-8" + inkscape:window-y="1072" + inkscape:window-maximized="1" + inkscape:object-paths="true" + inkscape:bbox-paths="true" + fit-margin-top="50" + fit-margin-left="50" + fit-margin-bottom="20" + fit-margin-right="50" /> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="レイヤー 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-11.097531,-173.29664)"> + <g + transform="matrix(0.28612302,0,0,0.28612302,17.176981,141.74334)" + id="text4489-6" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:141.03404236px;line-height:476.69509888px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#212d3a;fill-opacity:1;stroke:none;stroke-width:0.92471898px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + aria-label="Mi"> + <path + sodipodi:nodetypes="zccssscssccscczzzccsccsscscsccz" + inkscape:connector-curvature="0" + id="path5210" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#212d3a;fill-opacity:1;stroke-width:0.92471898px" + d="m 75.196381,231.17126 c -5.855419,0.0202 -10.885068,-3.50766 -13.2572,-7.61584 -1.266603,-1.79454 -3.772419,-2.43291 -3.807919,0 v 11.2332 c 0,4.51309 -1.645397,8.41504 -4.936191,11.70583 -3.196772,3.19677 -7.098714,4.79516 -11.705826,4.79516 -4.513089,0 -8.415031,-1.59839 -11.705825,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -61.7729 c 0,-3.47884 0.987238,-6.6286 2.961715,-9.44928 2.068499,-2.91471 4.701135,-4.9362 7.897906,-6.06447 1.786431,-0.65816 3.666885,-0.98724 5.641362,-0.98724 5.077225,0 9.308247,1.97448 12.693064,5.92343 1.786431,1.97448 2.820681,3.00873 3.102749,3.10275 0,0 13.408119,16.21319 13.78421,16.49526 0.376091,0.28206 1.480789,2.43848 4.127113,2.43848 2.646324,0 3.89218,-2.15642 4.26827,-2.43848 0.376091,-0.28207 13.784088,-16.49526 13.784088,-16.49526 0.09402,0.094 1.081261,-0.94022 2.961715,-3.10275 3.478837,-3.94895 7.756866,-5.92343 12.834096,-5.92343 1.88045,0 3.76091,0.32908 5.64136,0.98724 3.19677,1.12827 5.7824,3.14976 7.75688,6.06447 2.06849,2.82068 3.10274,5.97044 3.10274,9.44928 v 61.7729 c 0,4.51309 -1.6454,8.41504 -4.93619,11.70583 -3.19677,3.19677 -7.09871,4.79516 -11.70582,4.79516 -4.51309,0 -8.41504,-1.59839 -11.705828,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -11.2332 c -0.277898,-3.06563 -2.987588,-1.13379 -3.948953,0 -2.538613,4.70114 -7.401781,7.59567 -13.2572,7.61584 z" /> + <path + inkscape:connector-curvature="0" + id="path5212" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#212d3a;fill-opacity:1;stroke-width:0.92471898px" + d="m 145.83461,185.00361 q -5.92343,0 -10.15445,-4.08999 -4.08999,-4.23102 -4.08999,-10.15445 0,-5.92343 4.08999,-10.01342 4.23102,-4.23102 10.15445,-4.23102 5.92343,0 10.15445,4.23102 4.23102,4.08999 4.23102,10.01342 0,5.92343 -4.23102,10.15445 -4.23102,4.08999 -10.15445,4.08999 z m 0.14103,2.82068 q 5.92343,0 10.01342,4.23102 4.23102,4.23102 4.23102,10.15445 v 34.83541 q 0,5.92343 -4.23102,10.15445 -4.08999,4.08999 -10.01342,4.08999 -5.92343,0 -10.15445,-4.08999 -4.23102,-4.23102 -4.23102,-10.15445 v -34.83541 q 0,-5.92343 4.23102,-10.15445 4.23102,-4.23102 10.15445,-4.23102 z" /> + </g> + <path + inkscape:connector-curvature="0" + id="path5199" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#212d3a;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 72.022691,200.53715 q 0.968125,0.24203 2.420312,0.5244 2.420313,0.40339 3.791824,1.29083 2.581666,1.69422 2.581666,5.08266 0,2.74302 -1.815234,4.47758 -2.097604,2.01693 -5.849089,2.01693 -2.743021,0 -6.131458,-0.76644 -1.089141,-0.24203 -1.774896,-1.08914 -0.645417,-0.84711 -0.645417,-1.89591 0,-1.29083 0.887448,-2.17828 0.927786,-0.92779 2.178281,-0.92779 0.363047,0 0.685756,0.0807 1.169817,0.24203 4.477578,0.60508 0.443724,0 0.968125,-0.0403 0.201693,0 0.201693,-0.24203 0.04034,-0.20169 -0.242032,-0.28237 -1.37151,-0.24203 -2.541328,-0.5244 -1.331172,-0.28237 -1.895911,-0.48406 -1.12948,-0.32271 -1.895912,-0.84711 -2.581667,-1.69422 -2.622005,-5.08266 0,-2.70268 1.855573,-4.47758 2.258958,-2.17828 6.413828,-1.97659 2.783359,0.12102 5.566719,0.7261 1.048802,0.24203 1.734557,1.08914 0.685756,0.84711 0.685756,1.93625 0,1.25049 -0.927787,2.17828 -0.887448,0.88745 -2.178281,0.88745 -0.322709,0 -0.645417,-0.0807 -1.169818,-0.24203 -4.517917,-0.56474 -0.403385,-0.0403 -0.766432,0 -0.322708,0.0403 -0.322708,0.24203 0.04034,0.24203 0.322708,0.32271 z" /> + <path + inkscape:connector-curvature="0" + id="path5201" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#212d3a;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 89.577027,200.53715 q 0.968125,0.24203 2.420312,0.5244 2.420313,0.40339 3.791823,1.29083 2.581667,1.69422 2.581667,5.08266 0,2.74302 -1.815234,4.47758 -2.097604,2.01693 -5.849089,2.01693 -2.743021,0 -6.131458,-0.76644 -1.089141,-0.24203 -1.774896,-1.08914 -0.645417,-0.84711 -0.645417,-1.89591 0,-1.29083 0.887448,-2.17828 0.927786,-0.92779 2.178281,-0.92779 0.363047,0 0.685755,0.0807 1.169818,0.24203 4.477579,0.60508 0.443724,0 0.968125,-0.0403 0.201692,0 0.201692,-0.24203 0.04034,-0.20169 -0.242031,-0.28237 -1.37151,-0.24203 -2.541328,-0.5244 -1.331172,-0.28237 -1.895912,-0.48406 -1.129479,-0.32271 -1.895911,-0.84711 -2.581667,-1.69422 -2.622005,-5.08266 0,-2.70268 1.855573,-4.47758 2.258958,-2.17828 6.413828,-1.97659 2.783359,0.12102 5.566719,0.7261 1.048802,0.24203 1.734557,1.08914 0.685755,0.84711 0.685755,1.93625 0,1.25049 -0.927786,2.17828 -0.887448,0.88745 -2.178281,0.88745 -0.322709,0 -0.645417,-0.0807 -1.169818,-0.24203 -4.517917,-0.56474 -0.403385,-0.0403 -0.766432,0 -0.322708,0.0403 -0.322708,0.24203 0.04034,0.24203 0.322708,0.32271 z" /> + <path + inkscape:connector-curvature="0" + id="path5203" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#212d3a;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 115.65209,203.87137 q 0.12101,0.0807 2.86404,2.78336 1.25049,1.21016 1.25049,2.94471 0,1.61354 -1.16982,2.86404 -1.16982,1.21016 -2.90437,1.21016 -1.65388,0 -2.86404,-1.16982 l -4.03385,-3.91284 q -0.16136,-0.12102 -0.32271,-0.12102 -0.32271,0 -0.32271,1.21016 0,1.69422 -1.21016,2.90438 -1.21015,1.16981 -2.90437,1.16981 -1.69422,0 -2.90438,-1.16981 -1.169807,-1.21016 -1.169807,-2.90438 v -18.79776 q 0,-1.69422 1.169807,-2.86404 1.21016,-1.21015 2.90438,-1.21015 1.69422,0 2.90437,1.21015 1.21016,1.16982 1.21016,2.86404 v 6.29281 q 0,0.40339 0.28237,0.5244 0.24203,0.12102 0.5244,-0.0807 0.16135,-0.0807 4.84063,-3.18675 1.0488,-0.64542 2.25895,-0.64542 2.21862,0 3.42878,1.81524 0.64542,1.0488 0.64542,2.25896 0,2.21862 -1.81524,3.42877 l -2.54133,1.61354 v 0.0403 l -0.0807,0.0403 q -0.56474,0.36305 -0.0403,0.88745 z" /> + <path + inkscape:connector-curvature="0" + id="path5205" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#212d3a;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 131.25181,213.92955 q -4.19521,0 -7.18026,-2.94472 -2.94472,-2.98505 -2.94472,-7.18026 0,-4.15487 2.94472,-7.09958 2.98505,-2.98505 7.18026,-2.98505 4.15487,0 6.97857,2.78335 0.92778,0.92779 0.92778,2.25896 0,1.33118 -0.92778,2.25896 l -4.67928,4.63893 q -1.00846,1.00847 -2.01692,1.00847 -1.45219,0 -2.25896,-0.80677 -0.80677,-0.80677 -0.80677,-2.13795 0,-1.29083 0.92778,-2.21862 l 0.80678,-0.84711 q 0.16135,-0.12101 0.0807,-0.24203 -0.12101,-0.0807 -0.32271,-0.0403 -0.80677,0.20169 -1.37151,0.80677 -1.12948,1.08914 -1.12948,2.622 0,1.5732 1.08915,2.70268 1.12947,1.08914 2.70268,1.08914 1.53286,0 2.622,-1.12947 0.92779,-0.92779 2.25896,-0.92779 1.33117,0 2.25896,0.92779 0.92779,0.92778 0.92779,2.25895 0,1.33118 -0.92779,2.25896 -2.98505,2.94472 -7.13992,2.94472 z" /> + <path + inkscape:connector-curvature="0" + id="path5207" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#212d3a;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 160.51049,198.1433 v 5.60705 q 0,0.56474 -0.0807,1.21016 v 7.38195 q 0,4.51792 -2.74302,7.2206 -2.70268,2.70269 -7.30128,2.70269 -2.66234,0 -4.80028,-1.00847 -2.13795,-0.96812 -2.13795,-3.3481 0,-0.80677 0.36305,-1.53286 0.96812,-2.17828 3.3481,-2.17828 0.56474,0 1.5732,0.32271 1.00847,0.3227 1.65388,0.3227 1.69422,0 2.21862,-0.72609 0.20169,-0.28237 0.0807,-0.44372 -0.16136,-0.24204 -0.56474,-0.16136 -0.68576,0.12102 -1.49253,0.12102 -4.07419,0 -6.97856,-2.90438 -2.90438,-2.90437 -2.90438,-6.97857 v -5.60705 q 0,-1.69422 1.16982,-2.86404 1.21015,-1.21016 2.90437,-1.21016 1.69422,0 2.90438,1.21016 1.21015,1.16982 1.21015,2.86404 v 5.60705 q 0,0.68576 0.48407,1.21016 0.5244,0.48406 1.21015,0.48406 0.7261,0 1.21016,-0.48406 0.48406,-0.5244 0.48406,-1.21016 v -5.60705 q 0,-1.69422 1.21016,-2.86404 1.21015,-1.21016 2.90437,-1.21016 1.69422,0 2.86404,1.21016 1.21016,1.16982 1.21016,2.86404 z" /> + </g> +</svg> diff --git a/binding.gyp b/binding.gyp new file mode 100644 index 0000000000..0349526d52 --- /dev/null +++ b/binding.gyp @@ -0,0 +1,9 @@ +{ + 'targets': [ + { + 'target_name': 'crypto_key', + 'sources': ['src/crypto_key.cc'], + 'include_dirs': ['<!(node -e "require(\'nan\')")'] + } + ] +} diff --git a/docker/Dockerfile b/docker/Dockerfile index ef04fc9e26..7cee650de3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -14,7 +14,7 @@ RUN pacman -S --noconfirm pacman RUN pacman-db-upgrade RUN pacman -S --noconfirm archlinux-keyring RUN pacman -Syyu --noconfirm -RUN pacman -S --noconfirm git nodejs npm mongodb redis graphicsmagick +RUN pacman -S --noconfirm git nodejs npm mongodb redis imagemagick COPY misskey.sh /root/misskey.sh RUN chmod u+x /root/misskey.sh diff --git a/docs/backup.md b/docs/backup.md new file mode 100644 index 0000000000..74ec2678e5 --- /dev/null +++ b/docs/backup.md @@ -0,0 +1,22 @@ +How to backup your Misskey +========================== + +Make sure **mongodb-tools** installed. + +--- + +In your shell: +``` shell +$ mongodump --archive=db-backup -u <YourUserName> -p <YourPassword> +``` + +For details, plese see [mongodump docs](https://docs.mongodb.com/manual/reference/program/mongodump/). + +Restore +------- + +``` shell +$ mongorestore --archive=db-backup +``` + +For details, please see [mongorestore docs](https://docs.mongodb.com/manual/reference/program/mongorestore/). diff --git a/docs/donate.ja.md b/docs/donate.ja.md new file mode 100644 index 0000000000..b19d7bc370 --- /dev/null +++ b/docs/donate.ja.md @@ -0,0 +1,26 @@ +# Misskeyにカンパする方法 +Misskeyのサポートにご興味をお持ちいただきありがとうございます! +Misskeyにカンパをしていただくと、貴方のお名前と好きなURLなどをMisskeyのリポジトリに刻む権利がもらえます。 + +Misskeyにカンパして開発・運営をサポートするには、次のいくつかの方法があります: + +## ConoHaカードを購入する +(本家)Misskeyは、ConoHaというVPSサービスを利用しています。ConoHaカードを購入して、 +カードに記載されているクーポンコードを syuilotan@yahoo.co.jp までお送りいただければ、 +そのクーポンをチャージしてサーバーの運営費に充てることができます。 + +ConoHaカードについてはこちらをご覧ください: https://www.conoha.jp/conohacard/ + +Amazonでも買えます: https://www.amazon.co.jp/dp/B01N9E3416 + +## Amazonギフトカード +これは間接的な方法です。 + +## 銀行振込 +syuilotan@yahoo.co.jp までお問い合わせください。 + +## 手渡し +オフ会を行ったときなどに行使できる方法です。 + +## その他 +なにかいいアイデアがあればお教えください。 diff --git a/docs/setup.en.md b/docs/setup.en.md index 3e48935346..88e20f6bc4 100644 --- a/docs/setup.en.md +++ b/docs/setup.en.md @@ -1,41 +1,28 @@ Misskey Setup and Installation Guide ================================================================ -We thank you for your interest in setup your Misskey server! +We thank you for your interest in setting up your Misskey server! This guide describes how to install and setup Misskey. [Japanese version also available - 日本語版もあります](./setup.ja.md) ---------------------------------------------------------------- -If you can use Docker, please see [Setup with Docker](./docker.en.md). - -*1.* Domains ----------------------------------------------------------------- -Misskey requires two domains called the primary domain and the secondary domain. - -* The primary domain is used to provide main service of Misskey. -* The secondary domain is used to avoid vulnerabilities such as XSS. - -**Ensure that the secondary domain is not a subdomain of the primary domain.** - -### Subdomains -Note that Misskey uses following subdomains: - -* **api**.*{primary domain}* -* **auth**.*{primary domain}* -* **about**.*{primary domain}* -* **stats**.*{primary domain}* -* **status**.*{primary domain}* -* **dev**.*{primary domain}* -* **file**.*{secondary domain}* - -*2.* reCAPTCHA tokens +*1.* reCAPTCHA tokens ---------------------------------------------------------------- Misskey requires reCAPTCHA tokens. Please visit https://www.google.com/recaptcha/intro/ and generate keys. -*3.* Install dependencies +*(optional)* Generating VAPID keys +---------------------------------------------------------------- +If you want to enable ServiceWroker, you need to generate VAPID keys: + +``` shell +npm install web-push -g +web-push generate-vapid-keys +``` + +*2.* Install dependencies ---------------------------------------------------------------- Please install and setup these softwares: @@ -43,52 +30,39 @@ Please install and setup these softwares: * *Node.js* and *npm* * **[MongoDB](https://www.mongodb.com/)** * **[Redis](https://redis.io/)** -* **[GraphicsMagick](http://www.graphicsmagick.org/)** +* **[ImageMagick](http://www.imagemagick.org/script/index.php)** ##### Optional * [Elasticsearch](https://www.elastic.co/) - used to provide searching feature instead of MongoDB -*4.* Install Misskey +*3.* Install Misskey ---------------------------------------------------------------- -There is **two ways** to install Misskey: - -### WAY 1) Using built code (recommended) -We have official release of Misskey. -The built code is automatically pushed to https://github.com/syuilo/misskey/tree/release after the CI test succeeds. - -1. `git clone -b release git://github.com/syuilo/misskey.git` -2. `cd misskey` -3. `npm install` - -#### Update -1. `git fetch` -2. `git reset --hard origin/release` -3. `npm install` - -### WAY 2) Using source code -If you want to build Misskey manually, you can do it via the -`build` command after download the source code of Misskey and install dependencies: - 1. `git clone -b master git://github.com/syuilo/misskey.git` 2. `cd misskey` 3. `npm install` -4. `npm run build` -#### Update -1. `git pull origin master` -2. `npm install` -3. `npm run build` +*4.* Prepare configuration +---------------------------------------------------------------- +1. Copy `example.yml` of `.config` directory +2. Rename it to `default.yml` +3. Edit it -*5.* That is it. +--- + +Or you can generate config file via `npm run config` command. + +*5.* Build Misskey +---------------------------------------------------------------- +1. `npm run build` + +*6.* That is it. ---------------------------------------------------------------- Well done! Now, you have an environment that run to Misskey. ### Launch Just `sudo npm start`. GLHF! -### Testing -Run `npm test` after building - -### Debugging :bug: -#### Show debug messages -Misskey uses [debug](https://github.com/visionmedia/debug) and the namespace is `misskey:*`. +### Way to Update to latest version of your Misskey +1. `git reset --hard && git pull origin master` +2. `npm install` +3. `npm run build` diff --git a/docs/setup.ja.md b/docs/setup.ja.md index 4f48a08088..a46c38cb21 100644 --- a/docs/setup.ja.md +++ b/docs/setup.ja.md @@ -8,35 +8,21 @@ Misskeyサーバーの構築にご関心をお寄せいただきありがとう ---------------------------------------------------------------- -Dockerを利用してMisskeyを構築することもできます: [Setup with Docker](./docker.en.md)。 -その場合、*3番目*以降の手順はスキップできます。 - -*1.* ドメインの用意 ----------------------------------------------------------------- -Misskeyはプライマリ ドメインとセカンダリ ドメインを必要とします。 - -* プライマリ ドメインはMisskeyの主要な部分を提供するために使われます。 -* セカンダリ ドメインはXSSといった脆弱性の対策に使われます。 - -**セカンダリ ドメインがプライマリ ドメインのサブドメインであってはなりません。** - -### サブドメイン -Misskeyは以下のサブドメインを使います: - -* **api**.*{primary domain}* -* **auth**.*{primary domain}* -* **about**.*{primary domain}* -* **stats**.*{primary domain}* -* **status**.*{primary domain}* -* **dev**.*{primary domain}* -* **file**.*{secondary domain}* - -*2.* reCAPTCHAトークンの用意 +*1.* reCAPTCHAトークンの用意 ---------------------------------------------------------------- MisskeyはreCAPTCHAトークンを必要とします。 https://www.google.com/recaptcha/intro/ にアクセスしてトークンを生成してください。 -*3.* 依存関係をインストールする +*(オプション)* VAPIDキーペアの生成 +---------------------------------------------------------------- +ServiceWorkerを有効にする場合、VAPIDキーペアを生成する必要があります: + +``` shell +npm install web-push -g +web-push generate-vapid-keys +``` + +*2.* 依存関係をインストールする ---------------------------------------------------------------- これらのソフトウェアをインストール・設定してください: @@ -44,54 +30,40 @@ https://www.google.com/recaptcha/intro/ にアクセスしてトークンを生 * *Node.js* と *npm* * **[MongoDB](https://www.mongodb.com/)** * **[Redis](https://redis.io/)** -* **[GraphicsMagick](http://www.graphicsmagick.org/)** +* **[ImageMagick](http://www.imagemagick.org/script/index.php)** ##### オプション * [Elasticsearch](https://www.elastic.co/) - 検索機能を向上させるために用います。 -*4.* Misskeyのインストール +*3.* Misskeyのインストール ---------------------------------------------------------------- -Misskeyをインストールするには**2つの方法**があります: - -### 方法 1) ビルドされたコードを利用する (推奨) -Misskeyには公式のリリースがあります。 -ビルドされたコードはCIテストに合格した後、自動で https://github.com/syuilo/misskey/tree/release にpushされています。 - -1. `git clone -b release git://github.com/syuilo/misskey.git` -2. `cd misskey` -3. `npm install` - -#### アップデートするには: -1. `git fetch` -2. `git reset --hard origin/release` -3. `npm install` - -### 方法 2) ソースコードを利用する -> 注: この方法では正しくビルド・動作できることは保証されません。 - -Misskeyを手動でビルドしたい場合は、Misskeyのソースコードと依存関係をインストールした後、 -`build`コマンドを用いることができます: - 1. `git clone -b master git://github.com/syuilo/misskey.git` 2. `cd misskey` 3. `npm install` -4. `npm run build` -#### アップデートするには: -1. `git pull origin master` -2. `npm install` -3. `npm run build` +*4.* 設定ファイルを用意する +---------------------------------------------------------------- +1. `.config`ディレクトリ内の`example.yml`をコピー +2. `default.yml`にリネーム +3. 編集する -*5.* 以上です! +--- + +または、`npm run config`コマンドを利用して、ガイドに従って情報を +入力して設定ファイルを生成することもできます。 + +*5.* Misskeyのビルド +---------------------------------------------------------------- +1. `npm run build` + +*6.* 以上です! ---------------------------------------------------------------- お疲れ様でした。これでMisskeyを動かす準備は整いました。 ### 起動 `sudo npm start`するだけです。GLHF! -### テスト -(ビルドされている状態で)`npm test` - -### デバッグ :bug: -#### デバッグメッセージを表示するようにする -Misskeyは[debug](https://github.com/visionmedia/debug)モジュールを利用しており、ネームスペースは`misskey:*`となっています。 +### Misskeyを最新バージョンにアップデートする方法: +1. `git reset --hard && git pull origin master` +2. `npm install` +3. `npm run build` diff --git a/gulpfile.ts b/gulpfile.ts index 4ee5fbce0e..fe3b040237 100644 --- a/gulpfile.ts +++ b/gulpfile.ts @@ -3,28 +3,32 @@ */ import * as childProcess from 'child_process'; +import * as fs from 'fs'; import * as Path from 'path'; import * as gulp from 'gulp'; import * as gutil from 'gulp-util'; import * as ts from 'gulp-typescript'; +const sourcemaps = require('gulp-sourcemaps'); import tslint from 'gulp-tslint'; -import * as es from 'event-stream'; import cssnano = require('gulp-cssnano'); import * as uglifyComposer from 'gulp-uglify/composer'; import pug = require('gulp-pug'); import * as rimraf from 'rimraf'; -import * as chalk from 'chalk'; +import chalk from 'chalk'; import imagemin = require('gulp-imagemin'); import * as rename from 'gulp-rename'; import * as mocha from 'gulp-mocha'; import * as replace from 'gulp-replace'; import * as htmlmin from 'gulp-htmlmin'; const uglifyes = require('uglify-es'); + +import { fa } from './src/build/fa'; import version from './src/version'; +import config from './src/config'; const uglify = uglifyComposer(uglifyes, console); -const env = process.env.NODE_ENV; +const env = process.env.NODE_ENV || 'development'; const isProduction = env === 'production'; const isDebug = !isProduction; @@ -35,40 +39,34 @@ if (isDebug) { const constants = require('./src/const.json'); +require('./src/client/docs/gulpfile.ts'); + gulp.task('build', [ - 'build:js', 'build:ts', 'build:copy', - 'build:client' + 'build:client', + 'doc' ]); gulp.task('rebuild', ['clean', 'build']); -gulp.task('build:js', () => - gulp.src(['./src/**/*.js', '!./src/web/**/*.js']) - .pipe(gulp.dest('./built/')) -); - gulp.task('build:ts', () => { - const tsProject = ts.createProject('./src/tsconfig.json'); + const tsProject = ts.createProject('./tsconfig.json'); return tsProject .src() + .pipe(sourcemaps.init()) .pipe(tsProject()) + .pipe(sourcemaps.write('.', { includeContent: false, sourceRoot: '../built' })) .pipe(gulp.dest('./built/')); }); gulp.task('build:copy', () => - es.merge( - gulp.src([ - './src/**/assets/**/*', - '!./src/web/app/**/assets/**/*' - ]).pipe(gulp.dest('./built/')) as any, - gulp.src([ - './src/web/about/**/*', - '!./src/web/about/**/*.pug' - ]).pipe(gulp.dest('./built/web/about/')) as any - ) + gulp.src([ + './build/Release/crypto_key.node', + './src/**/assets/**/*', + '!./src/client/app/**/assets/**/*' + ]).pipe(gulp.dest('./built/')) ); gulp.task('test', ['lint', 'mocha']); @@ -81,10 +79,20 @@ gulp.task('lint', () => .pipe(tslint.report()) ); +gulp.task('format', () => +gulp.src('./src/**/*.ts') + .pipe(tslint({ + formatter: 'verbose', + fix: true + })) + .pipe(tslint.report()) +); + gulp.task('mocha', () => gulp.src([]) .pipe(mocha({ - //compilers: 'ts:ts-node/register' + exit: true, + compilers: 'ts:ts-node/register' } as any)) ); @@ -100,55 +108,43 @@ gulp.task('default', ['build']); gulp.task('build:client', [ 'build:ts', - 'build:js', - 'webpack', 'build:client:script', 'build:client:pug', 'copy:client' ]); -gulp.task('webpack', done => { - const webpack = childProcess.spawn( - Path.join('.', 'node_modules', '.bin', 'webpack'), - ['--config', './webpack/webpack.config.ts'], { - shell: true, - stdio: 'inherit' - }); - - webpack.on('exit', done); -}); - gulp.task('build:client:script', () => - gulp.src(['./src/web/app/boot.js', './src/web/app/safe.js']) + gulp.src(['./src/client/app/boot.js', './src/client/app/safe.js']) .pipe(replace('VERSION', JSON.stringify(version))) + .pipe(replace('API', JSON.stringify(config.api_url))) + .pipe(replace('ENV', JSON.stringify(env))) .pipe(isProduction ? uglify({ toplevel: true - }) : gutil.noop()) - .pipe(gulp.dest('./built/web/assets/')) as any + } as any) : gutil.noop()) + .pipe(gulp.dest('./built/client/assets/')) as any ); gulp.task('build:client:styles', () => - gulp.src('./src/web/app/init.css') + gulp.src('./src/client/app/init.css') .pipe(isProduction ? (cssnano as any)() : gutil.noop()) - .pipe(gulp.dest('./built/web/assets/')) + .pipe(gulp.dest('./built/client/assets/')) ); gulp.task('copy:client', [ - 'build:client:script', - 'webpack' + 'build:client:script' ], () => gulp.src([ './assets/**/*', - './src/web/assets/**/*', - './src/web/app/*/assets/**/*' + './src/client/assets/**/*', + './src/client/app/*/assets/**/*' ]) .pipe(isProduction ? (imagemin as any)() : gutil.noop()) .pipe(rename(path => { path.dirname = path.dirname.replace('assets', '.'); })) - .pipe(gulp.dest('./built/web/assets/')) + .pipe(gulp.dest('./built/client/assets/')) ); gulp.task('build:client:pug', [ @@ -156,10 +152,13 @@ gulp.task('build:client:pug', [ 'build:client:script', 'build:client:styles' ], () => - gulp.src('./src/web/app/base.pug') + gulp.src('./src/client/app/base.pug') .pipe(pug({ locals: { - themeColor: constants.themeColor + themeColor: constants.themeColor, + facss: fa.dom.css(), + //hljscss: fs.readFileSync('./node_modules/highlight.js/styles/default.css', 'utf8') + hljscss: fs.readFileSync('./src/client/assets/code-highlight.css', 'utf8') } })) .pipe(htmlmin({ @@ -189,7 +188,10 @@ gulp.task('build:client:pug', [ // 属性の値がデフォルトと同じなら省略する e.g. // <input type="text"> to // <input> - removeRedundantAttributes: true + removeRedundantAttributes: true, + + // CSSも圧縮する + minifyCSS: true })) - .pipe(gulp.dest('./built/web/app/')) + .pipe(gulp.dest('./built/client/app/')) ); diff --git a/locales/en.yml b/locales/en.yml index 55a588f99f..900571124f 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -1,4 +1,6 @@ common: + misskey: "Note everything and share it others using Misskey." + time: unknown: "unknown" future: "future" @@ -11,6 +13,15 @@ common: months_ago: "{}month(s) ago" years_ago: "{}year(s) ago" + weekday-short: + sunday: "S" + monday: "M" + tuesday: "T" + wednesday: "W" + thursday: "T" + friday: "F" + satruday: "S" + reactions: like: "Like" love: "Love" @@ -22,14 +33,32 @@ common: confused: "Confused" pudding: "Pudding" + note_categories: + music: "Music" + game: "Video Game" + anime: "Anime" + it: "IT" + gadgets: "Gadgets" + photography: "Photography" + input-message-here: "Enter message here" send: "Send" delete: "Delete" loading: "Loading" ok: "OK" update-available: "New version of Misskey is now available({newer}, current is {current}). Reload page to apply update." + my-token-regenerated: "Your token is just regenerated, so you will signout." tags: + mk-nav-links: + about: "About" + stats: "Stats" + status: "Status" + wiki: "Wiki" + donors: "Donors" + repository: "Repository" + develop: "Developers" + mk-messaging-form: attach-from-local: "Attach file from your pc" attach-from-drive: "Attach file from the drive" @@ -55,8 +84,27 @@ common: mk-error: title: "Unable to connect to the server" - description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから再度お試しください。" + description: "There is a problem with Internet connection, or the server may be down or maintaining. Please {try again} later." thanks: "Thank you for using Misskey." + troubleshoot: "Troubleshoot" + + troubleshooter: + title: "TroubleShooting" + network: "Network connection" + checking-network: "Checking network connection" + internet: "Internet connection" + checking-internet: "Checking internet connection" + server: "Server connection" + checking-server: "Checking server connection" + finding: "Finding a problem" + no-network: "There is no Network connection" + no-network-desc: "Please make sure you are connected to the Network." + no-internet: "There is no Internet connection" + no-internet-desc: "Please make sure you are connected to the Internet." + no-server: "Unable to connect to the server" + no-server-desc: "The network connection of your PC is normal, but you could not connect to Misskey's server. There is a possibility that the server is down or maintaining, please try to access it again after a while." + success: "Successfully connect to the Misskey's server" + success-desc: "It seems to be able to connect normally. Please reload the page." mk-forkit: open-github-link: "View source on Github" @@ -76,12 +124,20 @@ common: show-result: "Show result" voted: "Voted" + mk-note-menu: + pin: "Pin" + pinned: "Pinned" + select: "Select category" + categorize: "Accept" + categorized: "Category reported. Thank you!" + mk-reaction-picker: choose-reaction: "Pick your reaction" mk-signin: username: "Username" password: "Password" + token: "Token" signing-in: "Signing in..." signin: "Sign in" @@ -127,8 +183,46 @@ common: mk-uploader: waiting: "Waiting" +docs: + edit-this-page-on-github: "Caught a mistake or want to contribute to the documentation? " + edit-this-page-on-github-link: "Edit this page on Github!" + + api: + entities: + properties: "Properties" + endpoints: + params: "Parameters" + res: "Response" + props: + name: "Name" + type: "Type" + optional: "Optional" + description: "Description" + yes: "Yes" + no: "No" + +ch: + tags: + mk-index: + new: "Create new channel" + channel-title: "Channel title" + + mk-channel-form: + textarea: "Write here" + upload: "Upload" + drive: "Drive" + note: "Do" + posting: "Doing" + desktop: tags: + mk-api-info: + intro: "APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。" + caution: "アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。" + regeneration-of-token: "万が一このトークンが漏れたりその可能性がある場合はトークンを再生成できます。" + regenerate-token: "Regenerate the token" + enter-password: "Please enter the password" + mk-drive-browser-base-contextmenu: create-folder: "Create a folder" upload: "Upload a file" @@ -172,7 +266,6 @@ desktop: mk-drive-browser-file: avatar: "Avatar" banner: "Banner" - wallpaper: "Wallpaper" mk-drive-browser-folder-contextmenu: move-to-this-folder: "Move to this folder" @@ -189,9 +282,16 @@ desktop: mk-drive-browser-nav-folder: drive: "Drive" + mk-selectdrive-page: + title: "Choose a file(s)" + ok: "OK" + cancel: "Cancel" + upload: "Upload a file(s) from you PC" + mk-ui-header-nav: home: "Home" messaging: "Messages" + ch: "Channels" info: "News" mk-ui-header-search: @@ -204,19 +304,64 @@ desktop: settings: "Settings" signout: "Sign out" + mk-ui-header-note-button: + note: "Compose new Post" + + mk-ui-header-notifications: + title: "Notifications" + + mk-profile-setting: + avatar: "Avatar" + choice-avatar: "Choice an image" + name: "Name" + location: "Location" + description: "Description" + birthday: "Birthday" + save: "Update profile" + + mk-password-setting: + reset: "Change your password" + enter-current-password: "Enter the current password" + enter-new-password: "Enter the new password" + enter-new-password-again: "Enter the new password again" + not-match: "New password not matched" + changed: "Password updated successfully" + + mk-2fa-setting: + intro: "If you set up 2-step verification, you will need not only a password at sign-in but also a pre-registered physical device (such as your smartphone), which will improve security. " + detail: "See details..." + url: "https://www.google.com/landing/2step/" + caution: "As a caveat, security improves, but you can not sign in to Misskey if you lose a registered device, etc." + register: "Register a device" + already-registered: "The setting has already been completed." + unregister: "Disable" + unregistered: "Two-step authentication has been disabled." + enter-password: "Enter the password" + authenticator: "First, you need install Google Authenticator to your device:" + howtoinstall: "How to install" + scan: "Next, please scan displayed QR code:" + done: "Please enter the token displaying in your device:" + submit: "Submit" + success: "Setup completed successfully!" + failed: "Failed to setup. please ensure that the token is correct." + info: "From the next sign in, enter the token that is displayed on the device in addition to the password." + + mk-mute-setting: + no-users: "No muted users" + mk-post-form: - post-placeholder: "What's happening?" - reply-placeholder: "Reply to this post..." - quote-placeholder: "Quote this post..." - post: "Post" + note-placeholder: "What's happening?" + reply-placeholder: "Reply to this note..." + quote-placeholder: "Quote this note..." + note: "Post" reply: "Reply" - repost: "Repost" + renote: "Renote" posted: "Posted!" replied: "Replied!" reposted: "Reposted!" - post-failed: "Failed to post" + note-failed: "Failed to note" reply-failed: "Failed to reply" - repost-failed: "Failed to repost" + renote-failed: "Failed to renote" posting: "Posting" attach-media-from-local: "Attach media from your pc" attach-media-from-drive: "Attach media from the drive" @@ -226,15 +371,29 @@ desktop: text-remain: "{} chars remaining" mk-post-form-window: - post: "New post" + note: "New note" reply: "Reply" attaches: "{} media attached" uploading-media: "Uploading {} media" - mk-timeline-post: + mk-note-page: + prev: "Previous note" + next: "Next note" + + mk-settings: + profile: "Profile" + mute: "Mute" + drive: "Drive" + security: "Security" + password: "Password" + 2fa: "Two-factor authentication" + other: "Other" + license: "License" + + mk-timeline-note: reposted-by: "Reposted by {}" reply: "Reply" - repost: "Repost" + renote: "Renote" add-reaction: "Add your reaction" detail: "Show detail" @@ -249,7 +408,7 @@ desktop: title: "Server info" toggle: "Toggle views" - mk-activity-home-widget: + mk-activity-widget: title: "Activity" toggle: "Toggle views" @@ -276,24 +435,79 @@ desktop: title: "Donation" text: "To manage Misskey we spend money for our domain server etc.. There's no incomes for us so we need your tip. If you're interested contact {}. Thank you for your contribution!" - mk-repost-form: + mk-channel-home-widget: + title: "Channel" + settings: "Widget settings" + get-started: "Please click the cog in the upper right to specify the channel to receive" + + mk-calendar-widget: + title: "{1} / {2}" + prev: "Previous month" + next: "Next month" + go: "Click to travel" + + mk-post-form-home-widget: + title: "Post" + note: "Post" + placeholder: "What's happening?" + + mk-access-log-home-widget: + title: "Access log" + + mk-messaging-home-widget: + title: "Messaging" + + mk-broadcast-home-widget: + fetching: "Fetching" + no-broadcasts: "No broadcasts" + have-a-nice-day: "Have a nice day!" + next: "Next" + + mk-renote-form: quote: "Quote..." cancel: "Cancel" - repost: "Repost" + renote: "Renote" reposting: "Reposting..." success: "Reposted!" - failure: "Failed to Repost" + failure: "Failed to Renote" - mk-repost-form-window: - title: "Are you sure you want to repost this post?" + mk-renote-form-window: + title: "Are you sure you want to renote this note?" + + mk-user: + last-used-at: "Last used at" + + follows-you: "Follows you" + mute: "Mute" + muted: "Muting" + unmute: "Unmute" + + photos: + title: "Photos" + loading: "Loading" + no-photos: "No photos" + + frequently-replied-users: + title: "Frequently replied" + loading: "Loading" + no-users: "No users" + + followers-you-know: + title: "Followers you know" + loading: "Loading" + no-users: "No users" mobile: tags: + mk-selectdrive-page: + select-file: "Select file(s)" + mk-drive-file-viewer: download: "Download" rename: "Rename" move: "Move" - hash: "Hash" + hash: "Hash (md5)" + exif: "EXIF" mk-entrance-signin: signup: "Sign up" @@ -325,19 +539,45 @@ mobile: mk-notifications-page: notifications: "Notifications" + read-all: "Are you sure you want to mark all unread notifications as read?" - mk-post-page: - submit: "Post" + mk-note-page: + title: "Post" + prev: "Previous note" + next: "Next note" mk-search-page: search: "Search" + mk-settings: + signed-in-as: "Signed in as {}" + mk-settings-page: profile: "Profile" applications: "Applications" twitter-integration: "Twitter integration" signin-history: "Sign in history" + link: "MisskeyLink" settings: "Settings" + signout: "Sign out" + + mk-profile-setting-page: + title: "Profile Settings" + + mk-profile-setting: + will-be-published: "These profiles will be published." + name: "Name" + location: "Location" + description: "Description" + birthday: "Birthday" + avatar: "Avatar" + banner: "Banner" + avatar-saved: "Avatar updated successfully" + banner-saved: "Banner updated successfully" + set-avatar: "Choose an avatar" + set-banner: "Choose a banner" + save: "Save" + saved: "Profile updated successfully" mk-user-followers-page: followers-of: "Followers of {}" @@ -366,40 +606,40 @@ mobile: unfollow: "Unfollow" mk-home-timeline: - empty-timeline: "There is no posts" + empty-timeline: "There is no notes" mk-notifications: more: "More" empty: "No notifications" - mk-post-detail: + mk-note-detail: reply: "Reply" reaction: "Reaction" mk-post-form: submit: "Post" - reply-placeholder: "Reply to this post..." - post-placeholder: "What's happening?" - attach-media-from-local: "Attach media from your device" + reply-placeholder: "Reply to this note..." + note-placeholder: "What's happening?" - mk-search-posts: - empty: "There is no post related to the 「{}」" + mk-search-notes: + empty: "There is no note related to the 「{}」" - mk-sub-post-content: + mk-sub-note-content: media-count: "{} media" poll: "Poll" - mk-timeline-post: + mk-timeline-note: reposted-by: "Reposted by {}" mk-timeline: - empty: "No posts" + empty: "No notes" load-more: "More" mk-ui-nav: home: "Home" notifications: "Notifications" messaging: "Messages" + ch: "Channels" drive: "Drive" settings: "Settings" about: "About Misskey" @@ -412,23 +652,58 @@ mobile: no-users: "No following." mk-user-timeline: - no-posts: "This user seems never post" - no-posts-with-media: "There is no posts with media" + no-notes: "This user seems never note" + no-notes-with-media: "There is no notes with media" + load-more: "More" mk-user: - is-followed: "Followed you" + follows-you: "Follows you" following: "Following" followers: "Followers" - posts: "Timeline" + notes: "Posts" + overview: "Overview" + timeline: "Timeline" media: "Media" + mk-user-overview: + recent-notes: "Recent notes" + images: "Images" + activity: "Activity" + keywords: "Keywords" + domains: "Domains" + frequently-replied-users: "Frequently talking users" + followers-you-know: "Followers you know" + last-used-at: "Last used at" + + mk-user-overview-notes: + loading: "Loading" + no-notes: "No notes" + + mk-user-overview-photos: + loading: "Loading" + no-photos: "No photos" + + mk-user-overview-keywords: + no-keywords: "No keywords" + + mk-user-overview-domains: + no-domains: "No domains" + + mk-user-overview-frequently-replied-users: + loading: "Loading" + no-users: "No users" + + mk-user-overview-followers-you-know: + loading: "Loading" + no-users: "No users" + mk-users-list: all: "All" known: "You know" load-more: "More" stats: - posts-count: "Number of all posts" + notes-count: "Number of all notes" users-count: "Number of all users" status: diff --git a/webpack/langs.ts b/locales/index.ts similarity index 79% rename from webpack/langs.ts rename to locales/index.ts index 409b25504a..ced3b4cb32 100644 --- a/webpack/langs.ts +++ b/locales/index.ts @@ -10,12 +10,12 @@ const loadLang = lang => yaml.safeLoad( const native = loadLang('ja'); -const langs = Object.entries({ - 'en': loadLang('en'), +const langs = { + //'en': loadLang('en'), 'ja': native -}); +}; -langs.map(([, locale]) => { +Object.entries(langs).map(([, locale]) => { // Extend native language (Japanese) locale = Object.assign({}, native, locale); }); diff --git a/locales/ja.yml b/locales/ja.yml index e5b2beaed1..4d4c853625 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -1,4 +1,6 @@ common: + misskey: "Misskeyに何でも投稿して皆と共有しましょう。" + time: unknown: "なぞのじかん" future: "未来" @@ -11,9 +13,18 @@ common: months_ago: "{}ヶ月前" years_ago: "{}年前" + weekday-short: + sunday: "日" + monday: "月" + tuesday: "火" + wednesday: "水" + thursday: "木" + friday: "金" + satruday: "土" + reactions: like: "いいね" - love: "ハート" + love: "しゅき" laugh: "笑" hmm: "ふぅ~む" surprise: "わお" @@ -22,14 +33,32 @@ common: confused: "こまこまのこまり" pudding: "Pudding" + note_categories: + music: "音楽" + game: "ゲーム" + anime: "アニメ" + it: "IT" + gadgets: "ガジェット" + photography: "写真" + input-message-here: "ここにメッセージを入力" send: "送信" delete: "削除" loading: "読み込み中" ok: "わかった" update-available: "Misskeyの新しいバージョンがあります({newer}。現在{current}を利用中)。ページを再度読み込みすると更新が適用されます。" + my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。" tags: + mk-nav-links: + about: "Misskeyについて" + stats: "統計" + status: "ステータス" + wiki: "Wiki" + donors: "ドナー" + repository: "リポジトリ" + develop: "開発者" + mk-messaging-form: attach-from-local: "PCからファイルを添付する" attach-from-drive: "ドライブからファイルを添付する" @@ -55,8 +84,27 @@ common: mk-error: title: "サーバーに接続できません" - description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから再度お試しください。" + description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから{再度お試し}ください。" thanks: "いつもMisskeyをご利用いただきありがとうございます。" + troubleshoot: "トラブルシュート" + + troubleshooter: + title: "トラブルシューティング" + network: "ネットワーク接続" + checking-network: "ネットワーク接続を確認中" + internet: "インターネット接続" + checking-internet: "インターネット接続を確認中" + server: "サーバー接続" + checking-server: "サーバー接続を確認中" + finding: "問題を調べています" + no-network: "ネットワークに接続されていません" + no-network-desc: "お使いのPCのネットワーク接続が正常か確認してください。" + no-internet: "インターネットに接続されていません" + no-internet-desc: "ネットワークには接続されていますが、インターネットには接続されていないようです。お使いのPCのインターネット接続が正常か確認してください。" + no-server: "Misskeyのサーバーに接続できません" + no-server-desc: "お使いのPCのインターネット接続は正常ですが、Misskeyのサーバーには接続できませんでした。サーバーがダウンまたはメンテナンスしている可能性があるので、しばらくしてから再度御アクセスください。" + success: "Misskeyのサーバーに接続できました" + success-desc: "正常に接続できるようです。ページを再度読み込みしてください。" mk-forkit: open-github-link: "View source on Github" @@ -76,12 +124,20 @@ common: show-result: "結果を見る" voted: "投票済み" + mk-note-menu: + pin: "ピン留め" + pinned: "ピン留めしました" + select: "カテゴリを選択" + categorize: "決定" + categorized: "カテゴリを報告しました。これによりMisskeyが賢くなり、投稿の自動カテゴライズに役立てられます。ご協力ありがとうございました。" + mk-reaction-picker: choose-reaction: "リアクションを選択" mk-signin: username: "ユーザー名" password: "パスワード" + token: "トークン" signing-in: "やってます..." signin: "サインイン" @@ -91,8 +147,8 @@ common: available: "利用できます" unavailable: "既に利用されています" error: "通信エラー" - invalid-format: "a~z、A~Z、0~9、-(ハイフン)が使えます" - too-short: "3文字以上でお願いします!" + invalid-format: "a~z、A~Z、0~9、_が使えます" + too-short: "1文字以上でお願いします!" too-long: "20文字以内でお願いします" password: "パスワード" password-placeholder: "8文字以上を推奨します" @@ -127,8 +183,46 @@ common: mk-uploader: waiting: "待機中" +docs: + edit-this-page-on-github: "間違いや改善点を見つけましたか?" + edit-this-page-on-github-link: "このページをGitHubで編集" + + api: + entities: + properties: "プロパティ" + endpoints: + params: "パラメータ" + res: "レスポンス" + props: + name: "名前" + type: "型" + optional: "オプション" + description: "説明" + yes: "はい" + no: "いいえ" + +ch: + tags: + mk-index: + new: "チャンネルを作成" + channel-title: "チャンネルのタイトル" + + mk-channel-form: + textarea: "書いて" + upload: "アップロード" + drive: "ドライブ" + note: "やる" + posting: "やってます" + desktop: tags: + mk-api-info: + intro: "APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。" + caution: "アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。" + regeneration-of-token: "万が一このトークンが漏れたりその可能性がある場合はトークンを再生成できます。" + regenerate-token: "トークンを再生成" + enter-password: "パスワードを入力してください" + mk-drive-browser-base-contextmenu: create-folder: "フォルダーを作成" upload: "ファイルをアップロード" @@ -172,7 +266,6 @@ desktop: mk-drive-browser-file: avatar: "アバター" banner: "バナー" - wallpaper: "壁紙" mk-drive-browser-folder-contextmenu: move-to-this-folder: "このフォルダへ移動" @@ -189,9 +282,16 @@ desktop: mk-drive-browser-nav-folder: drive: "ドライブ" + mk-selectdrive-page: + title: "ファイルを選択してください" + ok: "決定" + cancel: "キャンセル" + upload: "PCからドライブにファイルをアップロード" + mk-ui-header-nav: home: "ホーム" messaging: "メッセージ" + ch: "チャンネル" info: "お知らせ" mk-ui-header-search: @@ -204,19 +304,64 @@ desktop: settings: "設定" signout: "サインアウト" + mk-ui-header-note-button: + note: "新規投稿" + + mk-ui-header-notifications: + title: "通知" + + mk-profile-setting: + avatar: "アバター" + choice-avatar: "画像を選択" + name: "名前" + location: "場所" + description: "自己紹介" + birthday: "誕生日" + save: "保存" + + mk-password-setting: + reset: "パスワードを変更する" + enter-current-password: "現在のパスワードを入力してください" + enter-new-password: "新しいパスワードを入力してください" + enter-new-password-again: "もう一度新しいパスワードを入力してください" + not-match: "新しいパスワードが一致しません" + changed: "パスワードを変更しました" + + mk-2fa-setting: + intro: "二段階認証を設定すると、サインイン時にパスワードだけでなく、予め登録しておいた物理的なデバイス(例えばあなたのスマートフォンなど)も必要になり、よりセキュリティが向上します。" + detail: "詳細..." + url: "https://www.google.co.jp/intl/ja/landing/2step/" + caution: "登録したデバイスを紛失するなどした場合、Misskeyにサインインできなくなりますのでご注意ください。" + register: "デバイスを登録する" + already-registered: "既に設定は完了しています。" + unregister: "設定を解除" + unregistered: "二段階認証が無効になりました。" + enter-password: "パスワードを入力してください" + authenticator: "まず、Google Authenticatorをお使いのデバイスにインストールします:" + howtoinstall: "インストール方法はこちら" + scan: "次に、表示されているQRコードをスキャンします:" + done: "お使いのデバイスに表示されているトークンを入力して完了します:" + submit: "完了" + success: "設定が完了しました!" + failed: "設定に失敗しました。トークンに誤りがないかご確認ください。" + info: "次回サインインからは、同様にパスワードに加えてデバイスに表示されているトークンを入力します。" + + mk-mute-setting: + no-users: "ミュートしているユーザーはいません" + mk-post-form: - post-placeholder: "いまどうしてる?" + note-placeholder: "いまどうしてる?" reply-placeholder: "この投稿への返信..." quote-placeholder: "この投稿を引用..." - post: "投稿" + note: "投稿" reply: "返信" - repost: "Repost" + renote: "Renote" posted: "投稿しました!" replied: "返信しました!" - reposted: "Repostしました!" - post-failed: "投稿に失敗しました" + reposted: "Renoteしました!" + note-failed: "投稿に失敗しました" reply-failed: "返信に失敗しました" - repost-failed: "Repostに失敗しました" + renote-failed: "Renoteに失敗しました" posting: "投稿中" attach-media-from-local: "PCからメディアを添付" attach-media-from-drive: "ドライブからメディアを添付" @@ -226,15 +371,29 @@ desktop: text-remain: "のこり{}文字" mk-post-form-window: - post: "新規投稿" + note: "新規投稿" reply: "返信" attaches: "添付: {}メディア" uploading-media: "{}個のメディアをアップロード中" - mk-timeline-post: - reposted-by: "{}がRepost" + mk-note-page: + prev: "前の投稿" + next: "次の投稿" + + mk-settings: + profile: "プロフィール" + mute: "ミュート" + drive: "ドライブ" + security: "セキュリティ" + password: "パスワード" + 2fa: "二段階認証" + other: "その他" + license: "ライセンス" + + mk-timeline-note: + reposted-by: "{}がRenote" reply: "返信" - repost: "Repost" + renote: "Renote" add-reaction: "リアクション" detail: "詳細" @@ -249,7 +408,7 @@ desktop: title: "サーバー情報" toggle: "表示を切り替え" - mk-activity-home-widget: + mk-activity-widget: title: "アクティビティ" toggle: "表示を切り替え" @@ -276,24 +435,79 @@ desktop: title: "寄付のお願い" text: "Misskeyの運営にはドメイン、サーバー等のコストが掛かります。Misskeyは広告を掲載したりしないため、収入を皆様からの寄付に頼っています。もしご興味があれば、{}までご連絡ください。ご協力ありがとうございます。" - mk-repost-form: + mk-channel-home-widget: + title: "チャンネル" + settings: "ウィジェットの設定" + get-started: "右上の歯車をクリックして受信するチャンネルを指定してください" + + mk-calendar-widget: + title: "{1}年 {2}月" + prev: "前の月" + next: "次の月" + go: "クリックして時間遡行" + + mk-post-form-home-widget: + title: "投稿" + note: "投稿" + placeholder: "いまどうしてる?" + + mk-access-log-home-widget: + title: "アクセスログ" + + mk-messaging-home-widget: + title: "メッセージ" + + mk-broadcast-home-widget: + fetching: "確認中" + no-broadcasts: "お知らせはありません" + have-a-nice-day: "良い一日を!" + next: "次" + + mk-renote-form: quote: "引用する..." cancel: "キャンセル" - repost: "Repost" + renote: "Renote" reposting: "しています..." - success: "Repostしました!" - failure: "Repostに失敗しました" + success: "Renoteしました!" + failure: "Renoteに失敗しました" - mk-repost-form-window: - title: "この投稿をRepostしますか?" + mk-renote-form-window: + title: "この投稿をRenoteしますか?" + + mk-user: + last-used-at: "最終アクセス" + + follows-you: "フォローされています" + mute: "ミュートする" + muted: "ミュートしています" + unmute: "ミュート解除" + + photos: + title: "フォト" + loading: "読み込み中" + no-photos: "写真はありません" + + frequently-replied-users: + title: "よく話すユーザー" + loading: "読み込み中" + no-users: "よく話すユーザーはいません" + + followers-you-know: + title: "知り合いのフォロワー" + loading: "読み込み中" + no-users: "知り合いのフォロワーはいません" mobile: tags: + mk-selectdrive-page: + select-file: "ファイルを選択" + mk-drive-file-viewer: download: "ダウンロード" rename: "名前を変更" move: "移動" - hash: "ハッシュ" + hash: "ハッシュ (md5)" + exif: "EXIF" mk-entrance-signin: signup: "新規登録" @@ -325,19 +539,45 @@ mobile: mk-notifications-page: notifications: "通知" + read-all: "すべての通知を既読にしますか?" - mk-post-page: - submit: "投稿" + mk-note-page: + title: "投稿" + prev: "前の投稿" + next: "次の投稿" mk-search-page: search: "検索" + mk-settings: + signed-in-as: "{}としてサインイン中" + mk-settings-page: profile: "プロフィール" applications: "アプリケーション" twitter-integration: "Twitter連携" - signin-history: "ログイン履歴" + signin-history: "サインイン履歴" + link: "Misskeyリンク" settings: "設定" + signout: "サインアウト" + + mk-profile-setting-page: + title: "プロフィール設定" + + mk-profile-setting: + will-be-published: "これらのプロフィールは公開されます。" + name: "名前" + location: "場所" + description: "自己紹介" + birthday: "誕生日" + avatar: "アバター" + banner: "バナー" + avatar-saved: "アバターを保存しました" + banner-saved: "バナーを保存しました" + set-avatar: "アバターを選択する" + set-banner: "バナーを選択する" + save: "保存" + saved: "プロフィールを保存しました" mk-user-followers-page: followers-of: "{}のフォロワー" @@ -372,25 +612,24 @@ mobile: more: "もっと見る" empty: "ありません!" - mk-post-detail: + mk-note-detail: reply: "返信" reaction: "リアクション" mk-post-form: submit: "投稿" reply-placeholder: "この投稿への返信..." - post-placeholder: "いまどうしてる?" - attach-media-from-local: "デバイスからメディアを添付" + note-placeholder: "いまどうしてる?" - mk-search-posts: + mk-search-notes: empty: "「{}」に関する投稿は見つかりませんでした。" - mk-sub-post-content: + mk-sub-note-content: media-count: "{}個のメディア" poll: "投票" - mk-timeline-post: - reposted-by: "{}がRepost" + mk-timeline-note: + reposted-by: "{}がRenote" mk-timeline: empty: "表示するものがありません" @@ -400,6 +639,7 @@ mobile: home: "ホーム" notifications: "通知" messaging: "メッセージ" + ch: "チャンネル" search: "検索" drive: "ドライブ" settings: "設定" @@ -412,24 +652,58 @@ mobile: no-users: "フォロー中のユーザーはいないようです。" mk-user-timeline: - no-posts: "このユーザーはまだ投稿していないようです。" - no-posts-with-media: "メディア付き投稿はありません。" + no-notes: "このユーザーはまだ投稿していないようです。" + no-notes-with-media: "メディア付き投稿はありません。" + load-more: "もっとみる" mk-user: - is-followed: "フォローされています" + follows-you: "フォローされています" following: "フォロー" followers: "フォロワー" - posts: "タイムライン" - posts-count: "ポスト" + notes: "投稿" + overview: "概要" + timeline: "タイムライン" media: "メディア" + mk-user-overview: + recent-notes: "最近の投稿" + images: "画像" + activity: "アクティビティ" + keywords: "キーワード" + domains: "頻出ドメイン" + frequently-replied-users: "よく会話するユーザー" + followers-you-know: "知り合いのフォロワー" + last-used-at: "最終ログイン" + + mk-user-overview-notes: + loading: "読み込み中" + no-notes: "投稿はありません" + + mk-user-overview-photos: + loading: "読み込み中" + no-photos: "写真はありません" + + mk-user-overview-keywords: + no-keywords: "キーワードはありません(十分な数の投稿をしていない可能性があります)" + + mk-user-overview-domains: + no-domains: "よく表れるドメインは検出されませんでした" + + mk-user-overview-frequently-replied-users: + loading: "読み込み中" + no-users: "よく会話するユーザーはいません" + + mk-user-overview-followers-you-know: + loading: "読み込み中" + no-users: "知り合いのユーザーはいません" + mk-users-list: all: "すべて" known: "知り合い" load-more: "もっと" stats: - posts-count: "投稿の数" + notes-count: "投稿の数" users-count: "アカウントの数" status: diff --git a/package.json b/package.json index 875ffc3c66..1e0e50551e 100644 --- a/package.json +++ b/package.json @@ -1,155 +1,219 @@ { - "name": "misskey", - "author": "syuilo <i@syuilo.com>", - "version": "0.0.2380", - "license": "MIT", - "description": "A miniblog-based SNS", - "bugs": "https://github.com/syuilo/misskey/issues", - "repository": "https://github.com/syuilo/misskey.git", - "main": "./built/index.js", - "private": true, - "scripts": { - "config": "node ./tools/init.js", - "start": "node ./built", - "debug": "DEBUG=misskey:* node ./built", - "swagger": "node ./swagger.js", - "build": "gulp build", - "rebuild": "gulp rebuild", - "clean": "gulp clean", - "cleanall": "gulp cleanall", - "lint": "gulp lint", - "test": "gulp test" - }, - "devDependencies": { - "@types/bcryptjs": "2.4.0", - "@types/body-parser": "1.16.4", - "@types/chai": "4.0.3", - "@types/chai-http": "3.0.2", - "@types/chalk": "0.4.31", - "@types/compression": "0.0.33", - "@types/cors": "2.8.1", - "@types/debug": "0.0.30", - "@types/deep-equal": "1.0.0", - "@types/elasticsearch": "5.0.14", - "@types/event-stream": "3.3.31", - "@types/express": "4.0.36", - "@types/gm": "1.17.32", - "@types/gulp": "4.0.3", - "@types/gulp-htmlmin": "1.3.30", - "@types/gulp-mocha": "0.0.30", - "@types/gulp-rename": "0.0.32", - "@types/gulp-replace": "0.0.30", - "@types/gulp-tslint": "3.6.31", - "@types/gulp-typescript": "2.13.0", - "@types/gulp-uglify": "0.0.30", - "@types/gulp-util": "3.0.31", - "@types/inquirer": "0.0.34", - "@types/is-root": "1.0.0", - "@types/is-url": "1.2.28", - "@types/js-yaml": "3.9.0", - "@types/mocha": "2.2.41", - "@types/mongodb": "2.2.9", - "@types/monk": "1.0.5", - "@types/morgan": "1.7.32", - "@types/ms": "0.7.29", - "@types/multer": "1.3.2", - "@types/node": "8.0.20", - "@types/ratelimiter": "2.1.28", - "@types/redis": "2.6.0", - "@types/request": "2.0.0", - "@types/rimraf": "2.0.0", - "@types/riot": "3.6.0", - "@types/serve-favicon": "2.2.28", - "@types/uuid": "3.4.0", - "@types/webpack": "3.0.8", - "@types/webpack-stream": "3.2.7", - "@types/websocket": "0.0.34", - "chai": "4.1.1", - "chai-http": "3.0.0", - "css-loader": "0.28.4", - "event-stream": "3.3.4", - "gulp": "3.9.1", - "gulp-cssnano": "2.1.2", - "gulp-imagemin": "3.3.0", - "gulp-htmlmin": "3.0.0", - "gulp-mocha": "4.3.1", - "gulp-pug": "3.3.0", - "gulp-rename": "1.2.2", - "gulp-replace": "0.6.1", - "gulp-tslint": "8.1.2", - "gulp-typescript": "3.2.1", - "gulp-uglify": "3.0.0", - "gulp-util": "3.0.8", - "mocha": "3.5.0", - "riot-tag-loader": "1.0.0", - "string-replace-webpack-plugin": "0.1.3", - "style-loader": "0.18.2", - "stylus": "0.54.5", - "stylus-loader": "3.0.1", - "swagger-jsdoc": "1.9.7", - "tslint": "5.6.0", - "uglify-es": "3.0.27", - "uglify-es-webpack-plugin": "0.10.0", - "uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony", - "webpack": "3.5.4" - }, - "dependencies": { - "accesses": "2.5.0", - "animejs": "2.0.2", - "autwh": "0.0.1", - "bcryptjs": "2.4.3", - "body-parser": "1.17.2", - "cafy": "2.4.0", - "chalk": "2.1.0", - "compression": "1.7.0", - "cors": "2.8.4", - "cropperjs": "1.0.0-rc.3", - "crypto": "1.0.1", - "debug": "3.0.0", - "deep-equal": "1.0.1", - "deepcopy": "0.6.3", - "diskusage": "^0.2.2", - "download": "6.2.5", - "elasticsearch": "13.3.1", - "escape-regexp": "0.0.1", - "express": "4.15.4", - "file-type": "5.2.0", - "fuckadblock": "3.2.1", - "gm": "1.23.0", - "inquirer": "3.2.1", - "is-root": "1.0.0", - "is-url": "1.2.2", - "js-yaml": "3.9.1", - "mongodb": "2.2.31", - "monk": "6.0.3", - "morgan": "1.8.2", - "ms": "2.0.0", - "multer": "1.3.0", - "nprogress": "0.2.0", - "os-utils": "0.0.14", - "page": "1.7.1", - "pictograph": "2.0.4", - "prominence": "0.2.0", - "pug": "2.0.0-rc.3", - "ratelimiter": "3.0.3", - "recaptcha-promise": "0.1.3", - "reconnecting-websocket": "3.1.1", - "redis": "2.8.0", - "request": "2.81.0", - "rimraf": "2.6.1", - "riot": "3.6.1", - "rndstr": "1.0.0", - "s-age": "1.1.0", - "serve-favicon": "2.4.3", - "summaly": "2.0.3", - "syuilo-password-strength": "0.0.1", - "tcp-port-used": "0.1.2", - "textarea-caret": "3.0.2", - "ts-node": "3.3.0", - "typescript": "2.4.2", - "uuid": "3.1.0", - "vhost": "3.0.2", - "websocket": "1.0.24", - "xev": "2.0.0" - } + "name": "misskey", + "author": "syuilo <i@syuilo.com>", + "version": "0.0.4771", + "codename": "nighthike", + "license": "MIT", + "description": "A miniblog-based SNS", + "bugs": "https://github.com/syuilo/misskey/issues", + "repository": "https://github.com/syuilo/misskey.git", + "main": "./built/index.js", + "private": true, + "scripts": { + "config": "node ./tools/init.js", + "start": "node ./built", + "debug": "DEBUG=misskey:* node ./built", + "swagger": "node ./swagger.js", + "build": "node --max_old_space_size=16384 ./node_modules/webpack/bin/webpack.js && gulp build", + "webpack": "node --max_old_space_size=16384 ./node_modules/webpack/bin/webpack.js", + "watch": "node --max_old_space_size=16384 ./node_modules/webpack/bin/webpack.js --watch", + "gulp": "gulp build", + "rebuild": "gulp rebuild", + "clean": "gulp clean", + "cleanall": "gulp cleanall", + "lint": "gulp lint", + "test": "gulp test", + "format": "gulp format" + }, + "dependencies": { + "@fortawesome/fontawesome": "1.0.1", + "@fortawesome/fontawesome-free-brands": "5.0.2", + "@fortawesome/fontawesome-free-regular": "5.0.2", + "@fortawesome/fontawesome-free-solid": "5.0.2", + "@prezzemolo/rap": "0.1.2", + "@prezzemolo/zip": "0.0.3", + "@types/bcryptjs": "2.4.1", + "@types/body-parser": "1.16.8", + "@types/chai": "4.1.2", + "@types/chai-http": "3.0.4", + "@types/compression": "0.0.36", + "@types/cookie": "0.3.1", + "@types/cors": "2.8.3", + "@types/debug": "0.0.30", + "@types/deep-equal": "1.0.1", + "@types/elasticsearch": "5.0.22", + "@types/eventemitter3": "2.0.2", + "@types/express": "4.11.1", + "@types/gm": "1.17.33", + "@types/gulp": "3.8.36", + "@types/gulp-htmlmin": "1.3.32", + "@types/gulp-mocha": "0.0.32", + "@types/gulp-rename": "0.0.33", + "@types/gulp-replace": "0.0.31", + "@types/gulp-uglify": "3.0.5", + "@types/gulp-util": "3.0.34", + "@types/inquirer": "0.0.41", + "@types/is-root": "1.0.0", + "@types/is-url": "1.2.28", + "@types/js-yaml": "3.11.1", + "@types/kue": "^0.11.8", + "@types/license-checker": "15.0.0", + "@types/mkdirp": "0.5.2", + "@types/mocha": "5.0.0", + "@types/mongodb": "3.0.12", + "@types/monk": "6.0.0", + "@types/morgan": "1.7.35", + "@types/ms": "0.7.30", + "@types/multer": "1.3.6", + "@types/node": "9.6.2", + "@types/nopt": "3.0.29", + "@types/proxy-addr": "2.0.0", + "@types/pug": "2.0.4", + "@types/qrcode": "0.8.1", + "@types/ratelimiter": "2.1.28", + "@types/redis": "2.8.6", + "@types/request": "2.47.0", + "@types/request-promise-native": "1.0.14", + "@types/rimraf": "2.0.2", + "@types/seedrandom": "2.4.27", + "@types/serve-favicon": "2.2.30", + "@types/speakeasy": "2.0.2", + "@types/tmp": "0.0.33", + "@types/uuid": "3.4.3", + "@types/webpack": "4.1.3", + "@types/webpack-stream": "3.2.10", + "@types/websocket": "0.0.38", + "@types/ws": "4.0.2", + "accesses": "2.5.0", + "animejs": "2.2.0", + "autosize": "4.0.1", + "autwh": "0.1.0", + "bcryptjs": "2.4.3", + "body-parser": "1.18.2", + "bootstrap-vue": "2.0.0-rc.6", + "cafy": "3.2.1", + "chai": "4.1.2", + "chai-http": "4.0.0", + "chalk": "2.3.2", + "compression": "1.7.2", + "cookie": "0.3.1", + "cors": "2.8.4", + "crc-32": "1.2.0", + "css-loader": "0.28.11", + "debug": "3.1.0", + "deep-equal": "1.0.1", + "deepcopy": "0.6.3", + "diskusage": "0.2.4", + "dompurify": "^1.0.3", + "elasticsearch": "14.2.2", + "element-ui": "2.3.3", + "emojilib": "2.2.12", + "escape-regexp": "0.0.1", + "eslint": "4.19.1", + "eslint-plugin-vue": "4.4.0", + "eventemitter3": "3.0.1", + "exif-js": "2.3.0", + "express": "4.16.3", + "file-loader": "1.1.11", + "file-type": "7.6.0", + "fuckadblock": "3.2.1", + "gm": "1.23.1", + "gulp": "3.9.1", + "gulp-cssnano": "2.1.3", + "gulp-htmlmin": "4.0.0", + "gulp-imagemin": "4.1.0", + "gulp-mocha": "5.0.0", + "gulp-pug": "4.0.0", + "gulp-rename": "1.2.2", + "gulp-replace": "0.6.1", + "gulp-sourcemaps": "2.6.4", + "gulp-stylus": "2.7.0", + "gulp-tslint": "8.1.3", + "gulp-typescript": "4.0.2", + "gulp-uglify": "3.0.0", + "gulp-util": "3.0.8", + "hard-source-webpack-plugin": "0.6.4", + "highlight.js": "9.12.0", + "html-minifier": "3.5.14", + "http-signature": "^1.2.0", + "inquirer": "5.2.0", + "is-root": "2.0.0", + "is-url": "1.2.4", + "js-yaml": "3.11.0", + "jsdom": "11.7.0", + "kue": "0.11.6", + "license-checker": "18.0.0", + "loader-utils": "1.1.0", + "mecab-async": "0.1.2", + "mkdirp": "0.5.1", + "mocha": "5.0.5", + "moji": "0.5.1", + "mongodb": "3.0.6", + "monk": "6.0.5", + "morgan": "1.9.0", + "ms": "2.1.1", + "multer": "1.3.0", + "nan": "2.10.0", + "node-sass": "4.8.3", + "node-sass-json-importer": "3.1.6", + "nopt": "4.0.1", + "nprogress": "0.2.0", + "object-assign-deep": "0.4.0", + "on-build-webpack": "0.1.0", + "os-utils": "0.0.14", + "progress-bar-webpack-plugin": "1.11.0", + "prominence": "0.2.0", + "proxy-addr": "2.0.3", + "pug": "2.0.3", + "punycode": "2.1.0", + "qrcode": "1.2.0", + "ratelimiter": "3.0.3", + "recaptcha-promise": "0.1.3", + "reconnecting-websocket": "3.2.2", + "redis": "2.8.0", + "request": "2.85.0", + "request-promise-native": "1.0.5", + "rimraf": "2.6.2", + "rndstr": "1.0.0", + "s-age": "1.1.2", + "sass-loader": "6.0.7", + "seedrandom": "2.4.3", + "serve-favicon": "2.5.0", + "speakeasy": "2.0.0", + "style-loader": "0.20.3", + "stylus": "0.54.5", + "stylus-loader": "3.0.2", + "summaly": "2.0.3", + "swagger-jsdoc": "1.9.7", + "syuilo-password-strength": "0.0.1", + "tcp-port-used": "0.1.2", + "textarea-caret": "3.1.0", + "tmp": "0.0.33", + "ts-loader": "4.1.0", + "ts-node": "5.0.1", + "tslint": "5.9.1", + "typescript": "2.8.1", + "typescript-eslint-parser": "14.0.0", + "uglify-es": "3.3.9", + "url-loader": "1.0.1", + "uuid": "3.2.1", + "v-animate-css": "0.0.2", + "vhost": "3.0.2", + "vue": "2.5.16", + "vue-cropperjs": "2.2.0", + "vue-js-modal": "1.3.12", + "vue-json-tree-view": "2.1.3", + "vue-loader": "14.2.2", + "vue-router": "3.0.1", + "vue-template-compiler": "2.5.16", + "vuedraggable": "2.16.0", + "web-push": "3.3.0", + "webfinger.js": "2.6.6", + "webpack": "4.5.0", + "webpack-cli": "2.0.14", + "webpack-replace-loader": "1.3.0", + "websocket": "1.0.25", + "ws": "5.1.1", + "xev": "2.0.0" + } } diff --git a/src/acct/parse.ts b/src/acct/parse.ts new file mode 100644 index 0000000000..ef1f55405d --- /dev/null +++ b/src/acct/parse.ts @@ -0,0 +1,4 @@ +export default acct => { + const splitted = acct.split('@', 2); + return { username: splitted[0], host: splitted[1] || null }; +}; diff --git a/src/acct/render.ts b/src/acct/render.ts new file mode 100644 index 0000000000..9afb03d88b --- /dev/null +++ b/src/acct/render.ts @@ -0,0 +1,3 @@ +export default user => { + return user.host === null ? user.username : `${user.username}@${user.host}`; +}; diff --git a/src/api/api-handler.ts b/src/api/api-handler.ts deleted file mode 100644 index fb603a0e2a..0000000000 --- a/src/api/api-handler.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as express from 'express'; - -import { Endpoint } from './endpoints'; -import authenticate from './authenticate'; -import { IAuthContext } from './authenticate'; -import _reply from './reply'; -import limitter from './limitter'; - -export default async (endpoint: Endpoint, req: express.Request, res: express.Response) => { - const reply = _reply.bind(null, res); - let ctx: IAuthContext; - - // Authentication - try { - ctx = await authenticate(req); - } catch (e) { - return reply(403, 'AUTHENTICATION_FAILED'); - } - - if (endpoint.secure && !ctx.isSecure) { - return reply(403, 'ACCESS_DENIED'); - } - - if (endpoint.withCredential && ctx.user == null) { - return reply(401, 'PLZ_SIGNIN'); - } - - if (ctx.app && endpoint.kind) { - if (!ctx.app.permission.some(p => p === endpoint.kind)) { - return reply(403, 'ACCESS_DENIED'); - } - } - - if (endpoint.withCredential && endpoint.limit) { - try { - await limitter(endpoint, ctx); // Rate limit - } catch (e) { - // drop request if limit exceeded - return reply(429); - } - } - - let exec = require(`${__dirname}/endpoints/${endpoint.name}`); - - if (endpoint.withFile) { - exec = exec.bind(null, req.file); - } - - // API invoking - try { - const res = await exec(req.body, ctx.user, ctx.app, ctx.isSecure); - reply(res); - } catch (e) { - reply(400, e); - } -}; diff --git a/src/api/authenticate.ts b/src/api/authenticate.ts deleted file mode 100644 index d4cc3fc41f..0000000000 --- a/src/api/authenticate.ts +++ /dev/null @@ -1,61 +0,0 @@ -import * as express from 'express'; -import App from './models/app'; -import User from './models/user'; -import AccessToken from './models/access-token'; -import isNativeToken from './common/is-native-token'; - -export interface IAuthContext { - /** - * App which requested - */ - app: any; - - /** - * Authenticated user - */ - user: any; - - /** - * Weather if the request is via the User-Native Token or not - */ - isSecure: boolean; -} - -export default (req: express.Request) => new Promise<IAuthContext>(async (resolve, reject) => { - const token = req.body['i'] as string; - - if (token == null) { - return resolve({ app: null, user: null, isSecure: false }); - } - - if (isNativeToken(token)) { - const user = await User - .findOne({ token: token }); - - if (user === null) { - return reject('user not found'); - } - - return resolve({ - app: null, - user: user, - isSecure: true - }); - } else { - const accessToken = await AccessToken.findOne({ - hash: token.toLowerCase() - }); - - if (accessToken === null) { - return reject('invalid signature'); - } - - const app = await App - .findOne({ _id: accessToken.app_id }); - - const user = await User - .findOne({ _id: accessToken.user_id }); - - return resolve({ app: app, user: user, isSecure: false }); - } -}); diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/add-file-to-drive.ts deleted file mode 100644 index 714eeb520d..0000000000 --- a/src/api/common/add-file-to-drive.ts +++ /dev/null @@ -1,172 +0,0 @@ -import * as mongodb from 'mongodb'; -import * as crypto from 'crypto'; -import * as gm from 'gm'; -import * as debug from 'debug'; -import fileType = require('file-type'); -import prominence = require('prominence'); -import DriveFile from '../models/drive-file'; -import DriveFolder from '../models/drive-folder'; -import serialize from '../serializers/drive-file'; -import event from '../event'; -import config from '../../conf'; - -const log = debug('misskey:register-drive-file'); - -/** - * Add file to drive - * - * @param user User who wish to add file - * @param fileName File name - * @param data Contents - * @param comment Comment - * @param type File type - * @param folderId Folder ID - * @param force If set to true, forcibly upload the file even if there is a file with the same hash. - * @return Object that represents added file - */ -export default ( - user: any, - data: Buffer, - name: string = null, - comment: string = null, - folderId: mongodb.ObjectID = null, - force: boolean = false -) => new Promise<any>(async (resolve, reject) => { - log(`registering ${name} (user: ${user.username})`); - - // File size - const size = data.byteLength; - - log(`size is ${size}`); - - // File type - let mime = 'application/octet-stream'; - const type = fileType(data); - if (type !== null) { - mime = type.mime; - - if (name === null) { - name = `untitled.${type.ext}`; - } - } else { - if (name === null) { - name = 'untitled'; - } - } - - log(`type is ${mime}`); - - // Generate hash - const hash = crypto - .createHash('sha256') - .update(data) - .digest('hex') as string; - - log(`hash is ${hash}`); - - if (!force) { - // Check if there is a file with the same hash - const much = await DriveFile.findOne({ - user_id: user._id, - hash: hash - }); - - if (much !== null) { - log('file with same hash is found'); - return resolve(much); - } else { - log('file with same hash is not found'); - } - } - - // Calculate drive usage - const usage = ((await DriveFile - .aggregate([ - { $match: { user_id: user._id } }, - { $project: { - datasize: true - }}, - { $group: { - _id: null, - usage: { $sum: '$datasize' } - }} - ]))[0] || { - usage: 0 - }).usage; - - log(`drive usage is ${usage}`); - - // If usage limit exceeded - if (usage + size > user.drive_capacity) { - return reject('no-free-space'); - } - - // If the folder is specified - let folder: any = null; - if (folderId !== null) { - folder = await DriveFolder - .findOne({ - _id: folderId, - user_id: user._id - }); - - if (folder === null) { - return reject('folder-not-found'); - } - } - - let properties: any = null; - - // If the file is an image - if (/^image\/.*$/.test(mime)) { - // Calculate width and height to save in property - const g = gm(data, name); - const size = await prominence(g).size(); - properties = { - width: size.width, - height: size.height - }; - - log('image width and height is calculated'); - } - - // Create DriveFile document - const file = await DriveFile.insert({ - created_at: new Date(), - user_id: user._id, - folder_id: folder !== null ? folder._id : null, - data: data, - datasize: size, - type: mime, - name: name, - comment: comment, - hash: hash, - properties: properties - }); - - delete file.data; - - log(`drive file has been created ${file._id}`); - - resolve(file); - - // Serialize - const fileObj = await serialize(file); - - // Publish drive_file_created event - event(user._id, 'drive_file_created', fileObj); - - // Register to search database - if (config.elasticsearch.enable) { - const es = require('../../db/elasticsearch'); - es.index({ - index: 'misskey', - type: 'drive_file', - id: file._id.toString(), - body: { - name: file.name, - user_id: user._id.toString() - } - }); - } -}); diff --git a/src/api/common/notify.ts b/src/api/common/notify.ts deleted file mode 100644 index e7ec37d4e4..0000000000 --- a/src/api/common/notify.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as mongo from 'mongodb'; -import Notification from '../models/notification'; -import event from '../event'; -import serialize from '../serializers/notification'; - -export default ( - notifiee: mongo.ObjectID, - notifier: mongo.ObjectID, - type: string, - content?: any -) => new Promise<any>(async (resolve, reject) => { - if (notifiee.equals(notifier)) { - return resolve(); - } - - // Create notification - const notification = await Notification.insert(Object.assign({ - created_at: new Date(), - notifiee_id: notifiee, - notifier_id: notifier, - type: type, - is_read: false - }, content)); - - resolve(notification); - - // Publish notification event - event(notifiee, 'notification', - await serialize(notification)); -}); diff --git a/src/api/common/text/elements/mention.ts b/src/api/common/text/elements/mention.ts deleted file mode 100644 index e0fac4dd76..0000000000 --- a/src/api/common/text/elements/mention.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Mention - */ - -module.exports = text => { - const match = text.match(/^@[a-zA-Z0-9\-]+/); - if (!match) return null; - const mention = match[0]; - return { - type: 'mention', - content: mention, - username: mention.substr(1) - }; -}; diff --git a/src/api/common/watch-post.ts b/src/api/common/watch-post.ts deleted file mode 100644 index 1a50f0edaa..0000000000 --- a/src/api/common/watch-post.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as mongodb from 'mongodb'; -import Watching from '../models/post-watching'; - -export default async (me: mongodb.ObjectID, post: object) => { - // 自分の投稿はwatchできない - if (me.equals((post as any).user_id)) { - return; - } - - // if watching now - const exist = await Watching.findOne({ - post_id: (post as any)._id, - user_id: me, - deleted_at: { $exists: false } - }); - - if (exist !== null) { - return; - } - - await Watching.insert({ - created_at: new Date(), - post_id: (post as any)._id, - user_id: me - }); -}; diff --git a/src/api/endpoints/aggregation/posts/reaction.ts b/src/api/endpoints/aggregation/posts/reaction.ts deleted file mode 100644 index eb99b9d088..0000000000 --- a/src/api/endpoints/aggregation/posts/reaction.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../../models/post'; -import Reaction from '../../../models/post-reaction'; - -/** - * Aggregate reaction of a post - * - * @param {any} params - * @return {Promise<any>} - */ -module.exports = (params) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); - - // Lookup post - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - const datas = await Reaction - .aggregate([ - { $match: { post_id: post._id } }, - { $project: { - created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST - }}, - { $project: { - date: { - year: { $year: '$created_at' }, - month: { $month: '$created_at' }, - day: { $dayOfMonth: '$created_at' } - } - }}, - { $group: { - _id: '$date', - count: { $sum: 1 } - }} - ]); - - datas.forEach(data => { - data.date = data._id; - delete data._id; - }); - - const graph = []; - - for (let i = 0; i < 30; i++) { - const day = new Date(new Date().setDate(new Date().getDate() - i)); - - const data = datas.filter(d => - d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate() - )[0]; - - if (data) { - graph.push(data); - } else { - graph.push({ - date: { - year: day.getFullYear(), - month: day.getMonth() + 1, // In JavaScript, month is zero-based. - day: day.getDate() - }, - count: 0 - }); - } - } - - res(graph); -}); diff --git a/src/api/endpoints/aggregation/posts/reactions.ts b/src/api/endpoints/aggregation/posts/reactions.ts deleted file mode 100644 index 2cd4588ae1..0000000000 --- a/src/api/endpoints/aggregation/posts/reactions.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../../models/post'; -import Reaction from '../../../models/post-reaction'; - -/** - * Aggregate reactions of a post - * - * @param {any} params - * @return {Promise<any>} - */ -module.exports = (params) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); - - // Lookup post - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - const startTime = new Date(new Date().setMonth(new Date().getMonth() - 1)); - - const reactions = await Reaction - .find({ - post_id: post._id, - $or: [ - { deleted_at: { $exists: false } }, - { deleted_at: { $gt: startTime } } - ] - }, { - _id: false, - post_id: false - }, { - sort: { created_at: -1 } - }); - - const graph = []; - - for (let i = 0; i < 30; i++) { - let day = new Date(new Date().setDate(new Date().getDate() - i)); - day = new Date(day.setMilliseconds(999)); - day = new Date(day.setSeconds(59)); - day = new Date(day.setMinutes(59)); - day = new Date(day.setHours(23)); - // day = day.getTime(); - - const count = reactions.filter(r => - r.created_at < day && (r.deleted_at == null || r.deleted_at > day) - ).length; - - graph.push({ - date: { - year: day.getFullYear(), - month: day.getMonth() + 1, // In JavaScript, month is zero-based. - day: day.getDate() - }, - count: count - }); - } - - res(graph); -}); diff --git a/src/api/endpoints/aggregation/posts/reply.ts b/src/api/endpoints/aggregation/posts/reply.ts deleted file mode 100644 index 02a60c8969..0000000000 --- a/src/api/endpoints/aggregation/posts/reply.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../../models/post'; - -/** - * Aggregate reply of a post - * - * @param {any} params - * @return {Promise<any>} - */ -module.exports = (params) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); - - // Lookup post - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - const datas = await Post - .aggregate([ - { $match: { reply_to: post._id } }, - { $project: { - created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST - }}, - { $project: { - date: { - year: { $year: '$created_at' }, - month: { $month: '$created_at' }, - day: { $dayOfMonth: '$created_at' } - } - }}, - { $group: { - _id: '$date', - count: { $sum: 1 } - }} - ]); - - datas.forEach(data => { - data.date = data._id; - delete data._id; - }); - - const graph = []; - - for (let i = 0; i < 30; i++) { - const day = new Date(new Date().setDate(new Date().getDate() - i)); - - const data = datas.filter(d => - d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate() - )[0]; - - if (data) { - graph.push(data); - } else { - graph.push({ - date: { - year: day.getFullYear(), - month: day.getMonth() + 1, // In JavaScript, month is zero-based. - day: day.getDate() - }, - count: 0 - }); - } - } - - res(graph); -}); diff --git a/src/api/endpoints/aggregation/posts/repost.ts b/src/api/endpoints/aggregation/posts/repost.ts deleted file mode 100644 index 217159caa7..0000000000 --- a/src/api/endpoints/aggregation/posts/repost.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../../models/post'; - -/** - * Aggregate repost of a post - * - * @param {any} params - * @return {Promise<any>} - */ -module.exports = (params) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); - - // Lookup post - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - const datas = await Post - .aggregate([ - { $match: { repost_id: post._id } }, - { $project: { - created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST - }}, - { $project: { - date: { - year: { $year: '$created_at' }, - month: { $month: '$created_at' }, - day: { $dayOfMonth: '$created_at' } - } - }}, - { $group: { - _id: '$date', - count: { $sum: 1 } - }} - ]); - - datas.forEach(data => { - data.date = data._id; - delete data._id; - }); - - const graph = []; - - for (let i = 0; i < 30; i++) { - const day = new Date(new Date().setDate(new Date().getDate() - i)); - - const data = datas.filter(d => - d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate() - )[0]; - - if (data) { - graph.push(data); - } else { - graph.push({ - date: { - year: day.getFullYear(), - month: day.getMonth() + 1, // In JavaScript, month is zero-based. - day: day.getDate() - }, - count: 0 - }); - } - } - - res(graph); -}); diff --git a/src/api/endpoints/aggregation/users/followers.ts b/src/api/endpoints/aggregation/users/followers.ts deleted file mode 100644 index 3022b2b002..0000000000 --- a/src/api/endpoints/aggregation/users/followers.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import User from '../../../models/user'; -import Following from '../../../models/following'; - -/** - * Aggregate followers of a user - * - * @param {any} params - * @return {Promise<any>} - */ -module.exports = (params) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); - - // Lookup user - const user = await User.findOne({ - _id: userId - }, { - fields: { - _id: true - } - }); - - if (user === null) { - return rej('user not found'); - } - - const startTime = new Date(new Date().setMonth(new Date().getMonth() - 1)); - - const following = await Following - .find({ - followee_id: user._id, - $or: [ - { deleted_at: { $exists: false } }, - { deleted_at: { $gt: startTime } } - ] - }, { - _id: false, - follower_id: false, - followee_id: false - }, { - sort: { created_at: -1 } - }); - - const graph = []; - - for (let i = 0; i < 30; i++) { - let day = new Date(new Date().setDate(new Date().getDate() - i)); - day = new Date(day.setMilliseconds(999)); - day = new Date(day.setSeconds(59)); - day = new Date(day.setMinutes(59)); - day = new Date(day.setHours(23)); - // day = day.getTime(); - - const count = following.filter(f => - f.created_at < day && (f.deleted_at == null || f.deleted_at > day) - ).length; - - graph.push({ - date: { - year: day.getFullYear(), - month: day.getMonth() + 1, // In JavaScript, month is zero-based. - day: day.getDate() - }, - count: count - }); - } - - res(graph); -}); diff --git a/src/api/endpoints/aggregation/users/following.ts b/src/api/endpoints/aggregation/users/following.ts deleted file mode 100644 index 92da7e6921..0000000000 --- a/src/api/endpoints/aggregation/users/following.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import User from '../../../models/user'; -import Following from '../../../models/following'; - -/** - * Aggregate following of a user - * - * @param {any} params - * @return {Promise<any>} - */ -module.exports = (params) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); - - // Lookup user - const user = await User.findOne({ - _id: userId - }, { - fields: { - _id: true - } - }); - - if (user === null) { - return rej('user not found'); - } - - const startTime = new Date(new Date().setMonth(new Date().getMonth() - 1)); - - const following = await Following - .find({ - follower_id: user._id, - $or: [ - { deleted_at: { $exists: false } }, - { deleted_at: { $gt: startTime } } - ] - }, { - _id: false, - follower_id: false, - followee_id: false - }, { - sort: { created_at: -1 } - }); - - const graph = []; - - for (let i = 0; i < 30; i++) { - let day = new Date(new Date().setDate(new Date().getDate() - i)); - day = new Date(day.setMilliseconds(999)); - day = new Date(day.setSeconds(59)); - day = new Date(day.setMinutes(59)); - day = new Date(day.setHours(23)); - - const count = following.filter(f => - f.created_at < day && (f.deleted_at == null || f.deleted_at > day) - ).length; - - graph.push({ - date: { - year: day.getFullYear(), - month: day.getMonth() + 1, // In JavaScript, month is zero-based. - day: day.getDate() - }, - count: count - }); - } - - res(graph); -}); diff --git a/src/api/endpoints/app/show.ts b/src/api/endpoints/app/show.ts deleted file mode 100644 index 054aab8596..0000000000 --- a/src/api/endpoints/app/show.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import App from '../../models/app'; -import serialize from '../../serializers/app'; - -/** - * @swagger - * /app/show: - * post: - * summary: Show an application's information - * description: Require app_id or name_id - * parameters: - * - - * name: app_id - * description: Application ID - * in: formData - * type: string - * - - * name: name_id - * description: Application unique name - * in: formData - * type: string - * - * responses: - * 200: - * description: Success - * schema: - * $ref: "#/definitions/Application" - * - * default: - * description: Failed - * schema: - * $ref: "#/definitions/Error" - */ - -/** - * Show an app - * - * @param {any} params - * @param {any} user - * @param {any} _ - * @param {any} isSecure - * @return {Promise<any>} - */ -module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => { - // Get 'app_id' parameter - const [appId, appIdErr] = $(params.app_id).optional.id().$; - if (appIdErr) return rej('invalid app_id param'); - - // Get 'name_id' parameter - const [nameId, nameIdErr] = $(params.name_id).optional.string().$; - if (nameIdErr) return rej('invalid name_id param'); - - if (appId === undefined && nameId === undefined) { - return rej('app_id or name_id is required'); - } - - // Lookup app - const app = appId !== undefined - ? await App.findOne({ _id: appId }) - : await App.findOne({ name_id_lower: nameId.toLowerCase() }); - - if (app === null) { - return rej('app not found'); - } - - // Send response - res(await serialize(app, user, { - includeSecret: isSecure && app.user_id.equals(user._id) - })); -}); diff --git a/src/api/endpoints/drive/files.ts b/src/api/endpoints/drive/files.ts deleted file mode 100644 index a68ae34817..0000000000 --- a/src/api/endpoints/drive/files.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import DriveFile from '../../models/drive-file'; -import serialize from '../../serializers/drive-file'; - -/** - * Get drive files - * - * @param {any} params - * @param {any} user - * @param {any} app - * @return {Promise<any>} - */ -module.exports = (params, user, app) => new Promise(async (res, rej) => { - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); - - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); - - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); - - // Check if both of since_id and max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); - } - - // Get 'folder_id' parameter - const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; - if (folderIdErr) return rej('invalid folder_id param'); - - // Construct query - const sort = { - _id: -1 - }; - const query = { - user_id: user._id, - folder_id: folderId - } as any; - if (sinceId) { - sort._id = 1; - query._id = { - $gt: sinceId - }; - } else if (maxId) { - query._id = { - $lt: maxId - }; - } - - // Issue query - const files = await DriveFile - .find(query, { - fields: { - data: false - }, - limit: limit, - sort: sort - }); - - // Serialize - res(await Promise.all(files.map(async file => - await serialize(file)))); -}); diff --git a/src/api/endpoints/drive/files/create.ts b/src/api/endpoints/drive/files/create.ts deleted file mode 100644 index 43dca7762a..0000000000 --- a/src/api/endpoints/drive/files/create.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Module dependencies - */ -import * as fs from 'fs'; -import $ from 'cafy'; -import { validateFileName } from '../../../models/drive-file'; -import serialize from '../../../serializers/drive-file'; -import create from '../../../common/add-file-to-drive'; - -/** - * Create a file - * - * @param {any} file - * @param {any} params - * @param {any} user - * @return {Promise<any>} - */ -module.exports = (file, params, user) => new Promise(async (res, rej) => { - if (file == null) { - return rej('file is required'); - } - - const buffer = fs.readFileSync(file.path); - fs.unlink(file.path, (err) => { if (err) console.log(err); }); - - // Get 'name' parameter - let name = file.originalname; - if (name !== undefined && name !== null) { - name = name.trim(); - if (name.length === 0) { - name = null; - } else if (name === 'blob') { - name = null; - } else if (!validateFileName(name)) { - return rej('invalid name'); - } - } else { - name = null; - } - - // Get 'folder_id' parameter - const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; - if (folderIdErr) return rej('invalid folder_id param'); - - // Create file - const driveFile = await create(user, buffer, name, null, folderId); - - // Serialize - const fileObj = await serialize(driveFile); - - // Response - res(fileObj); -}); diff --git a/src/api/endpoints/drive/files/show.ts b/src/api/endpoints/drive/files/show.ts deleted file mode 100644 index 8dbc297e4f..0000000000 --- a/src/api/endpoints/drive/files/show.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import DriveFile from '../../../models/drive-file'; -import serialize from '../../../serializers/drive-file'; - -/** - * Show a file - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'file_id' parameter - const [fileId, fileIdErr] = $(params.file_id).id().$; - if (fileIdErr) return rej('invalid file_id param'); - - // Fetch file - const file = await DriveFile - .findOne({ - _id: fileId, - user_id: user._id - }, { - fields: { - data: false - } - }); - - if (file === null) { - return rej('file-not-found'); - } - - // Serialize - res(await serialize(file, { - detail: true - })); -}); diff --git a/src/api/endpoints/drive/files/update.ts b/src/api/endpoints/drive/files/update.ts deleted file mode 100644 index 1cfbdd8f0b..0000000000 --- a/src/api/endpoints/drive/files/update.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import DriveFolder from '../../../models/drive-folder'; -import DriveFile from '../../../models/drive-file'; -import { validateFileName } from '../../../models/drive-file'; -import serialize from '../../../serializers/drive-file'; -import event from '../../../event'; - -/** - * Update a file - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'file_id' parameter - const [fileId, fileIdErr] = $(params.file_id).id().$; - if (fileIdErr) return rej('invalid file_id param'); - - // Fetch file - const file = await DriveFile - .findOne({ - _id: fileId, - user_id: user._id - }, { - fields: { - data: false - } - }); - - if (file === null) { - return rej('file-not-found'); - } - - // Get 'name' parameter - const [name, nameErr] = $(params.name).optional.string().pipe(validateFileName).$; - if (nameErr) return rej('invalid name param'); - if (name) file.name = name; - - // Get 'folder_id' parameter - const [folderId, folderIdErr] = $(params.folder_id).optional.nullable.id().$; - if (folderIdErr) return rej('invalid folder_id param'); - - if (folderId !== undefined) { - if (folderId === null) { - file.folder_id = null; - } else { - // Fetch folder - const folder = await DriveFolder - .findOne({ - _id: folderId, - user_id: user._id - }); - - if (folder === null) { - return rej('folder-not-found'); - } - - file.folder_id = folder._id; - } - } - - DriveFile.update(file._id, { - $set: { - name: file.name, - folder_id: file.folder_id - } - }); - - // Serialize - const fileObj = await serialize(file); - - // Response - res(fileObj); - - // Publish drive_file_updated event - event(user._id, 'drive_file_updated', fileObj); -}); diff --git a/src/api/endpoints/drive/files/upload_from_url.ts b/src/api/endpoints/drive/files/upload_from_url.ts deleted file mode 100644 index 46cfffb69c..0000000000 --- a/src/api/endpoints/drive/files/upload_from_url.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Module dependencies - */ -import * as URL from 'url'; -const download = require('download'); -import $ from 'cafy'; -import { validateFileName } from '../../../models/drive-file'; -import serialize from '../../../serializers/drive-file'; -import create from '../../../common/add-file-to-drive'; - -/** - * Create a file from a URL - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'url' parameter - // TODO: Validate this url - const [url, urlErr] = $(params.url).string().$; - if (urlErr) return rej('invalid url param'); - - let name = URL.parse(url).pathname.split('/').pop(); - if (!validateFileName(name)) { - name = null; - } - - // Get 'folder_id' parameter - const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; - if (folderIdErr) return rej('invalid folder_id param'); - - // Download file - const data = await download(url); - - // Create file - const driveFile = await create(user, data, name, null, folderId); - - // Serialize - const fileObj = await serialize(driveFile); - - // Response - res(fileObj); -}); diff --git a/src/api/endpoints/drive/folders.ts b/src/api/endpoints/drive/folders.ts deleted file mode 100644 index d49ef0af03..0000000000 --- a/src/api/endpoints/drive/folders.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import DriveFolder from '../../models/drive-folder'; -import serialize from '../../serializers/drive-folder'; - -/** - * Get drive folders - * - * @param {any} params - * @param {any} user - * @param {any} app - * @return {Promise<any>} - */ -module.exports = (params, user, app) => new Promise(async (res, rej) => { - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); - - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); - - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); - - // Check if both of since_id and max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); - } - - // Get 'folder_id' parameter - const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; - if (folderIdErr) return rej('invalid folder_id param'); - - // Construct query - const sort = { - _id: -1 - }; - const query = { - user_id: user._id, - parent_id: folderId - } as any; - if (sinceId) { - sort._id = 1; - query._id = { - $gt: sinceId - }; - } else if (maxId) { - query._id = { - $lt: maxId - }; - } - - // Issue query - const folders = await DriveFolder - .find(query, { - limit: limit, - sort: sort - }); - - // Serialize - res(await Promise.all(folders.map(async folder => - await serialize(folder)))); -}); diff --git a/src/api/endpoints/following/create.ts b/src/api/endpoints/following/create.ts deleted file mode 100644 index b4a2217b16..0000000000 --- a/src/api/endpoints/following/create.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import User from '../../models/user'; -import Following from '../../models/following'; -import notify from '../../common/notify'; -import event from '../../event'; -import serializeUser from '../../serializers/user'; - -/** - * Follow a user - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - const follower = user; - - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); - - // 自分自身 - if (user._id.equals(userId)) { - return rej('followee is yourself'); - } - - // Get followee - const followee = await User.findOne({ - _id: userId - }, { - fields: { - data: false, - profile: false - } - }); - - if (followee === null) { - return rej('user not found'); - } - - // Check if already following - const exist = await Following.findOne({ - follower_id: follower._id, - followee_id: followee._id, - deleted_at: { $exists: false } - }); - - if (exist !== null) { - return rej('already following'); - } - - // Create following - await Following.insert({ - created_at: new Date(), - follower_id: follower._id, - followee_id: followee._id - }); - - // Send response - res(); - - // Increment following count - User.update(follower._id, { - $inc: { - following_count: 1 - } - }); - - // Increment followers count - User.update({ _id: followee._id }, { - $inc: { - followers_count: 1 - } - }); - - // Publish follow event - event(follower._id, 'follow', await serializeUser(followee, follower)); - event(followee._id, 'followed', await serializeUser(follower, followee)); - - // Notify - notify(followee._id, follower._id, 'follow'); -}); diff --git a/src/api/endpoints/following/delete.ts b/src/api/endpoints/following/delete.ts deleted file mode 100644 index aa1639ef6c..0000000000 --- a/src/api/endpoints/following/delete.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import User from '../../models/user'; -import Following from '../../models/following'; -import event from '../../event'; -import serializeUser from '../../serializers/user'; - -/** - * Unfollow a user - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - const follower = user; - - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); - - // Check if the followee is yourself - if (user._id.equals(userId)) { - return rej('followee is yourself'); - } - - // Get followee - const followee = await User.findOne({ - _id: userId - }, { - fields: { - data: false, - profile: false - } - }); - - if (followee === null) { - return rej('user not found'); - } - - // Check not following - const exist = await Following.findOne({ - follower_id: follower._id, - followee_id: followee._id, - deleted_at: { $exists: false } - }); - - if (exist === null) { - return rej('already not following'); - } - - // Delete following - await Following.update({ - _id: exist._id - }, { - $set: { - deleted_at: new Date() - } - }); - - // Send response - res(); - - // Decrement following count - User.update({ _id: follower._id }, { - $inc: { - following_count: -1 - } - }); - - // Decrement followers count - User.update({ _id: followee._id }, { - $inc: { - followers_count: -1 - } - }); - - // Publish follow event - event(follower._id, 'unfollow', await serializeUser(followee, follower)); -}); diff --git a/src/api/endpoints/i.ts b/src/api/endpoints/i.ts deleted file mode 100644 index ae75f11d54..0000000000 --- a/src/api/endpoints/i.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Module dependencies - */ -import User from '../models/user'; -import serialize from '../serializers/user'; - -/** - * Show myself - * - * @param {any} params - * @param {any} user - * @param {any} app - * @param {Boolean} isSecure - * @return {Promise<any>} - */ -module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => { - // Serialize - res(await serialize(user, user, { - detail: true, - includeSecrets: isSecure - })); - - // Update lastUsedAt - User.update({ _id: user._id }, { - $set: { - last_used_at: new Date() - } - }); -}); diff --git a/src/api/endpoints/i/appdata/get.ts b/src/api/endpoints/i/appdata/get.ts deleted file mode 100644 index a1a57fa13a..0000000000 --- a/src/api/endpoints/i/appdata/get.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Appdata from '../../../models/appdata'; - -/** - * Get app data - * - * @param {any} params - * @param {any} user - * @param {any} app - * @param {Boolean} isSecure - * @return {Promise<any>} - */ -module.exports = (params, user, app, isSecure) => new Promise(async (res, rej) => { - // Get 'key' parameter - const [key = null, keyError] = $(params.key).optional.nullable.string().match(/[a-z_]+/).$; - if (keyError) return rej('invalid key param'); - - if (isSecure) { - if (!user.data) { - return res(); - } - if (key !== null) { - const data = {}; - data[key] = user.data[key]; - res(data); - } else { - res(user.data); - } - } else { - const select = {}; - if (key !== null) { - select[`data.${key}`] = true; - } - const appdata = await Appdata.findOne({ - app_id: app._id, - user_id: user._id - }, { - fields: select - }); - - if (appdata) { - res(appdata.data); - } else { - res(); - } - } -}); diff --git a/src/api/endpoints/i/appdata/set.ts b/src/api/endpoints/i/appdata/set.ts deleted file mode 100644 index 24f192de6b..0000000000 --- a/src/api/endpoints/i/appdata/set.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Appdata from '../../../models/appdata'; -import User from '../../../models/user'; -import serialize from '../../../serializers/user'; -import event from '../../../event'; - -/** - * Set app data - * - * @param {any} params - * @param {any} user - * @param {any} app - * @param {Boolean} isSecure - * @return {Promise<any>} - */ -module.exports = (params, user, app, isSecure) => new Promise(async (res, rej) => { - // Get 'data' parameter - const [data, dataError] = $(params.data).optional.object() - .pipe(obj => { - const hasInvalidData = Object.entries(obj).some(([k, v]) => - $(k).string().match(/^[a-z_]+$/).isNg() && $(v).string().isNg()); - return !hasInvalidData; - }).$; - if (dataError) return rej('invalid data param'); - - // Get 'key' parameter - const [key, keyError] = $(params.key).optional.string().match(/[a-z_]+/).$; - if (keyError) return rej('invalid key param'); - - // Get 'value' parameter - const [value, valueError] = $(params.value).optional.string().$; - if (valueError) return rej('invalid value param'); - - const set = {}; - if (data) { - Object.entries(data).forEach(([k, v]) => { - set[`data.${k}`] = v; - }); - } else { - set[`data.${key}`] = value; - } - - if (isSecure) { - const _user = await User.findOneAndUpdate(user._id, { - $set: set - }); - - res(204); - - // Publish i updated event - event(user._id, 'i_updated', await serialize(_user, user, { - detail: true, - includeSecrets: true - })); - } else { - await Appdata.update({ - app_id: app._id, - user_id: user._id - }, Object.assign({ - app_id: app._id, - user_id: user._id - }, { - $set: set - }), { - upsert: true - }); - - res(204); - } -}); diff --git a/src/api/endpoints/i/update.ts b/src/api/endpoints/i/update.ts deleted file mode 100644 index 111a4b1909..0000000000 --- a/src/api/endpoints/i/update.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import User from '../../models/user'; -import { isValidName, isValidDescription, isValidLocation, isValidBirthday } from '../../models/user'; -import serialize from '../../serializers/user'; -import event from '../../event'; -import config from '../../../conf'; - -/** - * Update myself - * - * @param {any} params - * @param {any} user - * @param {any} _ - * @param {boolean} isSecure - * @return {Promise<any>} - */ -module.exports = async (params, user, _, isSecure) => new Promise(async (res, rej) => { - // Get 'name' parameter - const [name, nameErr] = $(params.name).optional.string().pipe(isValidName).$; - if (nameErr) return rej('invalid name param'); - if (name) user.name = name; - - // Get 'description' parameter - const [description, descriptionErr] = $(params.description).optional.nullable.string().pipe(isValidDescription).$; - if (descriptionErr) return rej('invalid description param'); - if (description !== undefined) user.description = description; - - // Get 'location' parameter - const [location, locationErr] = $(params.location).optional.nullable.string().pipe(isValidLocation).$; - if (locationErr) return rej('invalid location param'); - if (location !== undefined) user.profile.location = location; - - // Get 'birthday' parameter - const [birthday, birthdayErr] = $(params.birthday).optional.nullable.string().pipe(isValidBirthday).$; - if (birthdayErr) return rej('invalid birthday param'); - if (birthday !== undefined) user.profile.birthday = birthday; - - // Get 'avatar_id' parameter - const [avatarId, avatarIdErr] = $(params.avatar_id).optional.id().$; - if (avatarIdErr) return rej('invalid avatar_id param'); - if (avatarId) user.avatar_id = avatarId; - - // Get 'banner_id' parameter - const [bannerId, bannerIdErr] = $(params.banner_id).optional.id().$; - if (bannerIdErr) return rej('invalid banner_id param'); - if (bannerId) user.banner_id = bannerId; - - await User.update(user._id, { - $set: { - name: user.name, - description: user.description, - avatar_id: user.avatar_id, - banner_id: user.banner_id, - profile: user.profile - } - }); - - // Serialize - const iObj = await serialize(user, user, { - detail: true, - includeSecrets: isSecure - }); - - // Send response - res(iObj); - - // Publish i updated event - event(user._id, 'i_updated', iObj); - - // Update search index - if (config.elasticsearch.enable) { - const es = require('../../../db/elasticsearch'); - - es.index({ - index: 'misskey', - type: 'user', - id: user._id.toString(), - body: { - name: user.name, - bio: user.bio - } - }); - } -}); diff --git a/src/api/endpoints/messaging/messages/create.ts b/src/api/endpoints/messaging/messages/create.ts deleted file mode 100644 index 8af55d850c..0000000000 --- a/src/api/endpoints/messaging/messages/create.ts +++ /dev/null @@ -1,143 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Message from '../../../models/messaging-message'; -import { isValidText } from '../../../models/messaging-message'; -import History from '../../../models/messaging-history'; -import User from '../../../models/user'; -import DriveFile from '../../../models/drive-file'; -import serialize from '../../../serializers/messaging-message'; -import publishUserStream from '../../../event'; -import { publishMessagingStream } from '../../../event'; -import config from '../../../../conf'; - -/** - * Create a message - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [recipientId, recipientIdErr] = $(params.user_id).id().$; - if (recipientIdErr) return rej('invalid user_id param'); - - // Myself - if (recipientId.equals(user._id)) { - return rej('cannot send message to myself'); - } - - // Fetch recipient - const recipient = await User.findOne({ - _id: recipientId - }, { - fields: { - _id: true - } - }); - - if (recipient === null) { - return rej('user not found'); - } - - // Get 'text' parameter - const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$; - if (textErr) return rej('invalid text'); - - // Get 'file_id' parameter - const [fileId, fileIdErr] = $(params.file_id).optional.id().$; - if (fileIdErr) return rej('invalid file_id param'); - - let file = null; - if (fileId !== undefined) { - file = await DriveFile.findOne({ - _id: fileId, - user_id: user._id - }, { - data: false - }); - - if (file === null) { - return rej('file not found'); - } - } - - // テキストが無いかつ添付ファイルも無かったらエラー - if (text === undefined && file === null) { - return rej('text or file is required'); - } - - // メッセージを作成 - const message = await Message.insert({ - created_at: new Date(), - file_id: file ? file._id : undefined, - recipient_id: recipient._id, - text: text ? text : undefined, - user_id: user._id, - is_read: false - }); - - // Serialize - const messageObj = await serialize(message); - - // Reponse - res(messageObj); - - // 自分のストリーム - publishMessagingStream(message.user_id, message.recipient_id, 'message', messageObj); - publishUserStream(message.user_id, 'messaging_message', messageObj); - - // 相手のストリーム - publishMessagingStream(message.recipient_id, message.user_id, 'message', messageObj); - publishUserStream(message.recipient_id, 'messaging_message', messageObj); - - // 3秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する - setTimeout(async () => { - const freshMessage = await Message.findOne({ _id: message._id }, { is_read: true }); - if (!freshMessage.is_read) { - publishUserStream(message.recipient_id, 'unread_messaging_message', messageObj); - } - }, 3000); - - // Register to search database - if (message.text && config.elasticsearch.enable) { - const es = require('../../../db/elasticsearch'); - - es.index({ - index: 'misskey', - type: 'messaging_message', - id: message._id.toString(), - body: { - text: message.text - } - }); - } - - // 履歴作成(自分) - History.update({ - user_id: user._id, - partner: recipient._id - }, { - updated_at: new Date(), - user_id: user._id, - partner: recipient._id, - message: message._id - }, { - upsert: true - }); - - // 履歴作成(相手) - History.update({ - user_id: recipient._id, - partner: user._id - }, { - updated_at: new Date(), - user_id: recipient._id, - partner: user._id, - message: message._id - }, { - upsert: true - }); -}); diff --git a/src/api/endpoints/messaging/unread.ts b/src/api/endpoints/messaging/unread.ts deleted file mode 100644 index 40bc83fe1c..0000000000 --- a/src/api/endpoints/messaging/unread.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Module dependencies - */ -import Message from '../../models/messaging-message'; - -/** - * Get count of unread messages - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - const count = await Message - .count({ - recipient_id: user._id, - is_read: false - }); - - res({ - count: count - }); -}); diff --git a/src/api/endpoints/notifications/mark_as_read.ts b/src/api/endpoints/notifications/mark_as_read.ts deleted file mode 100644 index 5cce33e850..0000000000 --- a/src/api/endpoints/notifications/mark_as_read.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Notification from '../../models/notification'; -import serialize from '../../serializers/notification'; -import event from '../../event'; - -/** - * Mark as read a notification - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - const [notificationId, notificationIdErr] = $(params.notification_id).id().$; - if (notificationIdErr) return rej('invalid notification_id param'); - - // Get notification - const notification = await Notification - .findOne({ - _id: notificationId, - i: user._id - }); - - if (notification === null) { - return rej('notification-not-found'); - } - - // Update - notification.is_read = true; - Notification.update({ _id: notification._id }, { - $set: { - is_read: true - } - }); - - // Response - res(); - - // Serialize - const notificationObj = await serialize(notification); - - // Publish read_notification event - event(user._id, 'read_notification', notificationObj); -}); diff --git a/src/api/endpoints/posts.ts b/src/api/endpoints/posts.ts deleted file mode 100644 index 23b9bd0b66..0000000000 --- a/src/api/endpoints/posts.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../models/post'; -import serialize from '../serializers/post'; - -/** - * Lists all posts - * - * @param {any} params - * @return {Promise<any>} - */ -module.exports = (params) => new Promise(async (res, rej) => { - // Get 'reply' parameter - const [reply, replyErr] = $(params.reply).optional.boolean().$; - if (replyErr) return rej('invalid reply param'); - - // Get 'repost' parameter - const [repost, repostErr] = $(params.repost).optional.boolean().$; - if (repostErr) return rej('invalid repost param'); - - // Get 'media' parameter - const [media, mediaErr] = $(params.media).optional.boolean().$; - if (mediaErr) return rej('invalid media param'); - - // Get 'poll' parameter - const [poll, pollErr] = $(params.poll).optional.boolean().$; - if (pollErr) return rej('invalid poll param'); - - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); - - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); - - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); - - // Check if both of since_id and max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); - } - - // Construct query - const sort = { - _id: -1 - }; - const query = {} as any; - if (sinceId) { - sort._id = 1; - query._id = { - $gt: sinceId - }; - } else if (maxId) { - query._id = { - $lt: maxId - }; - } - - if (reply != undefined) { - query.reply_to_id = reply ? { $exists: true, $ne: null } : null; - } - - if (repost != undefined) { - query.repost_id = repost ? { $exists: true, $ne: null } : null; - } - - if (media != undefined) { - query.media_ids = media ? { $exists: true, $ne: null } : null; - } - - if (poll != undefined) { - query.poll = poll ? { $exists: true, $ne: null } : null; - } - - // Issue query - const posts = await Post - .find(query, { - limit: limit, - sort: sort - }); - - // Serialize - res(await Promise.all(posts.map(async post => await serialize(post)))); -}); diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts deleted file mode 100644 index eb979402c4..0000000000 --- a/src/api/endpoints/posts/create.ts +++ /dev/null @@ -1,409 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import deepEqual = require('deep-equal'); -import parse from '../../common/text'; -import Post from '../../models/post'; -import { isValidText } from '../../models/post'; -import User from '../../models/user'; -import Following from '../../models/following'; -import DriveFile from '../../models/drive-file'; -import Watching from '../../models/post-watching'; -import serialize from '../../serializers/post'; -import notify from '../../common/notify'; -import watch from '../../common/watch-post'; -import event from '../../event'; -import config from '../../../conf'; - -/** - * Create a post - * - * @param {any} params - * @param {any} user - * @param {any} app - * @return {Promise<any>} - */ -module.exports = (params, user, app) => new Promise(async (res, rej) => { - // Get 'text' parameter - const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$; - if (textErr) return rej('invalid text'); - - // Get 'media_ids' parameter - const [mediaIds, mediaIdsErr] = $(params.media_ids).optional.array('id').unique().range(1, 4).$; - if (mediaIdsErr) return rej('invalid media_ids'); - - let files = []; - if (mediaIds !== undefined) { - // Fetch files - // forEach だと途中でエラーなどがあっても return できないので - // 敢えて for を使っています。 - for (const mediaId of mediaIds) { - // Fetch file - // SELECT _id - const entity = await DriveFile.findOne({ - _id: mediaId, - user_id: user._id - }, { - _id: true - }); - - if (entity === null) { - return rej('file not found'); - } else { - files.push(entity); - } - } - } else { - files = null; - } - - // Get 'repost_id' parameter - const [repostId, repostIdErr] = $(params.repost_id).optional.id().$; - if (repostIdErr) return rej('invalid repost_id'); - - let repost = null; - if (repostId !== undefined) { - // Fetch repost to post - repost = await Post.findOne({ - _id: repostId - }); - - if (repost == null) { - return rej('repostee is not found'); - } else if (repost.repost_id && !repost.text && !repost.media_ids) { - return rej('cannot repost to repost'); - } - - // Fetch recently post - const latestPost = await Post.findOne({ - user_id: user._id - }, { - sort: { - _id: -1 - } - }); - - // 直近と同じRepost対象かつ引用じゃなかったらエラー - if (latestPost && - latestPost.repost_id && - latestPost.repost_id.equals(repost._id) && - text === undefined && files === null) { - return rej('cannot repost same post that already reposted in your latest post'); - } - - // 直近がRepost対象かつ引用じゃなかったらエラー - if (latestPost && - latestPost._id.equals(repost._id) && - text === undefined && files === null) { - return rej('cannot repost your latest post'); - } - } - - // Get 'in_reply_to_post_id' parameter - const [inReplyToPostId, inReplyToPostIdErr] = $(params.reply_to_id).optional.id().$; - if (inReplyToPostIdErr) return rej('invalid in_reply_to_post_id'); - - let inReplyToPost = null; - if (inReplyToPostId !== undefined) { - // Fetch reply - inReplyToPost = await Post.findOne({ - _id: inReplyToPostId - }); - - if (inReplyToPost === null) { - return rej('in reply to post is not found'); - } - - // 返信対象が引用でないRepostだったらエラー - if (inReplyToPost.repost_id && !inReplyToPost.text && !inReplyToPost.media_ids) { - return rej('cannot reply to repost'); - } - } - - // Get 'poll' parameter - const [poll, pollErr] = $(params.poll).optional.strict.object() - .have('choices', $().array('string') - .unique() - .range(2, 10) - .each(c => c.length > 0 && c.length < 50)) - .$; - if (pollErr) return rej('invalid poll'); - - if (poll) { - (poll as any).choices = (poll as any).choices.map((choice, i) => ({ - id: i, // IDを付与 - text: choice.trim(), - votes: 0 - })); - } - - // テキストが無いかつ添付ファイルが無いかつRepostも無いかつ投票も無かったらエラー - if (text === undefined && files === null && repost === null && poll === undefined) { - return rej('text, media_ids, repost_id or poll is required'); - } - - // 直近の投稿と重複してたらエラー - // TODO: 直近の投稿が一日前くらいなら重複とは見なさない - if (user.latest_post) { - if (deepEqual({ - text: user.latest_post.text, - reply: user.latest_post.reply_to_id ? user.latest_post.reply_to_id.toString() : null, - repost: user.latest_post.repost_id ? user.latest_post.repost_id.toString() : null, - media_ids: (user.latest_post.media_ids || []).map(id => id.toString()) - }, { - text: text, - reply: inReplyToPost ? inReplyToPost._id.toString() : null, - repost: repost ? repost._id.toString() : null, - media_ids: (files || []).map(file => file._id.toString()) - })) { - return rej('duplicate'); - } - } - - // 投稿を作成 - const post = await Post.insert({ - created_at: new Date(), - media_ids: files ? files.map(file => file._id) : undefined, - reply_to_id: inReplyToPost ? inReplyToPost._id : undefined, - repost_id: repost ? repost._id : undefined, - poll: poll, - text: text, - user_id: user._id, - app_id: app ? app._id : null - }); - - // Serialize - const postObj = await serialize(post); - - // Reponse - res(postObj); - - // ----------------------------------------------------------- - // Post processes - - User.update({ _id: user._id }, { - $set: { - latest_post: post - } - }); - - const mentions = []; - - function addMention(mentionee, type) { - // Reject if already added - if (mentions.some(x => x.equals(mentionee))) return; - - // Add mention - mentions.push(mentionee); - - // Publish event - if (!user._id.equals(mentionee)) { - event(mentionee, type, postObj); - } - } - - // Publish event to myself's stream - event(user._id, 'post', postObj); - - // Fetch all followers - const followers = await Following - .find({ - followee_id: user._id, - // 削除されたドキュメントは除く - deleted_at: { $exists: false } - }, { - follower_id: true, - _id: false - }); - - // Publish event to followers stream - followers.forEach(following => - event(following.follower_id, 'post', postObj)); - - // Increment my posts count - User.update({ _id: user._id }, { - $inc: { - posts_count: 1 - } - }); - - // If has in reply to post - if (inReplyToPost) { - // Increment replies count - Post.update({ _id: inReplyToPost._id }, { - $inc: { - replies_count: 1 - } - }); - - // 自分自身へのリプライでない限りは通知を作成 - notify(inReplyToPost.user_id, user._id, 'reply', { - post_id: post._id - }); - - // Fetch watchers - Watching - .find({ - post_id: inReplyToPost._id, - user_id: { $ne: user._id }, - // 削除されたドキュメントは除く - deleted_at: { $exists: false } - }, { - fields: { - user_id: true - } - }) - .then(watchers => { - watchers.forEach(watcher => { - notify(watcher.user_id, user._id, 'reply', { - post_id: post._id - }); - }); - }); - - // この投稿をWatchする - // TODO: ユーザーが「返信したときに自動でWatchする」設定を - // オフにしていた場合はしない - watch(user._id, inReplyToPost); - - // Add mention - addMention(inReplyToPost.user_id, 'reply'); - } - - // If it is repost - if (repost) { - // Notify - const type = text ? 'quote' : 'repost'; - notify(repost.user_id, user._id, type, { - post_id: post._id - }); - - // Fetch watchers - Watching - .find({ - post_id: repost._id, - user_id: { $ne: user._id }, - // 削除されたドキュメントは除く - deleted_at: { $exists: false } - }, { - fields: { - user_id: true - } - }) - .then(watchers => { - watchers.forEach(watcher => { - notify(watcher.user_id, user._id, type, { - post_id: post._id - }); - }); - }); - - // この投稿をWatchする - // TODO: ユーザーが「Repostしたときに自動でWatchする」設定を - // オフにしていた場合はしない - watch(user._id, repost); - - // If it is quote repost - if (text) { - // Add mention - addMention(repost.user_id, 'quote'); - } else { - // Publish event - if (!user._id.equals(repost.user_id)) { - event(repost.user_id, 'repost', postObj); - } - } - - // 今までで同じ投稿をRepostしているか - const existRepost = await Post.findOne({ - user_id: user._id, - repost_id: repost._id, - _id: { - $ne: post._id - } - }); - - if (!existRepost) { - // Update repostee status - Post.update({ _id: repost._id }, { - $inc: { - repost_count: 1 - } - }); - } - } - - // If has text content - if (text) { - // Analyze - const tokens = parse(text); - /* - // Extract a hashtags - const hashtags = tokens - .filter(t => t.type == 'hashtag') - .map(t => t.hashtag) - // Drop dupulicates - .filter((v, i, s) => s.indexOf(v) == i); - - // ハッシュタグをデータベースに登録 - registerHashtags(user, hashtags); - */ - // Extract an '@' mentions - const atMentions = tokens - .filter(t => t.type == 'mention') - .map(m => m.username) - // Drop dupulicates - .filter((v, i, s) => s.indexOf(v) == i); - - // Resolve all mentions - await Promise.all(atMentions.map(async (mention) => { - // Fetch mentioned user - // SELECT _id - const mentionee = await User - .findOne({ - username_lower: mention.toLowerCase() - }, { _id: true }); - - // When mentioned user not found - if (mentionee == null) return; - - // 既に言及されたユーザーに対する返信や引用repostの場合も無視 - if (inReplyToPost && inReplyToPost.user_id.equals(mentionee._id)) return; - if (repost && repost.user_id.equals(mentionee._id)) return; - - // Add mention - addMention(mentionee._id, 'mention'); - - // Create notification - notify(mentionee._id, user._id, 'mention', { - post_id: post._id - }); - - return; - })); - } - - // Register to search database - if (text && config.elasticsearch.enable) { - const es = require('../../../db/elasticsearch'); - - es.index({ - index: 'misskey', - type: 'post', - id: post._id.toString(), - body: { - text: post.text - } - }); - } - - // Append mentions data - if (mentions.length > 0) { - Post.update({ _id: post._id }, { - $set: { - mentions: mentions - } - }); - } -}); diff --git a/src/api/endpoints/posts/favorites/create.ts b/src/api/endpoints/posts/favorites/create.ts deleted file mode 100644 index f9dee271b5..0000000000 --- a/src/api/endpoints/posts/favorites/create.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Favorite from '../../../models/favorite'; -import Post from '../../../models/post'; - -/** - * Favorite a post - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); - - // Get favoritee - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - // if already favorited - const exist = await Favorite.findOne({ - post_id: post._id, - user_id: user._id - }); - - if (exist !== null) { - return rej('already favorited'); - } - - // Create favorite - await Favorite.insert({ - created_at: new Date(), - post_id: post._id, - user_id: user._id - }); - - // Send response - res(); -}); diff --git a/src/api/endpoints/posts/polls/vote.ts b/src/api/endpoints/posts/polls/vote.ts deleted file mode 100644 index 5a4fd1c268..0000000000 --- a/src/api/endpoints/posts/polls/vote.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Vote from '../../../models/poll-vote'; -import Post from '../../../models/post'; -import Watching from '../../../models/post-watching'; -import notify from '../../../common/notify'; -import watch from '../../../common/watch-post'; -import { publishPostStream } from '../../../event'; - -/** - * Vote poll of a post - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); - - // Get votee - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - if (post.poll == null) { - return rej('poll not found'); - } - - // Get 'choice' parameter - const [choice, choiceError] = - $(params.choice).number() - .pipe(c => post.poll.choices.some(x => x.id == c)) - .$; - if (choiceError) return rej('invalid choice param'); - - // if already voted - const exist = await Vote.findOne({ - post_id: post._id, - user_id: user._id - }); - - if (exist !== null) { - return rej('already voted'); - } - - // Create vote - await Vote.insert({ - created_at: new Date(), - post_id: post._id, - user_id: user._id, - choice: choice - }); - - // Send response - res(); - - const inc = {}; - inc[`poll.choices.${findWithAttr(post.poll.choices, 'id', choice)}.votes`] = 1; - - // Increment votes count - await Post.update({ _id: post._id }, { - $inc: inc - }); - - publishPostStream(post._id, 'poll_voted'); - - // Notify - notify(post.user_id, user._id, 'poll_vote', { - post_id: post._id, - choice: choice - }); - - // Fetch watchers - Watching - .find({ - post_id: post._id, - user_id: { $ne: user._id }, - // 削除されたドキュメントは除く - deleted_at: { $exists: false } - }, { - fields: { - user_id: true - } - }) - .then(watchers => { - watchers.forEach(watcher => { - notify(watcher.user_id, user._id, 'poll_vote', { - post_id: post._id, - choice: choice - }); - }); - }); - - // この投稿をWatchする - // TODO: ユーザーが「投票したときに自動でWatchする」設定を - // オフにしていた場合はしない - watch(user._id, post); -}); - -function findWithAttr(array, attr, value) { - for (let i = 0; i < array.length; i += 1) { - if (array[i][attr] === value) { - return i; - } - } - return -1; -} diff --git a/src/api/endpoints/posts/reactions/create.ts b/src/api/endpoints/posts/reactions/create.ts deleted file mode 100644 index eecb928123..0000000000 --- a/src/api/endpoints/posts/reactions/create.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Reaction from '../../../models/post-reaction'; -import Post from '../../../models/post'; -import Watching from '../../../models/post-watching'; -import notify from '../../../common/notify'; -import watch from '../../../common/watch-post'; -import { publishPostStream } from '../../../event'; - -/** - * React to a post - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); - - // Get 'reaction' parameter - const [reaction, reactionErr] = $(params.reaction).string().or([ - 'like', - 'love', - 'laugh', - 'hmm', - 'surprise', - 'congrats', - 'angry', - 'confused', - 'pudding' - ]).$; - if (reactionErr) return rej('invalid reaction param'); - - // Fetch reactee - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - // Myself - if (post.user_id.equals(user._id)) { - return rej('cannot react to my post'); - } - - // if already reacted - const exist = await Reaction.findOne({ - post_id: post._id, - user_id: user._id, - deleted_at: { $exists: false } - }); - - if (exist !== null) { - return rej('already reacted'); - } - - // Create reaction - await Reaction.insert({ - created_at: new Date(), - post_id: post._id, - user_id: user._id, - reaction: reaction - }); - - // Send response - res(); - - const inc = {}; - inc[`reaction_counts.${reaction}`] = 1; - - // Increment reactions count - await Post.update({ _id: post._id }, { - $inc: inc - }); - - publishPostStream(post._id, 'reacted'); - - // Notify - notify(post.user_id, user._id, 'reaction', { - post_id: post._id, - reaction: reaction - }); - - // Fetch watchers - Watching - .find({ - post_id: post._id, - user_id: { $ne: user._id }, - // 削除されたドキュメントは除く - deleted_at: { $exists: false } - }, { - fields: { - user_id: true - } - }) - .then(watchers => { - watchers.forEach(watcher => { - notify(watcher.user_id, user._id, 'reaction', { - post_id: post._id, - reaction: reaction - }); - }); - }); - - // この投稿をWatchする - // TODO: ユーザーが「リアクションしたときに自動でWatchする」設定を - // オフにしていた場合はしない - watch(user._id, post); -}); diff --git a/src/api/endpoints/posts/reactions/delete.ts b/src/api/endpoints/posts/reactions/delete.ts deleted file mode 100644 index 922c57ab18..0000000000 --- a/src/api/endpoints/posts/reactions/delete.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Reaction from '../../../models/post-reaction'; -import Post from '../../../models/post'; -// import event from '../../../event'; - -/** - * Unreact to a post - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); - - // Fetch unreactee - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - // if already unreacted - const exist = await Reaction.findOne({ - post_id: post._id, - user_id: user._id, - deleted_at: { $exists: false } - }); - - if (exist === null) { - return rej('never reacted'); - } - - // Delete reaction - await Reaction.update({ - _id: exist._id - }, { - $set: { - deleted_at: new Date() - } - }); - - // Send response - res(); - - const dec = {}; - dec[`reaction_counts.${exist.reaction}`] = -1; - - // Decrement reactions count - Post.update({ _id: post._id }, { - $inc: dec - }); -}); diff --git a/src/api/endpoints/posts/reposts.ts b/src/api/endpoints/posts/reposts.ts deleted file mode 100644 index b701ff7574..0000000000 --- a/src/api/endpoints/posts/reposts.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../models/post'; -import serialize from '../../serializers/post'; - -/** - * Show a reposts of a post - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); - - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); - - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); - - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); - - // Check if both of since_id and max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); - } - - // Lookup post - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - // Construct query - const sort = { - _id: -1 - }; - const query = { - repost_id: post._id - } as any; - if (sinceId) { - sort._id = 1; - query._id = { - $gt: sinceId - }; - } else if (maxId) { - query._id = { - $lt: maxId - }; - } - - // Issue query - const reposts = await Post - .find(query, { - limit: limit, - sort: sort - }); - - // Serialize - res(await Promise.all(reposts.map(async post => - await serialize(post, user)))); -}); diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts deleted file mode 100644 index b434f64342..0000000000 --- a/src/api/endpoints/posts/search.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import $ from 'cafy'; -const escapeRegexp = require('escape-regexp'); -import Post from '../../models/post'; -import serialize from '../../serializers/post'; -import config from '../../../conf'; - -/** - * Search a post - * - * @param {any} params - * @param {any} me - * @return {Promise<any>} - */ -module.exports = (params, me) => new Promise(async (res, rej) => { - // Get 'query' parameter - const [query, queryError] = $(params.query).string().pipe(x => x != '').$; - if (queryError) return rej('invalid query param'); - - // Get 'offset' parameter - const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; - if (offsetErr) return rej('invalid offset param'); - - // Get 'max' parameter - const [max = 10, maxErr] = $(params.max).optional.number().range(1, 30).$; - if (maxErr) return rej('invalid max param'); - - // If Elasticsearch is available, search by $ - // If not, search by MongoDB - (config.elasticsearch.enable ? byElasticsearch : byNative) - (res, rej, me, query, offset, max); -}); - -// Search by MongoDB -async function byNative(res, rej, me, query, offset, max) { - const escapedQuery = escapeRegexp(query); - - // Search posts - const posts = await Post - .find({ - text: new RegExp(escapedQuery) - }, { - sort: { - _id: -1 - }, - limit: max, - skip: offset - }); - - // Serialize - res(await Promise.all(posts.map(async post => - await serialize(post, me)))); -} - -// Search by Elasticsearch -async function byElasticsearch(res, rej, me, query, offset, max) { - const es = require('../../db/elasticsearch'); - - es.search({ - index: 'misskey', - type: 'post', - body: { - size: max, - from: offset, - query: { - simple_query_string: { - fields: ['text'], - query: query, - default_operator: 'and' - } - }, - sort: [ - { _doc: 'desc' } - ], - highlight: { - pre_tags: ['<mark>'], - post_tags: ['</mark>'], - encoder: 'html', - fields: { - text: {} - } - } - } - }, async (error, response) => { - if (error) { - console.error(error); - return res(500); - } - - if (response.hits.total === 0) { - return res([]); - } - - const hits = response.hits.hits.map(hit => new mongo.ObjectID(hit._id)); - - // Fetch found posts - const posts = await Post - .find({ - _id: { - $in: hits - } - }, { - sort: { - _id: -1 - } - }); - - posts.map(post => { - post._highlight = response.hits.hits.filter(hit => post._id.equals(hit._id))[0].highlight.text[0]; - }); - - // Serialize - res(await Promise.all(posts.map(async post => - await serialize(post, me)))); - }); -} diff --git a/src/api/endpoints/posts/show.ts b/src/api/endpoints/posts/show.ts deleted file mode 100644 index 5bfe4f6605..0000000000 --- a/src/api/endpoints/posts/show.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../models/post'; -import serialize from '../../serializers/post'; - -/** - * Show a post - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); - - // Get post - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - // Serialize - res(await serialize(post, user, { - detail: true - })); -}); diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts deleted file mode 100644 index 314e992344..0000000000 --- a/src/api/endpoints/posts/timeline.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../models/post'; -import getFriends from '../../common/get-friends'; -import serialize from '../../serializers/post'; - -/** - * Get timeline of myself - * - * @param {any} params - * @param {any} user - * @param {any} app - * @return {Promise<any>} - */ -module.exports = (params, user, app) => new Promise(async (res, rej) => { - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); - - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); - - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); - - // Check if both of since_id and max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); - } - - // ID list of the user $self and other users who the user follows - const followingIds = await getFriends(user._id); - - // Construct query - const sort = { - _id: -1 - }; - const query = { - user_id: { - $in: followingIds - } - } as any; - if (sinceId) { - sort._id = 1; - query._id = { - $gt: sinceId - }; - } else if (maxId) { - query._id = { - $lt: maxId - }; - } - - // Issue query - const timeline = await Post - .find(query, { - limit: limit, - sort: sort - }); - - // Serialize - res(await Promise.all(timeline.map(async post => - await serialize(post, user) - ))); -}); diff --git a/src/api/endpoints/users.ts b/src/api/endpoints/users.ts deleted file mode 100644 index 134f262fb1..0000000000 --- a/src/api/endpoints/users.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import User from '../models/user'; -import serialize from '../serializers/user'; - -/** - * Lists all users - * - * @param {any} params - * @param {any} me - * @return {Promise<any>} - */ -module.exports = (params, me) => new Promise(async (res, rej) => { - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); - - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); - - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); - - // Check if both of since_id and max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); - } - - // Construct query - const sort = { - _id: -1 - }; - const query = {} as any; - if (sinceId) { - sort._id = 1; - query._id = { - $gt: sinceId - }; - } else if (maxId) { - query._id = { - $lt: maxId - }; - } - - // Issue query - const users = await User - .find(query, { - limit: limit, - sort: sort - }); - - // Serialize - res(await Promise.all(users.map(async user => - await serialize(user, me)))); -}); diff --git a/src/api/endpoints/users/posts.ts b/src/api/endpoints/users/posts.ts deleted file mode 100644 index e37b660773..0000000000 --- a/src/api/endpoints/users/posts.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../models/post'; -import User from '../../models/user'; -import serialize from '../../serializers/post'; - -/** - * Get posts of a user - * - * @param {any} params - * @param {any} me - * @return {Promise<any>} - */ -module.exports = (params, me) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).optional.id().$; - if (userIdErr) return rej('invalid user_id param'); - - // Get 'username' parameter - const [username, usernameErr] = $(params.username).optional.string().$; - if (usernameErr) return rej('invalid username param'); - - if (userId === undefined && username === undefined) { - return rej('user_id or username is required'); - } - - // Get 'include_replies' parameter - const [includeReplies = true, includeRepliesErr] = $(params.include_replies).optional.boolean().$; - if (includeRepliesErr) return rej('invalid include_replies param'); - - // Get 'with_media' parameter - const [withMedia = false, withMediaErr] = $(params.with_media).optional.boolean().$; - if (withMediaErr) return rej('invalid with_media param'); - - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); - - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); - - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); - - // Check if both of since_id and max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); - } - - const q = userId !== undefined - ? { _id: userId } - : { username_lower: username.toLowerCase() } ; - - // Lookup user - const user = await User.findOne(q, { - fields: { - _id: true - } - }); - - if (user === null) { - return rej('user not found'); - } - - // Construct query - const sort = { - _id: -1 - }; - const query = { - user_id: user._id - } as any; - if (sinceId) { - sort._id = 1; - query._id = { - $gt: sinceId - }; - } else if (maxId) { - query._id = { - $lt: maxId - }; - } - - if (!includeReplies) { - query.reply_to_id = null; - } - - if (withMedia) { - query.media_ids = { - $exists: true, - $ne: null - }; - } - - // Issue query - const posts = await Post - .find(query, { - limit: limit, - sort: sort - }); - - // Serialize - res(await Promise.all(posts.map(async (post) => - await serialize(post, me) - ))); -}); diff --git a/src/api/endpoints/users/show.ts b/src/api/endpoints/users/show.ts deleted file mode 100644 index 8e74b0fe3f..0000000000 --- a/src/api/endpoints/users/show.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import User from '../../models/user'; -import serialize from '../../serializers/user'; - -/** - * Show a user - * - * @param {any} params - * @param {any} me - * @return {Promise<any>} - */ -module.exports = (params, me) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).optional.id().$; - if (userIdErr) return rej('invalid user_id param'); - - // Get 'username' parameter - const [username, usernameErr] = $(params.username).optional.string().$; - if (usernameErr) return rej('invalid username param'); - - if (userId === undefined && username === undefined) { - return rej('user_id or username is required'); - } - - const q = userId !== undefined - ? { _id: userId } - : { username_lower: username.toLowerCase() }; - - // Lookup user - const user = await User.findOne(q, { - fields: { - data: false - } - }); - - if (user === null) { - return rej('user not found'); - } - - // Send response - res(await serialize(user, me, { - detail: true - })); -}); diff --git a/src/api/event.ts b/src/api/event.ts deleted file mode 100644 index 9613a9f7cc..0000000000 --- a/src/api/event.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as mongo from 'mongodb'; -import * as redis from 'redis'; -import config from '../conf'; - -type ID = string | mongo.ObjectID; - -class MisskeyEvent { - private redisClient: redis.RedisClient; - - constructor() { - // Connect to Redis - this.redisClient = redis.createClient( - config.redis.port, config.redis.host); - } - - public publishUserStream(userId: ID, type: string, value?: any): void { - this.publish(`user-stream:${userId}`, type, typeof value === 'undefined' ? null : value); - } - - public publishPostStream(postId: ID, type: string, value?: any): void { - this.publish(`post-stream:${postId}`, type, typeof value === 'undefined' ? null : value); - } - - public publishMessagingStream(userId: ID, otherpartyId: ID, type: string, value?: any): void { - this.publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value); - } - - private publish(channel: string, type: string, value?: any): void { - const message = value == null ? - { type: type } : - { type: type, body: value }; - - this.redisClient.publish(`misskey:${channel}`, JSON.stringify(message)); - } -} - -const ev = new MisskeyEvent(); - -export default ev.publishUserStream.bind(ev); - -export const publishPostStream = ev.publishPostStream.bind(ev); - -export const publishMessagingStream = ev.publishMessagingStream.bind(ev); diff --git a/src/api/models/access-token.ts b/src/api/models/access-token.ts deleted file mode 100644 index 2a8a512ddc..0000000000 --- a/src/api/models/access-token.ts +++ /dev/null @@ -1,8 +0,0 @@ -import db from '../../db/mongodb'; - -const collection = db.get('access_tokens'); - -(collection as any).index('token'); // fuck type definition -(collection as any).index('hash'); // fuck type definition - -export default collection as any; // fuck type definition diff --git a/src/api/models/app.ts b/src/api/models/app.ts deleted file mode 100644 index bf5dc80c2c..0000000000 --- a/src/api/models/app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import db from '../../db/mongodb'; - -const collection = db.get('apps'); - -(collection as any).index('name_id'); // fuck type definition -(collection as any).index('name_id_lower'); // fuck type definition -(collection as any).index('secret'); // fuck type definition - -export default collection as any; // fuck type definition - -export function isValidNameId(nameId: string): boolean { - return typeof nameId == 'string' && /^[a-zA-Z0-9\-]{3,30}$/.test(nameId); -} diff --git a/src/api/models/appdata.ts b/src/api/models/appdata.ts deleted file mode 100644 index 3e68354fa4..0000000000 --- a/src/api/models/appdata.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('appdata') as any; // fuck type definition diff --git a/src/api/models/auth-session.ts b/src/api/models/auth-session.ts deleted file mode 100644 index b264a133e9..0000000000 --- a/src/api/models/auth-session.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('auth_sessions') as any; // fuck type definition diff --git a/src/api/models/drive-file.ts b/src/api/models/drive-file.ts deleted file mode 100644 index 4c7204b1f4..0000000000 --- a/src/api/models/drive-file.ts +++ /dev/null @@ -1,17 +0,0 @@ -import db from '../../db/mongodb'; - -const collection = db.get('drive_files'); - -(collection as any).index('hash'); // fuck type definition - -export default collection as any; // fuck type definition - -export function validateFileName(name: string): boolean { - return ( - (name.trim().length > 0) && - (name.length <= 200) && - (name.indexOf('\\') === -1) && - (name.indexOf('/') === -1) && - (name.indexOf('..') === -1) - ); -} diff --git a/src/api/models/drive-folder.ts b/src/api/models/drive-folder.ts deleted file mode 100644 index f81ffe855d..0000000000 --- a/src/api/models/drive-folder.ts +++ /dev/null @@ -1,10 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('drive_folders') as any; // fuck type definition - -export function isValidFolderName(name: string): boolean { - return ( - (name.trim().length > 0) && - (name.length <= 200) - ); -} diff --git a/src/api/models/drive-tag.ts b/src/api/models/drive-tag.ts deleted file mode 100644 index 991c935e81..0000000000 --- a/src/api/models/drive-tag.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('drive_tags') as any; // fuck type definition diff --git a/src/api/models/favorite.ts b/src/api/models/favorite.ts deleted file mode 100644 index e01d9e343c..0000000000 --- a/src/api/models/favorite.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('favorites') as any; // fuck type definition diff --git a/src/api/models/following.ts b/src/api/models/following.ts deleted file mode 100644 index cb3db9b539..0000000000 --- a/src/api/models/following.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('following') as any; // fuck type definition diff --git a/src/api/models/messaging-history.ts b/src/api/models/messaging-history.ts deleted file mode 100644 index c06987e451..0000000000 --- a/src/api/models/messaging-history.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('messaging_histories') as any; // fuck type definition diff --git a/src/api/models/messaging-message.ts b/src/api/models/messaging-message.ts deleted file mode 100644 index 18afa57e44..0000000000 --- a/src/api/models/messaging-message.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../../db/mongodb'; - -export default db.get('messaging_messages') as any; // fuck type definition - -export interface IMessagingMessage { - _id: mongo.ObjectID; -} - -export function isValidText(text: string): boolean { - return text.length <= 1000 && text.trim() != ''; -} diff --git a/src/api/models/notification.ts b/src/api/models/notification.ts deleted file mode 100644 index 1c1f429a0d..0000000000 --- a/src/api/models/notification.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('notifications') as any; // fuck type definition diff --git a/src/api/models/poll-vote.ts b/src/api/models/poll-vote.ts deleted file mode 100644 index af77a2643e..0000000000 --- a/src/api/models/poll-vote.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('poll_votes') as any; // fuck type definition diff --git a/src/api/models/post-reaction.ts b/src/api/models/post-reaction.ts deleted file mode 100644 index 282ae5bd21..0000000000 --- a/src/api/models/post-reaction.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('post_reactions') as any; // fuck type definition diff --git a/src/api/models/post-watching.ts b/src/api/models/post-watching.ts deleted file mode 100644 index 41d37e2703..0000000000 --- a/src/api/models/post-watching.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('post_watching') as any; // fuck type definition diff --git a/src/api/models/post.ts b/src/api/models/post.ts deleted file mode 100644 index baab63f991..0000000000 --- a/src/api/models/post.ts +++ /dev/null @@ -1,7 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('posts') as any; // fuck type definition - -export function isValidText(text: string): boolean { - return text.length <= 1000 && text.trim() != ''; -} diff --git a/src/api/models/signin.ts b/src/api/models/signin.ts deleted file mode 100644 index 385a348f2e..0000000000 --- a/src/api/models/signin.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('signin') as any; // fuck type definition diff --git a/src/api/models/user.ts b/src/api/models/user.ts deleted file mode 100644 index cd16459891..0000000000 --- a/src/api/models/user.ts +++ /dev/null @@ -1,36 +0,0 @@ -import db from '../../db/mongodb'; - -const collection = db.get('users'); - -(collection as any).index('username'); // fuck type definition -(collection as any).index('token'); // fuck type definition - -export default collection as any; // fuck type definition - -export function validateUsername(username: string): boolean { - return typeof username == 'string' && /^[a-zA-Z0-9\-]{3,20}$/.test(username); -} - -export function validatePassword(password: string): boolean { - return typeof password == 'string' && password != ''; -} - -export function isValidName(name: string): boolean { - return typeof name == 'string' && name.length < 30 && name.trim() != ''; -} - -export function isValidDescription(description: string): boolean { - return typeof description == 'string' && description.length < 500 && description.trim() != ''; -} - -export function isValidLocation(location: string): boolean { - return typeof location == 'string' && location.length < 50 && location.trim() != ''; -} - -export function isValidBirthday(birthday: string): boolean { - return typeof birthday == 'string' && /^([0-9]{4})\-([0-9]{2})-([0-9]{2})$/.test(birthday); -} - -export interface IUser { - name: string; -} diff --git a/src/api/private/signin.ts b/src/api/private/signin.ts deleted file mode 100644 index afa83e50c3..0000000000 --- a/src/api/private/signin.ts +++ /dev/null @@ -1,74 +0,0 @@ -import * as express from 'express'; -import * as bcrypt from 'bcryptjs'; -import User from '../models/user'; -import Signin from '../models/signin'; -import serialize from '../serializers/signin'; -import event from '../event'; -import config from '../../conf'; - -export default async (req: express.Request, res: express.Response) => { - res.header('Access-Control-Allow-Credentials', 'true'); - - const username = req.body['username']; - const password = req.body['password']; - - if (typeof username != 'string') { - res.sendStatus(400); - return; - } - - if (typeof password != 'string') { - res.sendStatus(400); - return; - } - - // Fetch user - const user = await User.findOne({ - username_lower: username.toLowerCase() - }, { - fields: { - data: false, - profile: false - } - }); - - if (user === null) { - res.status(404).send({ - error: 'user not found' - }); - return; - } - - // Compare password - const same = bcrypt.compareSync(password, user.password); - - if (same) { - const expires = 1000 * 60 * 60 * 24 * 365; // One Year - res.cookie('i', user.token, { - path: '/', - domain: `.${config.host}`, - secure: config.url.substr(0, 5) === 'https', - httpOnly: false, - expires: new Date(Date.now() + expires), - maxAge: expires - }); - - res.sendStatus(204); - } else { - res.status(400).send({ - error: 'incorrect password' - }); - } - - // Append signin history - const record = await Signin.insert({ - created_at: new Date(), - user_id: user._id, - ip: req.ip, - headers: req.headers, - success: same - }); - - // Publish signin event - event(user._id, 'signin', await serialize(record)); -}; diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts deleted file mode 100644 index 2375c22845..0000000000 --- a/src/api/private/signup.ts +++ /dev/null @@ -1,109 +0,0 @@ -import * as express from 'express'; -import * as bcrypt from 'bcryptjs'; -import rndstr from 'rndstr'; -import recaptcha = require('recaptcha-promise'); -import User from '../models/user'; -import { validateUsername, validatePassword } from '../models/user'; -import serialize from '../serializers/user'; -import config from '../../conf'; - -recaptcha.init({ - secret_key: config.recaptcha.secretKey -}); - -export default async (req: express.Request, res: express.Response) => { - // Verify recaptcha - // ただしテスト時はこの機構は障害となるため無効にする - if (process.env.NODE_ENV !== 'test') { - const success = await recaptcha(req.body['g-recaptcha-response']); - - if (!success) { - res.status(400).send('recaptcha-failed'); - return; - } - } - - const username = req.body['username']; - const password = req.body['password']; - const name = '名無し'; - - // Validate username - if (!validateUsername(username)) { - res.sendStatus(400); - return; - } - - // Validate password - if (!validatePassword(password)) { - res.sendStatus(400); - return; - } - - // Fetch exist user that same username - const usernameExist = await User - .count({ - username_lower: username.toLowerCase() - }, { - limit: 1 - }); - - // Check username already used - if (usernameExist !== 0) { - res.sendStatus(400); - return; - } - - // Generate hash of password - const salt = bcrypt.genSaltSync(8); - const hash = bcrypt.hashSync(password, salt); - - // Generate secret - const secret = `!${rndstr('a-zA-Z0-9', 32)}`; - - // Create account - const account = await User.insert({ - token: secret, - avatar_id: null, - banner_id: null, - created_at: new Date(), - description: null, - email: null, - followers_count: 0, - following_count: 0, - links: null, - name: name, - password: hash, - posts_count: 0, - likes_count: 0, - liked_count: 0, - drive_capacity: 1073741824, // 1GB - username: username, - username_lower: username.toLowerCase(), - profile: { - bio: null, - birthday: null, - blood: null, - gender: null, - handedness: null, - height: null, - location: null, - weight: null - } - }); - - // Response - res.send(await serialize(account)); - - // Create search index - if (config.elasticsearch.enable) { - const es = require('../../db/elasticsearch'); - es.index({ - index: 'misskey', - type: 'user', - id: account._id.toString(), - body: { - username: username - } - }); - } -}; diff --git a/src/api/reply.ts b/src/api/reply.ts deleted file mode 100644 index e47fc85b9b..0000000000 --- a/src/api/reply.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as express from 'express'; - -export default (res: express.Response, x?: any, y?: any) => { - if (x === undefined) { - res.sendStatus(204); - } else if (typeof x === 'number') { - res.status(x).send({ - error: x === 500 ? 'INTERNAL_ERROR' : y - }); - } else { - res.send(x); - } -}; diff --git a/src/api/serializers/drive-file.ts b/src/api/serializers/drive-file.ts deleted file mode 100644 index b4e2ab064a..0000000000 --- a/src/api/serializers/drive-file.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import DriveFile from '../models/drive-file'; -import serializeDriveFolder from './drive-folder'; -import serializeDriveTag from './drive-tag'; -import deepcopy = require('deepcopy'); -import config from '../../conf'; - -/** - * Serialize a drive file - * - * @param {any} file - * @param {any} options? - * @return {Promise<any>} - */ -export default ( - file: any, - options?: { - detail: boolean - } -) => new Promise<any>(async (resolve, reject) => { - const opts = Object.assign({ - detail: false - }, options); - - let _file: any; - - // Populate the file if 'file' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(file)) { - _file = await DriveFile.findOne({ - _id: file - }, { - fields: { - data: false - } - }); - } else if (typeof file === 'string') { - _file = await DriveFile.findOne({ - _id: new mongo.ObjectID(file) - }, { - fields: { - data: false - } - }); - } else { - _file = deepcopy(file); - } - - // Rename _id to id - _file.id = _file._id; - delete _file._id; - - delete _file.data; - - _file.url = `${config.drive_url}/${_file.id}/${encodeURIComponent(_file.name)}`; - - if (opts.detail && _file.folder_id) { - // Populate folder - _file.folder = await serializeDriveFolder(_file.folder_id, { - detail: true - }); - } - - if (opts.detail && _file.tags) { - // Populate tags - _file.tags = await _file.tags.map(async (tag: any) => - await serializeDriveTag(tag) - ); - } - - resolve(_file); -}); diff --git a/src/api/serializers/drive-tag.ts b/src/api/serializers/drive-tag.ts deleted file mode 100644 index 2f152381bd..0000000000 --- a/src/api/serializers/drive-tag.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import DriveTag from '../models/drive-tag'; -import deepcopy = require('deepcopy'); - -/** - * Serialize a drive tag - * - * @param {any} tag - * @return {Promise<any>} - */ -const self = ( - tag: any -) => new Promise<any>(async (resolve, reject) => { - let _tag: any; - - // Populate the tag if 'tag' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(tag)) { - _tag = await DriveTag.findOne({ _id: tag }); - } else if (typeof tag === 'string') { - _tag = await DriveTag.findOne({ _id: new mongo.ObjectID(tag) }); - } else { - _tag = deepcopy(tag); - } - - // Rename _id to id - _tag.id = _tag._id; - delete _tag._id; - - resolve(_tag); -}); - -export default self; diff --git a/src/api/serializers/messaging-message.ts b/src/api/serializers/messaging-message.ts deleted file mode 100644 index 4ab95e42a3..0000000000 --- a/src/api/serializers/messaging-message.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import Message from '../models/messaging-message'; -import serializeUser from './user'; -import serializeDriveFile from './drive-file'; -import parse from '../common/text'; - -/** - * Serialize a message - * - * @param {any} message - * @param {any} me? - * @param {any} options? - * @return {Promise<any>} - */ -export default ( - message: any, - me?: any, - options?: { - populateRecipient: boolean - } -) => new Promise<any>(async (resolve, reject) => { - const opts = options || { - populateRecipient: true - }; - - let _message: any; - - // Populate the message if 'message' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(message)) { - _message = await Message.findOne({ - _id: message - }); - } else if (typeof message === 'string') { - _message = await Message.findOne({ - _id: new mongo.ObjectID(message) - }); - } else { - _message = deepcopy(message); - } - - // Rename _id to id - _message.id = _message._id; - delete _message._id; - - // Parse text - if (_message.text) { - _message.ast = parse(_message.text); - } - - // Populate user - _message.user = await serializeUser(_message.user_id, me); - - if (_message.file) { - // Populate file - _message.file = await serializeDriveFile(_message.file_id); - } - - if (opts.populateRecipient) { - // Populate recipient - _message.recipient = await serializeUser(_message.recipient_id, me); - } - - resolve(_message); -}); diff --git a/src/api/serializers/notification.ts b/src/api/serializers/notification.ts deleted file mode 100644 index ac919dc8b0..0000000000 --- a/src/api/serializers/notification.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import Notification from '../models/notification'; -import serializeUser from './user'; -import serializePost from './post'; -import deepcopy = require('deepcopy'); - -/** - * Serialize a notification - * - * @param {any} notification - * @return {Promise<any>} - */ -export default (notification: any) => new Promise<any>(async (resolve, reject) => { - let _notification: any; - - // Populate the notification if 'notification' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(notification)) { - _notification = await Notification.findOne({ - _id: notification - }); - } else if (typeof notification === 'string') { - _notification = await Notification.findOne({ - _id: new mongo.ObjectID(notification) - }); - } else { - _notification = deepcopy(notification); - } - - // Rename _id to id - _notification.id = _notification._id; - delete _notification._id; - - // Rename notifier_id to user_id - _notification.user_id = _notification.notifier_id; - delete _notification.notifier_id; - - const me = _notification.notifiee_id; - delete _notification.notifiee_id; - - // Populate notifier - _notification.user = await serializeUser(_notification.user_id, me); - - switch (_notification.type) { - case 'follow': - // nope - break; - case 'mention': - case 'reply': - case 'repost': - case 'quote': - case 'reaction': - case 'poll_vote': - // Populate post - _notification.post = await serializePost(_notification.post_id, me); - break; - default: - console.error(`Unknown type: ${_notification.type}`); - break; - } - - resolve(_notification); -}); diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts deleted file mode 100644 index 3c96884dd1..0000000000 --- a/src/api/serializers/post.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import Post from '../models/post'; -import Reaction from '../models/post-reaction'; -import Vote from '../models/poll-vote'; -import serializeApp from './app'; -import serializeUser from './user'; -import serializeDriveFile from './drive-file'; -import parse from '../common/text'; - -/** - * Serialize a post - * - * @param {any} post - * @param {any} me? - * @param {any} options? - * @return {Promise<any>} - */ -const self = ( - post: any, - me?: any, - options?: { - detail: boolean - } -) => new Promise<any>(async (resolve, reject) => { - const opts = options || { - detail: true, - }; - - let _post: any; - - // Populate the post if 'post' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(post)) { - _post = await Post.findOne({ - _id: post - }); - } else if (typeof post === 'string') { - _post = await Post.findOne({ - _id: new mongo.ObjectID(post) - }); - } else { - _post = deepcopy(post); - } - - const id = _post._id; - - // Rename _id to id - _post.id = _post._id; - delete _post._id; - - delete _post.mentions; - - // Parse text - if (_post.text) { - _post.ast = parse(_post.text); - } - - // Populate user - _post.user = await serializeUser(_post.user_id, me); - - // Populate app - if (_post.app_id) { - _post.app = await serializeApp(_post.app_id); - } - - if (_post.media_ids) { - // Populate media - _post.media = await Promise.all(_post.media_ids.map(async fileId => - await serializeDriveFile(fileId) - )); - } - - if (_post.reply_to_id && opts.detail) { - // Populate reply to post - _post.reply_to = await self(_post.reply_to_id, me, { - detail: false - }); - } - - if (_post.repost_id && opts.detail) { - // Populate repost - _post.repost = await self(_post.repost_id, me, { - detail: _post.text == null - }); - } - - // Poll - if (me && _post.poll && opts.detail) { - const vote = await Vote - .findOne({ - user_id: me._id, - post_id: id - }); - - if (vote != null) { - _post.poll.choices.filter(c => c.id == vote.choice)[0].is_voted = true; - } - } - - // Fetch my reaction - if (me && opts.detail) { - const reaction = await Reaction - .findOne({ - user_id: me._id, - post_id: id, - deleted_at: { $exists: false } - }); - - if (reaction) { - _post.my_reaction = reaction.reaction; - } - } - - resolve(_post); -}); - -export default self; diff --git a/src/api/serializers/signin.ts b/src/api/serializers/signin.ts deleted file mode 100644 index 4068067678..0000000000 --- a/src/api/serializers/signin.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Module dependencies - */ -import deepcopy = require('deepcopy'); - -/** - * Serialize a signin record - * - * @param {any} record - * @return {Promise<any>} - */ -export default ( - record: any -) => new Promise<any>(async (resolve, reject) => { - - const _record = deepcopy(record); - - // Rename _id to id - _record.id = _record._id; - delete _record._id; - - resolve(_record); -}); diff --git a/src/api/serializers/user.ts b/src/api/serializers/user.ts deleted file mode 100644 index bdbc749589..0000000000 --- a/src/api/serializers/user.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import User from '../models/user'; -import Following from '../models/following'; -import getFriends from '../common/get-friends'; -import config from '../../conf'; - -/** - * Serialize a user - * - * @param {any} user - * @param {any} me? - * @param {any} options? - * @return {Promise<any>} - */ -export default ( - user: any, - me?: any, - options?: { - detail?: boolean, - includeSecrets?: boolean - } -) => new Promise<any>(async (resolve, reject) => { - - const opts = Object.assign({ - detail: false, - includeSecrets: false - }, options); - - let _user: any; - - const fields = opts.detail ? { - data: false - } : { - data: false, - profile: false - }; - - // Populate the user if 'user' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(user)) { - _user = await User.findOne({ - _id: user - }, { fields }); - } else if (typeof user === 'string') { - _user = await User.findOne({ - _id: new mongo.ObjectID(user) - }, { fields }); - } else { - _user = deepcopy(user); - } - - // Me - if (me && !mongo.ObjectID.prototype.isPrototypeOf(me)) { - if (typeof me === 'string') { - me = new mongo.ObjectID(me); - } else { - me = me._id; - } - } - - // Rename _id to id - _user.id = _user._id; - delete _user._id; - - // Remove needless properties - delete _user.lates_post; - - // Remove private properties - delete _user.password; - delete _user.token; - delete _user.username_lower; - if (_user.twitter) { - delete _user.twitter.access_token; - delete _user.twitter.access_token_secret; - } - - // Visible via only the official client - if (!opts.includeSecrets) { - delete _user.data; - delete _user.email; - } - - _user.avatar_url = _user.avatar_id != null - ? `${config.drive_url}/${_user.avatar_id}` - : `${config.drive_url}/default-avatar.jpg`; - - _user.banner_url = _user.banner_id != null - ? `${config.drive_url}/${_user.banner_id}` - : null; - - if (!me || !me.equals(_user.id) || !opts.detail) { - delete _user.avatar_id; - delete _user.banner_id; - - delete _user.drive_capacity; - } - - if (me && !me.equals(_user.id)) { - // If the user is following - const follow = await Following.findOne({ - follower_id: me, - followee_id: _user.id, - deleted_at: { $exists: false } - }); - _user.is_following = follow !== null; - - // If the user is followed - const follow2 = await Following.findOne({ - follower_id: _user.id, - followee_id: me, - deleted_at: { $exists: false } - }); - _user.is_followed = follow2 !== null; - } - - if (me && !me.equals(_user.id) && opts.detail) { - const myFollowingIds = await getFriends(me); - - // Get following you know count - const followingYouKnowCount = await Following.count({ - followee_id: { $in: myFollowingIds }, - follower_id: _user.id, - deleted_at: { $exists: false } - }); - _user.following_you_know_count = followingYouKnowCount; - - // Get followers you know count - const followersYouKnowCount = await Following.count({ - followee_id: _user.id, - follower_id: { $in: myFollowingIds }, - deleted_at: { $exists: false } - }); - _user.followers_you_know_count = followersYouKnowCount; - } - - resolve(_user); -}); -/* -function img(url) { - return { - thumbnail: { - large: `${url}`, - medium: '', - small: '' - } - }; -} -*/ diff --git a/src/api/service/twitter.ts b/src/api/service/twitter.ts deleted file mode 100644 index 9fb274aacb..0000000000 --- a/src/api/service/twitter.ts +++ /dev/null @@ -1,78 +0,0 @@ -import * as express from 'express'; -// import * as Twitter from 'twitter'; -// const Twitter = require('twitter'); -import autwh from 'autwh'; -import redis from '../../db/redis'; -import User from '../models/user'; -import serialize from '../serializers/user'; -import event from '../event'; -import config from '../../conf'; - -module.exports = (app: express.Application) => { - app.get('/disconnect/twitter', async (req, res): Promise<any> => { - if (res.locals.user == null) return res.send('plz signin'); - const user = await User.findOneAndUpdate({ - token: res.locals.user - }, { - $set: { - twitter: null - } - }); - - res.send(`Twitterの連携を解除しました :v:`); - - // Publish i updated event - event(user._id, 'i_updated', await serialize(user, user, { - detail: true, - includeSecrets: true - })); - }); - - if (config.twitter == null) { - app.get('/connect/twitter', (req, res) => { - res.send('現在Twitterへ接続できません'); - }); - return; - } - - const twAuth = autwh({ - consumerKey: config.twitter.consumer_key, - consumerSecret: config.twitter.consumer_secret, - callbackUrl: `${config.api_url}/tw/cb` - }); - - app.get('/connect/twitter', async (req, res): Promise<any> => { - if (res.locals.user == null) return res.send('plz signin'); - const ctx = await twAuth.begin(); - redis.set(res.locals.user, JSON.stringify(ctx)); - res.redirect(ctx.url); - }); - - app.get('/tw/cb', (req, res): any => { - if (res.locals.user == null) return res.send('plz signin'); - redis.get(res.locals.user, async (_, ctx) => { - const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier); - - const user = await User.findOneAndUpdate({ - token: res.locals.user - }, { - $set: { - twitter: { - access_token: result.accessToken, - access_token_secret: result.accessTokenSecret, - user_id: result.userId, - screen_name: result.screenName - } - } - }); - - res.send(`Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`); - - // Publish i updated event - event(user._id, 'i_updated', await serialize(user, user, { - detail: true, - includeSecrets: true - })); - }); - }); -}; diff --git a/src/api/stream/home.ts b/src/api/stream/home.ts deleted file mode 100644 index 2ab8d3025b..0000000000 --- a/src/api/stream/home.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as websocket from 'websocket'; -import * as redis from 'redis'; -import * as debug from 'debug'; - -import serializePost from '../serializers/post'; - -const log = debug('misskey'); - -export default function homeStream(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void { - // Subscribe Home stream channel - subscriber.subscribe(`misskey:user-stream:${user._id}`); - - subscriber.on('message', async (channel, data) => { - switch (channel.split(':')[1]) { - case 'user-stream': - connection.send(data); - break; - case 'post-stream': - const postId = channel.split(':')[2]; - log(`RECEIVED: ${postId} ${data} by @${user.username}`); - const post = await serializePost(postId, user, { - detail: true - }); - connection.send(JSON.stringify({ - type: 'post-updated', - body: { - post: post - } - })); - break; - } - }); - - connection.on('message', data => { - const msg = JSON.parse(data.utf8Data); - - switch (msg.type) { - case 'capture': - if (!msg.id) return; - const postId = msg.id; - log(`CAPTURE: ${postId} by @${user.username}`); - subscriber.subscribe(`misskey:post-stream:${postId}`); - break; - } - }); -} diff --git a/src/api/streaming.ts b/src/api/streaming.ts deleted file mode 100644 index c71132100c..0000000000 --- a/src/api/streaming.ts +++ /dev/null @@ -1,95 +0,0 @@ -import * as http from 'http'; -import * as websocket from 'websocket'; -import * as redis from 'redis'; -import config from '../conf'; -import User from './models/user'; -import AccessToken from './models/access-token'; -import isNativeToken from './common/is-native-token'; - -import homeStream from './stream/home'; -import messagingStream from './stream/messaging'; -import serverStream from './stream/server'; - -module.exports = (server: http.Server) => { - /** - * Init websocket server - */ - const ws = new websocket.server({ - httpServer: server - }); - - ws.on('request', async (request) => { - const connection = request.accept(); - - if (request.resourceURL.pathname === '/server') { - serverStream(request, connection); - return; - } - - const user = await authenticate(connection, request.resourceURL.query.i); - - if (user == null) { - connection.send('authentication-failed'); - connection.close(); - return; - } - - // Connect to Redis - const subscriber = redis.createClient( - config.redis.port, config.redis.host); - - connection.on('close', () => { - subscriber.unsubscribe(); - subscriber.quit(); - }); - - const channel = - request.resourceURL.pathname === '/' ? homeStream : - request.resourceURL.pathname === '/messaging' ? messagingStream : - null; - - if (channel !== null) { - channel(request, connection, subscriber, user); - } else { - connection.close(); - } - }); -}; - -function authenticate(connection: websocket.connection, token: string): Promise<any> { - if (token == null) { - return Promise.resolve(null); - } - - return new Promise(async (resolve, reject) => { - if (isNativeToken(token)) { - // Fetch user - // SELECT _id - const user = await User - .findOne({ - token: token - }); - - resolve(user); - } else { - const accessToken = await AccessToken.findOne({ - hash: token - }); - - if (accessToken == null) { - return reject('invalid signature'); - } - - // Fetch user - // SELECT _id - const user = await User - .findOne({ _id: accessToken.user_id }, { - fields: { - _id: true - } - }); - - resolve(user); - } - }); -} diff --git a/src/build/fa.ts b/src/build/fa.ts new file mode 100644 index 0000000000..0c21be9504 --- /dev/null +++ b/src/build/fa.ts @@ -0,0 +1,57 @@ +/** + * Replace fontawesome symbols + */ + +import * as fontawesome from '@fortawesome/fontawesome'; +import * as regular from '@fortawesome/fontawesome-free-regular'; +import * as solid from '@fortawesome/fontawesome-free-solid'; +import * as brands from '@fortawesome/fontawesome-free-brands'; + +// Add icons +fontawesome.library.add(regular); +fontawesome.library.add(solid); +fontawesome.library.add(brands); + +export const pattern = /%fa:(.+?)%/g; + +export const replacement = (_, key) => { + const args = key.split(' '); + let prefix = 'fas'; + const classes = []; + let transform = ''; + let name; + + args.forEach(arg => { + if (arg == 'R' || arg == 'S' || arg == 'B') { + prefix = + arg == 'R' ? 'far' : + arg == 'S' ? 'fas' : + arg == 'B' ? 'fab' : + ''; + } else if (arg[0] == '.') { + classes.push('fa-' + arg.substr(1)); + } else if (arg[0] == '-') { + transform = arg.substr(1).split('|').join(' '); + } else { + name = arg; + } + }); + + const icon = fontawesome.icon({ prefix, iconName: name }, { + classes: classes + }); + + if (icon) { + icon.transform = fontawesome.parse.transform(transform); + return `<i data-fa class="${name}">${icon.html[0]}</i>`; + } else { + console.warn(`'${name}' not found in fa`); + return ''; + } +}; + +export default (src: string) => { + return src.replace(pattern, replacement); +}; + +export const fa = fontawesome; diff --git a/src/build/i18n.ts b/src/build/i18n.ts new file mode 100644 index 0000000000..b9b7403214 --- /dev/null +++ b/src/build/i18n.ts @@ -0,0 +1,57 @@ +/** + * Replace i18n texts + */ + +import locale from '../../locales'; + +export default class Replacer { + private lang: string; + + public pattern = /"%i18n:(.+?)%"|'%i18n:(.+?)%'|%i18n:(.+?)%/g; + + constructor(lang: string) { + this.lang = lang; + + this.get = this.get.bind(this); + this.replacement = this.replacement.bind(this); + } + + private get(key: string) { + const texts = locale[this.lang]; + + if (texts == null) { + console.warn(`lang '${this.lang}' is not supported`); + return key; // Fallback + } + + let text = texts; + + // Check the key existance + const error = key.split('.').some(k => { + if (text.hasOwnProperty(k)) { + text = text[k]; + return false; + } else { + return true; + } + }); + + if (error) { + console.warn(`key '${key}' not found in '${this.lang}'`); + return key; // Fallback + } else { + return text; + } + } + + public replacement(match, a, b, c) { + const key = a || b || c; + if (match[0] == '"') { + return '"' + this.get(key).replace(/"/g, '\\"') + '"'; + } else if (match[0] == "'") { + return '\'' + this.get(key).replace(/'/g, '\\\'') + '\''; + } else { + return this.get(key); + } + } +} diff --git a/src/build/license.ts b/src/build/license.ts new file mode 100644 index 0000000000..d36af665cd --- /dev/null +++ b/src/build/license.ts @@ -0,0 +1,13 @@ +import * as fs from 'fs'; + +const license = fs.readFileSync(__dirname + '/../../LICENSE', 'utf-8'); + +const licenseHtml = license + .replace(/\r\n/g, '\n') + .replace(/(.)\n(.)/g, '$1 $2') + .replace(/(^|\n)(.*?)($|\n)/g, '<p>$2</p>'); + +export { + license, + licenseHtml +}; diff --git a/src/client/app/animation.styl b/src/client/app/animation.styl new file mode 100644 index 0000000000..8f121b313b --- /dev/null +++ b/src/client/app/animation.styl @@ -0,0 +1,12 @@ +.zoom-in-top-enter-active, +.zoom-in-top-leave-active { + opacity: 1; + transform: scaleY(1); + transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); + transform-origin: center top; +} +.zoom-in-top-enter, +.zoom-in-top-leave-active { + opacity: 0; + transform: scaleY(0); +} diff --git a/src/web/app/base.styl b/src/client/app/app.styl similarity index 70% rename from src/web/app/base.styl rename to src/client/app/app.styl index 81c039f0a3..431b9daa65 100644 --- a/src/web/app/base.styl +++ b/src/client/app/app.styl @@ -1,34 +1,14 @@ -json('../../const.json') - -@charset 'utf-8' - -$theme-color = themeColor -$theme-color-foreground = themeColorForeground - -@import './reset' - -/* - ::selection - background $theme-color - color #fff -*/ - -* - tap-highlight-color rgba($theme-color, 0.7) - -webkit-tap-highlight-color rgba($theme-color, 0.7) - -html, body - margin 0 - padding 0 - scroll-behavior smooth - text-size-adjust 100% - font-family sans-serif +@import "../style" +@import "../animation" html &.progress &, * cursor progress !important +body + overflow-wrap break-word + #error padding 32px color #fff @@ -92,17 +72,6 @@ html 100% transform rotate(360deg) -a - text-decoration none - color $theme-color - cursor pointer - - &:hover - text-decoration underline - - * - cursor pointer - code font-family Consolas, 'Courier New', Courier, Monaco, monospace @@ -155,12 +124,5 @@ pre overflow auto tab-size 2 -mk-locker - display block - position fixed - top 0 - left 0 - z-index 65536 - width 100% - height 100% - cursor wait +[data-fa] + display inline-block diff --git a/src/client/app/app.vue b/src/client/app/app.vue new file mode 100644 index 0000000000..7a46e7dea0 --- /dev/null +++ b/src/client/app/app.vue @@ -0,0 +1,3 @@ +<template> +<router-view id="app"></router-view> +</template> diff --git a/src/web/app/auth/assets/logo.svg b/src/client/app/auth/assets/logo.svg similarity index 100% rename from src/web/app/auth/assets/logo.svg rename to src/client/app/auth/assets/logo.svg diff --git a/src/client/app/auth/script.ts b/src/client/app/auth/script.ts new file mode 100644 index 0000000000..31c758ebc2 --- /dev/null +++ b/src/client/app/auth/script.ts @@ -0,0 +1,25 @@ +/** + * Authorize Form + */ + +// Style +import './style.styl'; + +import init from '../init'; + +import Index from './views/index.vue'; + +/** + * init + */ +init(async (launch) => { + document.title = 'Misskey | アプリの連携'; + + // Launch the app + const [app] = launch(); + + // Routing + app.$router.addRoutes([ + { path: '/:token', component: Index }, + ]); +}); diff --git a/src/web/app/auth/style.styl b/src/client/app/auth/style.styl similarity index 79% rename from src/web/app/auth/style.styl rename to src/client/app/auth/style.styl index 046a5ff6ee..bd25e1b572 100644 --- a/src/web/app/auth/style.styl +++ b/src/client/app/auth/style.styl @@ -1,4 +1,5 @@ -@import "../base" +@import "../app" +@import "../reset" html background #eee diff --git a/src/client/app/auth/views/form.vue b/src/client/app/auth/views/form.vue new file mode 100644 index 0000000000..b323907eb0 --- /dev/null +++ b/src/client/app/auth/views/form.vue @@ -0,0 +1,141 @@ +<template> +<div class="form"> + <header> + <h1><i>{{ app.name }}</i>があなたのアカウントにアクセスすることを<b>許可</b>しますか?</h1> + <img :src="`${app.iconUrl}?thumbnail&size=64`"/> + </header> + <div class="app"> + <section> + <h2>{{ app.name }}</h2> + <p class="nid">{{ app.nameId }}</p> + <p class="description">{{ app.description }}</p> + </section> + <section> + <h2>このアプリは次の権限を要求しています:</h2> + <ul> + <template v-for="p in app.permission"> + <li v-if="p == 'account-read'">アカウントの情報を見る。</li> + <li v-if="p == 'account-write'">アカウントの情報を操作する。</li> + <li v-if="p == 'note-write'">投稿する。</li> + <li v-if="p == 'like-write'">いいねしたりいいね解除する。</li> + <li v-if="p == 'following-write'">フォローしたりフォロー解除する。</li> + <li v-if="p == 'drive-read'">ドライブを見る。</li> + <li v-if="p == 'drive-write'">ドライブを操作する。</li> + <li v-if="p == 'notification-read'">通知を見る。</li> + <li v-if="p == 'notification-write'">通知を操作する。</li> + </template> + </ul> + </section> + </div> + <div class="action"> + <button @click="cancel">キャンセル</button> + <button @click="accept">アクセスを許可</button> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['session'], + computed: { + app(): any { + return this.session.app; + } + }, + methods: { + cancel() { + (this as any).api('auth/deny', { + token: this.session.token + }).then(() => { + this.$emit('denied'); + }); + }, + + accept() { + (this as any).api('auth/accept', { + token: this.session.token + }).then(() => { + this.$emit('accepted'); + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.form + + > header + > h1 + margin 0 + padding 32px 32px 20px 32px + font-size 24px + font-weight normal + color #777 + + i + color #77aeca + + &:before + content '「' + + &:after + content '」' + + b + color #666 + + > img + display block + z-index 1 + width 84px + height 84px + margin 0 auto -38px auto + border solid 5px #fff + border-radius 100% + box-shadow 0 2px 2px rgba(0, 0, 0, 0.1) + + > .app + padding 44px 16px 0 16px + color #555 + background #eee + box-shadow 0 2px 2px rgba(0, 0, 0, 0.1) inset + + &:after + content '' + display block + clear both + + > section + float left + width 50% + padding 8px + text-align left + + > h2 + margin 0 + font-size 16px + color #777 + + > .action + padding 16px + + > button + margin 0 8px + padding 0 + + @media (max-width 600px) + > header + > img + box-shadow none + + > .app + box-shadow none + + @media (max-width 500px) + > header + > h1 + font-size 16px + +</style> diff --git a/src/client/app/auth/views/index.vue b/src/client/app/auth/views/index.vue new file mode 100644 index 0000000000..e1e1b265e1 --- /dev/null +++ b/src/client/app/auth/views/index.vue @@ -0,0 +1,149 @@ +<template> +<div class="index"> + <main v-if="os.isSignedIn"> + <p class="fetching" v-if="fetching">読み込み中<mk-ellipsis/></p> + <x-form + ref="form" + v-if="state == 'waiting'" + :session="session" + @denied="state = 'denied'" + @accepted="accepted" + /> + <div class="denied" v-if="state == 'denied'"> + <h1>アプリケーションの連携をキャンセルしました。</h1> + <p>このアプリがあなたのアカウントにアクセスすることはありません。</p> + </div> + <div class="accepted" v-if="state == 'accepted'"> + <h1>{{ session.app.isAuthorized ? 'このアプリは既に連携済みです' : 'アプリケーションの連携を許可しました' }}</h1> + <p v-if="session.app.callbackUrl">アプリケーションに戻っています<mk-ellipsis/></p> + <p v-if="!session.app.callbackUrl">アプリケーションに戻って、やっていってください。</p> + </div> + <div class="error" v-if="state == 'fetch-session-error'"> + <p>セッションが存在しません。</p> + </div> + </main> + <main class="signin" v-if="!os.isSignedIn"> + <h1>サインインしてください</h1> + <mk-signin/> + </main> + <footer><img src="/assets/auth/logo.svg" alt="Misskey"/></footer> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XForm from './form.vue'; + +export default Vue.extend({ + components: { + XForm + }, + data() { + return { + state: null, + session: null, + fetching: true + }; + }, + computed: { + token(): string { + return this.$route.params.token; + } + }, + mounted() { + if (!this.$root.$data.os.isSignedIn) return; + + // Fetch session + (this as any).api('auth/session/show', { + token: this.token + }).then(session => { + this.session = session; + this.fetching = false; + + // 既に連携していた場合 + if (this.session.app.isAuthorized) { + this.$root.$data.os.api('auth/accept', { + token: this.session.token + }).then(() => { + this.accepted(); + }); + } else { + this.state = 'waiting'; + } + }).catch(error => { + this.state = 'fetch-session-error'; + }); + }, + methods: { + accepted() { + this.state = 'accepted'; + if (this.session.app.callbackUrl) { + location.href = this.session.app.callbackUrl + '?token=' + this.session.token; + } + } + } +}); +</script> + +<style lang="stylus" scoped> +.index + + > main + width 100% + max-width 500px + margin 0 auto + text-align center + background #fff + box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2) + + > .fetching + margin 0 + padding 32px + color #555 + + > div + padding 64px + + > h1 + margin 0 0 8px 0 + padding 0 + font-size 20px + font-weight normal + + > p + margin 0 + color #555 + + &.denied > h1 + color #e65050 + + &.accepted > h1 + color #54af7c + + &.signin + padding 32px 32px 16px 32px + + > h1 + margin 0 0 22px 0 + padding 0 + font-size 20px + font-weight normal + color #555 + + @media (max-width 600px) + max-width none + box-shadow none + + @media (max-width 500px) + > div + > h1 + font-size 16px + + > footer + > img + display block + width 64px + height 64px + margin 0 auto + +</style> diff --git a/src/web/app/base.pug b/src/client/app/base.pug similarity index 56% rename from src/web/app/base.pug rename to src/client/app/base.pug index b1ca80deb9..32a95a6c99 100644 --- a/src/web/app/base.pug +++ b/src/client/app/base.pug @@ -9,24 +9,29 @@ html meta(name='application-name' content='Misskey') meta(name='theme-color' content=themeColor) meta(name='referrer' content='origin') + link(rel='manifest' href='/manifest.json') title Misskey style - include ./../../../built/web/assets/init.css + include ./../../../built/client/assets/init.css script - include ./../../../built/web/assets/boot.js + include ./../../../built/client/assets/boot.js script - include ./../../../built/web/assets/safe.js + include ./../../../built/client/assets/safe.js - script(src='https://use.fontawesome.com/db921426cb.js' async) + //- FontAwesome style + style #{facss} + + //- highlight.js style + style #{hljscss} body noscript: p | JavaScriptを有効にしてください br - | Please turn on JavaScript + | Please turn on your JavaScript div#ini: p span . span . diff --git a/src/web/app/boot.js b/src/client/app/boot.js similarity index 51% rename from src/web/app/boot.js rename to src/client/app/boot.js index ac6c18d649..0846e4bd55 100644 --- a/src/web/app/boot.js +++ b/src/client/app/boot.js @@ -21,18 +21,20 @@ // Get the current url information const url = new URL(location.href); - // Extarct the (sub) domain part of the current url - // - // e.g. - // misskey.alice => misskey - // misskey.strawberry.pasta => misskey - // dev.misskey.arisu.tachibana => dev - let app = url.host.split('.')[0]; + //#region Detect app name + let app = null; + + if (url.pathname == '/docs') app = 'docs'; + if (url.pathname == '/dev') app = 'dev'; + if (url.pathname == '/auth') app = 'auth'; + //#endregion // Detect the user language // Note: The default language is English let lang = navigator.language.split('-')[0]; if (!/^(en|ja)$/.test(lang)) lang = 'en'; + if (localStorage.getItem('lang')) lang = localStorage.getItem('lang'); + if (ENV != 'production') lang = 'ja'; // Detect the user agent const ua = navigator.userAgent.toLowerCase(); @@ -55,16 +57,64 @@ } // Switch desktop or mobile version - if (app == 'misskey') { + if (app == null) { app = isMobile ? 'mobile' : 'desktop'; } + // Script version + const ver = localStorage.getItem('v') || VERSION; + + // Whether in debug mode + const isDebug = localStorage.getItem('debug') == 'true'; + + // Whether use raw version script + const raw = (localStorage.getItem('useRawScript') == 'true' && isDebug) + || ENV != 'production'; + // Load an app script // Note: 'async' make it possible to load the script asyncly. // 'defer' make it possible to run the script when the dom loaded. const script = document.createElement('script'); - script.setAttribute('src', `/assets/${app}.${VERSION}.${lang}.js`); + script.setAttribute('src', `/assets/${app}.${ver}.${lang}.${raw ? 'raw' : 'min'}.js`); script.setAttribute('async', 'true'); script.setAttribute('defer', 'true'); head.appendChild(script); + + // 1秒経ってもスクリプトがロードされない場合はバージョンが古くて + // 404になっているせいかもしれないので、バージョンを確認して古ければ更新する + // + // 読み込まれたスクリプトからこのタイマーを解除できるように、 + // グローバルにタイマーIDを代入しておく + window.mkBootTimer = window.setTimeout(async () => { + // Fetch meta + const res = await fetch(API + '/meta', { + method: 'POST', + cache: 'no-cache' + }); + + // Parse + const meta = await res.json(); + + // Compare versions + if (meta.version != ver) { + alert( + 'Misskeyの新しいバージョンがあります。ページを再度読み込みします。' + + '\n\n' + + 'New version of Misskey available. The page will be reloaded.'); + + // Clear cache (serive worker) + try { + navigator.serviceWorker.controller.postMessage('clear'); + + navigator.serviceWorker.getRegistrations().then(registrations => { + registrations.forEach(registration => registration.unregister()); + }); + } catch (e) { + console.error(e); + } + + // Force reload + location.reload(true); + } + }, 1000); } diff --git a/src/web/app/dev/script.js b/src/client/app/ch/script.ts similarity index 54% rename from src/web/app/dev/script.js rename to src/client/app/ch/script.ts index 39d7fc891e..4c6b6dfd1b 100644 --- a/src/web/app/dev/script.js +++ b/src/client/app/ch/script.ts @@ -1,5 +1,5 @@ /** - * Developer Center + * Channels */ // Style @@ -7,12 +7,9 @@ import './style.styl'; require('./tags'); import init from '../init'; -import route from './router'; /** * init */ -init(me => { - // Start routing - route(me); +init(() => { }); diff --git a/src/client/app/ch/style.styl b/src/client/app/ch/style.styl new file mode 100644 index 0000000000..21ca648cbe --- /dev/null +++ b/src/client/app/ch/style.styl @@ -0,0 +1,10 @@ +@import "../app" + +html + padding 8px + background #efefef + +#wait + top auto + bottom 15px + left 15px diff --git a/src/client/app/ch/tags/channel.tag b/src/client/app/ch/tags/channel.tag new file mode 100644 index 0000000000..c0561c9b92 --- /dev/null +++ b/src/client/app/ch/tags/channel.tag @@ -0,0 +1,409 @@ +<mk-channel> + <mk-header/> + <hr> + <main v-if="!fetching"> + <h1>{ channel.title }</h1> + + <div v-if="$root.$data.os.isSignedIn"> + <p v-if="channel.isWatching">このチャンネルをウォッチしています <a @click="unwatch">ウォッチ解除</a></p> + <p v-if="!channel.isWatching"><a @click="watch">このチャンネルをウォッチする</a></p> + </div> + + <div class="share"> + <mk-twitter-button/> + <mk-line-button/> + </div> + + <div class="body"> + <p v-if="notesFetching">読み込み中<mk-ellipsis/></p> + <div v-if="!notesFetching"> + <p v-if="notes == null || notes.length == 0">まだ投稿がありません</p> + <template v-if="notes != null"> + <mk-channel-note each={ note in notes.slice().reverse() } note={ note } form={ parent.refs.form }/> + </template> + </div> + </div> + <hr> + <mk-channel-form v-if="$root.$data.os.isSignedIn" channel={ channel } ref="form"/> + <div v-if="!$root.$data.os.isSignedIn"> + <p>参加するには<a href={ _URL_ }>ログインまたは新規登録</a>してください</p> + </div> + <hr> + <footer> + <small><a href={ _URL_ }>Misskey</a> ver { _VERSION_ } (葵 aoi)</small> + </footer> + </main> + <style lang="stylus" scoped> + :scope + display block + + > main + > h1 + font-size 1.5em + color #f00 + + > .share + > * + margin-right 4px + + > .body + margin 8px 0 0 0 + + > mk-channel-form + max-width 500px + + </style> + <script lang="typescript"> + import Progress from '../../common/scripts/loading'; + import ChannelStream from '../../common/scripts/streaming/channel-stream'; + + this.mixin('i'); + this.mixin('api'); + + this.id = this.opts.id; + this.fetching = true; + this.notesFetching = true; + this.channel = null; + this.notes = null; + this.connection = new ChannelStream(this.id); + this.unreadCount = 0; + + this.on('mount', () => { + document.documentElement.style.background = '#efefef'; + + Progress.start(); + + let fetched = false; + + // チャンネル概要読み込み + this.$root.$data.os.api('channels/show', { + channelId: this.id + }).then(channel => { + if (fetched) { + Progress.done(); + } else { + Progress.set(0.5); + fetched = true; + } + + this.update({ + fetching: false, + channel: channel + }); + + document.title = channel.title + ' | Misskey' + }); + + // 投稿読み込み + this.$root.$data.os.api('channels/notes', { + channelId: this.id + }).then(notes => { + if (fetched) { + Progress.done(); + } else { + Progress.set(0.5); + fetched = true; + } + + this.update({ + notesFetching: false, + notes: notes + }); + }); + + this.connection.on('note', this.onNote); + document.addEventListener('visibilitychange', this.onVisibilitychange, false); + }); + + this.on('unmount', () => { + this.connection.off('note', this.onNote); + this.connection.close(); + document.removeEventListener('visibilitychange', this.onVisibilitychange); + }); + + this.onNote = note => { + this.notes.unshift(note); + this.update(); + + if (document.hidden && this.$root.$data.os.isSignedIn && note.userId !== this.$root.$data.os.i.id) { + this.unreadCount++; + document.title = `(${this.unreadCount}) ${this.channel.title} | Misskey`; + } + }; + + this.onVisibilitychange = () => { + if (!document.hidden) { + this.unreadCount = 0; + document.title = this.channel.title + ' | Misskey' + } + }; + + this.watch = () => { + this.$root.$data.os.api('channels/watch', { + channelId: this.id + }).then(() => { + this.channel.isWatching = true; + this.update(); + }, e => { + alert('error'); + }); + }; + + this.unwatch = () => { + this.$root.$data.os.api('channels/unwatch', { + channelId: this.id + }).then(() => { + this.channel.isWatching = false; + this.update(); + }, e => { + alert('error'); + }); + }; + </script> +</mk-channel> + +<mk-channel-note> + <header> + <a class="index" @click="reply">{ note.index }:</a> + <a class="name" href={ _URL_ + '/@' + acct }><b>{ getUserName(note.user) }</b></a> + <mk-time time={ note.createdAt }/> + <mk-time time={ note.createdAt } mode="detail"/> + <span>ID:<i>{ acct }</i></span> + </header> + <div> + <a v-if="note.reply">>>{ note.reply.index }</a> + { note.text } + <div class="media" v-if="note.media"> + <template each={ file in note.media }> + <a href={ file.url } target="_blank"> + <img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/> + </a> + </template> + </div> + </div> + <style lang="stylus" scoped> + :scope + display block + margin 0 + padding 0 + + > header + position -webkit-sticky + position sticky + z-index 1 + top 0 + background rgba(239, 239, 239, 0.9) + + > .index + margin-right 0.25em + color #000 + + > .name + margin-right 0.5em + color #008000 + + > mk-time + margin-right 0.5em + + &:first-of-type + display none + + @media (max-width 600px) + > mk-time + &:first-of-type + display initial + + &:last-of-type + display none + + > div + padding 0 0 1em 2em + + > .media + > a + display inline-block + + > img + max-width 100% + vertical-align bottom + + </style> + <script lang="typescript"> + import getAcct from '../../../../acct/render'; + import getUserName from '../../../../renderers/get-user-name'; + + this.note = this.opts.note; + this.form = this.opts.form; + this.acct = getAcct(this.note.user); + this.name = getUserName(this.note.user); + + this.reply = () => { + this.form.update({ + reply: this.note + }); + }; + </script> +</mk-channel-note> + +<mk-channel-form> + <p v-if="reply"><b>>>{ reply.index }</b> ({ getUserName(reply.user) }): <a @click="clearReply">[x]</a></p> + <textarea ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder="%i18n:ch.tags.mk-channel-form.textarea%"></textarea> + <div class="actions"> + <button @click="selectFile">%fa:upload%%i18n:ch.tags.mk-channel-form.upload%</button> + <button @click="drive">%fa:cloud%%i18n:ch.tags.mk-channel-form.drive%</button> + <button :class="{ wait: wait }" ref="submit" disabled={ wait || (refs.text.value.length == 0) } @click="note"> + <template v-if="!wait">%fa:paper-plane%</template>{ wait ? '%i18n:ch.tags.mk-channel-form.posting%' : '%i18n:ch.tags.mk-channel-form.note%' }<mk-ellipsis v-if="wait"/> + </button> + </div> + <mk-uploader ref="uploader"/> + <ol v-if="files"> + <li each={ files }>{ name }</li> + </ol> + <input ref="file" type="file" accept="image/*" multiple="multiple" onchange={ changeFile }/> + <style lang="stylus" scoped> + :scope + display block + + > textarea + width 100% + max-width 100% + min-width 100% + min-height 5em + + > .actions + display flex + + > button + > [data-fa] + margin-right 0.25em + + &:last-child + margin-left auto + + &.wait + cursor wait + + > input[type='file'] + display none + + </style> + <script lang="typescript"> + import getUserName from '../../../../renderers/get-user-name'; + + this.mixin('api'); + + this.channel = this.opts.channel; + this.files = null; + + this.on('mount', () => { + this.$refs.uploader.on('uploaded', file => { + this.update({ + files: [file] + }); + }); + }); + + this.upload = file => { + this.$refs.uploader.upload(file); + }; + + this.clearReply = () => { + this.update({ + reply: null + }); + }; + + this.clear = () => { + this.clearReply(); + this.update({ + files: null + }); + this.$refs.text.value = ''; + }; + + this.note = () => { + this.update({ + wait: true + }); + + const files = this.files && this.files.length > 0 + ? this.files.map(f => f.id) + : undefined; + + this.$root.$data.os.api('notes/create', { + text: this.$refs.text.value == '' ? undefined : this.$refs.text.value, + mediaIds: files, + replyId: this.reply ? this.reply.id : undefined, + channelId: this.channel.id + }).then(data => { + this.clear(); + }).catch(err => { + alert('失敗した'); + }).then(() => { + this.update({ + wait: false + }); + }); + }; + + this.changeFile = () => { + Array.from(this.$refs.file.files).forEach(this.upload); + }; + + this.selectFile = () => { + this.$refs.file.click(); + }; + + this.drive = () => { + window['cb'] = files => { + this.update({ + files: files + }); + }; + + window.open(_URL_ + '/selectdrive?multiple=true', + 'drive_window', + 'height=500,width=800'); + }; + + this.onkeydown = e => { + if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post(); + }; + + this.onpaste = e => { + Array.from(e.clipboardData.items).forEach(item => { + if (item.kind == 'file') { + this.upload(item.getAsFile()); + } + }); + }; + + this.getUserName = getUserName; + </script> +</mk-channel-form> + +<mk-twitter-button> + <a href="https://twitter.com/share?ref_src=twsrc%5Etfw" class="twitter-share-button" data-show-count="false">Tweet</a> + <script lang="typescript"> + this.on('mount', () => { + const head = document.getElementsByTagName('head')[0]; + const script = document.createElement('script'); + script.setAttribute('src', 'https://platform.twitter.com/widgets.js'); + script.setAttribute('async', 'async'); + head.appendChild(script); + }); + </script> +</mk-twitter-button> + +<mk-line-button> + <div class="line-it-button" data-lang="ja" data-type="share-a" data-url={ _CH_URL_ } style="display: none;"></div> + <script lang="typescript"> + this.on('mount', () => { + const head = document.getElementsByTagName('head')[0]; + const script = document.createElement('script'); + script.setAttribute('src', 'https://d.line-scdn.net/r/web/social-plugin/js/thirdparty/loader.min.js'); + script.setAttribute('async', 'async'); + head.appendChild(script); + }); + </script> +</mk-line-button> diff --git a/src/client/app/ch/tags/header.tag b/src/client/app/ch/tags/header.tag new file mode 100644 index 0000000000..901123d63b --- /dev/null +++ b/src/client/app/ch/tags/header.tag @@ -0,0 +1,20 @@ +<mk-header> + <div> + <a href={ _CH_URL_ }>Index</a> | <a href={ _URL_ }>Misskey</a> + </div> + <div> + <a v-if="!$root.$data.os.isSignedIn" href={ _URL_ }>ログイン(新規登録)</a> + <a v-if="$root.$data.os.isSignedIn" href={ _URL_ + '/@' + I.username }>{ I.username }</a> + </div> + <style lang="stylus" scoped> + :scope + display flex + + > div:last-child + margin-left auto + + </style> + <script lang="typescript"> + this.mixin('i'); + </script> +</mk-header> diff --git a/src/client/app/ch/tags/index.tag b/src/client/app/ch/tags/index.tag new file mode 100644 index 0000000000..88df2ec45d --- /dev/null +++ b/src/client/app/ch/tags/index.tag @@ -0,0 +1,37 @@ +<mk-index> + <mk-header/> + <hr> + <button @click="n">%i18n:ch.tags.mk-index.new%</button> + <hr> + <ul v-if="channels"> + <li each={ channels }><a href={ '/' + this.id }>{ this.title }</a></li> + </ul> + <style lang="stylus" scoped> + :scope + display block + + </style> + <script lang="typescript"> + this.mixin('api'); + + this.on('mount', () => { + this.$root.$data.os.api('channels', { + limit: 100 + }).then(channels => { + this.update({ + channels: channels + }); + }); + }); + + this.n = () => { + const title = window.prompt('%i18n:ch.tags.mk-index.channel-title%'); + + this.$root.$data.os.api('channels/create', { + title: title + }).then(channel => { + location.href = '/' + channel.id; + }); + }; + </script> +</mk-index> diff --git a/src/client/app/ch/tags/index.ts b/src/client/app/ch/tags/index.ts new file mode 100644 index 0000000000..12ffdaeb84 --- /dev/null +++ b/src/client/app/ch/tags/index.ts @@ -0,0 +1,3 @@ +require('./index.tag'); +require('./channel.tag'); +require('./header.tag'); diff --git a/src/client/app/common/define-widget.ts b/src/client/app/common/define-widget.ts new file mode 100644 index 0000000000..7b98c0903f --- /dev/null +++ b/src/client/app/common/define-widget.ts @@ -0,0 +1,79 @@ +import Vue from 'vue'; + +export default function<T extends object>(data: { + name: string; + props?: () => T; +}) { + return Vue.extend({ + props: { + widget: { + type: Object + }, + isMobile: { + type: Boolean, + default: false + }, + isCustomizeMode: { + type: Boolean, + default: false + } + }, + computed: { + id(): string { + return this.widget.id; + } + }, + data() { + return { + props: data.props ? data.props() : {} as T, + bakedOldProps: null, + preventSave: false + }; + }, + created() { + if (this.props) { + Object.keys(this.props).forEach(prop => { + if (this.widget.data.hasOwnProperty(prop)) { + this.props[prop] = this.widget.data[prop]; + } + }); + } + + this.bakeProps(); + + this.$watch('props', newProps => { + if (this.preventSave) { + this.preventSave = false; + this.bakeProps(); + return; + } + if (this.bakedOldProps == JSON.stringify(newProps)) return; + + this.bakeProps(); + + if (this.isMobile) { + (this as any).api('i/update_mobile_home', { + id: this.id, + data: newProps + }).then(() => { + (this as any).os.i.clientSettings.mobileHome.find(w => w.id == this.id).data = newProps; + }); + } else { + (this as any).api('i/update_home', { + id: this.id, + data: newProps + }).then(() => { + (this as any).os.i.clientSettings.home.find(w => w.id == this.id).data = newProps; + }); + } + }, { + deep: true + }); + }, + methods: { + bakeProps() { + this.bakedOldProps = JSON.stringify(this.props); + } + } + }); +} diff --git a/src/client/app/common/mios.ts b/src/client/app/common/mios.ts new file mode 100644 index 0000000000..5e0c7d2f3b --- /dev/null +++ b/src/client/app/common/mios.ts @@ -0,0 +1,588 @@ +import Vue from 'vue'; +import { EventEmitter } from 'eventemitter3'; +import * as merge from 'object-assign-deep'; +import * as uuid from 'uuid'; + +import { hostname, apiUrl, swPublickey, version, lang, googleMapsApiKey } from '../config'; +import Progress from './scripts/loading'; +import Connection from './scripts/streaming/stream'; +import { HomeStreamManager } from './scripts/streaming/home'; +import { DriveStreamManager } from './scripts/streaming/drive'; +import { ServerStreamManager } from './scripts/streaming/server'; +import { RequestsStreamManager } from './scripts/streaming/requests'; +import { MessagingIndexStreamManager } from './scripts/streaming/messaging-index'; +import { OthelloStreamManager } from './scripts/streaming/othello'; + +import Err from '../common/views/components/connect-failed.vue'; + +//#region api requests +let spinner = null; +let pending = 0; +//#endregion + +export type API = { + chooseDriveFile: (opts: { + title?: string; + currentFolder?: any; + multiple?: boolean; + }) => Promise<any>; + + chooseDriveFolder: (opts: { + title?: string; + currentFolder?: any; + }) => Promise<any>; + + dialog: (opts: { + title: string; + text: string; + actions?: Array<{ + text: string; + id?: string; + }>; + }) => Promise<string>; + + input: (opts: { + title: string; + placeholder?: string; + default?: string; + }) => Promise<string>; + + post: (opts?: { + reply?: any; + renote?: any; + }) => void; + + notify: (message: string) => void; +}; + +/** + * Misskey Operating System + */ +export default class MiOS extends EventEmitter { + /** + * Misskeyの /meta で取得できるメタ情報 + */ + private meta: { + data: { [x: string]: any }; + chachedAt: Date; + }; + + private isMetaFetching = false; + + public app: Vue; + + public new(vm, props) { + const w = new vm({ + parent: this.app, + propsData: props + }).$mount(); + document.body.appendChild(w.$el); + } + + /** + * A signing user + */ + public i: { [x: string]: any }; + + /** + * Whether signed in + */ + public get isSignedIn() { + return this.i != null; + } + + /** + * Whether is debug mode + */ + public get debug() { + return localStorage.getItem('debug') == 'true'; + } + + /** + * Whether enable sounds + */ + public get isEnableSounds() { + return localStorage.getItem('enableSounds') == 'true'; + } + + public apis: API; + + /** + * A connection manager of home stream + */ + public stream: HomeStreamManager; + + /** + * Connection managers + */ + public streams: { + driveStream: DriveStreamManager; + serverStream: ServerStreamManager; + requestsStream: RequestsStreamManager; + messagingIndexStream: MessagingIndexStreamManager; + othelloStream: OthelloStreamManager; + } = { + driveStream: null, + serverStream: null, + requestsStream: null, + messagingIndexStream: null, + othelloStream: null + }; + + /** + * A registration of service worker + */ + private swRegistration: ServiceWorkerRegistration = null; + + /** + * Whether should register ServiceWorker + */ + private shouldRegisterSw: boolean; + + /** + * ウィンドウシステム + */ + public windows = new WindowSystem(); + + /** + * MiOSインスタンスを作成します + * @param shouldRegisterSw ServiceWorkerを登録するかどうか + */ + constructor(shouldRegisterSw = false) { + super(); + + this.shouldRegisterSw = shouldRegisterSw; + + //#region BIND + this.log = this.log.bind(this); + this.logInfo = this.logInfo.bind(this); + this.logWarn = this.logWarn.bind(this); + this.logError = this.logError.bind(this); + this.init = this.init.bind(this); + this.api = this.api.bind(this); + this.getMeta = this.getMeta.bind(this); + this.registerSw = this.registerSw.bind(this); + //#endregion + + if (this.debug) { + (window as any).os = this; + } + } + + private googleMapsIniting = false; + + public getGoogleMaps() { + return new Promise((res, rej) => { + if ((window as any).google && (window as any).google.maps) { + res((window as any).google.maps); + } else { + this.once('init-google-maps', () => { + res((window as any).google.maps); + }); + + //#region load google maps api + if (!this.googleMapsIniting) { + this.googleMapsIniting = true; + (window as any).initGoogleMaps = () => { + this.emit('init-google-maps'); + }; + const head = document.getElementsByTagName('head')[0]; + const script = document.createElement('script'); + script.setAttribute('src', `https://maps.googleapis.com/maps/api/js?key=${googleMapsApiKey}&callback=initGoogleMaps`); + script.setAttribute('async', 'true'); + script.setAttribute('defer', 'true'); + head.appendChild(script); + } + //#endregion + } + }); + } + + public log(...args) { + if (!this.debug) return; + console.log.apply(null, args); + } + + public logInfo(...args) { + if (!this.debug) return; + console.info.apply(null, args); + } + + public logWarn(...args) { + if (!this.debug) return; + console.warn.apply(null, args); + } + + public logError(...args) { + if (!this.debug) return; + console.error.apply(null, args); + } + + public signout() { + localStorage.removeItem('me'); + document.cookie = `i=; domain=${hostname}; expires=Thu, 01 Jan 1970 00:00:01 GMT;`; + location.href = '/'; + } + + /** + * Initialize MiOS (boot) + * @param callback A function that call when initialized + */ + public async init(callback) { + //#region Init stream managers + this.streams.serverStream = new ServerStreamManager(this); + this.streams.requestsStream = new RequestsStreamManager(this); + + this.once('signedin', () => { + // Init home stream manager + this.stream = new HomeStreamManager(this, this.i); + + // Init other stream manager + this.streams.driveStream = new DriveStreamManager(this, this.i); + this.streams.messagingIndexStream = new MessagingIndexStreamManager(this, this.i); + this.streams.othelloStream = new OthelloStreamManager(this, this.i); + }); + //#endregion + + // ユーザーをフェッチしてコールバックする + const fetchme = (token, cb) => { + let me = null; + + // Return when not signed in + if (token == null) { + return done(); + } + + // Fetch user + fetch(`${apiUrl}/i`, { + method: 'POST', + body: JSON.stringify({ + i: token + }) + }) + // When success + .then(res => { + // When failed to authenticate user + if (res.status !== 200) { + return this.signout(); + } + + // Parse response + res.json().then(i => { + me = i; + me.token = token; + done(); + }); + }) + // When failure + .catch(() => { + // Render the error screen + document.body.innerHTML = '<div id="err"></div>'; + new Vue({ + render: createEl => createEl(Err) + }).$mount('#err'); + + Progress.done(); + }); + + function done() { + if (cb) cb(me); + } + }; + + // フェッチが完了したとき + const fetched = me => { + if (me) { + // デフォルトの設定をマージ + me.clientSettings = Object.assign({ + fetchOnScroll: true, + showMaps: true, + showPostFormOnTopOfTl: false, + gradientWindowHeader: false + }, me.clientSettings); + + // ローカルストレージにキャッシュ + localStorage.setItem('me', JSON.stringify(me)); + } + + this.i = me; + + this.emit('signedin'); + + // Finish init + callback(); + + //#region Note + + // Init service worker + if (this.shouldRegisterSw) this.registerSw(); + + //#endregion + }; + + // Get cached account data + const cachedMe = JSON.parse(localStorage.getItem('me')); + + // キャッシュがあったとき + if (cachedMe) { + if (cachedMe.token == null) { + this.signout(); + return; + } + + // とりあえずキャッシュされたデータでお茶を濁して(?)おいて、 + fetched(cachedMe); + + // 後から新鮮なデータをフェッチ + fetchme(cachedMe.token, freshData => { + merge(cachedMe, freshData); + }); + } else { + // Get token from cookie + const i = (document.cookie.match(/i=(!\w+)/) || [null, null])[1]; + + fetchme(i, fetched); + } + } + + /** + * Register service worker + */ + private registerSw() { + // Check whether service worker and push manager supported + const isSwSupported = + ('serviceWorker' in navigator) && ('PushManager' in window); + + // Reject when browser not service worker supported + if (!isSwSupported) return; + + // Reject when not signed in to Misskey + if (!this.isSignedIn) return; + + // When service worker activated + navigator.serviceWorker.ready.then(registration => { + this.log('[sw] ready: ', registration); + + this.swRegistration = registration; + + // Options of pushManager.subscribe + // SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters + const opts = { + // A boolean indicating that the returned push subscription + // will only be used for messages whose effect is made visible to the user. + userVisibleOnly: true, + + // A public key your push server will use to send + // messages to client apps via a push server. + applicationServerKey: urlBase64ToUint8Array(swPublickey) + }; + + // Subscribe push notification + this.swRegistration.pushManager.subscribe(opts).then(subscription => { + this.log('[sw] Subscribe OK:', subscription); + + function encode(buffer: ArrayBuffer) { + return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer))); + } + + // Register + this.api('sw/register', { + endpoint: subscription.endpoint, + auth: encode(subscription.getKey('auth')), + publickey: encode(subscription.getKey('p256dh')) + }); + }) + // When subscribe failed + .catch(async (err: Error) => { + this.logError('[sw] Subscribe Error:', err); + + // 通知が許可されていなかったとき + if (err.name == 'NotAllowedError') { + this.logError('[sw] Subscribe failed due to notification not allowed'); + return; + } + + // 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが + // 既に存在していることが原因でエラーになった可能性があるので、 + // そのサブスクリプションを解除しておく + const subscription = await this.swRegistration.pushManager.getSubscription(); + if (subscription) subscription.unsubscribe(); + }); + }); + + // Whether use raw version script + const raw = (localStorage.getItem('useRawScript') == 'true' && this.debug) + || process.env.NODE_ENV != 'production'; + + // The path of service worker script + const sw = `/sw.${version}.${lang}.${raw ? 'raw' : 'min'}.js`; + + // Register service worker + navigator.serviceWorker.register(sw).then(registration => { + // 登録成功 + this.logInfo('[sw] Registration successful with scope: ', registration.scope); + }).catch(err => { + // 登録失敗 :( + this.logError('[sw] Registration failed: ', err); + }); + } + + public requests = []; + + /** + * Misskey APIにリクエストします + * @param endpoint エンドポイント名 + * @param data パラメータ + */ + public api(endpoint: string, data: { [x: string]: any } = {}): Promise<{ [x: string]: any }> { + if (++pending === 1) { + spinner = document.createElement('div'); + spinner.setAttribute('id', 'wait'); + document.body.appendChild(spinner); + } + + // Append a credential + if (this.isSignedIn) (data as any).i = this.i.token; + + const viaStream = localStorage.getItem('enableExperimental') == 'true'; + + return new Promise((resolve, reject) => { + if (viaStream) { + const stream = this.stream.borrow(); + const id = Math.random().toString(); + + stream.once(`api-res:${id}`, res => { + if (res.res) { + resolve(res.res); + } else { + reject(res.e); + } + }); + + stream.send({ + type: 'api', + id, + endpoint, + data + }); + } else { + const req = { + id: uuid(), + date: new Date(), + name: endpoint, + data, + res: null, + status: null + }; + + if (this.debug) { + this.requests.push(req); + } + + // Send request + fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, { + method: 'POST', + body: JSON.stringify(data), + credentials: endpoint === 'signin' ? 'include' : 'omit', + cache: 'no-cache' + }).then(async (res) => { + if (--pending === 0) spinner.parentNode.removeChild(spinner); + + const body = res.status === 204 ? null : await res.json(); + + if (this.debug) { + req.status = res.status; + req.res = body; + } + + if (res.status === 200) { + resolve(body); + } else if (res.status === 204) { + resolve(); + } else { + reject(body.error); + } + }).catch(reject); + /*}*/ + }); + } + + /** + * Misskeyのメタ情報を取得します + * @param force キャッシュを無視するか否か + */ + public getMeta(force = false) { + return new Promise<{ [x: string]: any }>(async (res, rej) => { + if (this.isMetaFetching) { + this.once('_meta_fetched_', () => { + res(this.meta.data); + }); + return; + } + + const expire = 1000 * 60; // 1min + + // forceが有効, meta情報を保持していない or 期限切れ + if (force || this.meta == null || Date.now() - this.meta.chachedAt.getTime() > expire) { + this.isMetaFetching = true; + const meta = await this.api('meta'); + this.meta = { + data: meta, + chachedAt: new Date() + }; + this.isMetaFetching = false; + this.emit('_meta_fetched_'); + res(meta); + } else { + res(this.meta.data); + } + }); + } + + public connections: Connection[] = []; + + public registerStreamConnection(connection: Connection) { + this.connections.push(connection); + } + + public unregisterStreamConnection(connection: Connection) { + this.connections = this.connections.filter(c => c != connection); + } +} + +class WindowSystem extends EventEmitter { + public windows = new Set(); + + public add(window) { + this.windows.add(window); + this.emit('added', window); + } + + public remove(window) { + this.windows.delete(window); + this.emit('removed', window); + } + + public getAll() { + return this.windows; + } +} + +/** + * Convert the URL safe base64 string to a Uint8Array + * @param base64String base64 string + */ +function urlBase64ToUint8Array(base64String: string): Uint8Array { + const padding = '='.repeat((4 - base64String.length % 4) % 4); + const base64 = (base64String + padding) + .replace(/\-/g, '+') + .replace(/_/g, '/'); + + const rawData = window.atob(base64); + const outputArray = new Uint8Array(rawData.length); + + for (let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + return outputArray; +} diff --git a/src/client/app/common/scripts/check-for-update.ts b/src/client/app/common/scripts/check-for-update.ts new file mode 100644 index 0000000000..81c1eb9812 --- /dev/null +++ b/src/client/app/common/scripts/check-for-update.ts @@ -0,0 +1,33 @@ +import MiOS from '../mios'; +import { version as current } from '../../config'; + +export default async function(mios: MiOS, force = false, silent = false) { + const meta = await mios.getMeta(force); + const newer = meta.version; + + if (newer != current) { + localStorage.setItem('should-refresh', 'true'); + localStorage.setItem('v', newer); + + // Clear cache (serive worker) + try { + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.postMessage('clear'); + } + + navigator.serviceWorker.getRegistrations().then(registrations => { + registrations.forEach(registration => registration.unregister()); + }); + } catch (e) { + console.error(e); + } + + if (!silent) { + alert('%i18n:common.update-available%'.replace('{newer}', newer).replace('{current}', current)); + } + + return newer; + } else { + return null; + } +} diff --git a/src/client/app/common/scripts/compose-notification.ts b/src/client/app/common/scripts/compose-notification.ts new file mode 100644 index 0000000000..c19b1c5ad0 --- /dev/null +++ b/src/client/app/common/scripts/compose-notification.ts @@ -0,0 +1,68 @@ +import getNoteSummary from '../../../../renderers/get-note-summary'; +import getReactionEmoji from '../../../../renderers/get-reaction-emoji'; +import getUserName from '../../../../renderers/get-user-name'; + +type Notification = { + title: string; + body: string; + icon: string; + onclick?: any; +}; + +// TODO: i18n + +export default function(type, data): Notification { + switch (type) { + case 'drive_file_created': + return { + title: 'ファイルがアップロードされました', + body: data.name, + icon: data.url + '?thumbnail&size=64' + }; + + case 'mention': + return { + title: `${getUserName(data.user)}さんから:`, + body: getNoteSummary(data), + icon: data.user.avatarUrl + '?thumbnail&size=64' + }; + + case 'reply': + return { + title: `${getUserName(data.user)}さんから返信:`, + body: getNoteSummary(data), + icon: data.user.avatarUrl + '?thumbnail&size=64' + }; + + case 'quote': + return { + title: `${getUserName(data.user)}さんが引用:`, + body: getNoteSummary(data), + icon: data.user.avatarUrl + '?thumbnail&size=64' + }; + + case 'reaction': + return { + title: `${getUserName(data.user)}: ${getReactionEmoji(data.reaction)}:`, + body: getNoteSummary(data.note), + icon: data.user.avatarUrl + '?thumbnail&size=64' + }; + + case 'unread_messaging_message': + return { + title: `${getUserName(data.user)}さんからメッセージ:`, + body: data.text, // TODO: getMessagingMessageSummary(data), + icon: data.user.avatarUrl + '?thumbnail&size=64' + }; + + case 'othello_invited': + return { + title: '対局への招待があります', + body: `${getUserName(data.parent)}さんから`, + icon: data.parent.avatarUrl + '?thumbnail&size=64' + }; + + default: + return null; + } +} diff --git a/src/web/app/common/scripts/contains.js b/src/client/app/common/scripts/contains.ts similarity index 100% rename from src/web/app/common/scripts/contains.js rename to src/client/app/common/scripts/contains.ts diff --git a/src/web/app/common/scripts/copy-to-clipboard.js b/src/client/app/common/scripts/copy-to-clipboard.ts similarity index 100% rename from src/web/app/common/scripts/copy-to-clipboard.js rename to src/client/app/common/scripts/copy-to-clipboard.ts diff --git a/src/web/app/common/scripts/date-stringify.js b/src/client/app/common/scripts/date-stringify.ts similarity index 100% rename from src/web/app/common/scripts/date-stringify.js rename to src/client/app/common/scripts/date-stringify.ts diff --git a/src/client/app/common/scripts/fuck-ad-block.ts b/src/client/app/common/scripts/fuck-ad-block.ts new file mode 100644 index 0000000000..9bcf7deeff --- /dev/null +++ b/src/client/app/common/scripts/fuck-ad-block.ts @@ -0,0 +1,21 @@ +require('fuckadblock'); + +declare const fuckAdBlock: any; + +export default (os) => { + function adBlockDetected() { + os.apis.dialog({ + title: '%fa:exclamation-triangle%広告ブロッカーを無効にしてください', + text: '<strong>Misskeyは広告を掲載していません</strong>が、広告をブロックする機能が有効だと一部の機能が利用できなかったり、不具合が発生する場合があります。', + actins: [{ + text: 'OK' + }] + }); + } + + if (fuckAdBlock === undefined) { + adBlockDetected(); + } else { + fuckAdBlock.onDetected(adBlockDetected); + } +}; diff --git a/src/web/app/common/scripts/gcd.js b/src/client/app/common/scripts/gcd.ts similarity index 100% rename from src/web/app/common/scripts/gcd.js rename to src/client/app/common/scripts/gcd.ts diff --git a/src/web/app/common/scripts/get-kao.js b/src/client/app/common/scripts/get-kao.ts similarity index 65% rename from src/web/app/common/scripts/get-kao.js rename to src/client/app/common/scripts/get-kao.ts index 0b77ee285a..2168c5be88 100644 --- a/src/web/app/common/scripts/get-kao.js +++ b/src/client/app/common/scripts/get-kao.ts @@ -1,5 +1,5 @@ export default () => [ '(=^・・^=)', 'v(‘ω’)v', - '🐡( '-' 🐡 )フグパンチ!!!!' + '🐡( \'-\' 🐡 )フグパンチ!!!!' ][Math.floor(Math.random() * 3)]; diff --git a/src/client/app/common/scripts/get-median.ts b/src/client/app/common/scripts/get-median.ts new file mode 100644 index 0000000000..91a415d5b2 --- /dev/null +++ b/src/client/app/common/scripts/get-median.ts @@ -0,0 +1,11 @@ +/** + * 中央値を求めます + * @param samples サンプル + */ +export default function(samples) { + if (!samples.length) return 0; + const numbers = samples.slice(0).sort((a, b) => a - b); + const middle = Math.floor(numbers.length / 2); + const isEven = numbers.length % 2 === 0; + return isEven ? (numbers[middle] + numbers[middle - 1]) / 2 : numbers[middle]; +} diff --git a/src/web/app/common/scripts/loading.js b/src/client/app/common/scripts/loading.ts similarity index 100% rename from src/web/app/common/scripts/loading.js rename to src/client/app/common/scripts/loading.ts diff --git a/src/client/app/common/scripts/parse-search-query.ts b/src/client/app/common/scripts/parse-search-query.ts new file mode 100644 index 0000000000..5f6ae3320a --- /dev/null +++ b/src/client/app/common/scripts/parse-search-query.ts @@ -0,0 +1,53 @@ +export default function(qs: string) { + const q = { + text: '' + }; + + qs.split(' ').forEach(x => { + if (/^([a-z_]+?):(.+?)$/.test(x)) { + const [key, value] = x.split(':'); + switch (key) { + case 'user': + q['includeUserUsernames'] = value.split(','); + break; + case 'exclude_user': + q['excludeUserUsernames'] = value.split(','); + break; + case 'follow': + q['following'] = value == 'null' ? null : value == 'true'; + break; + case 'reply': + q['reply'] = value == 'null' ? null : value == 'true'; + break; + case 'renote': + q['renote'] = value == 'null' ? null : value == 'true'; + break; + case 'media': + q['media'] = value == 'null' ? null : value == 'true'; + break; + case 'poll': + q['poll'] = value == 'null' ? null : value == 'true'; + break; + case 'until': + case 'since': + // YYYY-MM-DD + if (/^[0-9]+\-[0-9]+\-[0-9]+$/) { + const [yyyy, mm, dd] = value.split('-'); + q[`${key}_date`] = (new Date(parseInt(yyyy, 10), parseInt(mm, 10) - 1, parseInt(dd, 10))).getTime(); + } + break; + default: + q[key] = value; + break; + } + } else { + q.text += x + ' '; + } + }); + + if (q.text) { + q.text = q.text.trim(); + } + + return q; +} diff --git a/src/client/app/common/scripts/streaming/channel.ts b/src/client/app/common/scripts/streaming/channel.ts new file mode 100644 index 0000000000..cab5f4edb4 --- /dev/null +++ b/src/client/app/common/scripts/streaming/channel.ts @@ -0,0 +1,13 @@ +import Stream from './stream'; +import MiOS from '../../mios'; + +/** + * Channel stream connection + */ +export default class Connection extends Stream { + constructor(os: MiOS, channelId) { + super(os, 'channel', { + channel: channelId + }); + } +} diff --git a/src/client/app/common/scripts/streaming/drive.ts b/src/client/app/common/scripts/streaming/drive.ts new file mode 100644 index 0000000000..7ff85b5946 --- /dev/null +++ b/src/client/app/common/scripts/streaming/drive.ts @@ -0,0 +1,34 @@ +import Stream from './stream'; +import StreamManager from './stream-manager'; +import MiOS from '../../mios'; + +/** + * Drive stream connection + */ +export class DriveStream extends Stream { + constructor(os: MiOS, me) { + super(os, 'drive', { + i: me.token + }); + } +} + +export class DriveStreamManager extends StreamManager<DriveStream> { + private me; + private os: MiOS; + + constructor(os: MiOS, me) { + super(); + + this.me = me; + this.os = os; + } + + public getConnection() { + if (this.connection == null) { + this.connection = new DriveStream(this.os, this.me); + } + + return this.connection; + } +} diff --git a/src/client/app/common/scripts/streaming/home.ts b/src/client/app/common/scripts/streaming/home.ts new file mode 100644 index 0000000000..e085801e15 --- /dev/null +++ b/src/client/app/common/scripts/streaming/home.ts @@ -0,0 +1,57 @@ +import * as merge from 'object-assign-deep'; + +import Stream from './stream'; +import StreamManager from './stream-manager'; +import MiOS from '../../mios'; + +/** + * Home stream connection + */ +export class HomeStream extends Stream { + constructor(os: MiOS, me) { + super(os, '', { + i: me.token + }); + + // 最終利用日時を更新するため定期的にaliveメッセージを送信 + setInterval(() => { + this.send({ type: 'alive' }); + me.lastUsedAt = new Date(); + }, 1000 * 60); + + // 自分の情報が更新されたとき + this.on('i_updated', i => { + if (os.debug) { + console.log('I updated:', i); + } + merge(me, i); + }); + + // トークンが再生成されたとき + // このままではAPIが利用できないので強制的にサインアウトさせる + this.on('my_token_regenerated', () => { + alert('%i18n:common.my-token-regenerated%'); + os.signout(); + }); + } +} + +export class HomeStreamManager extends StreamManager<HomeStream> { + private me; + private os: MiOS; + + constructor(os: MiOS, me) { + super(); + + this.me = me; + this.os = os; + } + + public getConnection() { + if (this.connection == null) { + this.connection = new HomeStream(this.os, this.me); + } + + return this.connection; + } +} diff --git a/src/client/app/common/scripts/streaming/messaging-index.ts b/src/client/app/common/scripts/streaming/messaging-index.ts new file mode 100644 index 0000000000..84e2174ec4 --- /dev/null +++ b/src/client/app/common/scripts/streaming/messaging-index.ts @@ -0,0 +1,34 @@ +import Stream from './stream'; +import StreamManager from './stream-manager'; +import MiOS from '../../mios'; + +/** + * Messaging index stream connection + */ +export class MessagingIndexStream extends Stream { + constructor(os: MiOS, me) { + super(os, 'messaging-index', { + i: me.token + }); + } +} + +export class MessagingIndexStreamManager extends StreamManager<MessagingIndexStream> { + private me; + private os: MiOS; + + constructor(os: MiOS, me) { + super(); + + this.me = me; + this.os = os; + } + + public getConnection() { + if (this.connection == null) { + this.connection = new MessagingIndexStream(this.os, this.me); + } + + return this.connection; + } +} diff --git a/src/client/app/common/scripts/streaming/messaging.ts b/src/client/app/common/scripts/streaming/messaging.ts new file mode 100644 index 0000000000..c1b5875cfb --- /dev/null +++ b/src/client/app/common/scripts/streaming/messaging.ts @@ -0,0 +1,20 @@ +import Stream from './stream'; +import MiOS from '../../mios'; + +/** + * Messaging stream connection + */ +export class MessagingStream extends Stream { + constructor(os: MiOS, me, otherparty) { + super(os, 'messaging', { + i: me.token, + otherparty + }); + + (this as any).on('_connected_', () => { + this.send({ + i: me.token + }); + }); + } +} diff --git a/src/client/app/common/scripts/streaming/othello-game.ts b/src/client/app/common/scripts/streaming/othello-game.ts new file mode 100644 index 0000000000..b85af8f72b --- /dev/null +++ b/src/client/app/common/scripts/streaming/othello-game.ts @@ -0,0 +1,11 @@ +import Stream from './stream'; +import MiOS from '../../mios'; + +export class OthelloGameStream extends Stream { + constructor(os: MiOS, me, game) { + super(os, 'othello-game', { + i: me ? me.token : null, + game: game.id + }); + } +} diff --git a/src/client/app/common/scripts/streaming/othello.ts b/src/client/app/common/scripts/streaming/othello.ts new file mode 100644 index 0000000000..f5d47431cd --- /dev/null +++ b/src/client/app/common/scripts/streaming/othello.ts @@ -0,0 +1,31 @@ +import StreamManager from './stream-manager'; +import Stream from './stream'; +import MiOS from '../../mios'; + +export class OthelloStream extends Stream { + constructor(os: MiOS, me) { + super(os, 'othello', { + i: me.token + }); + } +} + +export class OthelloStreamManager extends StreamManager<OthelloStream> { + private me; + private os: MiOS; + + constructor(os: MiOS, me) { + super(); + + this.me = me; + this.os = os; + } + + public getConnection() { + if (this.connection == null) { + this.connection = new OthelloStream(this.os, this.me); + } + + return this.connection; + } +} diff --git a/src/client/app/common/scripts/streaming/requests.ts b/src/client/app/common/scripts/streaming/requests.ts new file mode 100644 index 0000000000..5bec30143f --- /dev/null +++ b/src/client/app/common/scripts/streaming/requests.ts @@ -0,0 +1,30 @@ +import Stream from './stream'; +import StreamManager from './stream-manager'; +import MiOS from '../../mios'; + +/** + * Requests stream connection + */ +export class RequestsStream extends Stream { + constructor(os: MiOS) { + super(os, 'requests'); + } +} + +export class RequestsStreamManager extends StreamManager<RequestsStream> { + private os: MiOS; + + constructor(os: MiOS) { + super(); + + this.os = os; + } + + public getConnection() { + if (this.connection == null) { + this.connection = new RequestsStream(this.os); + } + + return this.connection; + } +} diff --git a/src/client/app/common/scripts/streaming/server.ts b/src/client/app/common/scripts/streaming/server.ts new file mode 100644 index 0000000000..3d35ef4d9d --- /dev/null +++ b/src/client/app/common/scripts/streaming/server.ts @@ -0,0 +1,30 @@ +import Stream from './stream'; +import StreamManager from './stream-manager'; +import MiOS from '../../mios'; + +/** + * Server stream connection + */ +export class ServerStream extends Stream { + constructor(os: MiOS) { + super(os, 'server'); + } +} + +export class ServerStreamManager extends StreamManager<ServerStream> { + private os: MiOS; + + constructor(os: MiOS) { + super(); + + this.os = os; + } + + public getConnection() { + if (this.connection == null) { + this.connection = new ServerStream(this.os); + } + + return this.connection; + } +} diff --git a/src/client/app/common/scripts/streaming/stream-manager.ts b/src/client/app/common/scripts/streaming/stream-manager.ts new file mode 100644 index 0000000000..568b8b0372 --- /dev/null +++ b/src/client/app/common/scripts/streaming/stream-manager.ts @@ -0,0 +1,108 @@ +import { EventEmitter } from 'eventemitter3'; +import * as uuid from 'uuid'; +import Connection from './stream'; + +/** + * ストリーム接続を管理するクラス + * 複数の場所から同じストリームを利用する際、接続をまとめたりする + */ +export default abstract class StreamManager<T extends Connection> extends EventEmitter { + private _connection: T = null; + + private disposeTimerId: any; + + /** + * コネクションを必要としているユーザー + */ + private users = []; + + protected set connection(connection: T) { + this._connection = connection; + + if (this._connection == null) { + this.emit('disconnected'); + } else { + this.emit('connected', this._connection); + + this._connection.on('_connected_', () => { + this.emit('_connected_'); + }); + + this._connection.on('_disconnected_', () => { + this.emit('_disconnected_'); + }); + + this._connection.user = 'Managed'; + } + } + + protected get connection() { + return this._connection; + } + + /** + * コネクションを持っているか否か + */ + public get hasConnection() { + return this._connection != null; + } + + public get state(): string { + if (!this.hasConnection) return 'no-connection'; + return this._connection.state; + } + + /** + * コネクションを要求します + */ + public abstract getConnection(): T; + + /** + * 現在接続しているコネクションを取得します + */ + public borrow() { + return this._connection; + } + + /** + * コネクションを要求するためのユーザーIDを発行します + */ + public use() { + // タイマー解除 + if (this.disposeTimerId) { + clearTimeout(this.disposeTimerId); + this.disposeTimerId = null; + } + + // ユーザーID生成 + const userId = uuid(); + + this.users.push(userId); + + this._connection.user = `Managed (${ this.users.length })`; + + return userId; + } + + /** + * コネクションを利用し終わってもう必要ないことを通知します + * @param userId use で発行したユーザーID + */ + public dispose(userId) { + this.users = this.users.filter(id => id != userId); + + this._connection.user = `Managed (${ this.users.length })`; + + // 誰もコネクションの利用者がいなくなったら + if (this.users.length == 0) { + // また直ぐに再利用される可能性があるので、一定時間待ち、 + // 新たな利用者が現れなければコネクションを切断する + this.disposeTimerId = setTimeout(() => { + this.disposeTimerId = null; + + this.connection.close(); + this.connection = null; + }, 3000); + } + } +} diff --git a/src/client/app/common/scripts/streaming/stream.ts b/src/client/app/common/scripts/streaming/stream.ts new file mode 100644 index 0000000000..3912186ad3 --- /dev/null +++ b/src/client/app/common/scripts/streaming/stream.ts @@ -0,0 +1,137 @@ +import { EventEmitter } from 'eventemitter3'; +import * as uuid from 'uuid'; +import * as ReconnectingWebsocket from 'reconnecting-websocket'; +import { wsUrl } from '../../../config'; +import MiOS from '../../mios'; + +/** + * Misskey stream connection + */ +export default class Connection extends EventEmitter { + public state: string; + private buffer: any[]; + public socket: ReconnectingWebsocket; + public name: string; + public connectedAt: Date; + public user: string = null; + public in: number = 0; + public out: number = 0; + public inout: Array<{ + type: 'in' | 'out', + at: Date, + data: string + }> = []; + public id: string; + public isSuspended = false; + private os: MiOS; + + constructor(os: MiOS, endpoint, params?) { + super(); + + //#region BIND + this.onOpen = this.onOpen.bind(this); + this.onClose = this.onClose.bind(this); + this.onMessage = this.onMessage.bind(this); + this.send = this.send.bind(this); + this.close = this.close.bind(this); + //#endregion + + this.id = uuid(); + this.os = os; + this.name = endpoint; + this.state = 'initializing'; + this.buffer = []; + + const query = params + ? Object.keys(params) + .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])) + .join('&') + : null; + + this.socket = new ReconnectingWebsocket(`${wsUrl}/${endpoint}${query ? '?' + query : ''}`); + this.socket.addEventListener('open', this.onOpen); + this.socket.addEventListener('close', this.onClose); + this.socket.addEventListener('message', this.onMessage); + + // Register this connection for debugging + this.os.registerStreamConnection(this); + } + + /** + * Callback of when open connection + */ + private onOpen() { + this.state = 'connected'; + this.emit('_connected_'); + + this.connectedAt = new Date(); + + // バッファーを処理 + const _buffer = [].concat(this.buffer); // Shallow copy + this.buffer = []; // Clear buffer + _buffer.forEach(data => { + this.send(data); // Resend each buffered messages + + if (this.os.debug) { + this.out++; + this.inout.push({ type: 'out', at: new Date(), data }); + } + }); + } + + /** + * Callback of when close connection + */ + private onClose() { + this.state = 'reconnecting'; + this.emit('_disconnected_'); + } + + /** + * Callback of when received a message from connection + */ + private onMessage(message) { + if (this.isSuspended) return; + + if (this.os.debug) { + this.in++; + this.inout.push({ type: 'in', at: new Date(), data: message.data }); + } + + try { + const msg = JSON.parse(message.data); + if (msg.type) this.emit(msg.type, msg.body); + } catch (e) { + // noop + } + } + + /** + * Send a message to connection + */ + public send(data) { + if (this.isSuspended) return; + + // まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する + if (this.state != 'connected') { + this.buffer.push(data); + return; + } + + if (this.os.debug) { + this.out++; + this.inout.push({ type: 'out', at: new Date(), data }); + } + + this.socket.send(JSON.stringify(data)); + } + + /** + * Close this connection + */ + public close() { + this.os.unregisterStreamConnection(this); + this.socket.removeEventListener('open', this.onOpen); + this.socket.removeEventListener('message', this.onMessage); + } +} diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue new file mode 100644 index 0000000000..5c8f61a2a2 --- /dev/null +++ b/src/client/app/common/views/components/autocomplete.vue @@ -0,0 +1,307 @@ +<template> +<div class="mk-autocomplete" @contextmenu.prevent="() => {}"> + <ol class="users" ref="suggests" v-if="users.length > 0"> + <li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1"> + <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/> + <span class="name">{{ user | userName }}</span> + <span class="username">@{{ user | acct }}</span> + </li> + </ol> + <ol class="emojis" ref="suggests" v-if="emojis.length > 0"> + <li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1"> + <span class="emoji">{{ emoji.emoji }}</span> + <span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span> + <span class="alias" v-if="emoji.alias">({{ emoji.alias }})</span> + </li> + </ol> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as emojilib from 'emojilib'; +import contains from '../../../common/scripts/contains'; + +const lib = Object.entries(emojilib.lib).filter((x: any) => { + return x[1].category != 'flags'; +}); + +const emjdb = lib.map((x: any) => ({ + emoji: x[1].char, + name: x[0], + alias: null +})); + +lib.forEach((x: any) => { + if (x[1].keywords) { + x[1].keywords.forEach(k => { + emjdb.push({ + emoji: x[1].char, + name: k, + alias: x[0] + }); + }); + } +}); + +emjdb.sort((a, b) => a.name.length - b.name.length); + +export default Vue.extend({ + props: ['type', 'q', 'textarea', 'complete', 'close', 'x', 'y'], + data() { + return { + fetching: true, + users: [], + emojis: [], + select: -1, + emojilib + } + }, + computed: { + items(): HTMLCollection { + return (this.$refs.suggests as Element).children; + } + }, + updated() { + //#region 位置調整 + const margin = 32; + + if (this.x + this.$el.offsetWidth > window.innerWidth - margin) { + this.$el.style.left = (this.x - this.$el.offsetWidth) + 'px'; + this.$el.style.marginLeft = '-16px'; + } else { + this.$el.style.left = this.x + 'px'; + this.$el.style.marginLeft = '0'; + } + + if (this.y + this.$el.offsetHeight > window.innerHeight - margin) { + this.$el.style.top = (this.y - this.$el.offsetHeight) + 'px'; + this.$el.style.marginTop = '0'; + } else { + this.$el.style.top = this.y + 'px'; + this.$el.style.marginTop = 'calc(1em + 8px)'; + } + //#endregion + }, + mounted() { + this.textarea.addEventListener('keydown', this.onKeydown); + + Array.from(document.querySelectorAll('body *')).forEach(el => { + el.addEventListener('mousedown', this.onMousedown); + }); + + this.$nextTick(() => { + this.exec(); + + this.$watch('q', () => { + this.$nextTick(() => { + this.exec(); + }); + }); + }); + }, + beforeDestroy() { + this.textarea.removeEventListener('keydown', this.onKeydown); + + Array.from(document.querySelectorAll('body *')).forEach(el => { + el.removeEventListener('mousedown', this.onMousedown); + }); + }, + methods: { + exec() { + this.select = -1; + if (this.$refs.suggests) { + Array.from(this.items).forEach(el => { + el.removeAttribute('data-selected'); + }); + } + + if (this.type == 'user') { + const cache = sessionStorage.getItem(this.q); + if (cache) { + const users = JSON.parse(cache); + this.users = users; + this.fetching = false; + } else { + (this as any).api('users/search_by_username', { + query: this.q, + limit: 30 + }).then(users => { + this.users = users; + this.fetching = false; + + // キャッシュ + sessionStorage.setItem(this.q, JSON.stringify(users)); + }); + } + } else if (this.type == 'emoji') { + const matched = []; + emjdb.some(x => { + if (x.name.indexOf(this.q) == 0 && !x.alias && !matched.some(y => y.emoji == x.emoji)) matched.push(x); + return matched.length == 30; + }); + if (matched.length < 30) { + emjdb.some(x => { + if (x.name.indexOf(this.q) == 0 && !matched.some(y => y.emoji == x.emoji)) matched.push(x); + return matched.length == 30; + }); + } + if (matched.length < 30) { + emjdb.some(x => { + if (x.name.indexOf(this.q) > -1 && !matched.some(y => y.emoji == x.emoji)) matched.push(x); + return matched.length == 30; + }); + } + this.emojis = matched; + } + }, + + onMousedown(e) { + if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close(); + }, + + onKeydown(e) { + const cancel = () => { + e.preventDefault(); + e.stopPropagation(); + }; + + switch (e.which) { + case 10: // [ENTER] + case 13: // [ENTER] + if (this.select !== -1) { + cancel(); + (this.items[this.select] as any).click(); + } else { + this.close(); + } + break; + + case 27: // [ESC] + cancel(); + this.close(); + break; + + case 38: // [↑] + if (this.select !== -1) { + cancel(); + this.selectPrev(); + } else { + this.close(); + } + break; + + case 9: // [TAB] + case 40: // [↓] + cancel(); + this.selectNext(); + break; + + default: + e.stopPropagation(); + this.textarea.focus(); + } + }, + + selectNext() { + if (++this.select >= this.items.length) this.select = 0; + this.applySelect(); + }, + + selectPrev() { + if (--this.select < 0) this.select = this.items.length - 1; + this.applySelect(); + }, + + applySelect() { + Array.from(this.items).forEach(el => { + el.removeAttribute('data-selected'); + }); + + this.items[this.select].setAttribute('data-selected', 'true'); + (this.items[this.select] as any).focus(); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-autocomplete + position fixed + z-index 65535 + margin-top calc(1em + 8px) + overflow hidden + background #fff + border solid 1px rgba(0, 0, 0, 0.1) + border-radius 4px + transition top 0.1s ease, left 0.1s ease + + > ol + display block + margin 0 + padding 4px 0 + max-height 190px + max-width 500px + overflow auto + list-style none + + > li + display block + padding 4px 12px + white-space nowrap + overflow hidden + font-size 0.9em + color rgba(0, 0, 0, 0.8) + cursor default + + &, * + user-select none + + &:hover + &[data-selected='true'] + background $theme-color + + &, * + color #fff !important + + &:active + background darken($theme-color, 10%) + + &, * + color #fff !important + + > .users > li + + .avatar + vertical-align middle + min-width 28px + min-height 28px + max-width 28px + max-height 28px + margin 0 8px 0 0 + border-radius 100% + + .name + margin 0 8px 0 0 + color rgba(0, 0, 0, 0.8) + + .username + color rgba(0, 0, 0, 0.3) + + > .emojis > li + + .emoji + display inline-block + margin 0 4px 0 0 + width 24px + + .name + color rgba(0, 0, 0, 0.8) + + .alias + margin 0 0 0 8px + color rgba(0, 0, 0, 0.3) + +</style> diff --git a/src/client/app/common/views/components/connect-failed.troubleshooter.vue b/src/client/app/common/views/components/connect-failed.troubleshooter.vue new file mode 100644 index 0000000000..cadbd36ba4 --- /dev/null +++ b/src/client/app/common/views/components/connect-failed.troubleshooter.vue @@ -0,0 +1,137 @@ +<template> +<div class="troubleshooter"> + <h1>%fa:wrench%%i18n:common.tags.mk-error.troubleshooter.title%</h1> + <div> + <p :data-wip="network == null"> + <template v-if="network != null"> + <template v-if="network">%fa:check%</template> + <template v-if="!network">%fa:times%</template> + </template> + {{ network == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-network%' : '%i18n:common.tags.mk-error.troubleshooter.network%' }}<mk-ellipsis v-if="network == null"/> + </p> + <p v-if="network == true" :data-wip="internet == null"> + <template v-if="internet != null"> + <template v-if="internet">%fa:check%</template> + <template v-if="!internet">%fa:times%</template> + </template> + {{ internet == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-internet%' : '%i18n:common.tags.mk-error.troubleshooter.internet%' }}<mk-ellipsis v-if="internet == null"/> + </p> + <p v-if="internet == true" :data-wip="server == null"> + <template v-if="server != null"> + <template v-if="server">%fa:check%</template> + <template v-if="!server">%fa:times%</template> + </template> + {{ server == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-server%' : '%i18n:common.tags.mk-error.troubleshooter.server%' }}<mk-ellipsis v-if="server == null"/> + </p> + </div> + <p v-if="!end">%i18n:common.tags.mk-error.troubleshooter.finding%<mk-ellipsis/></p> + <p v-if="network === false"><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-network%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-network-desc%</p> + <p v-if="internet === false"><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-internet%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-internet-desc%</p> + <p v-if="server === false"><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-server%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-server-desc%</p> + <p v-if="server === true" class="success"><b>%fa:info-circle%%i18n:common.tags.mk-error.troubleshooter.success%</b><br>%i18n:common.tags.mk-error.troubleshooter.success-desc%</p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { apiUrl } from '../../../config'; + +export default Vue.extend({ + data() { + return { + network: navigator.onLine, + end: false, + internet: null, + server: null + }; + }, + mounted() { + if (!this.network) { + this.end = true; + return; + } + + // Check internet connection + fetch('https://google.com?rand=' + Math.random(), { + mode: 'no-cors' + }).then(() => { + this.internet = true; + + // Check misskey server is available + fetch(`${apiUrl}/meta`).then(() => { + this.end = true; + this.server = true; + }) + .catch(() => { + this.end = true; + this.server = false; + }); + }) + .catch(() => { + this.end = true; + this.internet = false; + }); + } +}); +</script> + +<style lang="stylus" scoped> +.troubleshooter + width 100% + max-width 500px + text-align left + background #fff + border-radius 8px + border solid 1px #ddd + + > h1 + margin 0 + padding 0.6em 1.2em + font-size 1em + color #444 + border-bottom solid 1px #eee + + > [data-fa] + margin-right 0.25em + + > div + overflow hidden + padding 0.6em 1.2em + + > p + margin 0.5em 0 + font-size 0.9em + color #444 + + &[data-wip] + color #888 + + > [data-fa] + margin-right 0.25em + + &.times + color #e03524 + + &.check + color #84c32f + + > p + margin 0 + padding 0.7em 1.2em + font-size 1em + color #444 + border-top solid 1px #eee + + > b + > [data-fa] + margin-right 0.25em + + &.success + > b + color #39adad + + &:not(.success) + > b + color #ad4339 + +</style> diff --git a/src/client/app/common/views/components/connect-failed.vue b/src/client/app/common/views/components/connect-failed.vue new file mode 100644 index 0000000000..185250dbd8 --- /dev/null +++ b/src/client/app/common/views/components/connect-failed.vue @@ -0,0 +1,106 @@ +<template> +<div class="mk-connect-failed"> + <img src="data:image/jpeg;base64,%base64:/assets/error.jpg%" alt=""/> + <h1>%i18n:common.tags.mk-error.title%</h1> + <p class="text"> + {{ '%i18n:common.tags.mk-error.description%'.substr(0, '%i18n:common.tags.mk-error.description%'.indexOf('{')) }} + <a @click="reload">{{ '%i18n:common.tags.mk-error.description%'.match(/\{(.+?)\}/)[1] }}</a> + {{ '%i18n:common.tags.mk-error.description%'.substr('%i18n:common.tags.mk-error.description%'.indexOf('}') + 1) }} + </p> + <button v-if="!troubleshooting" @click="troubleshooting = true">%i18n:common.tags.mk-error.troubleshoot%</button> + <x-troubleshooter v-if="troubleshooting"/> + <p class="thanks">%i18n:common.tags.mk-error.thanks%</p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XTroubleshooter from './connect-failed.troubleshooter.vue'; + +export default Vue.extend({ + components: { + XTroubleshooter + }, + data() { + return { + troubleshooting: false + }; + }, + mounted() { + document.title = 'Oops!'; + document.documentElement.style.background = '#f8f8f8'; + }, + methods: { + reload() { + location.reload(); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-connect-failed + width 100% + padding 32px 18px + text-align center + + > img + display block + height 200px + margin 0 auto + pointer-events none + user-select none + + > h1 + display block + margin 1.25em auto 0.65em auto + font-size 1.5em + color #555 + + > .text + display block + margin 0 auto + max-width 600px + font-size 1em + color #666 + + > button + display block + margin 1em auto 0 auto + padding 8px 10px + color $theme-color-foreground + background $theme-color + + &:focus + outline solid 3px rgba($theme-color, 0.3) + + &:hover + background lighten($theme-color, 10%) + + &:active + background darken($theme-color, 10%) + + > .troubleshooter + margin 1em auto 0 auto + + > .thanks + display block + margin 2em auto 0 auto + padding 2em 0 0 0 + max-width 600px + font-size 0.9em + font-style oblique + color #aaa + border-top solid 1px #eee + + @media (max-width 500px) + padding 24px 18px + font-size 80% + + > img + height 150px + +</style> + diff --git a/src/client/app/common/views/components/ellipsis.vue b/src/client/app/common/views/components/ellipsis.vue new file mode 100644 index 0000000000..07349902de --- /dev/null +++ b/src/client/app/common/views/components/ellipsis.vue @@ -0,0 +1,26 @@ +<template> + <span class="mk-ellipsis"> + <span>.</span><span>.</span><span>.</span> + </span> +</template> + +<style lang="stylus" scoped> +.mk-ellipsis + > span + animation ellipsis 1.4s infinite ease-in-out both + + &:nth-child(1) + animation-delay 0s + + &:nth-child(2) + animation-delay 0.16s + + &:nth-child(3) + animation-delay 0.32s + + @keyframes ellipsis + 0%, 80%, 100% + opacity 1 + 40% + opacity 0 +</style> diff --git a/src/client/app/common/views/components/file-type-icon.vue b/src/client/app/common/views/components/file-type-icon.vue new file mode 100644 index 0000000000..b7e868d1f7 --- /dev/null +++ b/src/client/app/common/views/components/file-type-icon.vue @@ -0,0 +1,17 @@ +<template> +<span class="mk-file-type-icon"> + <template v-if="kind == 'image'">%fa:file-image%</template> +</span> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['type'], + computed: { + kind(): string { + return this.type.split('/')[0]; + } + } +}); +</script> diff --git a/src/client/app/common/views/components/forkit.vue b/src/client/app/common/views/components/forkit.vue new file mode 100644 index 0000000000..6f334b965a --- /dev/null +++ b/src/client/app/common/views/components/forkit.vue @@ -0,0 +1,42 @@ +<template> +<a class="a" href="https://github.com/syuilo/misskey" target="_blank" title="%i18n:common.tags.mk-forkit.open-github-link%" aria-label="%i18n:common.tags.mk-forkit.open-github-link%"> + <svg width="80" height="80" viewBox="0 0 250 250" aria-hidden="aria-hidden"> + <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path> + <path class="octo-arm" d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor"></path> + <path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor"></path> + </svg> +</a> +</template> + +<style lang="stylus" scoped> +@import '~const.styl' + +.a + display block + position absolute + top 0 + right 0 + + > svg + display block + //fill #151513 + //color #fff + fill $theme-color + color $theme-color-foreground + + .octo-arm + transform-origin 130px 106px + + &:hover + .octo-arm + animation octocat-wave 560ms ease-in-out + + @keyframes octocat-wave + 0%, 100% + transform rotate(0) + 20%, 60% + transform rotate(-25deg) + 40%, 80% + transform rotate(10deg) + +</style> diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts new file mode 100644 index 0000000000..6bfe43a800 --- /dev/null +++ b/src/client/app/common/views/components/index.ts @@ -0,0 +1,51 @@ +import Vue from 'vue'; + +import signin from './signin.vue'; +import signup from './signup.vue'; +import forkit from './forkit.vue'; +import nav from './nav.vue'; +import noteHtml from './note-html'; +import poll from './poll.vue'; +import pollEditor from './poll-editor.vue'; +import reactionIcon from './reaction-icon.vue'; +import reactionsViewer from './reactions-viewer.vue'; +import time from './time.vue'; +import timer from './timer.vue'; +import mediaList from './media-list.vue'; +import uploader from './uploader.vue'; +import specialMessage from './special-message.vue'; +import streamIndicator from './stream-indicator.vue'; +import ellipsis from './ellipsis.vue'; +import messaging from './messaging.vue'; +import messagingRoom from './messaging-room.vue'; +import urlPreview from './url-preview.vue'; +import twitterSetting from './twitter-setting.vue'; +import fileTypeIcon from './file-type-icon.vue'; +import Switch from './switch.vue'; +import Othello from './othello.vue'; +import welcomeTimeline from './welcome-timeline.vue'; + +Vue.component('mk-signin', signin); +Vue.component('mk-signup', signup); +Vue.component('mk-forkit', forkit); +Vue.component('mk-nav', nav); +Vue.component('mk-note-html', noteHtml); +Vue.component('mk-poll', poll); +Vue.component('mk-poll-editor', pollEditor); +Vue.component('mk-reaction-icon', reactionIcon); +Vue.component('mk-reactions-viewer', reactionsViewer); +Vue.component('mk-time', time); +Vue.component('mk-timer', timer); +Vue.component('mk-media-list', mediaList); +Vue.component('mk-uploader', uploader); +Vue.component('mk-special-message', specialMessage); +Vue.component('mk-stream-indicator', streamIndicator); +Vue.component('mk-ellipsis', ellipsis); +Vue.component('mk-messaging', messaging); +Vue.component('mk-messaging-room', messagingRoom); +Vue.component('mk-url-preview', urlPreview); +Vue.component('mk-twitter-setting', twitterSetting); +Vue.component('mk-file-type-icon', fileTypeIcon); +Vue.component('mk-switch', Switch); +Vue.component('mk-othello', Othello); +Vue.component('mk-welcome-timeline', welcomeTimeline); diff --git a/src/client/app/common/views/components/media-list.vue b/src/client/app/common/views/components/media-list.vue new file mode 100644 index 0000000000..64172ad0b4 --- /dev/null +++ b/src/client/app/common/views/components/media-list.vue @@ -0,0 +1,57 @@ +<template> +<div class="mk-media-list" :data-count="mediaList.length"> + <template v-for="media in mediaList"> + <mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')" :inline-playable="mediaList.length === 1"/> + <mk-media-image :image="media" :key="media.id" v-else /> + </template> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['mediaList'], +}); +</script> + +<style lang="stylus" scoped> +.mk-media-list + display grid + grid-gap 4px + height 256px + + @media (max-width 500px) + height 192px + + &[data-count="1"] + grid-template-rows 1fr + &[data-count="2"] + grid-template-columns 1fr 1fr + grid-template-rows 1fr + &[data-count="3"] + grid-template-columns 1fr 0.5fr + grid-template-rows 1fr 1fr + :nth-child(1) + grid-row 1 / 3 + :nth-child(3) + grid-column 2 / 3 + grid-row 2/3 + &[data-count="4"] + grid-template-columns 1fr 1fr + grid-template-rows 1fr 1fr + + :nth-child(1) + grid-column 1 / 2 + grid-row 1 / 2 + :nth-child(2) + grid-column 2 / 3 + grid-row 1 / 2 + :nth-child(3) + grid-column 1 / 2 + grid-row 2 / 3 + :nth-child(4) + grid-column 2 / 3 + grid-row 2 / 3 + +</style> diff --git a/src/client/app/common/views/components/messaging-room.form.vue b/src/client/app/common/views/components/messaging-room.form.vue new file mode 100644 index 0000000000..704f2016d8 --- /dev/null +++ b/src/client/app/common/views/components/messaging-room.form.vue @@ -0,0 +1,305 @@ +<template> +<div class="mk-messaging-form" + @dragover.stop="onDragover" + @drop.stop="onDrop" +> + <textarea + v-model="text" + ref="textarea" + @keypress="onKeypress" + @paste="onPaste" + placeholder="%i18n:common.input-message-here%" + v-autocomplete="'text'" + ></textarea> + <div class="file" @click="file = null" v-if="file">{{ file.name }}</div> + <mk-uploader ref="uploader" @uploaded="onUploaded"/> + <button class="send" @click="send" :disabled="!canSend || sending" title="%i18n:common.send%"> + <template v-if="!sending">%fa:paper-plane%</template><template v-if="sending">%fa:spinner .spin%</template> + </button> + <button class="attach-from-local" @click="chooseFile" title="%i18n:common.tags.mk-messaging-form.attach-from-local%"> + %fa:upload% + </button> + <button class="attach-from-drive" @click="chooseFileFromDrive" title="%i18n:common.tags.mk-messaging-form.attach-from-drive%"> + %fa:R folder-open% + </button> + <input ref="file" type="file" @change="onChangeFile"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as autosize from 'autosize'; + +export default Vue.extend({ + props: ['user'], + data() { + return { + text: null, + file: null, + sending: false + }; + }, + computed: { + draftId(): string { + return this.user.id; + }, + canSend(): boolean { + return (this.text != null && this.text != '') || this.file != null; + }, + room(): any { + return this.$parent; + } + }, + watch: { + text() { + this.saveDraft(); + }, + file() { + this.saveDraft(); + + if (this.room.isBottom()) { + this.room.scrollToBottom(); + } + } + }, + mounted() { + autosize(this.$refs.textarea); + + // 書きかけの投稿を復元 + const draft = JSON.parse(localStorage.getItem('message_drafts') || '{}')[this.draftId]; + if (draft) { + this.text = draft.data.text; + this.file = draft.data.file; + } + }, + methods: { + onPaste(e) { + const data = e.clipboardData; + const items = data.items; + + if (items.length == 1) { + if (items[0].kind == 'file') { + this.upload(items[0].getAsFile()); + } + } else { + if (items[0].kind == 'file') { + alert('メッセージに添付できるのはひとつのファイルのみです'); + } + } + }, + + onDragover(e) { + const isFile = e.dataTransfer.items[0].kind == 'file'; + const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; + if (isFile || isDriveFile) { + e.preventDefault(); + e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; + } + }, + + onDrop(e): void { + // ファイルだったら + if (e.dataTransfer.files.length == 1) { + e.preventDefault(); + this.upload(e.dataTransfer.files[0]); + return; + } else if (e.dataTransfer.files.length > 1) { + e.preventDefault(); + alert('メッセージに添付できるのはひとつのファイルのみです'); + return; + } + + //#region ドライブのファイル + const driveFile = e.dataTransfer.getData('mk_drive_file'); + if (driveFile != null && driveFile != '') { + this.file = JSON.parse(driveFile); + e.preventDefault(); + } + //#endregion + }, + + onKeypress(e) { + if ((e.which == 10 || e.which == 13) && e.ctrlKey) { + this.send(); + } + }, + + chooseFile() { + (this.$refs.file as any).click(); + }, + + chooseFileFromDrive() { + (this as any).apis.chooseDriveFile({ + multiple: false + }).then(file => { + this.file = file; + }); + }, + + onChangeFile() { + this.upload((this.$refs.file as any).files[0]); + }, + + upload(file) { + (this.$refs.uploader as any).upload(file); + }, + + onUploaded(file) { + this.file = file; + }, + + send() { + this.sending = true; + (this as any).api('messaging/messages/create', { + userId: this.user.id, + text: this.text ? this.text : undefined, + fileId: this.file ? this.file.id : undefined + }).then(message => { + this.clear(); + }).catch(err => { + console.error(err); + }).then(() => { + this.sending = false; + }); + }, + + clear() { + this.text = ''; + this.file = null; + this.deleteDraft(); + }, + + saveDraft() { + const data = JSON.parse(localStorage.getItem('message_drafts') || '{}'); + + data[this.draftId] = { + updatedAt: new Date(), + data: { + text: this.text, + file: this.file + } + } + + localStorage.setItem('message_drafts', JSON.stringify(data)); + }, + + deleteDraft() { + const data = JSON.parse(localStorage.getItem('message_drafts') || '{}'); + + delete data[this.draftId]; + + localStorage.setItem('message_drafts', JSON.stringify(data)); + }, + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-messaging-form + > textarea + cursor auto + display block + width 100% + min-width 100% + max-width 100% + height 64px + margin 0 + padding 8px + resize none + font-size 1em + color #000 + outline none + border none + border-top solid 1px #eee + border-radius 0 + box-shadow none + background transparent + + > .file + padding 8px + color #444 + background #eee + cursor pointer + + > .send + position absolute + bottom 0 + right 0 + margin 0 + padding 10px 14px + font-size 1em + color #aaa + transition color 0.1s ease + + &:hover + color $theme-color + + &:active + color darken($theme-color, 10%) + transition color 0s ease + + .files + display block + margin 0 + padding 0 8px + list-style none + + &:after + content '' + display block + clear both + + > li + display block + float left + margin 4px + padding 0 + width 64px + height 64px + background-color #eee + background-repeat no-repeat + background-position center center + background-size cover + cursor move + + &:hover + > .remove + display block + + > .remove + display none + position absolute + right -6px + top -6px + margin 0 + padding 0 + background transparent + outline none + border none + border-radius 0 + box-shadow none + cursor pointer + + .attach-from-local + .attach-from-drive + margin 0 + padding 10px 14px + font-size 1em + font-weight normal + text-decoration none + color #aaa + transition color 0.1s ease + + &:hover + color $theme-color + + &:active + color darken($theme-color, 10%) + transition color 0s ease + + input[type=file] + display none + +</style> diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue new file mode 100644 index 0000000000..60e5258b63 --- /dev/null +++ b/src/client/app/common/views/components/messaging-room.message.vue @@ -0,0 +1,265 @@ +<template> +<div class="message" :data-is-me="isMe"> + <router-link class="avatar-anchor" :to="message.user | userPage" :title="message.user | acct" target="_blank"> + <img class="avatar" :src="`${message.user.avatarUrl}?thumbnail&size=80`" alt=""/> + </router-link> + <div class="content"> + <div class="balloon" :data-no-text="message.text == null"> + <p class="read" v-if="isMe && message.isRead">%i18n:common.tags.mk-messaging-message.is-read%</p> + <button class="delete-button" v-if="isMe" title="%i18n:common.delete%"> + <img src="/assets/desktop/messaging/delete.png" alt="Delete"/> + </button> + <div class="content" v-if="!message.isDeleted"> + <mk-note-html class="text" v-if="message.text" ref="text" :text="message.text" :i="os.i"/> + <div class="file" v-if="message.file"> + <a :href="message.file.url" target="_blank" :title="message.file.name"> + <img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/> + <p v-else>{{ message.file.name }}</p> + </a> + </div> + </div> + <div class="content" v-if="message.isDeleted"> + <p class="is-deleted">%i18n:common.tags.mk-messaging-message.deleted%</p> + </div> + </div> + <div></div> + <mk-url-preview v-for="url in urls" :url="url" :key="url"/> + <footer> + <mk-time :time="message.createdAt"/> + <template v-if="message.is_edited">%fa:pencil-alt%</template> + </footer> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import parse from '../../../../../text/parse'; + +export default Vue.extend({ + props: { + message: { + required: true + } + }, + computed: { + isMe(): boolean { + return this.message.userId == (this as any).os.i.id; + }, + urls(): string[] { + if (this.message.text) { + const ast = parse(this.message.text); + return ast + .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) + .map(t => t.url); + } else { + return null; + } + } + } +}); +</script> + +<style lang="stylus" scoped> +.message + $me-balloon-color = #23A7B6 + + padding 10px 12px 10px 12px + background-color transparent + + > .avatar-anchor + display block + position absolute + top 10px + + > .avatar + display block + min-width 54px + min-height 54px + max-width 54px + max-height 54px + margin 0 + border-radius 8px + transition all 0.1s ease + + > .content + + > .balloon + display block + padding 0 + max-width calc(100% - 16px) + min-height 38px + border-radius 16px + + &:before + content "" + pointer-events none + display block + position absolute + top 12px + + & + * + clear both + + &:hover + > .delete-button + display block + + > .delete-button + display none + position absolute + z-index 1 + top -4px + right -4px + margin 0 + padding 0 + cursor pointer + outline none + border none + border-radius 0 + box-shadow none + background transparent + + > img + vertical-align bottom + width 16px + height 16px + cursor pointer + + > .read + user-select none + display block + position absolute + z-index 1 + bottom -4px + left -12px + margin 0 + color rgba(0, 0, 0, 0.5) + font-size 11px + + > .content + + > .is-deleted + display block + margin 0 + padding 0 + overflow hidden + overflow-wrap break-word + font-size 1em + color rgba(0, 0, 0, 0.5) + + > .text + display block + margin 0 + padding 8px 16px + overflow hidden + overflow-wrap break-word + font-size 1em + color rgba(0, 0, 0, 0.8) + + & + .file + > a + border-radius 0 0 16px 16px + + > .file + > a + display block + max-width 100% + max-height 512px + border-radius 16px + overflow hidden + text-decoration none + + &:hover + text-decoration none + + > p + background #ccc + + > * + display block + margin 0 + width 100% + height 100% + + > p + padding 30px + text-align center + color #555 + background #ddd + + > .mk-url-preview + margin 8px 0 + + > footer + display block + margin 2px 0 0 0 + font-size 10px + color rgba(0, 0, 0, 0.4) + + > [data-fa] + margin-left 4px + + &:not([data-is-me]) + > .avatar-anchor + left 12px + + > .content + padding-left 66px + + > .balloon + float left + background #eee + + &[data-no-text] + background transparent + + &:not([data-no-text]):before + left -14px + border-top solid 8px transparent + border-right solid 8px #eee + border-bottom solid 8px transparent + border-left solid 8px transparent + + > footer + text-align left + + &[data-is-me] + > .avatar-anchor + right 12px + + > .content + padding-right 66px + + > .balloon + float right + background $me-balloon-color + + &[data-no-text] + background transparent + + &:not([data-no-text]):before + right -14px + left auto + border-top solid 8px transparent + border-right solid 8px transparent + border-bottom solid 8px transparent + border-left solid 8px $me-balloon-color + + > .content + + > p.is-deleted + color rgba(255, 255, 255, 0.5) + + > .text >>> + &, * + color #fff !important + + > footer + text-align right + + &[data-is-deleted] + > .baloon + opacity 0.5 + +</style> diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue new file mode 100644 index 0000000000..d30c64d74a --- /dev/null +++ b/src/client/app/common/views/components/messaging-room.vue @@ -0,0 +1,377 @@ +<template> +<div class="mk-messaging-room" + @dragover.prevent.stop="onDragover" + @drop.prevent.stop="onDrop" +> + <div class="stream"> + <p class="init" v-if="init">%fa:spinner .spin%%i18n:common.loading%</p> + <p class="empty" v-if="!init && messages.length == 0">%fa:info-circle%%i18n:common.tags.mk-messaging-room.empty%</p> + <p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages">%fa:flag%%i18n:common.tags.mk-messaging-room.no-history%</p> + <button class="more" :class="{ fetching: fetchingMoreMessages }" v-if="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages"> + <template v-if="fetchingMoreMessages">%fa:spinner .pulse .fw%</template>{{ fetchingMoreMessages ? '%i18n:common.loading%' : '%i18n:common.tags.mk-messaging-room.more%' }} + </button> + <template v-for="(message, i) in _messages"> + <x-message :message="message" :key="message.id"/> + <p class="date" v-if="i != messages.length - 1 && message._date != _messages[i + 1]._date"> + <span>{{ _messages[i + 1]._datetext }}</span> + </p> + </template> + </div> + <footer> + <div ref="notifications" class="notifications"></div> + <x-form :user="user" ref="form"/> + </footer> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { MessagingStream } from '../../scripts/streaming/messaging'; +import XMessage from './messaging-room.message.vue'; +import XForm from './messaging-room.form.vue'; +import { url } from '../../../config'; + +export default Vue.extend({ + components: { + XMessage, + XForm + }, + + props: ['user', 'isNaked'], + + data() { + return { + init: true, + fetchingMoreMessages: false, + messages: [], + existMoreMessages: false, + connection: null + }; + }, + + computed: { + _messages(): any[] { + return (this.messages as any).map(message => { + const date = new Date(message.createdAt).getDate(); + const month = new Date(message.createdAt).getMonth() + 1; + message._date = date; + message._datetext = `${month}月 ${date}日`; + return message; + }); + }, + + form(): any { + return this.$refs.form; + } + }, + + mounted() { + this.connection = new MessagingStream((this as any).os, (this as any).os.i, this.user.id); + + this.connection.on('message', this.onMessage); + this.connection.on('read', this.onRead); + + document.addEventListener('visibilitychange', this.onVisibilitychange); + + this.fetchMessages().then(() => { + this.init = false; + this.scrollToBottom(); + }); + }, + + beforeDestroy() { + this.connection.off('message', this.onMessage); + this.connection.off('read', this.onRead); + this.connection.close(); + + document.removeEventListener('visibilitychange', this.onVisibilitychange); + }, + + methods: { + onDragover(e) { + const isFile = e.dataTransfer.items[0].kind == 'file'; + const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; + + if (isFile || isDriveFile) { + e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; + } else { + e.dataTransfer.dropEffect = 'none'; + } + }, + + onDrop(e): void { + // ファイルだったら + if (e.dataTransfer.files.length == 1) { + this.form.upload(e.dataTransfer.files[0]); + return; + } else if (e.dataTransfer.files.length > 1) { + alert('メッセージに添付できるのはひとつのファイルのみです'); + return; + } + + //#region ドライブのファイル + const driveFile = e.dataTransfer.getData('mk_drive_file'); + if (driveFile != null && driveFile != '') { + const file = JSON.parse(driveFile); + this.form.file = file; + } + //#endregion + }, + + fetchMessages() { + return new Promise((resolve, reject) => { + const max = this.existMoreMessages ? 20 : 10; + + (this as any).api('messaging/messages', { + userId: this.user.id, + limit: max + 1, + untilId: this.existMoreMessages ? this.messages[0].id : undefined + }).then(messages => { + if (messages.length == max + 1) { + this.existMoreMessages = true; + messages.pop(); + } else { + this.existMoreMessages = false; + } + + this.messages.unshift.apply(this.messages, messages.reverse()); + resolve(); + }); + }); + }, + + fetchMoreMessages() { + this.fetchingMoreMessages = true; + this.fetchMessages().then(() => { + this.fetchingMoreMessages = false; + }); + }, + + onMessage(message) { + // サウンドを再生する + if ((this as any).os.isEnableSounds) { + const sound = new Audio(`${url}/assets/message.mp3`); + sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1; + sound.play(); + } + + const isBottom = this.isBottom(); + + this.messages.push(message); + if (message.userId != (this as any).os.i.id && !document.hidden) { + this.connection.send({ + type: 'read', + id: message.id + }); + } + + if (isBottom) { + // Scroll to bottom + this.$nextTick(() => { + this.scrollToBottom(); + }); + } else if (message.userId != (this as any).os.i.id) { + // Notify + this.notify('%i18n:common.tags.mk-messaging-room.new-message%'); + } + }, + + onRead(ids) { + if (!Array.isArray(ids)) ids = [ids]; + ids.forEach(id => { + if (this.messages.some(x => x.id == id)) { + const exist = this.messages.map(x => x.id).indexOf(id); + this.messages[exist].isRead = true; + } + }); + }, + + isBottom() { + const asobi = 64; + const current = this.isNaked + ? window.scrollY + window.innerHeight + : this.$el.scrollTop + this.$el.offsetHeight; + const max = this.isNaked + ? document.body.offsetHeight + : this.$el.scrollHeight; + return current > (max - asobi); + }, + + scrollToBottom() { + if (this.isNaked) { + window.scroll(0, document.body.offsetHeight); + } else { + this.$el.scrollTop = this.$el.scrollHeight; + } + }, + + notify(message) { + const n = document.createElement('p') as any; + n.innerHTML = '%fa:arrow-circle-down%' + message; + n.onclick = () => { + this.scrollToBottom(); + n.parentNode.removeChild(n); + }; + (this.$refs.notifications as any).appendChild(n); + + setTimeout(() => { + n.style.opacity = 0; + setTimeout(() => n.parentNode.removeChild(n), 1000); + }, 4000); + }, + + onVisibilitychange() { + if (document.hidden) return; + this.messages.forEach(message => { + if (message.userId !== (this as any).os.i.id && !message.isRead) { + this.connection.send({ + type: 'read', + id: message.id + }); + } + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-messaging-room + display flex + flex 1 + flex-direction column + height 100% + + > .stream + width 100% + max-width 600px + margin 0 auto + flex 1 + + > .init + width 100% + margin 0 + padding 16px 8px 8px 8px + text-align center + font-size 0.8em + color rgba(0, 0, 0, 0.4) + + [data-fa] + margin-right 4px + + > .empty + width 100% + margin 0 + padding 16px 8px 8px 8px + text-align center + font-size 0.8em + color rgba(0, 0, 0, 0.4) + + [data-fa] + margin-right 4px + + > .no-history + display block + margin 0 + padding 16px + text-align center + font-size 0.8em + color rgba(0, 0, 0, 0.4) + + [data-fa] + margin-right 4px + + > .more + display block + margin 16px auto + padding 0 12px + line-height 24px + color #fff + background rgba(0, 0, 0, 0.3) + border-radius 12px + + &:hover + background rgba(0, 0, 0, 0.4) + + &:active + background rgba(0, 0, 0, 0.5) + + &.fetching + cursor wait + + > [data-fa] + margin-right 4px + + > .message + // something + + > .date + display block + margin 8px 0 + text-align center + + &:before + content '' + display block + position absolute + height 1px + width 90% + top 16px + left 0 + right 0 + margin 0 auto + background rgba(0, 0, 0, 0.1) + + > span + display inline-block + margin 0 + padding 0 16px + //font-weight bold + line-height 32px + color rgba(0, 0, 0, 0.3) + background #fff + + > footer + position -webkit-sticky + position sticky + z-index 2 + bottom 0 + width 100% + max-width 600px + margin 0 auto + padding 0 + background rgba(255, 255, 255, 0.95) + background-clip content-box + + > .notifications + position absolute + top -48px + width 100% + padding 8px 0 + text-align center + + &:empty + display none + + > p + display inline-block + margin 0 + padding 0 12px 0 28px + cursor pointer + line-height 32px + font-size 12px + color $theme-color-foreground + background $theme-color + border-radius 16px + transition opacity 1s ease + + > [data-fa] + position absolute + top 0 + left 10px + line-height 32px + font-size 16px + +</style> diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue new file mode 100644 index 0000000000..e6c32f80d8 --- /dev/null +++ b/src/client/app/common/views/components/messaging.vue @@ -0,0 +1,463 @@ +<template> +<div class="mk-messaging" :data-compact="compact"> + <div class="search" v-if="!compact" :style="{ top: headerTop + 'px' }"> + <div class="form"> + <label for="search-input">%fa:search%</label> + <input v-model="q" type="search" @input="search" @keydown="onSearchKeydown" placeholder="%i18n:common.tags.mk-messaging.search-user%"/> + </div> + <div class="result"> + <ol class="users" v-if="result.length > 0" ref="searchResult"> + <li v-for="(user, i) in result" + @keydown.enter="navigate(user)" + @keydown="onSearchResultKeydown(i)" + @click="navigate(user)" + tabindex="-1" + > + <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/> + <span class="name">{{ user | userName }}</span> + <span class="username">@{{ user | acct }}</span> + </li> + </ol> + </div> + </div> + <div class="history" v-if="messages.length > 0"> + <template> + <a v-for="message in messages" + class="user" + :href="`/i/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`" + :data-is-me="isMe(message)" + :data-is-read="message.isRead" + @click.prevent="navigate(isMe(message) ? message.recipient : message.user)" + :key="message.id" + > + <div> + <img class="avatar" :src="`${isMe(message) ? message.recipient.avatarUrl : message.user.avatarUrl}?thumbnail&size=64`" alt=""/> + <header> + <span class="name">{{ isMe(message) ? message.recipient : message.user | userName }}</span> + <span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span> + <mk-time :time="message.createdAt"/> + </header> + <div class="body"> + <p class="text"><span class="me" v-if="isMe(message)">%i18n:common.tags.mk-messaging.you%:</span>{{ message.text }}</p> + </div> + </div> + </a> + </template> + </div> + <p class="no-history" v-if="!fetching && messages.length == 0">%i18n:common.tags.mk-messaging.no-history%</p> + <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import getAcct from '../../../../../acct/render'; + +export default Vue.extend({ + props: { + compact: { + type: Boolean, + default: false + }, + headerTop: { + type: Number, + default: 0 + } + }, + data() { + return { + fetching: true, + moreFetching: false, + messages: [], + q: null, + result: [], + connection: null, + connectionId: null + }; + }, + mounted() { + this.connection = (this as any).os.streams.messagingIndexStream.getConnection(); + this.connectionId = (this as any).os.streams.messagingIndexStream.use(); + + this.connection.on('message', this.onMessage); + this.connection.on('read', this.onRead); + + (this as any).api('messaging/history').then(messages => { + this.messages = messages; + this.fetching = false; + }); + }, + beforeDestroy() { + this.connection.off('message', this.onMessage); + this.connection.off('read', this.onRead); + (this as any).os.streams.messagingIndexStream.dispose(this.connectionId); + }, + methods: { + getAcct, + isMe(message) { + return message.userId == (this as any).os.i.id; + }, + onMessage(message) { + this.messages = this.messages.filter(m => !( + (m.recipientId == message.recipientId && m.userId == message.userId) || + (m.recipientId == message.userId && m.userId == message.recipientId))); + + this.messages.unshift(message); + }, + onRead(ids) { + ids.forEach(id => { + const found = this.messages.find(m => m.id == id); + if (found) found.isRead = true; + }); + }, + search() { + if (this.q == '') { + this.result = []; + return; + } + (this as any).api('users/search', { + query: this.q, + max: 5 + }).then(users => { + this.result = users; + }); + }, + navigate(user) { + this.$emit('navigate', user); + }, + onSearchKeydown(e) { + switch (e.which) { + case 9: // [TAB] + case 40: // [↓] + e.preventDefault(); + e.stopPropagation(); + (this.$refs.searchResult as any).childNodes[0].focus(); + break; + } + }, + onSearchResultKeydown(i, e) { + const list = this.$refs.searchResult as any; + + const cancel = () => { + e.preventDefault(); + e.stopPropagation(); + }; + + switch (true) { + case e.which == 27: // [ESC] + cancel(); + (this.$refs.search as any).focus(); + break; + + case e.which == 9 && e.shiftKey: // [TAB] + [Shift] + case e.which == 38: // [↑] + cancel(); + (list.childNodes[i].previousElementSibling || list.childNodes[this.result.length - 1]).focus(); + break; + + case e.which == 9: // [TAB] + case e.which == 40: // [↓] + cancel(); + (list.childNodes[i].nextElementSibling || list.childNodes[0]).focus(); + break; + } + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-messaging + + &[data-compact] + font-size 0.8em + + > .history + > a + &:last-child + border-bottom none + + &:not([data-is-me]):not([data-is-read]) + > div + background-image none + border-left solid 4px #3aa2dc + + > div + padding 16px + + > header + > .mk-time + font-size 1em + + > .avatar + width 42px + height 42px + margin 0 12px 0 0 + + > .search + display block + position -webkit-sticky + position sticky + top 0 + left 0 + z-index 1 + width 100% + background #fff + box-shadow 0 0px 2px rgba(0, 0, 0, 0.2) + + > .form + padding 8px + background #f7f7f7 + + > label + display block + position absolute + top 0 + left 8px + z-index 1 + height 100% + width 38px + pointer-events none + + > [data-fa] + display block + position absolute + top 0 + right 0 + bottom 0 + left 0 + width 1em + line-height 56px + margin auto + color #555 + + > input + margin 0 + padding 0 0 0 32px + width 100% + font-size 1em + line-height 38px + color #000 + outline none + border solid 1px #eee + border-radius 5px + box-shadow none + transition color 0.5s ease, border 0.5s ease + + &:hover + border solid 1px #ddd + transition border 0.2s ease + + &:focus + color darken($theme-color, 20%) + border solid 1px $theme-color + transition color 0, border 0 + + > .result + display block + top 0 + left 0 + z-index 2 + width 100% + margin 0 + padding 0 + background #fff + + > .users + margin 0 + padding 0 + list-style none + + > li + display inline-block + z-index 1 + width 100% + padding 8px 32px + vertical-align top + white-space nowrap + overflow hidden + color rgba(0, 0, 0, 0.8) + text-decoration none + transition none + cursor pointer + + &:hover + &:focus + color #fff + background $theme-color + + .name + color #fff + + .username + color #fff + + &:active + color #fff + background darken($theme-color, 10%) + + .name + color #fff + + .username + color #fff + + .avatar + vertical-align middle + min-width 32px + min-height 32px + max-width 32px + max-height 32px + margin 0 8px 0 0 + border-radius 6px + + .name + margin 0 8px 0 0 + /*font-weight bold*/ + font-weight normal + color rgba(0, 0, 0, 0.8) + + .username + font-weight normal + color rgba(0, 0, 0, 0.3) + + > .history + + > a + display block + text-decoration none + background #fff + border-bottom solid 1px #eee + + * + pointer-events none + user-select none + + &:hover + background #fafafa + + > .avatar + filter saturate(200%) + + &:active + background #eee + + &[data-is-read] + &[data-is-me] + opacity 0.8 + + &:not([data-is-me]):not([data-is-read]) + > div + background-image url("/assets/unread.svg") + background-repeat no-repeat + background-position 0 center + + &:after + content "" + display block + clear both + + > div + max-width 500px + margin 0 auto + padding 20px 30px + + &:after + content "" + display block + clear both + + > header + display flex + align-items center + margin-bottom 2px + white-space nowrap + overflow hidden + + > .name + margin 0 + padding 0 + overflow hidden + text-overflow ellipsis + font-size 1em + color rgba(0, 0, 0, 0.9) + font-weight bold + transition all 0.1s ease + + > .username + margin 0 8px + color rgba(0, 0, 0, 0.5) + + > .mk-time + margin 0 0 0 auto + color rgba(0, 0, 0, 0.5) + font-size 80% + + > .avatar + float left + width 54px + height 54px + margin 0 16px 0 0 + border-radius 8px + transition all 0.1s ease + + > .body + + > .text + display block + margin 0 0 0 0 + padding 0 + overflow hidden + overflow-wrap break-word + font-size 1.1em + color rgba(0, 0, 0, 0.8) + + .me + color rgba(0, 0, 0, 0.4) + + > .image + display block + max-width 100% + max-height 512px + + > .no-history + margin 0 + padding 2em 1em + text-align center + color #999 + font-weight 500 + + > .fetching + margin 0 + padding 16px + text-align center + color #aaa + + > [data-fa] + margin-right 4px + + // TODO: element base media query + @media (max-width 400px) + > .search + > .result + > .users + > li + padding 8px 16px + + > .history + > a + &:not([data-is-me]):not([data-is-read]) + > div + background-image none + border-left solid 4px #3aa2dc + + > div + padding 16px + font-size 14px + + > .avatar + margin 0 12px 0 0 + +</style> diff --git a/src/client/app/common/views/components/nav.vue b/src/client/app/common/views/components/nav.vue new file mode 100644 index 0000000000..8ce75d3529 --- /dev/null +++ b/src/client/app/common/views/components/nav.vue @@ -0,0 +1,41 @@ +<template> +<span class="mk-nav"> + <a :href="aboutUrl">%i18n:common.tags.mk-nav-links.about%</a> + <i>・</i> + <a :href="statsUrl">%i18n:common.tags.mk-nav-links.stats%</a> + <i>・</i> + <a :href="statusUrl">%i18n:common.tags.mk-nav-links.status%</a> + <i>・</i> + <a href="http://zawazawa.jp/misskey/">%i18n:common.tags.mk-nav-links.wiki%</a> + <i>・</i> + <a href="https://github.com/syuilo/misskey/blob/master/DONORS.md">%i18n:common.tags.mk-nav-links.donors%</a> + <i>・</i> + <a href="https://github.com/syuilo/misskey">%i18n:common.tags.mk-nav-links.repository%</a> + <i>・</i> + <a :href="devUrl">%i18n:common.tags.mk-nav-links.develop%</a> + <i>・</i> + <a href="https://twitter.com/misskey_xyz" target="_blank">Follow us on %fa:B twitter%</a> +</span> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { docsUrl, statsUrl, statusUrl, devUrl, lang } from '../../../config'; + +export default Vue.extend({ + data() { + return { + aboutUrl: `${docsUrl}/${lang}/about`, + statsUrl, + statusUrl, + devUrl + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-nav + a + color inherit +</style> diff --git a/src/client/app/common/views/components/note-html.ts b/src/client/app/common/views/components/note-html.ts new file mode 100644 index 0000000000..24e750a671 --- /dev/null +++ b/src/client/app/common/views/components/note-html.ts @@ -0,0 +1,157 @@ +import Vue from 'vue'; +import * as emojilib from 'emojilib'; +import parse from '../../../../../text/parse'; +import getAcct from '../../../../../acct/render'; +import { url } from '../../../config'; +import MkUrl from './url.vue'; + +const flatten = list => list.reduce( + (a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), [] +); + +export default Vue.component('mk-note-html', { + props: { + text: { + type: String, + required: true + }, + ast: { + type: [], + required: false + }, + shouldBreak: { + type: Boolean, + default: true + }, + i: { + type: Object, + default: null + } + }, + + render(createElement) { + let ast; + + if (this.ast == null) { + // Parse text to ast + ast = parse(this.text); + } else { + ast = this.ast; + } + + // Parse ast to DOM + const els = flatten(ast.map(token => { + switch (token.type) { + case 'text': + const text = token.content.replace(/(\r\n|\n|\r)/g, '\n'); + + if (this.shouldBreak) { + const x = text.split('\n') + .map(t => t == '' ? [createElement('br')] : [createElement('span', t), createElement('br')]); + x[x.length - 1].pop(); + return x; + } else { + return createElement('span', text.replace(/\n/g, ' ')); + } + + case 'bold': + return createElement('strong', token.bold); + + case 'url': + return createElement(MkUrl, { + props: { + url: token.content, + target: '_blank' + } + }); + + case 'link': + return createElement('a', { + attrs: { + class: 'link', + href: token.url, + target: '_blank', + title: token.url + } + }, token.title); + + case 'mention': + return (createElement as any)('a', { + attrs: { + href: `${url}/@${getAcct(token)}`, + target: '_blank', + dataIsMe: (this as any).i && getAcct((this as any).i) == getAcct(token) + }, + directives: [{ + name: 'user-preview', + value: token.content + }] + }, token.content); + + case 'hashtag': + return createElement('a', { + attrs: { + href: `${url}/search?q=${token.content}`, + target: '_blank' + } + }, token.content); + + case 'code': + return createElement('pre', [ + createElement('code', { + domProps: { + innerHTML: token.html + } + }) + ]); + + case 'inline-code': + return createElement('code', { + domProps: { + innerHTML: token.html + } + }); + + case 'quote': + const text2 = token.quote.replace(/(\r\n|\n|\r)/g, '\n'); + + if (this.shouldBreak) { + const x = text2.split('\n') + .map(t => [createElement('span', t), createElement('br')]); + x[x.length - 1].pop(); + return createElement('div', { + attrs: { + class: 'quote' + } + }, x); + } else { + return createElement('span', { + attrs: { + class: 'quote' + } + }, text2.replace(/\n/g, ' ')); + } + + case 'emoji': + const emoji = emojilib.lib[token.emoji]; + return createElement('span', emoji ? emoji.char : token.content); + + default: + console.log('unknown ast type:', token.type); + } + })); + + const _els = []; + els.forEach((el, i) => { + if (el.tag == 'br') { + if (els[i - 1].tag != 'div') { + _els.push(el); + } + } else { + _els.push(el); + } + }); + + return createElement('span', _els); + } +}); diff --git a/src/client/app/common/views/components/note-menu.vue b/src/client/app/common/views/components/note-menu.vue new file mode 100644 index 0000000000..d053748728 --- /dev/null +++ b/src/client/app/common/views/components/note-menu.vue @@ -0,0 +1,141 @@ +<template> +<div class="mk-note-menu"> + <div class="backdrop" ref="backdrop" @click="close"></div> + <div class="popover" :class="{ compact }" ref="popover"> + <button v-if="note.userId == os.i.id" @click="pin">%i18n:common.tags.mk-note-menu.pin%</button> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as anime from 'animejs'; + +export default Vue.extend({ + props: ['note', 'source', 'compact'], + mounted() { + this.$nextTick(() => { + const popover = this.$refs.popover as any; + + const rect = this.source.getBoundingClientRect(); + const width = popover.offsetWidth; + const height = popover.offsetHeight; + + if (this.compact) { + const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); + const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2); + popover.style.left = (x - (width / 2)) + 'px'; + popover.style.top = (y - (height / 2)) + 'px'; + } else { + const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); + const y = rect.top + window.pageYOffset + this.source.offsetHeight; + popover.style.left = (x - (width / 2)) + 'px'; + popover.style.top = y + 'px'; + } + + anime({ + targets: this.$refs.backdrop, + opacity: 1, + duration: 100, + easing: 'linear' + }); + + anime({ + targets: this.$refs.popover, + opacity: 1, + scale: [0.5, 1], + duration: 500 + }); + }); + }, + methods: { + pin() { + (this as any).api('i/pin', { + noteId: this.note.id + }).then(() => { + this.$destroy(); + }); + }, + + close() { + (this.$refs.backdrop as any).style.pointerEvents = 'none'; + anime({ + targets: this.$refs.backdrop, + opacity: 0, + duration: 200, + easing: 'linear' + }); + + (this.$refs.popover as any).style.pointerEvents = 'none'; + anime({ + targets: this.$refs.popover, + opacity: 0, + scale: 0.5, + duration: 200, + easing: 'easeInBack', + complete: () => this.$destroy() + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +$border-color = rgba(27, 31, 35, 0.15) + +.mk-note-menu + position initial + + > .backdrop + position fixed + top 0 + left 0 + z-index 10000 + width 100% + height 100% + background rgba(0, 0, 0, 0.1) + opacity 0 + + > .popover + position absolute + z-index 10001 + background #fff + border 1px solid $border-color + border-radius 4px + box-shadow 0 3px 12px rgba(27, 31, 35, 0.15) + transform scale(0.5) + opacity 0 + + $balloon-size = 16px + + &:not(.compact) + margin-top $balloon-size + transform-origin center -($balloon-size) + + &:before + content "" + display block + position absolute + top -($balloon-size * 2) + left s('calc(50% - %s)', $balloon-size) + border-top solid $balloon-size transparent + border-left solid $balloon-size transparent + border-right solid $balloon-size transparent + border-bottom solid $balloon-size $border-color + + &:after + content "" + display block + position absolute + top -($balloon-size * 2) + 1.5px + left s('calc(50% - %s)', $balloon-size) + border-top solid $balloon-size transparent + border-left solid $balloon-size transparent + border-right solid $balloon-size transparent + border-bottom solid $balloon-size #fff + + > button + display block + padding 16px + +</style> diff --git a/src/client/app/common/views/components/othello.game.vue b/src/client/app/common/views/components/othello.game.vue new file mode 100644 index 0000000000..b9d946de96 --- /dev/null +++ b/src/client/app/common/views/components/othello.game.vue @@ -0,0 +1,324 @@ +<template> +<div class="root"> + <header><b>{{ blackUser.name }}</b>(黒) vs <b>{{ whiteUser.name }}</b>(白)</header> + + <div style="overflow: hidden"> + <p class="turn" v-if="!iAmPlayer && !game.isEnded">{{ turnUser.name }}のターンです<mk-ellipsis/></p> + <p class="turn" v-if="logPos != logs.length">{{ turnUser.name }}のターン</p> + <p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">相手のターンです<mk-ellipsis/></p> + <p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">あなたのターンです</p> + <p class="result" v-if="game.isEnded && logPos == logs.length"> + <template v-if="game.winner"><b>{{ game.winner.name }}</b>の勝ち{{ game.settings.isLlotheo ? ' (ロセオ)' : '' }}</template> + <template v-else>引き分け</template> + </p> + </div> + + <div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }"> + <div v-for="(stone, i) in o.board" + :class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.isEnded, myTurn: !game.isEnded && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id, i) : null, prev: o.prevPos == i }" + @click="set(i)" + :title="'[' + (o.transformPosToXy(i)[0] + 1) + ', ' + (o.transformPosToXy(i)[1] + 1) + '] (' + i + ')'" + > + <img v-if="stone === true" :src="`${blackUser.avatarUrl}?thumbnail&size=128`" alt=""> + <img v-if="stone === false" :src="`${whiteUser.avatarUrl}?thumbnail&size=128`" alt=""> + </div> + </div> + + <p class="status"><b>{{ logPos }}ターン目</b> 黒:{{ o.blackCount }} 白:{{ o.whiteCount }} 合計:{{ o.blackCount + o.whiteCount }}</p> + + <div class="player" v-if="game.isEnded"> + <el-button-group> + <el-button type="primary" @click="logPos = 0" :disabled="logPos == 0">%fa:angle-double-left%</el-button> + <el-button type="primary" @click="logPos--" :disabled="logPos == 0">%fa:angle-left%</el-button> + </el-button-group> + <span>{{ logPos }} / {{ logs.length }}</span> + <el-button-group> + <el-button type="primary" @click="logPos++" :disabled="logPos == logs.length">%fa:angle-right%</el-button> + <el-button type="primary" @click="logPos = logs.length" :disabled="logPos == logs.length">%fa:angle-double-right%</el-button> + </el-button-group> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as CRC32 from 'crc-32'; +import Othello, { Color } from '../../../../../othello/core'; +import { url } from '../../../config'; + +export default Vue.extend({ + props: ['initGame', 'connection'], + + data() { + return { + game: null, + o: null as Othello, + logs: [], + logPos: 0, + pollingClock: null + }; + }, + + computed: { + iAmPlayer(): boolean { + if (!(this as any).os.isSignedIn) return false; + return this.game.user1Id == (this as any).os.i.id || this.game.user2Id == (this as any).os.i.id; + }, + myColor(): Color { + if (!this.iAmPlayer) return null; + if (this.game.user1Id == (this as any).os.i.id && this.game.black == 1) return true; + if (this.game.user2Id == (this as any).os.i.id && this.game.black == 2) return true; + return false; + }, + opColor(): Color { + if (!this.iAmPlayer) return null; + return this.myColor === true ? false : true; + }, + blackUser(): any { + return this.game.black == 1 ? this.game.user1 : this.game.user2; + }, + whiteUser(): any { + return this.game.black == 1 ? this.game.user2 : this.game.user1; + }, + turnUser(): any { + if (this.o.turn === true) { + return this.game.black == 1 ? this.game.user1 : this.game.user2; + } else if (this.o.turn === false) { + return this.game.black == 1 ? this.game.user2 : this.game.user1; + } else { + return null; + } + }, + isMyTurn(): boolean { + if (this.turnUser == null) return null; + return this.turnUser.id == (this as any).os.i.id; + } + }, + + watch: { + logPos(v) { + if (!this.game.isEnded) return; + this.o = new Othello(this.game.settings.map, { + isLlotheo: this.game.settings.isLlotheo, + canPutEverywhere: this.game.settings.canPutEverywhere, + loopedBoard: this.game.settings.loopedBoard + }); + this.logs.forEach((log, i) => { + if (i < v) { + this.o.put(log.color, log.pos); + } + }); + this.$forceUpdate(); + } + }, + + created() { + this.game = this.initGame; + + this.o = new Othello(this.game.settings.map, { + isLlotheo: this.game.settings.isLlotheo, + canPutEverywhere: this.game.settings.canPutEverywhere, + loopedBoard: this.game.settings.loopedBoard + }); + + this.game.logs.forEach(log => { + this.o.put(log.color, log.pos); + }); + + this.logs = this.game.logs; + this.logPos = this.logs.length; + + // 通信を取りこぼしてもいいように定期的にポーリングさせる + if (this.game.isStarted && !this.game.isEnded) { + this.pollingClock = setInterval(() => { + const crc32 = CRC32.str(this.logs.map(x => x.pos.toString()).join('')); + this.connection.send({ + type: 'check', + crc32 + }); + }, 3000); + } + }, + + mounted() { + this.connection.on('set', this.onSet); + this.connection.on('rescue', this.onRescue); + }, + + beforeDestroy() { + this.connection.off('set', this.onSet); + this.connection.off('rescue', this.onRescue); + + clearInterval(this.pollingClock); + }, + + methods: { + set(pos) { + if (this.game.isEnded) return; + if (!this.iAmPlayer) return; + if (!this.isMyTurn) return; + if (!this.o.canPut(this.myColor, pos)) return; + + this.o.put(this.myColor, pos); + + // サウンドを再生する + if ((this as any).os.isEnableSounds) { + const sound = new Audio(`${url}/assets/othello-put-me.mp3`); + sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1; + sound.play(); + } + + this.connection.send({ + type: 'set', + pos + }); + + this.checkEnd(); + + this.$forceUpdate(); + }, + + onSet(x) { + this.logs.push(x); + this.logPos++; + this.o.put(x.color, x.pos); + this.checkEnd(); + this.$forceUpdate(); + + // サウンドを再生する + if ((this as any).os.isEnableSounds && x.color != this.myColor) { + const sound = new Audio(`${url}/assets/othello-put-you.mp3`); + sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1; + sound.play(); + } + }, + + checkEnd() { + this.game.isEnded = this.o.isEnded; + if (this.game.isEnded) { + if (this.o.winner === true) { + this.game.winnerId = this.game.black == 1 ? this.game.user1Id : this.game.user2Id; + this.game.winner = this.game.black == 1 ? this.game.user1 : this.game.user2; + } else if (this.o.winner === false) { + this.game.winnerId = this.game.black == 1 ? this.game.user2Id : this.game.user1Id; + this.game.winner = this.game.black == 1 ? this.game.user2 : this.game.user1; + } else { + this.game.winnerId = null; + this.game.winner = null; + } + } + }, + + // 正しいゲーム情報が送られてきたとき + onRescue(game) { + this.game = game; + + this.o = new Othello(this.game.settings.map, { + isLlotheo: this.game.settings.isLlotheo, + canPutEverywhere: this.game.settings.canPutEverywhere, + loopedBoard: this.game.settings.loopedBoard + }); + + this.game.logs.forEach(log => { + this.o.put(log.color, log.pos, true); + }); + + this.logs = this.game.logs; + this.logPos = this.logs.length; + + this.checkEnd(); + this.$forceUpdate(); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.root + text-align center + + > header + padding 8px + border-bottom dashed 1px #c4cdd4 + + > .board + display grid + grid-gap 4px + width 350px + height 350px + margin 0 auto + + > div + background transparent + border-radius 6px + overflow hidden + + * + pointer-events none + user-select none + + &.empty + border solid 2px #eee + + &.empty.can + background #eee + + &.empty.myTurn + border-color #ddd + + &.can + background #eee + cursor pointer + + &:hover + border-color darken($theme-color, 10%) + background $theme-color + + &:active + background darken($theme-color, 10%) + + &.prev + box-shadow 0 0 0 4px rgba($theme-color, 0.7) + + &.isEnded + border-color #ddd + + &.none + border-color transparent !important + + > img + display block + width 100% + height 100% + + > .graph + display grid + grid-template-columns repeat(61, 1fr) + width 300px + height 38px + margin 0 auto 16px auto + + > div + &:not(:empty) + background #ccc + + > div:first-child + background #333 + + > div:last-child + background #ccc + + > .status + margin 0 + padding 16px 0 + + > .player + padding-bottom 32px + + > span + display inline-block + margin 0 8px + min-width 70px +</style> diff --git a/src/client/app/common/views/components/othello.gameroom.vue b/src/client/app/common/views/components/othello.gameroom.vue new file mode 100644 index 0000000000..dba9ccd16d --- /dev/null +++ b/src/client/app/common/views/components/othello.gameroom.vue @@ -0,0 +1,42 @@ +<template> +<div> + <x-room v-if="!g.isStarted" :game="g" :connection="connection"/> + <x-game v-else :init-game="g" :connection="connection"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XGame from './othello.game.vue'; +import XRoom from './othello.room.vue'; +import { OthelloGameStream } from '../../scripts/streaming/othello-game'; + +export default Vue.extend({ + components: { + XGame, + XRoom + }, + props: ['game'], + data() { + return { + connection: null, + g: null + }; + }, + created() { + this.g = this.game; + this.connection = new OthelloGameStream((this as any).os, (this as any).os.i, this.game); + this.connection.on('started', this.onStarted); + }, + beforeDestroy() { + this.connection.off('started', this.onStarted); + this.connection.close(); + }, + methods: { + onStarted(game) { + Object.assign(this.g, game); + this.$forceUpdate(); + } + } +}); +</script> diff --git a/src/client/app/common/views/components/othello.room.vue b/src/client/app/common/views/components/othello.room.vue new file mode 100644 index 0000000000..86368b3cc3 --- /dev/null +++ b/src/client/app/common/views/components/othello.room.vue @@ -0,0 +1,297 @@ +<template> +<div class="root"> + <header><b>{{ game.user1.name }}</b> vs <b>{{ game.user2.name }}</b></header> + + <div> + <p>ゲームの設定</p> + + <el-card class="map"> + <div slot="header"> + <el-select :class="$style.mapSelect" v-model="mapName" placeholder="マップを選択" @change="onMapChange"> + <el-option label="ランダム" :value="null"/> + <el-option-group v-for="c in mapCategories" :key="c" :label="c"> + <el-option v-for="m in maps" v-if="m.category == c" :key="m.name" :label="m.name" :value="m.name"> + <span style="float: left">{{ m.name }}</span> + <span style="float: right; color: #8492a6; font-size: 13px" v-if="m.author">(by <i>{{ m.author }}</i>)</span> + </el-option> + </el-option-group> + </el-select> + </div> + <div :class="$style.board" v-if="game.settings.map != null" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }"> + <div v-for="(x, i) in game.settings.map.join('')" + :data-none="x == ' '" + @click="onPixelClick(i, x)" + > + <template v-if="x == 'b'">%fa:circle%</template> + <template v-if="x == 'w'">%fa:circle R%</template> + </div> + </div> + </el-card> + + <el-card class="bw"> + <div slot="header"> + <span>先手/後手</span> + </div> + <el-radio v-model="game.settings.bw" label="random" @change="updateSettings">ランダム</el-radio> + <el-radio v-model="game.settings.bw" :label="1" @change="updateSettings">{{ game.user1.name }}が黒</el-radio> + <el-radio v-model="game.settings.bw" :label="2" @change="updateSettings">{{ game.user2.name }}が黒</el-radio> + </el-card> + + <el-card class="rules"> + <div slot="header"> + <span>ルール</span> + </div> + <mk-switch v-model="game.settings.isLlotheo" @change="updateSettings" text="石の少ない方が勝ち(ロセオ)"/> + <mk-switch v-model="game.settings.loopedBoard" @change="updateSettings" text="ループマップ"/> + <mk-switch v-model="game.settings.canPutEverywhere" @change="updateSettings" text="どこでも置けるモード"/> + </el-card> + + <el-card class="bot-form" v-if="form"> + <div slot="header"> + <span>Botの設定</span> + </div> + <el-alert v-for="message in messages" + :title="message.text" + :type="message.type" + :key="message.id" + /> + <template v-for="item in form"> + <mk-switch v-if="item.type == 'button'" v-model="item.value" :key="item.id" :text="item.label" @change="onChangeForm($event, item)">{{ item.desc || '' }}</mk-switch> + + <el-card v-if="item.type == 'radio'" :key="item.id"> + <div slot="header"> + <span>{{ item.label }}</span> + </div> + <el-radio v-for="(r, i) in item.items" :key="item.id + ':' + i" v-model="item.value" :label="r.value" @change="onChangeForm($event, item)">{{ r.label }}</el-radio> + </el-card> + + <el-card v-if="item.type == 'textbox'" :key="item.id"> + <div slot="header"> + <span>{{ item.label }}</span> + </div> + <el-input v-model="item.value" @change="onChangeForm($event, item)"/> + </el-card> + </template> + </el-card> + </div> + + <footer> + <p class="status"> + <template v-if="isAccepted && isOpAccepted">ゲームは数秒後に開始されます<mk-ellipsis/></template> + <template v-if="isAccepted && !isOpAccepted">相手の準備が完了するのを待っています<mk-ellipsis/></template> + <template v-if="!isAccepted && isOpAccepted">あなたの準備が完了するのを待っています</template> + <template v-if="!isAccepted && !isOpAccepted">準備中<mk-ellipsis/></template> + </p> + + <div class="actions"> + <el-button @click="exit">キャンセル</el-button> + <el-button type="primary" @click="accept" v-if="!isAccepted">準備完了</el-button> + <el-button type="primary" @click="cancel" v-if="isAccepted">準備続行</el-button> + </div> + </footer> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as maps from '../../../../../othello/maps'; + +export default Vue.extend({ + props: ['game', 'connection'], + + data() { + return { + o: null, + isLlotheo: false, + mapName: maps.eighteight.name, + maps: maps, + form: null, + messages: [] + }; + }, + + computed: { + mapCategories(): string[] { + const categories = Object.entries(maps).map(x => x[1].category); + return categories.filter((item, pos) => categories.indexOf(item) == pos); + }, + isAccepted(): boolean { + if (this.game.user1Id == (this as any).os.i.id && this.game.user1Accepted) return true; + if (this.game.user2Id == (this as any).os.i.id && this.game.user2Accepted) return true; + return false; + }, + isOpAccepted(): boolean { + if (this.game.user1Id != (this as any).os.i.id && this.game.user1Accepted) return true; + if (this.game.user2Id != (this as any).os.i.id && this.game.user2Accepted) return true; + return false; + } + }, + + created() { + this.connection.on('change-accepts', this.onChangeAccepts); + this.connection.on('update-settings', this.onUpdateSettings); + this.connection.on('init-form', this.onInitForm); + this.connection.on('message', this.onMessage); + + if (this.game.user1Id != (this as any).os.i.id && this.game.settings.form1) this.form = this.game.settings.form1; + if (this.game.user2Id != (this as any).os.i.id && this.game.settings.form2) this.form = this.game.settings.form2; + }, + + beforeDestroy() { + this.connection.off('change-accepts', this.onChangeAccepts); + this.connection.off('update-settings', this.onUpdateSettings); + this.connection.off('init-form', this.onInitForm); + this.connection.off('message', this.onMessage); + }, + + methods: { + exit() { + + }, + + accept() { + this.connection.send({ + type: 'accept' + }); + }, + + cancel() { + this.connection.send({ + type: 'cancel-accept' + }); + }, + + onChangeAccepts(accepts) { + this.game.user1Accepted = accepts.user1; + this.game.user2Accepted = accepts.user2; + this.$forceUpdate(); + }, + + updateSettings() { + this.connection.send({ + type: 'update-settings', + settings: this.game.settings + }); + }, + + onUpdateSettings(settings) { + this.game.settings = settings; + if (this.game.settings.map == null) { + this.mapName = null; + } else { + const foundMap = Object.entries(maps).find(x => x[1].data.join('') == this.game.settings.map.join('')); + this.mapName = foundMap ? foundMap[1].name : '-Custom-'; + } + }, + + onInitForm(x) { + if (x.userId == (this as any).os.i.id) return; + this.form = x.form; + }, + + onMessage(x) { + if (x.userId == (this as any).os.i.id) return; + this.messages.unshift(x.message); + }, + + onChangeForm(v, item) { + this.connection.send({ + type: 'update-form', + id: item.id, + value: v + }); + }, + + onMapChange(v) { + if (v == null) { + this.game.settings.map = null; + } else { + this.game.settings.map = Object.entries(maps).find(x => x[1].name == v)[1].data; + } + this.$forceUpdate(); + this.updateSettings(); + }, + + onPixelClick(pos, pixel) { + const x = pos % this.game.settings.map[0].length; + const y = Math.floor(pos / this.game.settings.map[0].length); + const newPixel = + pixel == ' ' ? '-' : + pixel == '-' ? 'b' : + pixel == 'b' ? 'w' : + ' '; + const line = this.game.settings.map[y].split(''); + line[x] = newPixel; + this.$set(this.game.settings.map, y, line.join('')); + this.$forceUpdate(); + this.updateSettings(); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.root + text-align center + background #f9f9f9 + + > header + padding 8px + border-bottom dashed 1px #c4cdd4 + + > div + padding 0 16px + + > .map + > .bw + > .rules + > .bot-form + max-width 400px + margin 0 auto 16px auto + + > footer + position sticky + bottom 0 + padding 16px + background rgba(255, 255, 255, 0.9) + border-top solid 1px #c4cdd4 + + > .status + margin 0 0 16px 0 +</style> + +<style lang="stylus" module> +.mapSelect + width 100% + +.board + display grid + grid-gap 4px + width 300px + height 300px + margin 0 auto + + > div + background transparent + border solid 2px #ddd + border-radius 6px + overflow hidden + cursor pointer + + * + pointer-events none + user-select none + width 100% + height 100% + + &[data-none] + border-color transparent + +</style> + +<style lang="stylus"> +.el-alert__content + position initial !important +</style> diff --git a/src/client/app/common/views/components/othello.vue b/src/client/app/common/views/components/othello.vue new file mode 100644 index 0000000000..8f7d9dfd6a --- /dev/null +++ b/src/client/app/common/views/components/othello.vue @@ -0,0 +1,311 @@ +<template> +<div class="mk-othello"> + <div v-if="game"> + <x-gameroom :game="game"/> + </div> + <div class="matching" v-else-if="matching"> + <h1><b>{{ matching.name }}</b>を待っています<mk-ellipsis/></h1> + <div class="cancel"> + <el-button round @click="cancel">キャンセル</el-button> + </div> + </div> + <div class="index" v-else> + <h1>Misskey %fa:circle%thell%fa:circle R%</h1> + <p>他のMisskeyユーザーとオセロで対戦しよう</p> + <div class="play"> + <el-button round>フリーマッチ(準備中)</el-button> + <el-button type="primary" round @click="match">指名</el-button> + <details> + <summary>遊び方</summary> + <div> + <p>オセロは、相手と交互に石をボードに置いてゆき、相手の石を挟んでひっくり返しながら、最終的に残った石が多い方が勝ちというボードゲームです。</p> + <dl> + <dt><b>フリーマッチ</b></dt> + <dd>ランダムなユーザーと対戦するモードです。</dd> + <dt><b>指名</b></dt> + <dd>指定したユーザーと対戦するモードです。</dd> + </dl> + </div> + </details> + </div> + <section v-if="invitations.length > 0"> + <h2>対局の招待があります!:</h2> + <div class="invitation" v-for="i in invitations" tabindex="-1" @click="accept(i)"> + <img :src="`${i.parent.avatarUrl}?thumbnail&size=32`" alt=""> + <span class="name"><b>{{ i.parent.name }}</b></span> + <span class="username">@{{ i.parent.username }}</span> + <mk-time :time="i.createdAt"/> + </div> + </section> + <section v-if="myGames.length > 0"> + <h2>自分の対局</h2> + <a class="game" v-for="g in myGames" tabindex="-1" @click.prevent="go(g)" :href="`/othello/${g.id}`"> + <img :src="`${g.user1.avatarUrl}?thumbnail&size=32`" alt=""> + <img :src="`${g.user2.avatarUrl}?thumbnail&size=32`" alt=""> + <span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span> + <span class="state">{{ g.isEnded ? '終了' : '進行中' }}</span> + </a> + </section> + <section v-if="games.length > 0"> + <h2>みんなの対局</h2> + <a class="game" v-for="g in games" tabindex="-1" @click.prevent="go(g)" :href="`/othello/${g.id}`"> + <img :src="`${g.user1.avatarUrl}?thumbnail&size=32`" alt=""> + <img :src="`${g.user2.avatarUrl}?thumbnail&size=32`" alt=""> + <span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span> + <span class="state">{{ g.isEnded ? '終了' : '進行中' }}</span> + </a> + </section> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XGameroom from './othello.gameroom.vue'; + +export default Vue.extend({ + components: { + XGameroom + }, + props: ['initGame'], + data() { + return { + game: null, + games: [], + gamesFetching: true, + gamesMoreFetching: false, + myGames: [], + matching: null, + invitations: [], + connection: null, + connectionId: null, + pingClock: null + }; + }, + watch: { + game(g) { + this.$emit('gamed', g); + } + }, + created() { + if (this.initGame) { + this.game = this.initGame; + } + }, + mounted() { + this.connection = (this as any).os.streams.othelloStream.getConnection(); + this.connectionId = (this as any).os.streams.othelloStream.use(); + + this.connection.on('matched', this.onMatched); + this.connection.on('invited', this.onInvited); + + (this as any).api('othello/games', { + my: true + }).then(games => { + this.myGames = games; + }); + + (this as any).api('othello/games').then(games => { + this.games = games; + this.gamesFetching = false; + }); + + (this as any).api('othello/invitations').then(invitations => { + this.invitations = this.invitations.concat(invitations); + }); + + this.pingClock = setInterval(() => { + if (this.matching) { + this.connection.send({ + type: 'ping', + id: this.matching.id + }); + } + }, 3000); + }, + beforeDestroy() { + this.connection.off('matched', this.onMatched); + this.connection.off('invited', this.onInvited); + (this as any).os.streams.othelloStream.dispose(this.connectionId); + + clearInterval(this.pingClock); + }, + methods: { + go(game) { + (this as any).api('othello/games/show', { + gameId: game.id + }).then(game => { + this.matching = null; + this.game = game; + }); + }, + match() { + (this as any).apis.input({ + title: 'ユーザー名を入力してください' + }).then(username => { + (this as any).api('users/show', { + username + }).then(user => { + (this as any).api('othello/match', { + userId: user.id + }).then(res => { + if (res == null) { + this.matching = user; + } else { + this.game = res; + } + }); + }); + }); + }, + cancel() { + this.matching = null; + (this as any).api('othello/match/cancel'); + }, + accept(invitation) { + (this as any).api('othello/match', { + userId: invitation.parent.id + }).then(game => { + if (game) { + this.matching = null; + this.game = game; + } + }); + }, + onMatched(game) { + this.matching = null; + this.game = game; + }, + onInvited(invite) { + this.invitations.unshift(invite); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-othello + color #677f84 + background #fff + + > .matching + > h1 + margin 0 + padding 24px + font-size 20px + text-align center + font-weight normal + + > .cancel + margin 0 auto + padding 24px 0 0 0 + max-width 200px + text-align center + border-top dashed 1px #c4cdd4 + + > .index + > h1 + margin 0 + padding 24px + font-size 24px + text-align center + font-weight normal + color #fff + background linear-gradient(to bottom, #8bca3e, #d6cf31) + + & + p + margin 0 + padding 12px + margin-bottom 12px + text-align center + font-size 14px + border-bottom solid 1px #d3d9dc + + > .play + margin 0 auto + padding 0 16px + max-width 500px + text-align center + + > details + margin 8px 0 + + > div + padding 16px + font-size 14px + text-align left + background #f5f5f5 + border-radius 8px + + > section + margin 0 auto + padding 0 16px 16px 16px + max-width 500px + border-top solid 1px #d3d9dc + + > h2 + margin 0 + padding 16px 0 8px 0 + font-size 16px + font-weight bold + + .invitation + margin 8px 0 + padding 8px + border solid 1px #e1e5e8 + border-radius 6px + cursor pointer + + * + pointer-events none + user-select none + + &:focus + border-color $theme-color + + &:hover + background #f5f5f5 + + &:active + background #eee + + > img + vertical-align bottom + border-radius 100% + + > span + margin 0 8px + line-height 32px + + .game + display block + margin 8px 0 + padding 8px + color #677f84 + border solid 1px #e1e5e8 + border-radius 6px + cursor pointer + + * + pointer-events none + user-select none + + &:focus + border-color $theme-color + + &:hover + background #f5f5f5 + + &:active + background #eee + + > img + vertical-align bottom + border-radius 100% + + > span + margin 0 8px + line-height 32px +</style> diff --git a/src/client/app/common/views/components/poll-editor.vue b/src/client/app/common/views/components/poll-editor.vue new file mode 100644 index 0000000000..47d901d7b1 --- /dev/null +++ b/src/client/app/common/views/components/poll-editor.vue @@ -0,0 +1,142 @@ +<template> +<div class="mk-poll-editor"> + <p class="caution" v-if="choices.length < 2"> + %fa:exclamation-triangle%%i18n:common.tags.mk-poll-editor.no-only-one-choice% + </p> + <ul ref="choices"> + <li v-for="(choice, i) in choices"> + <input :value="choice" @input="onInput(i, $event)" :placeholder="'%i18n:common.tags.mk-poll-editor.choice-n%'.replace('{}', i + 1)"> + <button @click="remove(i)" title="%i18n:common.tags.mk-poll-editor.remove%"> + %fa:times% + </button> + </li> + </ul> + <button class="add" v-if="choices.length < 10" @click="add">%i18n:common.tags.mk-poll-editor.add%</button> + <button class="destroy" @click="destroy" title="%i18n:common.tags.mk-poll-editor.destroy%"> + %fa:times% + </button> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + data() { + return { + choices: ['', ''] + }; + }, + watch: { + choices() { + this.$emit('updated'); + } + }, + methods: { + onInput(i, e) { + Vue.set(this.choices, i, e.target.value); + }, + + add() { + this.choices.push(''); + this.$nextTick(() => { + (this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus(); + }); + }, + + remove(i) { + this.choices = this.choices.filter((_, _i) => _i != i); + }, + + destroy() { + this.$emit('destroyed'); + }, + + get() { + return { + choices: this.choices.filter(choice => choice != '') + } + }, + + set(data) { + if (data.choices.length == 0) return; + this.choices = data.choices; + if (data.choices.length == 1) this.choices = this.choices.concat(''); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-poll-editor + padding 8px + + > .caution + margin 0 0 8px 0 + font-size 0.8em + color #f00 + + > [data-fa] + margin-right 4px + + > ul + display block + margin 0 + padding 0 + list-style none + + > li + display block + margin 8px 0 + padding 0 + width 100% + + &:first-child + margin-top 0 + + &:last-child + margin-bottom 0 + + > input + padding 6px 8px + width 300px + font-size 14px + border solid 1px rgba($theme-color, 0.1) + border-radius 4px + + &:hover + border-color rgba($theme-color, 0.2) + + &:focus + border-color rgba($theme-color, 0.5) + + > button + padding 4px 8px + color rgba($theme-color, 0.4) + + &:hover + color rgba($theme-color, 0.6) + + &:active + color darken($theme-color, 30%) + + > .add + margin 8px 0 0 0 + vertical-align top + color $theme-color + + > .destroy + position absolute + top 0 + right 0 + padding 4px 8px + color rgba($theme-color, 0.4) + + &:hover + color rgba($theme-color, 0.6) + + &:active + color darken($theme-color, 30%) + +</style> diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue new file mode 100644 index 0000000000..eb29aa8837 --- /dev/null +++ b/src/client/app/common/views/components/poll.vue @@ -0,0 +1,124 @@ +<template> +<div class="mk-poll" :data-is-voted="isVoted"> + <ul> + <li v-for="choice in poll.choices" :key="choice.id" @click="vote(choice.id)" :class="{ voted: choice.voted }" :title="!isVoted ? '%i18n:common.tags.mk-poll.vote-to%'.replace('{}', choice.text) : ''"> + <div class="backdrop" :style="{ 'width': (showResult ? (choice.votes / total * 100) : 0) + '%' }"></div> + <span> + <template v-if="choice.isVoted">%fa:check%</template> + <span>{{ choice.text }}</span> + <span class="votes" v-if="showResult">({{ '%i18n:common.tags.mk-poll.vote-count%'.replace('{}', choice.votes) }})</span> + </span> + </li> + </ul> + <p v-if="total > 0"> + <span>{{ '%i18n:common.tags.mk-poll.total-users%'.replace('{}', total) }}</span> + <span>・</span> + <a v-if="!isVoted" @click="toggleShowResult">{{ showResult ? '%i18n:common.tags.mk-poll.vote%' : '%i18n:common.tags.mk-poll.show-result%' }}</a> + <span v-if="isVoted">%i18n:common.tags.mk-poll.voted%</span> + </p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['note'], + data() { + return { + showResult: false + }; + }, + computed: { + poll(): any { + return this.note.poll; + }, + total(): number { + return this.poll.choices.reduce((a, b) => a + b.votes, 0); + }, + isVoted(): boolean { + return this.poll.choices.some(c => c.isVoted); + } + }, + created() { + this.showResult = this.isVoted; + }, + methods: { + toggleShowResult() { + this.showResult = !this.showResult; + }, + vote(id) { + if (this.poll.choices.some(c => c.isVoted)) return; + (this as any).api('notes/polls/vote', { + noteId: this.note.id, + choice: id + }).then(() => { + this.poll.choices.forEach(c => { + if (c.id == id) { + c.votes++; + Vue.set(c, 'isVoted', true); + } + }); + this.showResult = true; + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-poll + + > ul + display block + margin 0 + padding 0 + list-style none + + > li + display block + margin 4px 0 + padding 4px 8px + width 100% + border solid 1px #eee + border-radius 4px + overflow hidden + cursor pointer + + &:hover + background rgba(0, 0, 0, 0.05) + + &:active + background rgba(0, 0, 0, 0.1) + + > .backdrop + position absolute + top 0 + left 0 + height 100% + background $theme-color + transition width 1s ease + + > span + > [data-fa] + margin-right 4px + + > .votes + margin-left 4px + + > p + a + color inherit + + &[data-is-voted] + > ul > li + cursor default + + &:hover + background transparent + + &:active + background transparent + +</style> diff --git a/src/client/app/common/views/components/reaction-icon.vue b/src/client/app/common/views/components/reaction-icon.vue new file mode 100644 index 0000000000..7d24f4f9e9 --- /dev/null +++ b/src/client/app/common/views/components/reaction-icon.vue @@ -0,0 +1,28 @@ +<template> +<span class="mk-reaction-icon"> + <img v-if="reaction == 'like'" src="/assets/reactions/like.png" alt="%i18n:common.reactions.like%"> + <img v-if="reaction == 'love'" src="/assets/reactions/love.png" alt="%i18n:common.reactions.love%"> + <img v-if="reaction == 'laugh'" src="/assets/reactions/laugh.png" alt="%i18n:common.reactions.laugh%"> + <img v-if="reaction == 'hmm'" src="/assets/reactions/hmm.png" alt="%i18n:common.reactions.hmm%"> + <img v-if="reaction == 'surprise'" src="/assets/reactions/surprise.png" alt="%i18n:common.reactions.surprise%"> + <img v-if="reaction == 'congrats'" src="/assets/reactions/congrats.png" alt="%i18n:common.reactions.congrats%"> + <img v-if="reaction == 'angry'" src="/assets/reactions/angry.png" alt="%i18n:common.reactions.angry%"> + <img v-if="reaction == 'confused'" src="/assets/reactions/confused.png" alt="%i18n:common.reactions.confused%"> + <img v-if="reaction == 'pudding'" src="/assets/reactions/pudding.png" alt="%i18n:common.reactions.pudding%"> +</span> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['reaction'] +}); +</script> + +<style lang="stylus" scoped> +.mk-reaction-icon + img + vertical-align middle + width 1em + height 1em +</style> diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue new file mode 100644 index 0000000000..fa1998dca9 --- /dev/null +++ b/src/client/app/common/views/components/reaction-picker.vue @@ -0,0 +1,191 @@ +<template> +<div class="mk-reaction-picker"> + <div class="backdrop" ref="backdrop" @click="close"></div> + <div class="popover" :class="{ compact }" ref="popover"> + <p v-if="!compact">{{ title }}</p> + <div> + <button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" title="%i18n:common.reactions.like%"><mk-reaction-icon reaction='like'/></button> + <button @click="react('love')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="2" title="%i18n:common.reactions.love%"><mk-reaction-icon reaction='love'/></button> + <button @click="react('laugh')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="3" title="%i18n:common.reactions.laugh%"><mk-reaction-icon reaction='laugh'/></button> + <button @click="react('hmm')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="4" title="%i18n:common.reactions.hmm%"><mk-reaction-icon reaction='hmm'/></button> + <button @click="react('surprise')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="5" title="%i18n:common.reactions.surprise%"><mk-reaction-icon reaction='surprise'/></button> + <button @click="react('congrats')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="6" title="%i18n:common.reactions.congrats%"><mk-reaction-icon reaction='congrats'/></button> + <button @click="react('angry')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="4" title="%i18n:common.reactions.angry%"><mk-reaction-icon reaction='angry'/></button> + <button @click="react('confused')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="5" title="%i18n:common.reactions.confused%"><mk-reaction-icon reaction='confused'/></button> + <button @click="react('pudding')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="6" title="%i18n:common.reactions.pudding%"><mk-reaction-icon reaction='pudding'/></button> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as anime from 'animejs'; + +const placeholder = '%i18n:common.tags.mk-reaction-picker.choose-reaction%'; + +export default Vue.extend({ + props: ['note', 'source', 'compact', 'cb'], + data() { + return { + title: placeholder + }; + }, + mounted() { + this.$nextTick(() => { + const popover = this.$refs.popover as any; + + const rect = this.source.getBoundingClientRect(); + const width = popover.offsetWidth; + const height = popover.offsetHeight; + + if (this.compact) { + const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); + const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2); + popover.style.left = (x - (width / 2)) + 'px'; + popover.style.top = (y - (height / 2)) + 'px'; + } else { + const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); + const y = rect.top + window.pageYOffset + this.source.offsetHeight; + popover.style.left = (x - (width / 2)) + 'px'; + popover.style.top = y + 'px'; + } + + anime({ + targets: this.$refs.backdrop, + opacity: 1, + duration: 100, + easing: 'linear' + }); + + anime({ + targets: this.$refs.popover, + opacity: 1, + scale: [0.5, 1], + duration: 500 + }); + }); + }, + methods: { + react(reaction) { + (this as any).api('notes/reactions/create', { + noteId: this.note.id, + reaction: reaction + }).then(() => { + if (this.cb) this.cb(); + this.$destroy(); + }); + }, + onMouseover(e) { + this.title = e.target.title; + }, + onMouseout(e) { + this.title = placeholder; + }, + close() { + (this.$refs.backdrop as any).style.pointerEvents = 'none'; + anime({ + targets: this.$refs.backdrop, + opacity: 0, + duration: 200, + easing: 'linear' + }); + + (this.$refs.popover as any).style.pointerEvents = 'none'; + anime({ + targets: this.$refs.popover, + opacity: 0, + scale: 0.5, + duration: 200, + easing: 'easeInBack', + complete: () => this.$destroy() + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +$border-color = rgba(27, 31, 35, 0.15) + +.mk-reaction-picker + position initial + + > .backdrop + position fixed + top 0 + left 0 + z-index 10000 + width 100% + height 100% + background rgba(0, 0, 0, 0.1) + opacity 0 + + > .popover + position absolute + z-index 10001 + background #fff + border 1px solid $border-color + border-radius 4px + box-shadow 0 3px 12px rgba(27, 31, 35, 0.15) + transform scale(0.5) + opacity 0 + + $balloon-size = 16px + + &:not(.compact) + margin-top $balloon-size + transform-origin center -($balloon-size) + + &:before + content "" + display block + position absolute + top -($balloon-size * 2) + left s('calc(50% - %s)', $balloon-size) + border-top solid $balloon-size transparent + border-left solid $balloon-size transparent + border-right solid $balloon-size transparent + border-bottom solid $balloon-size $border-color + + &:after + content "" + display block + position absolute + top -($balloon-size * 2) + 1.5px + left s('calc(50% - %s)', $balloon-size) + border-top solid $balloon-size transparent + border-left solid $balloon-size transparent + border-right solid $balloon-size transparent + border-bottom solid $balloon-size #fff + + > p + display block + margin 0 + padding 8px 10px + font-size 14px + color #586069 + border-bottom solid 1px #e1e4e8 + + > div + padding 4px + width 240px + text-align center + + > button + padding 0 + width 40px + height 40px + font-size 24px + border-radius 2px + + &:hover + background #eee + + &:active + background $theme-color + box-shadow inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15) + +</style> diff --git a/src/client/app/common/views/components/reactions-viewer.vue b/src/client/app/common/views/components/reactions-viewer.vue new file mode 100644 index 0000000000..1afcf525d2 --- /dev/null +++ b/src/client/app/common/views/components/reactions-viewer.vue @@ -0,0 +1,49 @@ +<template> +<div class="mk-reactions-viewer"> + <template v-if="reactions"> + <span v-if="reactions.like"><mk-reaction-icon reaction='like'/><span>{{ reactions.like }}</span></span> + <span v-if="reactions.love"><mk-reaction-icon reaction='love'/><span>{{ reactions.love }}</span></span> + <span v-if="reactions.laugh"><mk-reaction-icon reaction='laugh'/><span>{{ reactions.laugh }}</span></span> + <span v-if="reactions.hmm"><mk-reaction-icon reaction='hmm'/><span>{{ reactions.hmm }}</span></span> + <span v-if="reactions.surprise"><mk-reaction-icon reaction='surprise'/><span>{{ reactions.surprise }}</span></span> + <span v-if="reactions.congrats"><mk-reaction-icon reaction='congrats'/><span>{{ reactions.congrats }}</span></span> + <span v-if="reactions.angry"><mk-reaction-icon reaction='angry'/><span>{{ reactions.angry }}</span></span> + <span v-if="reactions.confused"><mk-reaction-icon reaction='confused'/><span>{{ reactions.confused }}</span></span> + <span v-if="reactions.pudding"><mk-reaction-icon reaction='pudding'/><span>{{ reactions.pudding }}</span></span> + </template> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['note'], + computed: { + reactions(): number { + return this.note.reactionCounts; + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-reactions-viewer + border-top dashed 1px #eee + border-bottom dashed 1px #eee + margin 4px 0 + + &:empty + display none + + > span + margin-right 8px + + > .mk-reaction-icon + font-size 1.4em + + > span + margin-left 4px + font-size 1.2em + color #444 + +</style> diff --git a/src/client/app/common/views/components/signin.vue b/src/client/app/common/views/components/signin.vue new file mode 100644 index 0000000000..da7472b8c7 --- /dev/null +++ b/src/client/app/common/views/components/signin.vue @@ -0,0 +1,142 @@ +<template> +<form class="mk-signin" :class="{ signing }" @submit.prevent="onSubmit"> + <label class="user-name"> + <input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" placeholder="%i18n:common.tags.mk-signin.username%" autofocus required @change="onUsernameChange"/>%fa:at% + </label> + <label class="password"> + <input v-model="password" type="password" placeholder="%i18n:common.tags.mk-signin.password%" required/>%fa:lock% + </label> + <label class="token" v-if="user && user.twoFactorEnabled"> + <input v-model="token" type="number" placeholder="%i18n:common.tags.mk-signin.token%" required/>%fa:lock% + </label> + <button type="submit" :disabled="signing">{{ signing ? '%i18n:common.tags.mk-signin.signing-in%' : '%i18n:common.tags.mk-signin.signin%' }}</button> + もしくは <a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a> +</form> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { apiUrl } from '../../../config'; + +export default Vue.extend({ + data() { + return { + signing: false, + user: null, + username: '', + password: '', + token: '', + apiUrl, + }; + }, + methods: { + onUsernameChange() { + (this as any).api('users/show', { + username: this.username + }).then(user => { + this.user = user; + }); + }, + onSubmit() { + this.signing = true; + + (this as any).api('signin', { + username: this.username, + password: this.password, + token: this.user && this.user.twoFactorEnabled ? this.token : undefined + }).then(() => { + location.reload(); + }).catch(() => { + alert('something happened'); + this.signing = false; + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-signin + &.signing + &, * + cursor wait !important + + label + display block + margin 12px 0 + + [data-fa] + display block + pointer-events none + position absolute + bottom 0 + top 0 + left 0 + z-index 1 + margin auto + padding 0 16px + height 1em + color #898786 + + input[type=text] + input[type=password] + input[type=number] + user-select text + display inline-block + cursor auto + padding 0 0 0 38px + margin 0 + width 100% + line-height 44px + font-size 1em + color rgba(0, 0, 0, 0.7) + background #fff + outline none + border solid 1px #eee + border-radius 4px + + &:hover + background rgba(255, 255, 255, 0.7) + border-color #ddd + + & + i + color #797776 + + &:focus + background #fff + border-color #ccc + + & + i + color #797776 + + [type=submit] + cursor pointer + padding 16px + margin -6px 0 0 0 + width 100% + font-size 1.2em + color rgba(0, 0, 0, 0.5) + outline none + border none + border-radius 0 + background transparent + transition all .5s ease + + &:hover + color $theme-color + transition all .2s ease + + &:focus + color $theme-color + transition all .2s ease + + &:active + color darken($theme-color, 30%) + transition all .2s ease + + &:disabled + opacity 0.7 + +</style> diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue new file mode 100644 index 0000000000..30fe7b7ad0 --- /dev/null +++ b/src/client/app/common/views/components/signup.vue @@ -0,0 +1,287 @@ +<template> +<form class="mk-signup" @submit.prevent="onSubmit" autocomplete="off"> + <label class="username"> + <p class="caption">%fa:at%%i18n:common.tags.mk-signup.username%</p> + <input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" placeholder="a~z、A~Z、0~9、-" autocomplete="off" required @input="onChangeUsername"/> + <p class="profile-page-url-preview" v-if="shouldShowProfileUrl">{{ `${url}/@${username}` }}</p> + <p class="info" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%%i18n:common.tags.mk-signup.checking%</p> + <p class="info" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.available%</p> + <p class="info" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.unavailable%</p> + <p class="info" v-if="usernameState == 'error'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.error%</p> + <p class="info" v-if="usernameState == 'invalid-format'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.invalid-format%</p> + <p class="info" v-if="usernameState == 'min-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.too-short%</p> + <p class="info" v-if="usernameState == 'max-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.too-long%</p> + </label> + <label class="password"> + <p class="caption">%fa:lock%%i18n:common.tags.mk-signup.password%</p> + <input v-model="password" type="password" placeholder="%i18n:common.tags.mk-signup.password-placeholder%" autocomplete="off" required @input="onChangePassword"/> + <div class="meter" v-show="passwordStrength != ''" :data-strength="passwordStrength"> + <div class="value" ref="passwordMetar"></div> + </div> + <p class="info" v-if="passwordStrength == 'low'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.weak-password%</p> + <p class="info" v-if="passwordStrength == 'medium'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.normal-password%</p> + <p class="info" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.strong-password%</p> + </label> + <label class="retype-password"> + <p class="caption">%fa:lock%%i18n:common.tags.mk-signup.password%(%i18n:common.tags.mk-signup.retype%)</p> + <input v-model="retypedPassword" type="password" placeholder="%i18n:common.tags.mk-signup.retype-placeholder%" autocomplete="off" required @input="onChangePasswordRetype"/> + <p class="info" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.password-matched%</p> + <p class="info" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.password-not-matched%</p> + </label> + <label class="recaptcha"> + <p class="caption"><template v-if="recaptchaed">%fa:toggle-on%</template><template v-if="!recaptchaed">%fa:toggle-off%</template>%i18n:common.tags.mk-signup.recaptcha%</p> + <div class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" :data-sitekey="recaptchaSitekey"></div> + </label> + <label class="agree-tou"> + <input name="agree-tou" type="checkbox" autocomplete="off" required/> + <p><a :href="touUrl" target="_blank">利用規約</a>に同意する</p> + </label> + <button type="submit">%i18n:common.tags.mk-signup.create%</button> +</form> +</template> + +<script lang="ts"> +import Vue from 'vue'; +const getPasswordStrength = require('syuilo-password-strength'); +import { url, docsUrl, lang, recaptchaSitekey } from '../../../config'; + +export default Vue.extend({ + data() { + return { + username: '', + password: '', + retypedPassword: '', + url, + touUrl: `${docsUrl}/${lang}/tou`, + recaptchaSitekey, + recaptchaed: false, + usernameState: null, + passwordStrength: '', + passwordRetypeState: null + } + }, + computed: { + shouldShowProfileUrl(): boolean { + return (this.username != '' && + this.usernameState != 'invalid-format' && + this.usernameState != 'min-range' && + this.usernameState != 'max-range'); + } + }, + methods: { + onChangeUsername() { + if (this.username == '') { + this.usernameState = null; + return; + } + + const err = + !this.username.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' : + this.username.length < 3 ? 'min-range' : + this.username.length > 20 ? 'max-range' : + null; + + if (err) { + this.usernameState = err; + return; + } + + this.usernameState = 'wait'; + + (this as any).api('username/available', { + username: this.username + }).then(result => { + this.usernameState = result.available ? 'ok' : 'unavailable'; + }).catch(err => { + this.usernameState = 'error'; + }); + }, + onChangePassword() { + if (this.password == '') { + this.passwordStrength = ''; + return; + } + + const strength = getPasswordStrength(this.password); + this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low'; + (this.$refs.passwordMetar as any).style.width = `${strength * 100}%`; + }, + onChangePasswordRetype() { + if (this.retypedPassword == '') { + this.passwordRetypeState = null; + return; + } + + this.passwordRetypeState = this.password == this.retypedPassword ? 'match' : 'not-match'; + }, + onSubmit() { + (this as any).api('signup', { + username: this.username, + password: this.password, + 'g-recaptcha-response': (window as any).grecaptcha.getResponse() + }).then(() => { + (this as any).api('signin', { + username: this.username, + password: this.password + }).then(() => { + location.href = '/'; + }); + }).catch(() => { + alert('%i18n:common.tags.mk-signup.some-error%'); + + (window as any).grecaptcha.reset(); + this.recaptchaed = false; + }); + } + }, + created() { + (window as any).onRecaptchaed = () => { + this.recaptchaed = true; + }; + + (window as any).onRecaptchaExpired = () => { + this.recaptchaed = false; + }; + }, + mounted() { + const head = document.getElementsByTagName('head')[0]; + const script = document.createElement('script'); + script.setAttribute('src', 'https://www.google.com/recaptcha/api.js'); + head.appendChild(script); + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-signup + min-width 302px + + label + display block + margin 0 0 16px 0 + + > .caption + margin 0 0 4px 0 + color #828888 + font-size 0.95em + + > [data-fa] + margin-right 0.25em + color #96adac + + > .info + display block + margin 4px 0 + font-size 0.8em + + > [data-fa] + margin-right 0.3em + + &.username + .profile-page-url-preview + display block + margin 4px 8px 0 4px + font-size 0.8em + color #888 + + &:empty + display none + + &:not(:empty) + .info + margin-top 0 + + &.password + .meter + display block + margin-top 8px + width 100% + height 8px + + &[data-strength=''] + display none + + &[data-strength='low'] + > .value + background #d73612 + + &[data-strength='medium'] + > .value + background #d7ca12 + + &[data-strength='high'] + > .value + background #61bb22 + + > .value + display block + width 0% + height 100% + background transparent + border-radius 4px + transition all 0.1s ease + + [type=text], [type=password] + user-select text + display inline-block + cursor auto + padding 0 12px + margin 0 + width 100% + line-height 44px + font-size 1em + color #333 !important + background #fff !important + outline none + border solid 1px rgba(0, 0, 0, 0.1) + border-radius 4px + box-shadow 0 0 0 114514px #fff inset + transition all .3s ease + + &:hover + border-color rgba(0, 0, 0, 0.2) + transition all .1s ease + + &:focus + color $theme-color !important + border-color $theme-color + box-shadow 0 0 0 1024px #fff inset, 0 0 0 4px rgba($theme-color, 10%) + transition all 0s ease + + &:disabled + opacity 0.5 + + .agree-tou + padding 4px + border-radius 4px + + &:hover + background #f4f4f4 + + &:active + background #eee + + &, * + cursor pointer + + p + display inline + color #555 + + button + margin 0 + padding 16px + width 100% + font-size 1em + color #fff + background $theme-color + border-radius 3px + + &:hover + background lighten($theme-color, 5%) + + &:active + background darken($theme-color, 5%) + +</style> diff --git a/src/client/app/common/views/components/special-message.vue b/src/client/app/common/views/components/special-message.vue new file mode 100644 index 0000000000..2fd4d6515e --- /dev/null +++ b/src/client/app/common/views/components/special-message.vue @@ -0,0 +1,42 @@ +<template> +<div class="mk-special-message"> + <p v-if="m == 1 && d == 1">%i18n:common.tags.mk-special-message.new-year%</p> + <p v-if="m == 12 && d == 25">%i18n:common.tags.mk-special-message.christmas%</p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + data() { + return { + now: new Date() + }; + }, + computed: { + d(): number { + return this.now.getDate(); + }, + m(): number { + return this.now.getMonth() + 1; + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-special-message + &:empty + display none + + > p + margin 0 + padding 4px + text-align center + font-size 14px + font-weight bold + text-transform uppercase + color #fff + background #ff1036 + +</style> diff --git a/src/client/app/common/views/components/stream-indicator.vue b/src/client/app/common/views/components/stream-indicator.vue new file mode 100644 index 0000000000..1f18fa76ed --- /dev/null +++ b/src/client/app/common/views/components/stream-indicator.vue @@ -0,0 +1,86 @@ +<template> +<div class="mk-stream-indicator"> + <p v-if=" stream.state == 'initializing' "> + %fa:spinner .pulse% + <span>%i18n:common.tags.mk-stream-indicator.connecting%<mk-ellipsis/></span> + </p> + <p v-if=" stream.state == 'reconnecting' "> + %fa:spinner .pulse% + <span>%i18n:common.tags.mk-stream-indicator.reconnecting%<mk-ellipsis/></span> + </p> + <p v-if=" stream.state == 'connected' "> + %fa:check% + <span>%i18n:common.tags.mk-stream-indicator.connected%</span> + </p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as anime from 'animejs'; + +export default Vue.extend({ + computed: { + stream() { + return (this as any).os.stream; + } + }, + created() { + (this as any).os.stream.on('_connected_', this.onConnected); + (this as any).os.stream.on('_disconnected_', this.onDisconnected); + + this.$nextTick(() => { + if (this.stream.state == 'connected') { + this.$el.style.opacity = '0'; + } + }); + }, + beforeDestroy() { + (this as any).os.stream.off('_connected_', this.onConnected); + (this as any).os.stream.off('_disconnected_', this.onDisconnected); + }, + methods: { + onConnected() { + setTimeout(() => { + anime({ + targets: this.$el, + opacity: 0, + easing: 'linear', + duration: 200 + }); + }, 1000); + }, + onDisconnected() { + anime({ + targets: this.$el, + opacity: 1, + easing: 'linear', + duration: 100 + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-stream-indicator + pointer-events none + position fixed + z-index 16384 + bottom 8px + right 8px + margin 0 + padding 6px 12px + font-size 0.9em + color #fff + background rgba(0, 0, 0, 0.8) + border-radius 4px + + > p + display block + margin 0 + + > [data-fa] + margin-right 0.25em + +</style> diff --git a/src/client/app/common/views/components/switch.vue b/src/client/app/common/views/components/switch.vue new file mode 100644 index 0000000000..19a4adc3de --- /dev/null +++ b/src/client/app/common/views/components/switch.vue @@ -0,0 +1,190 @@ +<template> +<div + class="mk-switch" + :class="{ disabled, checked }" + role="switch" + :aria-checked="checked" + :aria-disabled="disabled" + @click="switchValue" + @mouseover="mouseenter" +> + <input + type="checkbox" + @change="handleChange" + ref="input" + :disabled="disabled" + @keydown.enter="switchValue" + > + <span class="button"> + <span :style="{ transform }"></span> + </span> + <span class="label"> + <span :aria-hidden="!checked">{{ text }}</span> + <p :aria-hidden="!checked"> + <slot></slot> + </p> + </span> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: { + value: { + type: Boolean, + default: false + }, + disabled: { + type: Boolean, + default: false + }, + text: String + },/* + created() { + if (!~[true, false].indexOf(this.value)) { + this.$emit('input', false); + } + },*/ + computed: { + checked(): boolean { + return this.value; + }, + transform(): string { + return this.checked ? 'translate3d(20px, 0, 0)' : ''; + } + }, + watch: { + value() { + (this.$el).style.transition = 'all 0.3s'; + (this.$refs.input as any).checked = this.checked; + } + }, + mounted() { + (this.$refs.input as any).checked = this.checked; + }, + methods: { + mouseenter() { + (this.$el).style.transition = 'all 0s'; + }, + handleChange() { + (this.$el).style.transition = 'all 0.3s'; + this.$emit('input', !this.checked); + this.$emit('change', !this.checked); + this.$nextTick(() => { + // set input's checked property + // in case parent refuses to change component's value + (this.$refs.input as any).checked = this.checked; + }); + }, + switchValue() { + !this.disabled && this.handleChange(); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-switch + display flex + margin 12px 0 + cursor pointer + transition all 0.3s + + > * + user-select none + + &.disabled + opacity 0.6 + cursor not-allowed + + &.checked + > .button + background-color $theme-color + border-color $theme-color + + > .label + > span + color $theme-color + + &:hover + > .label + > span + color darken($theme-color, 10%) + + > .button + background darken($theme-color, 10%) + border-color darken($theme-color, 10%) + + &:hover + > .label + > span + color #2e3338 + + > .button + background #ced2da + border-color #ced2da + + > input + position absolute + width 0 + height 0 + opacity 0 + margin 0 + + &:focus + .button + &:after + content "" + pointer-events none + position absolute + top -5px + right -5px + bottom -5px + left -5px + border 2px solid rgba($theme-color, 0.3) + border-radius 14px + + > .button + display inline-block + margin 0 + width 40px + min-width 40px + height 20px + min-height 20px + background #dcdfe6 + border 1px solid #dcdfe6 + outline none + border-radius 10px + transition inherit + + > * + position absolute + top 1px + left 1px + border-radius 100% + transition transform 0.3s + width 16px + height 16px + background-color #fff + + > .label + margin-left 8px + display block + font-size 15px + cursor pointer + transition inherit + + > span + display block + line-height 20px + color #4a535a + transition inherit + + > p + margin 0 + //font-size 90% + color #9daab3 + +</style> diff --git a/src/client/app/common/views/components/time.vue b/src/client/app/common/views/components/time.vue new file mode 100644 index 0000000000..6e0d2b0dcb --- /dev/null +++ b/src/client/app/common/views/components/time.vue @@ -0,0 +1,76 @@ +<template> +<time class="mk-time"> + <span v-if=" mode == 'relative' ">{{ relative }}</span> + <span v-if=" mode == 'absolute' ">{{ absolute }}</span> + <span v-if=" mode == 'detail' ">{{ absolute }} ({{ relative }})</span> +</time> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + time: { + type: [Date, String], + required: true + }, + mode: { + type: String, + default: 'relative' + } + }, + data() { + return { + tickId: null, + now: new Date() + }; + }, + computed: { + _time(): Date { + return typeof this.time == 'string' ? new Date(this.time) : this.time; + }, + absolute(): string { + const time = this._time; + return ( + time.getFullYear() + '年' + + (time.getMonth() + 1) + '月' + + time.getDate() + '日' + + ' ' + + time.getHours() + '時' + + time.getMinutes() + '分'); + }, + relative(): string { + const time = this._time; + const ago = (this.now.getTime() - time.getTime()) / 1000/*ms*/; + return ( + ago >= 31536000 ? '%i18n:common.time.years_ago%' .replace('{}', (~~(ago / 31536000)).toString()) : + ago >= 2592000 ? '%i18n:common.time.months_ago%' .replace('{}', (~~(ago / 2592000)).toString()) : + ago >= 604800 ? '%i18n:common.time.weeks_ago%' .replace('{}', (~~(ago / 604800)).toString()) : + ago >= 86400 ? '%i18n:common.time.days_ago%' .replace('{}', (~~(ago / 86400)).toString()) : + ago >= 3600 ? '%i18n:common.time.hours_ago%' .replace('{}', (~~(ago / 3600)).toString()) : + ago >= 60 ? '%i18n:common.time.minutes_ago%'.replace('{}', (~~(ago / 60)).toString()) : + ago >= 10 ? '%i18n:common.time.seconds_ago%'.replace('{}', (~~(ago % 60)).toString()) : + ago >= 0 ? '%i18n:common.time.just_now%' : + ago < 0 ? '%i18n:common.time.future%' : + '%i18n:common.time.unknown%'); + } + }, + created() { + if (this.mode == 'relative' || this.mode == 'detail') { + this.tick(); + this.tickId = setInterval(this.tick, 1000); + } + }, + destroyed() { + if (this.mode === 'relative' || this.mode === 'detail') { + clearInterval(this.tickId); + } + }, + methods: { + tick() { + this.now = new Date(); + } + } +}); +</script> diff --git a/src/client/app/common/views/components/timer.vue b/src/client/app/common/views/components/timer.vue new file mode 100644 index 0000000000..a3c4f01b77 --- /dev/null +++ b/src/client/app/common/views/components/timer.vue @@ -0,0 +1,49 @@ +<template> +<time class="mk-time"> + {{ hh }}:{{ mm }}:{{ ss }} +</time> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + time: { + type: [Date, String], + required: true + } + }, + data() { + return { + tickId: null, + hh: null, + mm: null, + ss: null + }; + }, + computed: { + _time(): Date { + return typeof this.time == 'string' ? new Date(this.time) : this.time; + } + }, + created() { + this.tick(); + this.tickId = setInterval(this.tick, 1000); + }, + destroyed() { + clearInterval(this.tickId); + }, + methods: { + tick() { + const now = new Date().getTime(); + const start = this._time.getTime(); + const ago = Math.floor((now - start) / 1000); + + this.hh = Math.floor(ago / (60 * 60)).toString().padStart(2, '0'); + this.mm = Math.floor(ago / 60).toString().padStart(2, '0'); + this.ss = (ago % 60).toString().padStart(2, '0'); + } + } +}); +</script> diff --git a/src/client/app/common/views/components/twitter-setting.vue b/src/client/app/common/views/components/twitter-setting.vue new file mode 100644 index 0000000000..00669cd833 --- /dev/null +++ b/src/client/app/common/views/components/twitter-setting.vue @@ -0,0 +1,66 @@ +<template> +<div class="mk-twitter-setting"> + <p>%i18n:common.tags.mk-twitter-setting.description%<a :href="`${docsUrl}/link-to-twitter`" target="_blank">%i18n:common.tags.mk-twitter-setting.detail%</a></p> + <p class="account" v-if="os.i.twitter" :title="`Twitter ID: ${os.i.twitter.userId}`">%i18n:common.tags.mk-twitter-setting.connected-to%: <a :href="`https://twitter.com/${os.i.twitter.screenName}`" target="_blank">@{{ os.i.twitter.screenName }}</a></p> + <p> + <a :href="`${apiUrl}/connect/twitter`" target="_blank" @click.prevent="connect">{{ os.i.twitter ? '%i18n:common.tags.mk-twitter-setting.reconnect%' : '%i18n:common.tags.mk-twitter-setting.connect%' }}</a> + <span v-if="os.i.twitter"> or </span> + <a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="os.i.twitter" @click.prevent="disconnect">%i18n:common.tags.mk-twitter-setting.disconnect%</a> + </p> + <p class="id" v-if="os.i.twitter">Twitter ID: {{ os.i.twitter.userId }}</p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { apiUrl, docsUrl } from '../../../config'; + +export default Vue.extend({ + data() { + return { + form: null, + apiUrl, + docsUrl + }; + }, + mounted() { + this.$watch('os.i', () => { + if ((this as any).os.i.twitter) { + if (this.form) this.form.close(); + } + }, { + deep: true + }); + }, + methods: { + connect() { + this.form = window.open(apiUrl + '/connect/twitter', + 'twitter_connect_window', + 'height=570, width=520'); + }, + + disconnect() { + window.open(apiUrl + '/disconnect/twitter', + 'twitter_disconnect_window', + 'height=570, width=520'); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-twitter-setting + color #4a535a + + .account + border solid 1px #e1e8ed + border-radius 4px + padding 16px + + a + font-weight bold + color inherit + + .id + color #8899a6 +</style> diff --git a/src/client/app/common/views/components/uploader.vue b/src/client/app/common/views/components/uploader.vue new file mode 100644 index 0000000000..ccad50dc37 --- /dev/null +++ b/src/client/app/common/views/components/uploader.vue @@ -0,0 +1,212 @@ +<template> +<div class="mk-uploader"> + <ol v-if="uploads.length > 0"> + <li v-for="ctx in uploads" :key="ctx.id"> + <div class="img" :style="{ backgroundImage: `url(${ ctx.img })` }"></div> + <p class="name">%fa:spinner .pulse%{{ ctx.name }}</p> + <p class="status"> + <span class="initing" v-if="ctx.progress == undefined">%i18n:common.tags.mk-uploader.waiting%<mk-ellipsis/></span> + <span class="kb" v-if="ctx.progress != undefined">{{ String(Math.floor(ctx.progress.value / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progress.max / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i></span> + <span class="percentage" v-if="ctx.progress != undefined">{{ Math.floor((ctx.progress.value / ctx.progress.max) * 100) }}</span> + </p> + <progress v-if="ctx.progress != undefined && ctx.progress.value != ctx.progress.max" :value="ctx.progress.value" :max="ctx.progress.max"></progress> + <div class="progress initing" v-if="ctx.progress == undefined"></div> + <div class="progress waiting" v-if="ctx.progress != undefined && ctx.progress.value == ctx.progress.max"></div> + </li> + </ol> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { apiUrl } from '../../../config'; + +export default Vue.extend({ + data() { + return { + uploads: [] + }; + }, + methods: { + upload(file, folder) { + if (folder && typeof folder == 'object') folder = folder.id; + + const id = Math.random(); + + const ctx = { + id: id, + name: file.name || 'untitled', + progress: undefined, + img: undefined + }; + + this.uploads.push(ctx); + this.$emit('change', this.uploads); + + const reader = new FileReader(); + reader.onload = (e: any) => { + ctx.img = e.target.result; + }; + reader.readAsDataURL(file); + + const data = new FormData(); + data.append('i', (this as any).os.i.token); + data.append('file', file); + + if (folder) data.append('folderId', folder); + + const xhr = new XMLHttpRequest(); + xhr.open('POST', apiUrl + '/drive/files/create', true); + xhr.onload = (e: any) => { + const driveFile = JSON.parse(e.target.response); + + this.$emit('uploaded', driveFile); + + this.uploads = this.uploads.filter(x => x.id != id); + this.$emit('change', this.uploads); + }; + + xhr.upload.onprogress = e => { + if (e.lengthComputable) { + if (ctx.progress == undefined) ctx.progress = {}; + ctx.progress.max = e.total; + ctx.progress.value = e.loaded; + } + }; + + xhr.send(data); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-uploader + overflow auto + + &:empty + display none + + > ol + display block + margin 0 + padding 0 + list-style none + + > li + display block + margin 8px 0 0 0 + padding 0 + height 36px + box-shadow 0 -1px 0 rgba($theme-color, 0.1) + border-top solid 8px transparent + + &:first-child + margin 0 + box-shadow none + border-top none + + > .img + display block + position absolute + top 0 + left 0 + width 36px + height 36px + background-size cover + background-position center center + + > .name + display block + position absolute + top 0 + left 44px + margin 0 + padding 0 + max-width 256px + font-size 0.8em + color rgba($theme-color, 0.7) + white-space nowrap + text-overflow ellipsis + overflow hidden + + > [data-fa] + margin-right 4px + + > .status + display block + position absolute + top 0 + right 0 + margin 0 + padding 0 + font-size 0.8em + + > .initing + color rgba($theme-color, 0.5) + + > .kb + color rgba($theme-color, 0.5) + + > .percentage + display inline-block + width 48px + text-align right + + color rgba($theme-color, 0.7) + + &:after + content '%' + + > progress + display block + position absolute + bottom 0 + right 0 + margin 0 + width calc(100% - 44px) + height 8px + background transparent + border none + border-radius 4px + overflow hidden + + &::-webkit-progress-value + background $theme-color + + &::-webkit-progress-bar + background rgba($theme-color, 0.1) + + > .progress + display block + position absolute + bottom 0 + right 0 + margin 0 + width calc(100% - 44px) + height 8px + border none + border-radius 4px + background linear-gradient( + 45deg, + lighten($theme-color, 30%) 25%, + $theme-color 25%, + $theme-color 50%, + lighten($theme-color, 30%) 50%, + lighten($theme-color, 30%) 75%, + $theme-color 75%, + $theme-color + ) + background-size 32px 32px + animation bg 1.5s linear infinite + + &.initing + opacity 0.3 + + @keyframes bg + from {background-position: 0 0;} + to {background-position: -64px 32px;} + +</style> diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue new file mode 100644 index 0000000000..e91e510550 --- /dev/null +++ b/src/client/app/common/views/components/url-preview.vue @@ -0,0 +1,142 @@ +<template> +<iframe v-if="youtubeId" type="text/html" height="250" + :src="`https://www.youtube.com/embed/${youtubeId}?origin=${misskeyUrl}`" + frameborder="0"/> +<div v-else> + <a class="mk-url-preview" :href="url" target="_blank" :title="url" v-if="!fetching"> + <div class="thumbnail" v-if="thumbnail" :style="`background-image: url(${thumbnail})`"></div> + <article> + <header> + <h1>{{ title }}</h1> + </header> + <p>{{ description }}</p> + <footer> + <img class="icon" v-if="icon" :src="icon"/> + <p>{{ sitename }}</p> + </footer> + </article> + </a> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { url as misskeyUrl } from '../../../config'; + +export default Vue.extend({ + props: ['url'], + data() { + return { + fetching: true, + title: null, + description: null, + thumbnail: null, + icon: null, + sitename: null, + youtubeId: null, + misskeyUrl + }; + }, + created() { + const url = new URL(this.url); + + if (url.hostname == 'www.youtube.com') { + this.youtubeId = url.searchParams.get('v'); + } else if (url.hostname == 'youtu.be') { + this.youtubeId = url.pathname; + } else { + fetch('/api:url?url=' + this.url).then(res => { + res.json().then(info => { + this.title = info.title; + this.description = info.description; + this.thumbnail = info.thumbnail; + this.icon = info.icon; + this.sitename = info.sitename; + + this.fetching = false; + }); + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +iframe + width 100% + +.mk-url-preview + display block + font-size 16px + border solid 1px #eee + border-radius 4px + overflow hidden + + &:hover + text-decoration none + border-color #ddd + + > article > header > h1 + text-decoration underline + + > .thumbnail + position absolute + width 100px + height 100% + background-position center + background-size cover + + & + article + left 100px + width calc(100% - 100px) + + > article + padding 16px + + > header + margin-bottom 8px + + > h1 + margin 0 + font-size 1em + color #555 + + > p + margin 0 + color #777 + font-size 0.8em + + > footer + margin-top 8px + height 16px + + > img + display inline-block + width 16px + height 16px + margin-right 4px + vertical-align top + + > p + display inline-block + margin 0 + color #666 + font-size 0.8em + line-height 16px + vertical-align top + + @media (max-width 500px) + font-size 8px + border none + + > .thumbnail + width 70px + + & + article + left 70px + width calc(100% - 70px) + + > article + padding 8px + +</style> diff --git a/src/client/app/common/views/components/url.vue b/src/client/app/common/views/components/url.vue new file mode 100644 index 0000000000..e6ffe4466d --- /dev/null +++ b/src/client/app/common/views/components/url.vue @@ -0,0 +1,57 @@ +<template> +<a class="mk-url" :href="url" :target="target"> + <span class="schema">{{ schema }}//</span> + <span class="hostname">{{ hostname }}</span> + <span class="port" v-if="port != ''">:{{ port }}</span> + <span class="pathname" v-if="pathname != ''">{{ pathname }}</span> + <span class="query">{{ query }}</span> + <span class="hash">{{ hash }}</span> + %fa:external-link-square-alt% +</a> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['url', 'target'], + data() { + return { + schema: null, + hostname: null, + port: null, + pathname: null, + query: null, + hash: null + }; + }, + created() { + const url = new URL(this.url); + this.schema = url.protocol; + this.hostname = url.hostname; + this.port = url.port; + this.pathname = url.pathname; + this.query = url.search; + this.hash = url.hash; + } +}); +</script> + +<style lang="stylus" scoped> +.mk-url + word-break break-all + > [data-fa] + padding-left 2px + font-size .9em + font-weight 400 + font-style normal + > .schema + opacity 0.5 + > .hostname + font-weight bold + > .pathname + opacity 0.8 + > .query + opacity 0.5 + > .hash + font-style italic +</style> diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue new file mode 100644 index 0000000000..a80bc04f7f --- /dev/null +++ b/src/client/app/common/views/components/welcome-timeline.vue @@ -0,0 +1,116 @@ +<template> +<div class="mk-welcome-timeline"> + <div v-for="note in notes"> + <router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.user.id"> + <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/> + </router-link> + <div class="body"> + <header> + <router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link> + <span class="username">@{{ note.user | acct }}</span> + <div class="info"> + <router-link class="created-at" :to="note | notePage"> + <mk-time :time="note.createdAt"/> + </router-link> + </div> + </header> + <div class="text"> + <mk-note-html :text="note.text"/> + </div> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + data() { + return { + fetching: true, + notes: [] + }; + }, + mounted() { + this.fetch(); + }, + methods: { + fetch(cb?) { + this.fetching = true; + (this as any).api('notes', { + reply: false, + renote: false, + media: false, + poll: false, + bot: false + }).then(notes => { + this.notes = notes; + this.fetching = false; + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-welcome-timeline + background #fff + + > div + padding 16px + overflow-wrap break-word + font-size .9em + color #4C4C4C + border-bottom 1px solid rgba(0, 0, 0, 0.05) + + &:after + content "" + display block + clear both + + > .avatar-anchor + display block + float left + position -webkit-sticky + position sticky + top 16px + + > img + display block + width 42px + height 42px + border-radius 6px + + > .body + float right + width calc(100% - 42px) + padding-left 12px + + > header + display flex + align-items center + margin-bottom 4px + white-space nowrap + + > .name + display block + margin 0 .5em 0 0 + padding 0 + overflow hidden + font-weight bold + text-overflow ellipsis + color #627079 + + > .username + margin 0 .5em 0 0 + color #ccc + + > .info + margin-left auto + font-size 0.9em + + > .created-at + color #c0c0c0 + +</style> diff --git a/src/client/app/common/views/directives/autocomplete.ts b/src/client/app/common/views/directives/autocomplete.ts new file mode 100644 index 0000000000..94635d301a --- /dev/null +++ b/src/client/app/common/views/directives/autocomplete.ts @@ -0,0 +1,194 @@ +import * as getCaretCoordinates from 'textarea-caret'; +import MkAutocomplete from '../components/autocomplete.vue'; + +export default { + bind(el, binding, vn) { + const self = el._autoCompleteDirective_ = {} as any; + self.x = new Autocomplete(el, vn.context, binding.value); + self.x.attach(); + }, + + unbind(el, binding, vn) { + const self = el._autoCompleteDirective_; + self.x.detach(); + } +}; + +/** + * オートコンプリートを管理するクラス。 + */ +class Autocomplete { + private suggestion: any; + private textarea: any; + private vm: any; + private model: any; + private currentType: string; + + private get text(): string { + return this.vm[this.model]; + } + + private set text(text: string) { + this.vm[this.model] = text; + } + + /** + * 対象のテキストエリアを与えてインスタンスを初期化します。 + */ + constructor(textarea, vm, model) { + //#region BIND + this.onInput = this.onInput.bind(this); + this.complete = this.complete.bind(this); + this.close = this.close.bind(this); + //#endregion + + this.suggestion = null; + this.textarea = textarea; + this.vm = vm; + this.model = model; + } + + /** + * このインスタンスにあるテキストエリアの入力のキャプチャを開始します。 + */ + public attach() { + this.textarea.addEventListener('input', this.onInput); + } + + /** + * このインスタンスにあるテキストエリアの入力のキャプチャを解除します。 + */ + public detach() { + this.textarea.removeEventListener('input', this.onInput); + this.close(); + } + + /** + * テキスト入力時 + */ + private onInput() { + const caret = this.textarea.selectionStart; + const text = this.text.substr(0, caret); + + const mentionIndex = text.lastIndexOf('@'); + const emojiIndex = text.lastIndexOf(':'); + + let opened = false; + + if (mentionIndex != -1 && mentionIndex > emojiIndex) { + const username = text.substr(mentionIndex + 1); + if (username != '' && username.match(/^[a-zA-Z0-9_]+$/)) { + this.open('user', username); + opened = true; + } + } + + if (emojiIndex != -1 && emojiIndex > mentionIndex) { + const emoji = text.substr(emojiIndex + 1); + if (emoji != '' && emoji.match(/^[\+\-a-z0-9_]+$/)) { + this.open('emoji', emoji); + opened = true; + } + } + + if (!opened) { + this.close(); + } + } + + /** + * サジェストを提示します。 + */ + private open(type, q) { + if (type != this.currentType) { + this.close(); + } + this.currentType = type; + + //#region サジェストを表示すべき位置を計算 + const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart); + + const rect = this.textarea.getBoundingClientRect(); + + const x = rect.left + caretPosition.left - this.textarea.scrollLeft; + const y = rect.top + caretPosition.top - this.textarea.scrollTop; + //#endregion + + if (this.suggestion) { + this.suggestion.x = x; + this.suggestion.y = y; + this.suggestion.q = q; + } else { + // サジェスト要素作成 + this.suggestion = new MkAutocomplete({ + propsData: { + textarea: this.textarea, + complete: this.complete, + close: this.close, + type: type, + q: q, + x, + y + } + }).$mount(); + + // 要素追加 + document.body.appendChild(this.suggestion.$el); + } + } + + /** + * サジェストを閉じます。 + */ + private close() { + if (this.suggestion == null) return; + + this.suggestion.$destroy(); + this.suggestion = null; + + this.textarea.focus(); + } + + /** + * オートコンプリートする + */ + private complete(type, value) { + this.close(); + + const caret = this.textarea.selectionStart; + + if (type == 'user') { + const source = this.text; + + const before = source.substr(0, caret); + const trimmedBefore = before.substring(0, before.lastIndexOf('@')); + const after = source.substr(caret); + + // 挿入 + this.text = trimmedBefore + '@' + value.username + ' ' + after; + + // キャレットを戻す + this.vm.$nextTick(() => { + this.textarea.focus(); + const pos = trimmedBefore.length + (value.username.length + 2); + this.textarea.setSelectionRange(pos, pos); + }); + } else if (type == 'emoji') { + const source = this.text; + + const before = source.substr(0, caret); + const trimmedBefore = before.substring(0, before.lastIndexOf(':')); + const after = source.substr(caret); + + // 挿入 + this.text = trimmedBefore + value + after; + + // キャレットを戻す + this.vm.$nextTick(() => { + this.textarea.focus(); + const pos = trimmedBefore.length + 1; + this.textarea.setSelectionRange(pos, pos); + }); + } + } +} diff --git a/src/client/app/common/views/directives/index.ts b/src/client/app/common/views/directives/index.ts new file mode 100644 index 0000000000..268f07a950 --- /dev/null +++ b/src/client/app/common/views/directives/index.ts @@ -0,0 +1,5 @@ +import Vue from 'vue'; + +import autocomplete from './autocomplete'; + +Vue.directive('autocomplete', autocomplete); diff --git a/src/client/app/common/views/filters/bytes.ts b/src/client/app/common/views/filters/bytes.ts new file mode 100644 index 0000000000..3afb11e9ae --- /dev/null +++ b/src/client/app/common/views/filters/bytes.ts @@ -0,0 +1,8 @@ +import Vue from 'vue'; + +Vue.filter('bytes', (v, digits = 0) => { + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + if (v == 0) return '0Byte'; + const i = Math.floor(Math.log(v) / Math.log(1024)); + return (v / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i]; +}); diff --git a/src/client/app/common/views/filters/index.ts b/src/client/app/common/views/filters/index.ts new file mode 100644 index 0000000000..1759c19c2c --- /dev/null +++ b/src/client/app/common/views/filters/index.ts @@ -0,0 +1,4 @@ +require('./bytes'); +require('./number'); +require('./user'); +require('./note'); diff --git a/src/client/app/common/views/filters/note.ts b/src/client/app/common/views/filters/note.ts new file mode 100644 index 0000000000..a611dc8685 --- /dev/null +++ b/src/client/app/common/views/filters/note.ts @@ -0,0 +1,5 @@ +import Vue from 'vue'; + +Vue.filter('notePage', note => { + return '/notes/' + note.id; +}); diff --git a/src/client/app/common/views/filters/number.ts b/src/client/app/common/views/filters/number.ts new file mode 100644 index 0000000000..d9f48229dd --- /dev/null +++ b/src/client/app/common/views/filters/number.ts @@ -0,0 +1,5 @@ +import Vue from 'vue'; + +Vue.filter('number', (n) => { + return n.toLocaleString(); +}); diff --git a/src/client/app/common/views/filters/user.ts b/src/client/app/common/views/filters/user.ts new file mode 100644 index 0000000000..c5bb39f674 --- /dev/null +++ b/src/client/app/common/views/filters/user.ts @@ -0,0 +1,15 @@ +import Vue from 'vue'; +import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; + +Vue.filter('acct', user => { + return getAcct(user); +}); + +Vue.filter('userName', user => { + return getUserName(user); +}); + +Vue.filter('userPage', (user, path?) => { + return '/@' + Vue.filter('acct')(user) + (path ? '/' + path : ''); +}); diff --git a/src/client/app/common/views/widgets/access-log.vue b/src/client/app/common/views/widgets/access-log.vue new file mode 100644 index 0000000000..f7bb17d833 --- /dev/null +++ b/src/client/app/common/views/widgets/access-log.vue @@ -0,0 +1,90 @@ +<template> +<div class="mkw-access-log"> + <mk-widget-container :show-header="props.design == 0"> + <template slot="header">%fa:server%%i18n:desktop.tags.mk-access-log-home-widget.title%</template> + + <div :class="$style.logs" ref="log"> + <p v-for="req in requests"> + <span :class="$style.ip" :style="`color:${ req.fg }; background:${ req.bg }`">{{ req.ip }}</span> + <b>{{ req.method }}</b> + <span>{{ req.path }}</span> + </p> + </div> + </mk-widget-container> +</div> +</template> + +<script lang="ts"> +import define from '../../../common/define-widget'; +import * as seedrandom from 'seedrandom'; + +export default define({ + name: 'broadcast', + props: () => ({ + design: 0 + }) +}).extend({ + data() { + return { + requests: [], + connection: null, + connectionId: null + }; + }, + mounted() { + this.connection = (this as any).os.streams.requestsStream.getConnection(); + this.connectionId = (this as any).os.streams.requestsStream.use(); + this.connection.on('request', this.onRequest); + }, + beforeDestroy() { + this.connection.off('request', this.onRequest); + (this as any).os.streams.requestsStream.dispose(this.connectionId); + }, + methods: { + onRequest(request) { + const random = seedrandom(request.ip); + const r = Math.floor(random() * 255); + const g = Math.floor(random() * 255); + const b = Math.floor(random() * 255); + const luma = (0.2126 * r) + (0.7152 * g) + (0.0722 * b); // SMPTE C, Rec. 709 weightings + request.bg = `rgb(${r}, ${g}, ${b})`; + request.fg = luma >= 165 ? '#000' : '#fff'; + + this.requests.push(request); + if (this.requests.length > 30) this.requests.shift(); + + (this.$refs.log as any).scrollTop = (this.$refs.log as any).scrollHeight; + }, + func() { + if (this.props.design == 1) { + this.props.design = 0; + } else { + this.props.design++; + } + } + } +}); +</script> + +<style lang="stylus" module> +.logs + max-height 250px + overflow auto + + > p + margin 0 + padding 8px + font-size 0.8em + color #555 + + &:nth-child(odd) + background rgba(0, 0, 0, 0.025) + + > b + margin-right 4px + +.ip + margin-right 4px + padding 0 4px + +</style> diff --git a/src/client/app/common/views/widgets/broadcast.vue b/src/client/app/common/views/widgets/broadcast.vue new file mode 100644 index 0000000000..bf41a5fc67 --- /dev/null +++ b/src/client/app/common/views/widgets/broadcast.vue @@ -0,0 +1,161 @@ +<template> +<div class="mkw-broadcast" + :data-found="broadcasts.length != 0" + :data-melt="props.design == 1" + :data-mobile="isMobile" +> + <div class="icon"> + <svg height="32" version="1.1" viewBox="0 0 32 32" width="32"> + <path class="tower" d="M16.04,11.24c1.79,0,3.239-1.45,3.239-3.24S17.83,4.76,16.04,4.76c-1.79,0-3.24,1.45-3.24,3.24 C12.78,9.78,14.24,11.24,16.04,11.24z M16.04,13.84c-0.82,0-1.66-0.2-2.4-0.6L7.34,29.98h2.98l1.72-2h8l1.681,2H24.7L18.42,13.24 C17.66,13.64,16.859,13.84,16.04,13.84z M16.02,14.8l2.02,7.2h-4L16.02,14.8z M12.04,25.98l2-2h4l2,2H12.04z"></path> + <path class="wave a" d="M4.66,1.04c-0.508-0.508-1.332-0.508-1.84,0c-1.86,1.92-2.8,4.44-2.8,6.94c0,2.52,0.94,5.04,2.8,6.96 c0.5,0.52,1.32,0.52,1.82,0s0.5-1.36,0-1.88C3.28,11.66,2.6,9.82,2.6,7.98S3.28,4.3,4.64,2.9C5.157,2.391,5.166,1.56,4.66,1.04z"></path> + <path class="wave b" d="M9.58,12.22c0.5-0.5,0.5-1.34,0-1.84C8.94,9.72,8.62,8.86,8.62,8s0.32-1.72,0.96-2.38c0.5-0.52,0.5-1.34,0-1.84 C9.346,3.534,9.02,3.396,8.68,3.4c-0.32,0-0.66,0.12-0.9,0.38C6.64,4.94,6.08,6.48,6.08,8s0.58,3.06,1.7,4.22 C8.28,12.72,9.1,12.72,9.58,12.22z"></path> + <path class="wave c" d="M22.42,3.78c-0.5,0.5-0.5,1.34,0,1.84c0.641,0.66,0.96,1.52,0.96,2.38s-0.319,1.72-0.96,2.38c-0.5,0.52-0.5,1.34,0,1.84 c0.487,0.497,1.285,0.505,1.781,0.018c0.007-0.006,0.013-0.012,0.02-0.018c1.139-1.16,1.699-2.7,1.699-4.22s-0.561-3.06-1.699-4.22 c-0.494-0.497-1.297-0.5-1.794-0.007C22.424,3.775,22.422,3.778,22.42,3.78z"></path> + <path class="wave d" d="M29.18,1.06c-0.479-0.502-1.273-0.522-1.775-0.044c-0.016,0.015-0.029,0.029-0.045,0.044c-0.5,0.52-0.5,1.36,0,1.88 c1.361,1.4,2.041,3.24,2.041,5.08s-0.68,3.66-2.041,5.08c-0.5,0.52-0.5,1.36,0,1.88c0.509,0.508,1.332,0.508,1.841,0 c1.86-1.92,2.8-4.44,2.8-6.96C31.99,5.424,30.98,2.931,29.18,1.06z"></path> + </svg> + </div> + <p class="fetching" v-if="fetching">%i18n:desktop.tags.mk-broadcast-home-widget.fetching%<mk-ellipsis/></p> + <h1 v-if="!fetching">{{ broadcasts.length == 0 ? '%i18n:desktop.tags.mk-broadcast-home-widget.no-broadcasts%' : broadcasts[i].title }}</h1> + <p v-if="!fetching"> + <span v-if="broadcasts.length != 0" v-html="broadcasts[i].text"></span> + <template v-if="broadcasts.length == 0">%i18n:desktop.tags.mk-broadcast-home-widget.have-a-nice-day%</template> + </p> + <a v-if="broadcasts.length > 1" @click="next">%i18n:desktop.tags.mk-broadcast-home-widget.next% >></a> +</div> +</template> + +<script lang="ts"> +import define from '../../../common/define-widget'; +import { lang } from '../../../config'; + +export default define({ + name: 'broadcast', + props: () => ({ + design: 0 + }) +}).extend({ + data() { + return { + i: 0, + fetching: true, + broadcasts: [] + }; + }, + mounted() { + (this as any).os.getMeta().then(meta => { + let broadcasts = []; + if (meta.broadcasts) { + meta.broadcasts.forEach(broadcast => { + if (broadcast[lang]) { + broadcasts.push(broadcast[lang]); + } + }); + } + this.broadcasts = broadcasts; + this.fetching = false; + }); + }, + methods: { + next() { + if (this.i == this.broadcasts.length - 1) { + this.i = 0; + } else { + this.i++; + } + }, + func() { + if (this.props.design == 1) { + this.props.design = 0; + } else { + this.props.design++; + } + } + } +}); +</script> + +<style lang="stylus" scoped> +.mkw-broadcast + padding 10px + border solid 1px #4078c0 + border-radius 6px + + &[data-melt] + border none + + &[data-found] + padding-left 50px + + > .icon + display block + + &:after + content "" + display block + clear both + + > .icon + display none + float left + margin-left -40px + + > svg + fill currentColor + color #4078c0 + + > .wave + opacity 1 + + &.a + animation wave 20s ease-in-out 2.1s infinite + &.b + animation wave 20s ease-in-out 2s infinite + &.c + animation wave 20s ease-in-out 2s infinite + &.d + animation wave 20s ease-in-out 2.1s infinite + + @keyframes wave + 0% + opacity 1 + 1.5% + opacity 0 + 3.5% + opacity 0 + 5% + opacity 1 + 6.5% + opacity 0 + 8.5% + opacity 0 + 10% + opacity 1 + + > h1 + margin 0 + font-size 0.95em + font-weight normal + color #4078c0 + + > p + display block + z-index 1 + margin 0 + font-size 0.7em + color #555 + + &.fetching + text-align center + + a + color #555 + text-decoration underline + + > a + display block + font-size 0.7em + + &[data-mobile] + > p + color #fff + +</style> diff --git a/src/client/app/common/views/widgets/calendar.vue b/src/client/app/common/views/widgets/calendar.vue new file mode 100644 index 0000000000..03f69a7597 --- /dev/null +++ b/src/client/app/common/views/widgets/calendar.vue @@ -0,0 +1,201 @@ +<template> +<div class="mkw-calendar" + :data-melt="props.design == 1" + :data-special="special" + :data-mobile="isMobile" +> + <div class="calendar" :data-is-holiday="isHoliday"> + <p class="month-and-year"> + <span class="year">{{ year }}年</span> + <span class="month">{{ month }}月</span> + </p> + <p class="day">{{ day }}日</p> + <p class="week-day">{{ weekDay }}曜日</p> + </div> + <div class="info"> + <div> + <p>今日:<b>{{ dayP.toFixed(1) }}%</b></p> + <div class="meter"> + <div class="val" :style="{ width: `${dayP}%` }"></div> + </div> + </div> + <div> + <p>今月:<b>{{ monthP.toFixed(1) }}%</b></p> + <div class="meter"> + <div class="val" :style="{ width: `${monthP}%` }"></div> + </div> + </div> + <div> + <p>今年:<b>{{ yearP.toFixed(1) }}%</b></p> + <div class="meter"> + <div class="val" :style="{ width: `${yearP}%` }"></div> + </div> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import define from '../../../common/define-widget'; +export default define({ + name: 'calendar', + props: () => ({ + design: 0 + }) +}).extend({ + data() { + return { + now: new Date(), + year: null, + month: null, + day: null, + weekDay: null, + yearP: null, + dayP: null, + monthP: null, + isHoliday: null, + special: null, + clock: null + }; + }, + created() { + this.tick(); + this.clock = setInterval(this.tick, 1000); + }, + beforeDestroy() { + clearInterval(this.clock); + }, + methods: { + func() { + if (this.isMobile) return; + if (this.props.design == 2) { + this.props.design = 0; + } else { + this.props.design++; + } + }, + tick() { + const now = new Date(); + const nd = now.getDate(); + const nm = now.getMonth(); + const ny = now.getFullYear(); + + this.year = ny; + this.month = nm + 1; + this.day = nd; + this.weekDay = ['日', '月', '火', '水', '木', '金', '土'][now.getDay()]; + + const dayNumer = now.getTime() - new Date(ny, nm, nd).getTime(); + const dayDenom = 1000/*ms*/ * 60/*s*/ * 60/*m*/ * 24/*h*/; + const monthNumer = now.getTime() - new Date(ny, nm, 1).getTime(); + const monthDenom = new Date(ny, nm + 1, 1).getTime() - new Date(ny, nm, 1).getTime(); + const yearNumer = now.getTime() - new Date(ny, 0, 1).getTime(); + const yearDenom = new Date(ny + 1, 0, 1).getTime() - new Date(ny, 0, 1).getTime(); + + this.dayP = dayNumer / dayDenom * 100; + this.monthP = monthNumer / monthDenom * 100; + this.yearP = yearNumer / yearDenom * 100; + + this.isHoliday = now.getDay() == 0 || now.getDay() == 6; + + this.special = + nm == 0 && nd == 1 ? 'on-new-years-day' : + false; + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mkw-calendar + padding 16px 0 + color #777 + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + &[data-special='on-new-years-day'] + border-color #ef95a0 + + &[data-melt] + background transparent + border none + + &[data-mobile] + border none + border-radius 8px + box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + + &:after + content "" + display block + clear both + + > .calendar + float left + width 60% + text-align center + + &[data-is-holiday] + > .day + color #ef95a0 + + > p + margin 0 + line-height 18px + font-size 14px + + > span + margin 0 4px + + > .day + margin 10px 0 + line-height 32px + font-size 28px + + > .info + display block + float left + width 40% + padding 0 16px 0 0 + + > div + margin-bottom 8px + + &:last-child + margin-bottom 4px + + > p + margin 0 0 2px 0 + font-size 12px + line-height 18px + color #888 + + > b + margin-left 2px + + > .meter + width 100% + overflow hidden + background #eee + border-radius 8px + + > .val + height 4px + background $theme-color + + &:nth-child(1) + > .meter > .val + background #f7796c + + &:nth-child(2) + > .meter > .val + background #a1de41 + + &:nth-child(3) + > .meter > .val + background #41ddde + +</style> diff --git a/src/client/app/common/views/widgets/donation.vue b/src/client/app/common/views/widgets/donation.vue new file mode 100644 index 0000000000..e218df06e1 --- /dev/null +++ b/src/client/app/common/views/widgets/donation.vue @@ -0,0 +1,58 @@ +<template> +<div class="mkw-donation" :data-mobile="isMobile"> + <article> + <h1>%fa:heart%%i18n:desktop.tags.mk-donation-home-widget.title%</h1> + <p> + {{ '%i18n:desktop.tags.mk-donation-home-widget.text%'.substr(0, '%i18n:desktop.tags.mk-donation-home-widget.text%'.indexOf('{')) }} + <a href="https://syuilo.com">@syuilo</a> + {{ '%i18n:desktop.tags.mk-donation-home-widget.text%'.substr('%i18n:desktop.tags.mk-donation-home-widget.text%'.indexOf('}') + 1) }} + </p> + </article> +</div> +</template> + +<script lang="ts"> +import define from '../../../common/define-widget'; +export default define({ + name: 'donation' +}); +</script> + +<style lang="stylus" scoped> +.mkw-donation + background #fff + border solid 1px #ead8bb + border-radius 6px + + > article + padding 20px + + > h1 + margin 0 0 5px 0 + font-size 1em + color #888 + + > [data-fa] + margin-right 0.25em + + > p + display block + z-index 1 + margin 0 + font-size 0.8em + color #999 + + &[data-mobile] + border none + background #ead8bb + border-radius 8px + box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + + > article + > h1 + color #7b8871 + + > p + color #777d71 + +</style> diff --git a/src/client/app/common/views/widgets/index.ts b/src/client/app/common/views/widgets/index.ts new file mode 100644 index 0000000000..e41030e85a --- /dev/null +++ b/src/client/app/common/views/widgets/index.ts @@ -0,0 +1,25 @@ +import Vue from 'vue'; + +import wAccessLog from './access-log.vue'; +import wVersion from './version.vue'; +import wRss from './rss.vue'; +import wServer from './server.vue'; +import wBroadcast from './broadcast.vue'; +import wCalendar from './calendar.vue'; +import wPhotoStream from './photo-stream.vue'; +import wSlideshow from './slideshow.vue'; +import wTips from './tips.vue'; +import wDonation from './donation.vue'; +import wNav from './nav.vue'; + +Vue.component('mkw-nav', wNav); +Vue.component('mkw-calendar', wCalendar); +Vue.component('mkw-photo-stream', wPhotoStream); +Vue.component('mkw-slideshow', wSlideshow); +Vue.component('mkw-tips', wTips); +Vue.component('mkw-donation', wDonation); +Vue.component('mkw-broadcast', wBroadcast); +Vue.component('mkw-server', wServer); +Vue.component('mkw-rss', wRss); +Vue.component('mkw-version', wVersion); +Vue.component('mkw-access-log', wAccessLog); diff --git a/src/client/app/common/views/widgets/nav.vue b/src/client/app/common/views/widgets/nav.vue new file mode 100644 index 0000000000..7bd5a7832f --- /dev/null +++ b/src/client/app/common/views/widgets/nav.vue @@ -0,0 +1,31 @@ +<template> +<div class="mkw-nav"> + <mk-widget-container> + <div :class="$style.body"> + <mk-nav/> + </div> + </mk-widget-container> +</div> +</template> + +<script lang="ts"> +import define from '../../../common/define-widget'; +export default define({ + name: 'nav' +}); +</script> + +<style lang="stylus" module> +.body + padding 16px + font-size 12px + color #aaa + background #fff + + a + color #999 + + i + color #ccc + +</style> diff --git a/src/client/app/common/views/widgets/photo-stream.vue b/src/client/app/common/views/widgets/photo-stream.vue new file mode 100644 index 0000000000..baafd40662 --- /dev/null +++ b/src/client/app/common/views/widgets/photo-stream.vue @@ -0,0 +1,104 @@ +<template> +<div class="mkw-photo-stream" :class="$style.root" :data-melt="props.design == 2"> + <mk-widget-container :show-header="props.design == 0" :naked="props.design == 2"> + <template slot="header">%fa:camera%%i18n:desktop.tags.mk-photo-stream-home-widget.title%</template> + + <p :class="$style.fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> + <div :class="$style.stream" v-if="!fetching && images.length > 0"> + <div v-for="image in images" :class="$style.img" :style="`background-image: url(${image.url}?thumbnail&size=256)`"></div> + </div> + <p :class="$style.empty" v-if="!fetching && images.length == 0">%i18n:desktop.tags.mk-photo-stream-home-widget.no-photos%</p> + </mk-widget-container> +</div> +</template> + +<script lang="ts"> +import define from '../../../common/define-widget'; +export default define({ + name: 'photo-stream', + props: () => ({ + design: 0 + }) +}).extend({ + data() { + return { + images: [], + fetching: true, + connection: null, + connectionId: null + }; + }, + mounted() { + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + + this.connection.on('drive_file_created', this.onDriveFileCreated); + + (this as any).api('drive/stream', { + type: 'image/*', + limit: 9 + }).then(images => { + this.images = images; + this.fetching = false; + }); + }, + beforeDestroy() { + this.connection.off('drive_file_created', this.onDriveFileCreated); + (this as any).os.stream.dispose(this.connectionId); + }, + methods: { + onDriveFileCreated(file) { + if (/^image\/.+$/.test(file.type)) { + this.images.unshift(file); + if (this.images.length > 9) this.images.pop(); + } + }, + func() { + if (this.props.design == 2) { + this.props.design = 0; + } else { + this.props.design++; + } + } + } +}); +</script> + +<style lang="stylus" module> +.root[data-melt] + .stream + padding 0 + + .img + border solid 4px transparent + border-radius 8px + +.stream + display -webkit-flex + display -moz-flex + display -ms-flex + display flex + justify-content center + flex-wrap wrap + padding 8px + + .img + flex 1 1 33% + width 33% + height 80px + background-position center center + background-size cover + border solid 2px transparent + border-radius 4px + +.fetching +.empty + margin 0 + padding 16px + text-align center + color #aaa + + > [data-fa] + margin-right 4px + +</style> diff --git a/src/client/app/common/views/widgets/rss.vue b/src/client/app/common/views/widgets/rss.vue new file mode 100644 index 0000000000..4d74b2f7a4 --- /dev/null +++ b/src/client/app/common/views/widgets/rss.vue @@ -0,0 +1,93 @@ +<template> +<div class="mkw-rss" :data-mobile="isMobile"> + <mk-widget-container :show-header="!props.compact"> + <template slot="header">%fa:rss-square%RSS</template> + <button slot="func" title="設定" @click="setting">%fa:cog%</button> + + <p :class="$style.fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> + <div :class="$style.feed" v-else> + <a v-for="item in items" :href="item.link" target="_blank">{{ item.title }}</a> + </div> + </mk-widget-container> +</div> +</template> + +<script lang="ts"> +import define from '../../../common/define-widget'; +export default define({ + name: 'rss', + props: () => ({ + compact: false + }) +}).extend({ + data() { + return { + url: 'http://news.yahoo.co.jp/pickup/rss.xml', + items: [], + fetching: true, + clock: null + }; + }, + mounted() { + this.fetch(); + this.clock = setInterval(this.fetch, 60000); + }, + beforeDestroy() { + clearInterval(this.clock); + }, + methods: { + func() { + this.props.compact = !this.props.compact; + }, + fetch() { + fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.url}`, { + cache: 'no-cache' + }).then(res => { + res.json().then(feed => { + this.items = feed.items; + this.fetching = false; + }); + }); + }, + setting() { + alert('not implemented yet'); + } + } +}); +</script> + +<style lang="stylus" module> +.feed + padding 12px 16px + font-size 0.9em + + > a + display block + padding 4px 0 + color #666 + border-bottom dashed 1px #eee + + &:last-child + border-bottom none + +.fetching + margin 0 + padding 16px + text-align center + color #aaa + + > [data-fa] + margin-right 4px + +&[data-mobile] + .feed + padding 0 + font-size 1em + + > a + padding 8px 16px + + &:nth-child(even) + background rgba(0, 0, 0, 0.05) + +</style> diff --git a/src/client/app/common/views/widgets/server.cpu-memory.vue b/src/client/app/common/views/widgets/server.cpu-memory.vue new file mode 100644 index 0000000000..d75a142568 --- /dev/null +++ b/src/client/app/common/views/widgets/server.cpu-memory.vue @@ -0,0 +1,127 @@ +<template> +<div class="cpu-memory"> + <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none"> + <defs> + <linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0"> + <stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop> + <stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop> + </linearGradient> + <mask :id="cpuMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY"> + <polygon + :points="cpuPolygonPoints" + fill="#fff" + fill-opacity="0.5"/> + <polyline + :points="cpuPolylinePoints" + fill="none" + stroke="#fff" + stroke-width="1"/> + </mask> + </defs> + <rect + x="-1" y="-1" + :width="viewBoxX + 2" :height="viewBoxY + 2" + :style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`"/> + <text x="1" y="5">CPU <tspan>{{ cpuP }}%</tspan></text> + </svg> + <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none"> + <defs> + <linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0"> + <stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop> + <stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop> + </linearGradient> + <mask :id="memMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY"> + <polygon + :points="memPolygonPoints" + fill="#fff" + fill-opacity="0.5"/> + <polyline + :points="memPolylinePoints" + fill="none" + stroke="#fff" + stroke-width="1"/> + </mask> + </defs> + <rect + x="-1" y="-1" + :width="viewBoxX + 2" :height="viewBoxY + 2" + :style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`"/> + <text x="1" y="5">MEM <tspan>{{ memP }}%</tspan></text> + </svg> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as uuid from 'uuid'; + +export default Vue.extend({ + props: ['connection'], + data() { + return { + viewBoxX: 50, + viewBoxY: 30, + stats: [], + cpuGradientId: uuid(), + cpuMaskId: uuid(), + memGradientId: uuid(), + memMaskId: uuid(), + cpuPolylinePoints: '', + memPolylinePoints: '', + cpuPolygonPoints: '', + memPolygonPoints: '', + cpuP: '', + memP: '' + }; + }, + mounted() { + this.connection.on('stats', this.onStats); + }, + beforeDestroy() { + this.connection.off('stats', this.onStats); + }, + methods: { + onStats(stats) { + stats.mem.used = stats.mem.total - stats.mem.free; + this.stats.push(stats); + if (this.stats.length > 50) this.stats.shift(); + + this.cpuPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - s.cpu_usage) * this.viewBoxY}`).join(' '); + this.memPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - (s.mem.used / s.mem.total)) * this.viewBoxY}`).join(' '); + + this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ this.cpuPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`; + this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ this.memPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`; + + this.cpuP = (stats.cpu_usage * 100).toFixed(0); + this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0); + } + } +}); +</script> + +<style lang="stylus" scoped> +.cpu-memory + > svg + display block + padding 10px + width 50% + float left + + &:first-child + padding-right 5px + + &:last-child + padding-left 5px + + > text + font-size 5px + fill rgba(0, 0, 0, 0.55) + + > tspan + opacity 0.5 + + &:after + content "" + display block + clear both +</style> diff --git a/src/client/app/common/views/widgets/server.cpu.vue b/src/client/app/common/views/widgets/server.cpu.vue new file mode 100644 index 0000000000..596c856da8 --- /dev/null +++ b/src/client/app/common/views/widgets/server.cpu.vue @@ -0,0 +1,68 @@ +<template> +<div class="cpu"> + <x-pie class="pie" :value="usage"/> + <div> + <p>%fa:microchip%CPU</p> + <p>{{ meta.cpu.cores }} Cores</p> + <p>{{ meta.cpu.model }}</p> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XPie from './server.pie.vue'; + +export default Vue.extend({ + components: { + XPie + }, + props: ['connection', 'meta'], + data() { + return { + usage: 0 + }; + }, + mounted() { + this.connection.on('stats', this.onStats); + }, + beforeDestroy() { + this.connection.off('stats', this.onStats); + }, + methods: { + onStats(stats) { + this.usage = stats.cpu_usage; + } + } +}); +</script> + +<style lang="stylus" scoped> +.cpu + > .pie + padding 10px + height 100px + float left + + > div + float left + width calc(100% - 100px) + padding 10px 10px 10px 0 + + > p + margin 0 + font-size 12px + color #505050 + + &:first-child + font-weight bold + + > [data-fa] + margin-right 4px + + &:after + content "" + display block + clear both + +</style> diff --git a/src/client/app/common/views/widgets/server.disk.vue b/src/client/app/common/views/widgets/server.disk.vue new file mode 100644 index 0000000000..2af1982a96 --- /dev/null +++ b/src/client/app/common/views/widgets/server.disk.vue @@ -0,0 +1,76 @@ +<template> +<div class="disk"> + <x-pie class="pie" :value="usage"/> + <div> + <p>%fa:R hdd%Storage</p> + <p>Total: {{ total | bytes(1) }}</p> + <p>Available: {{ available | bytes(1) }}</p> + <p>Used: {{ used | bytes(1) }}</p> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XPie from './server.pie.vue'; + +export default Vue.extend({ + components: { + XPie + }, + props: ['connection'], + data() { + return { + usage: 0, + total: 0, + used: 0, + available: 0 + }; + }, + mounted() { + this.connection.on('stats', this.onStats); + }, + beforeDestroy() { + this.connection.off('stats', this.onStats); + }, + methods: { + onStats(stats) { + stats.disk.used = stats.disk.total - stats.disk.free; + this.usage = stats.disk.used / stats.disk.total; + this.total = stats.disk.total; + this.used = stats.disk.used; + this.available = stats.disk.available; + } + } +}); +</script> + +<style lang="stylus" scoped> +.disk + > .pie + padding 10px + height 100px + float left + + > div + float left + width calc(100% - 100px) + padding 10px 10px 10px 0 + + > p + margin 0 + font-size 12px + color #505050 + + &:first-child + font-weight bold + + > [data-fa] + margin-right 4px + + &:after + content "" + display block + clear both + +</style> diff --git a/src/client/app/common/views/widgets/server.info.vue b/src/client/app/common/views/widgets/server.info.vue new file mode 100644 index 0000000000..d243629506 --- /dev/null +++ b/src/client/app/common/views/widgets/server.info.vue @@ -0,0 +1,25 @@ +<template> +<div class="info"> + <p>Maintainer: <b><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></b></p> + <p>Machine: {{ meta.machine }}</p> + <p>Node: {{ meta.node }}</p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['meta'] +}); +</script> + +<style lang="stylus" scoped> +.info + padding 10px 14px + + > p + margin 0 + font-size 12px + color #505050 +</style> diff --git a/src/client/app/common/views/widgets/server.memory.vue b/src/client/app/common/views/widgets/server.memory.vue new file mode 100644 index 0000000000..834a62671d --- /dev/null +++ b/src/client/app/common/views/widgets/server.memory.vue @@ -0,0 +1,76 @@ +<template> +<div class="memory"> + <x-pie class="pie" :value="usage"/> + <div> + <p>%fa:flask%Memory</p> + <p>Total: {{ total | bytes(1) }}</p> + <p>Used: {{ used | bytes(1) }}</p> + <p>Free: {{ free | bytes(1) }}</p> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XPie from './server.pie.vue'; + +export default Vue.extend({ + components: { + XPie + }, + props: ['connection'], + data() { + return { + usage: 0, + total: 0, + used: 0, + free: 0 + }; + }, + mounted() { + this.connection.on('stats', this.onStats); + }, + beforeDestroy() { + this.connection.off('stats', this.onStats); + }, + methods: { + onStats(stats) { + stats.mem.used = stats.mem.total - stats.mem.free; + this.usage = stats.mem.used / stats.mem.total; + this.total = stats.mem.total; + this.used = stats.mem.used; + this.free = stats.mem.free; + } + } +}); +</script> + +<style lang="stylus" scoped> +.memory + > .pie + padding 10px + height 100px + float left + + > div + float left + width calc(100% - 100px) + padding 10px 10px 10px 0 + + > p + margin 0 + font-size 12px + color #505050 + + &:first-child + font-weight bold + + > [data-fa] + margin-right 4px + + &:after + content "" + display block + clear both + +</style> diff --git a/src/client/app/common/views/widgets/server.pie.vue b/src/client/app/common/views/widgets/server.pie.vue new file mode 100644 index 0000000000..ce2cff1d00 --- /dev/null +++ b/src/client/app/common/views/widgets/server.pie.vue @@ -0,0 +1,61 @@ +<template> +<svg viewBox="0 0 1 1" preserveAspectRatio="none"> + <circle + :r="r" + cx="50%" cy="50%" + fill="none" + stroke-width="0.1" + stroke="rgba(0, 0, 0, 0.05)"/> + <circle + :r="r" + cx="50%" cy="50%" + :stroke-dasharray="Math.PI * (r * 2)" + :stroke-dashoffset="strokeDashoffset" + fill="none" + stroke-width="0.1" + :stroke="color"/> + <text x="50%" y="50%" dy="0.05" text-anchor="middle">{{ (value * 100).toFixed(0) }}%</text> +</svg> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + value: { + type: Number, + required: true + } + }, + data() { + return { + r: 0.4 + }; + }, + computed: { + color(): string { + return `hsl(${180 - (this.value * 180)}, 80%, 70%)`; + }, + strokeDashoffset(): number { + return (1 - this.value) * (Math.PI * (this.r * 2)); + } + } +}); +</script> + +<style lang="stylus" scoped> +svg + display block + height 100% + + > circle + transform-origin center + transform rotate(-90deg) + transition stroke-dashoffset 0.5s ease + + > text + font-size 0.15px + fill rgba(0, 0, 0, 0.6) + +</style> diff --git a/src/client/app/common/views/widgets/server.uptimes.vue b/src/client/app/common/views/widgets/server.uptimes.vue new file mode 100644 index 0000000000..06713d83ce --- /dev/null +++ b/src/client/app/common/views/widgets/server.uptimes.vue @@ -0,0 +1,46 @@ +<template> +<div class="uptimes"> + <p>Uptimes</p> + <p>Process: {{ process ? process.toFixed(0) : '---' }}s</p> + <p>OS: {{ os ? os.toFixed(0) : '---' }}s</p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['connection'], + data() { + return { + process: 0, + os: 0 + }; + }, + mounted() { + this.connection.on('stats', this.onStats); + }, + beforeDestroy() { + this.connection.off('stats', this.onStats); + }, + methods: { + onStats(stats) { + this.process = stats.process_uptime; + this.os = stats.os_uptime; + } + } +}); +</script> + +<style lang="stylus" scoped> +.uptimes + padding 10px 14px + + > p + margin 0 + font-size 12px + color #505050 + + &:first-child + font-weight bold +</style> diff --git a/src/client/app/common/views/widgets/server.vue b/src/client/app/common/views/widgets/server.vue new file mode 100644 index 0000000000..3d5248998f --- /dev/null +++ b/src/client/app/common/views/widgets/server.vue @@ -0,0 +1,93 @@ +<template> +<div class="mkw-server"> + <mk-widget-container :show-header="props.design == 0" :naked="props.design == 2"> + <template slot="header">%fa:server%%i18n:desktop.tags.mk-server-home-widget.title%</template> + <button slot="func" @click="toggle" title="%i18n:desktop.tags.mk-server-home-widget.toggle%">%fa:sort%</button> + + <p :class="$style.fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> + <template v-if="!fetching"> + <x-cpu-memory v-show="props.view == 0" :connection="connection"/> + <x-cpu v-show="props.view == 1" :connection="connection" :meta="meta"/> + <x-memory v-show="props.view == 2" :connection="connection"/> + <x-disk v-show="props.view == 3" :connection="connection"/> + <x-uptimes v-show="props.view == 4" :connection="connection"/> + <x-info v-show="props.view == 5" :connection="connection" :meta="meta"/> + </template> + </mk-widget-container> +</div> +</template> + +<script lang="ts"> +import define from '../../../common/define-widget'; +import XCpuMemory from './server.cpu-memory.vue'; +import XCpu from './server.cpu.vue'; +import XMemory from './server.memory.vue'; +import XDisk from './server.disk.vue'; +import XUptimes from './server.uptimes.vue'; +import XInfo from './server.info.vue'; + +export default define({ + name: 'server', + props: () => ({ + design: 0, + view: 0 + }) +}).extend({ + components: { + XCpuMemory, + XCpu, + XMemory, + XDisk, + XUptimes, + XInfo + }, + data() { + return { + fetching: true, + meta: null, + connection: null, + connectionId: null + }; + }, + mounted() { + (this as any).os.getMeta().then(meta => { + this.meta = meta; + this.fetching = false; + }); + + this.connection = (this as any).os.streams.serverStream.getConnection(); + this.connectionId = (this as any).os.streams.serverStream.use(); + }, + beforeDestroy() { + (this as any).os.streams.serverStream.dispose(this.connectionId); + }, + methods: { + toggle() { + if (this.props.view == 5) { + this.props.view = 0; + } else { + this.props.view++; + } + }, + func() { + if (this.props.design == 2) { + this.props.design = 0; + } else { + this.props.design++; + } + } + } +}); +</script> + +<style lang="stylus" module> +.fetching + margin 0 + padding 16px + text-align center + color #aaa + + > [data-fa] + margin-right 4px + +</style> diff --git a/src/client/app/common/views/widgets/slideshow.vue b/src/client/app/common/views/widgets/slideshow.vue new file mode 100644 index 0000000000..ad32299f37 --- /dev/null +++ b/src/client/app/common/views/widgets/slideshow.vue @@ -0,0 +1,159 @@ +<template> +<div class="mkw-slideshow" :data-mobile="isMobile"> + <div @click="choose"> + <p v-if="props.folder === undefined"> + <template v-if="isCustomizeMode">フォルダを指定するには、カスタマイズモードを終了してください</template> + <template v-else>クリックしてフォルダを指定してください</template> + </p> + <p v-if="props.folder !== undefined && images.length == 0 && !fetching">このフォルダには画像がありません</p> + <div ref="slideA" class="slide a"></div> + <div ref="slideB" class="slide b"></div> + </div> +</div> +</template> + +<script lang="ts"> +import * as anime from 'animejs'; +import define from '../../../common/define-widget'; +export default define({ + name: 'slideshow', + props: () => ({ + folder: undefined, + size: 0 + }) +}).extend({ + data() { + return { + images: [], + fetching: true, + clock: null + }; + }, + mounted() { + this.$nextTick(() => { + this.applySize(); + }); + + if (this.props.folder !== undefined) { + this.fetch(); + } + + this.clock = setInterval(this.change, 10000); + }, + beforeDestroy() { + clearInterval(this.clock); + }, + methods: { + func() { + this.resize(); + }, + applySize() { + let h; + + if (this.props.size == 1) { + h = 250; + } else { + h = 170; + } + + this.$el.style.height = `${h}px`; + }, + resize() { + if (this.props.size == 1) { + this.props.size = 0; + } else { + this.props.size++; + } + + this.applySize(); + }, + change() { + if (this.images.length == 0) return; + + const index = Math.floor(Math.random() * this.images.length); + const img = `url(${ this.images[index].url }?thumbnail&size=1024)`; + + (this.$refs.slideB as any).style.backgroundImage = img; + + anime({ + targets: this.$refs.slideB, + opacity: 1, + duration: 1000, + easing: 'linear', + complete: () => { + // 既にこのウィジェットがunmountされていたら要素がない + if ((this.$refs.slideA as any) == null) return; + + (this.$refs.slideA as any).style.backgroundImage = img; + anime({ + targets: this.$refs.slideB, + opacity: 0, + duration: 0 + }); + } + }); + }, + fetch() { + this.fetching = true; + + (this as any).api('drive/files', { + folderId: this.props.folder, + type: 'image/*', + limit: 100 + }).then(images => { + this.images = images; + this.fetching = false; + (this.$refs.slideA as any).style.backgroundImage = ''; + (this.$refs.slideB as any).style.backgroundImage = ''; + this.change(); + }); + }, + choose() { + (this as any).apis.chooseDriveFolder().then(folder => { + this.props.folder = folder ? folder.id : null; + this.fetch(); + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mkw-slideshow + overflow hidden + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + &[data-mobile] + border none + border-radius 8px + box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + + > div + width 100% + height 100% + cursor pointer + + > p + display block + margin 1em + text-align center + color #888 + + > * + pointer-events none + + > .slide + position absolute + top 0 + left 0 + width 100% + height 100% + background-size cover + background-position center + + &.b + opacity 0 + +</style> diff --git a/src/client/app/common/views/widgets/tips.vue b/src/client/app/common/views/widgets/tips.vue new file mode 100644 index 0000000000..bdecc068e1 --- /dev/null +++ b/src/client/app/common/views/widgets/tips.vue @@ -0,0 +1,108 @@ +<template> +<div class="mkw-tips"> + <p ref="tip">%fa:R lightbulb%<span v-html="tip"></span></p> +</div> +</template> + +<script lang="ts"> +import * as anime from 'animejs'; +import define from '../../../common/define-widget'; + +const tips = [ + '<kbd>t</kbd>でタイムラインにフォーカスできます', + '<kbd>p</kbd>または<kbd>n</kbd>で投稿フォームを開きます', + '投稿フォームにはファイルをドラッグ&ドロップできます', + '投稿フォームにクリップボードにある画像データをペーストできます', + 'ドライブにファイルをドラッグ&ドロップしてアップロードできます', + 'ドライブでファイルをドラッグしてフォルダ移動できます', + 'ドライブでフォルダをドラッグしてフォルダ移動できます', + 'ホームは設定からカスタマイズできます', + 'MisskeyはMIT Licenseです', + 'タイムマシンウィジェットを利用すると、簡単に過去のタイムラインに遡れます', + '投稿の ... をクリックして、投稿をユーザーページにピン留めできます', + 'ドライブの容量は(デフォルトで)1GBです', + '投稿に添付したファイルは全てドライブに保存されます', + 'ホームのカスタマイズ中、ウィジェットを右クリックしてデザインを変更できます', + 'タイムライン上部にもウィジェットを設置できます', + '投稿をダブルクリックすると詳細が見れます', + '「**」でテキストを囲むと**強調表示**されます', + 'チャンネルウィジェットを利用すると、よく利用するチャンネルを素早く確認できます', + 'いくつかのウィンドウはブラウザの外に切り離すことができます', + 'カレンダーウィジェットのパーセンテージは、経過の割合を示しています', + 'APIを利用してbotの開発なども行えます', + 'MisskeyはLINEを通じてでも利用できます', + 'まゆかわいいよまゆ', + 'Misskeyは2014年にサービスを開始しました', + '対応ブラウザではMisskeyを開いていなくても通知を受け取れます' +] + +export default define({ + name: 'tips' +}).extend({ + data() { + return { + tip: null, + clock: null + }; + }, + mounted() { + this.$nextTick(() => { + this.set(); + }); + + this.clock = setInterval(this.change, 20000); + }, + beforeDestroy() { + clearInterval(this.clock); + }, + methods: { + set() { + this.tip = tips[Math.floor(Math.random() * tips.length)]; + }, + change() { + anime({ + targets: this.$refs.tip, + opacity: 0, + duration: 500, + easing: 'linear', + complete: this.set + }); + + setTimeout(() => { + anime({ + targets: this.$refs.tip, + opacity: 1, + duration: 500, + easing: 'linear' + }); + }, 500); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mkw-tips + overflow visible !important + + > p + display block + margin 0 + padding 0 12px + text-align center + font-size 0.7em + color #999 + + > [data-fa] + margin-right 4px + + kbd + display inline + padding 0 6px + margin 0 2px + font-size 1em + font-family inherit + border solid 1px #999 + border-radius 2px + +</style> diff --git a/src/client/app/common/views/widgets/version.vue b/src/client/app/common/views/widgets/version.vue new file mode 100644 index 0000000000..30b632b396 --- /dev/null +++ b/src/client/app/common/views/widgets/version.vue @@ -0,0 +1,29 @@ +<template> +<p>ver {{ version }} ({{ codename }})</p> +</template> + +<script lang="ts"> +import { version, codename } from '../../../config'; +import define from '../../../common/define-widget'; +export default define({ + name: 'version' +}).extend({ + data() { + return { + version, + codename + }; + } +}); +</script> + +<style lang="stylus" scoped> +p + display block + margin 0 + padding 0 12px + text-align center + font-size 0.7em + color #aaa + +</style> diff --git a/src/client/app/config.ts b/src/client/app/config.ts new file mode 100644 index 0000000000..522d7ff056 --- /dev/null +++ b/src/client/app/config.ts @@ -0,0 +1,37 @@ +declare const _HOST_: string; +declare const _HOSTNAME_: string; +declare const _URL_: string; +declare const _API_URL_: string; +declare const _WS_URL_: string; +declare const _DOCS_URL_: string; +declare const _STATS_URL_: string; +declare const _STATUS_URL_: string; +declare const _DEV_URL_: string; +declare const _LANG_: string; +declare const _RECAPTCHA_SITEKEY_: string; +declare const _SW_PUBLICKEY_: string; +declare const _THEME_COLOR_: string; +declare const _COPYRIGHT_: string; +declare const _VERSION_: string; +declare const _CODENAME_: string; +declare const _LICENSE_: string; +declare const _GOOGLE_MAPS_API_KEY_: string; + +export const host = _HOST_; +export const hostname = _HOSTNAME_; +export const url = _URL_; +export const apiUrl = _API_URL_; +export const wsUrl = _WS_URL_; +export const docsUrl = _DOCS_URL_; +export const statsUrl = _STATS_URL_; +export const statusUrl = _STATUS_URL_; +export const devUrl = _DEV_URL_; +export const lang = _LANG_; +export const recaptchaSitekey = _RECAPTCHA_SITEKEY_; +export const swPublickey = _SW_PUBLICKEY_; +export const themeColor = _THEME_COLOR_; +export const copyright = _COPYRIGHT_; +export const version = _VERSION_; +export const codename = _CODENAME_; +export const license = _LICENSE_; +export const googleMapsApiKey = _GOOGLE_MAPS_API_KEY_; diff --git a/src/client/app/desktop/api/choose-drive-file.ts b/src/client/app/desktop/api/choose-drive-file.ts new file mode 100644 index 0000000000..fbda600e6e --- /dev/null +++ b/src/client/app/desktop/api/choose-drive-file.ts @@ -0,0 +1,30 @@ +import { url } from '../../config'; +import MkChooseFileFromDriveWindow from '../views/components/choose-file-from-drive-window.vue'; + +export default function(opts) { + return new Promise((res, rej) => { + const o = opts || {}; + + if (document.body.clientWidth > 800) { + const w = new MkChooseFileFromDriveWindow({ + propsData: { + title: o.title, + multiple: o.multiple, + initFolder: o.currentFolder + } + }).$mount(); + w.$once('selected', file => { + res(file); + }); + document.body.appendChild(w.$el); + } else { + window['cb'] = file => { + res(file); + }; + + window.open(url + '/selectdrive', + 'choose_drive_window', + 'height=500, width=800'); + } + }); +} diff --git a/src/client/app/desktop/api/choose-drive-folder.ts b/src/client/app/desktop/api/choose-drive-folder.ts new file mode 100644 index 0000000000..9b33a20d9a --- /dev/null +++ b/src/client/app/desktop/api/choose-drive-folder.ts @@ -0,0 +1,17 @@ +import MkChooseFolderFromDriveWindow from '../views/components/choose-folder-from-drive-window.vue'; + +export default function(opts) { + return new Promise((res, rej) => { + const o = opts || {}; + const w = new MkChooseFolderFromDriveWindow({ + propsData: { + title: o.title, + initFolder: o.currentFolder + } + }).$mount(); + w.$once('selected', folder => { + res(folder); + }); + document.body.appendChild(w.$el); + }); +} diff --git a/src/client/app/desktop/api/contextmenu.ts b/src/client/app/desktop/api/contextmenu.ts new file mode 100644 index 0000000000..b70d7122d3 --- /dev/null +++ b/src/client/app/desktop/api/contextmenu.ts @@ -0,0 +1,16 @@ +import Ctx from '../views/components/context-menu.vue'; + +export default function(e, menu, opts?) { + const o = opts || {}; + const vm = new Ctx({ + propsData: { + menu, + x: e.pageX - window.pageXOffset, + y: e.pageY - window.pageYOffset, + } + }).$mount(); + vm.$once('closed', () => { + if (o.closed) o.closed(); + }); + document.body.appendChild(vm.$el); +} diff --git a/src/client/app/desktop/api/dialog.ts b/src/client/app/desktop/api/dialog.ts new file mode 100644 index 0000000000..07935485b0 --- /dev/null +++ b/src/client/app/desktop/api/dialog.ts @@ -0,0 +1,19 @@ +import Dialog from '../views/components/dialog.vue'; + +export default function(opts) { + return new Promise<string>((res, rej) => { + const o = opts || {}; + const d = new Dialog({ + propsData: { + title: o.title, + text: o.text, + modal: o.modal, + buttons: o.actions + } + }).$mount(); + d.$once('clicked', id => { + res(id); + }); + document.body.appendChild(d.$el); + }); +} diff --git a/src/client/app/desktop/api/input.ts b/src/client/app/desktop/api/input.ts new file mode 100644 index 0000000000..ce26a8112f --- /dev/null +++ b/src/client/app/desktop/api/input.ts @@ -0,0 +1,20 @@ +import InputDialog from '../views/components/input-dialog.vue'; + +export default function(opts) { + return new Promise<string>((res, rej) => { + const o = opts || {}; + const d = new InputDialog({ + propsData: { + title: o.title, + placeholder: o.placeholder, + default: o.default, + type: o.type || 'text', + allowEmpty: o.allowEmpty + } + }).$mount(); + d.$once('done', text => { + res(text); + }); + document.body.appendChild(d.$el); + }); +} diff --git a/src/client/app/desktop/api/notify.ts b/src/client/app/desktop/api/notify.ts new file mode 100644 index 0000000000..1f89f40ce6 --- /dev/null +++ b/src/client/app/desktop/api/notify.ts @@ -0,0 +1,10 @@ +import Notification from '../views/components/ui-notification.vue'; + +export default function(message) { + const vm = new Notification({ + propsData: { + message + } + }).$mount(); + document.body.appendChild(vm.$el); +} diff --git a/src/client/app/desktop/api/post.ts b/src/client/app/desktop/api/post.ts new file mode 100644 index 0000000000..b569610e1d --- /dev/null +++ b/src/client/app/desktop/api/post.ts @@ -0,0 +1,21 @@ +import PostFormWindow from '../views/components/post-form-window.vue'; +import RenoteFormWindow from '../views/components/renote-form-window.vue'; + +export default function(opts) { + const o = opts || {}; + if (o.renote) { + const vm = new RenoteFormWindow({ + propsData: { + renote: o.renote + } + }).$mount(); + document.body.appendChild(vm.$el); + } else { + const vm = new PostFormWindow({ + propsData: { + reply: o.reply + } + }).$mount(); + document.body.appendChild(vm.$el); + } +} diff --git a/src/client/app/desktop/api/update-avatar.ts b/src/client/app/desktop/api/update-avatar.ts new file mode 100644 index 0000000000..dc89adeb86 --- /dev/null +++ b/src/client/app/desktop/api/update-avatar.ts @@ -0,0 +1,98 @@ +import OS from '../../common/mios'; +import { apiUrl } from '../../config'; +import CropWindow from '../views/components/crop-window.vue'; +import ProgressDialog from '../views/components/progress-dialog.vue'; + +export default (os: OS) => (cb, file = null) => { + const fileSelected = file => { + + const w = new CropWindow({ + propsData: { + image: file, + title: 'アバターとして表示する部分を選択', + aspectRatio: 1 / 1 + } + }).$mount(); + + w.$once('cropped', blob => { + const data = new FormData(); + data.append('i', os.i.token); + data.append('file', blob, file.name + '.cropped.png'); + + os.api('drive/folders/find', { + name: 'アイコン' + }).then(iconFolder => { + if (iconFolder.length === 0) { + os.api('drive/folders/create', { + name: 'アイコン' + }).then(iconFolder => { + upload(data, iconFolder); + }); + } else { + upload(data, iconFolder[0]); + } + }); + }); + + w.$once('skipped', () => { + set(file); + }); + + document.body.appendChild(w.$el); + }; + + const upload = (data, folder) => { + const dialog = new ProgressDialog({ + propsData: { + title: '新しいアバターをアップロードしています' + } + }).$mount(); + document.body.appendChild(dialog.$el); + + if (folder) data.append('folderId', folder.id); + + const xhr = new XMLHttpRequest(); + xhr.open('POST', apiUrl + '/drive/files/create', true); + xhr.onload = e => { + const file = JSON.parse((e.target as any).response); + (dialog as any).close(); + set(file); + }; + + xhr.upload.onprogress = e => { + if (e.lengthComputable) (dialog as any).update(e.loaded, e.total); + }; + + xhr.send(data); + }; + + const set = file => { + os.api('i/update', { + avatarId: file.id + }).then(i => { + os.i.avatarId = i.avatarId; + os.i.avatarUrl = i.avatarUrl; + + os.apis.dialog({ + title: '%fa:info-circle%アバターを更新しました', + text: '新しいアバターが反映されるまで時間がかかる場合があります。', + actions: [{ + text: 'わかった' + }] + }); + + if (cb) cb(i); + }); + }; + + if (file) { + fileSelected(file); + } else { + os.apis.chooseDriveFile({ + multiple: false, + title: '%fa:image%アバターにする画像を選択' + }).then(file => { + fileSelected(file); + }); + } +}; diff --git a/src/client/app/desktop/api/update-banner.ts b/src/client/app/desktop/api/update-banner.ts new file mode 100644 index 0000000000..bc3f783e35 --- /dev/null +++ b/src/client/app/desktop/api/update-banner.ts @@ -0,0 +1,104 @@ +import OS from '../../common/mios'; +import { apiUrl } from '../../config'; +import CropWindow from '../views/components/crop-window.vue'; +import ProgressDialog from '../views/components/progress-dialog.vue'; + +export default (os: OS) => { + + const cropImage = file => new Promise((resolve, reject) => { + const w = new CropWindow({ + propsData: { + image: file, + title: 'バナーとして表示する部分を選択', + aspectRatio: 16 / 9 + } + }).$mount(); + + w.$once('cropped', blob => { + const data = new FormData(); + data.append('i', os.i.token); + data.append('file', blob, file.name + '.cropped.png'); + + os.api('drive/folders/find', { + name: 'バナー' + }).then(bannerFolder => { + if (bannerFolder.length === 0) { + os.api('drive/folders/create', { + name: 'バナー' + }).then(iconFolder => { + resolve(upload(data, iconFolder)); + }); + } else { + resolve(upload(data, bannerFolder[0])); + } + }); + }); + + w.$once('skipped', () => { + resolve(file); + }); + + w.$once('cancelled', reject); + + document.body.appendChild(w.$el); + }); + + const upload = (data, folder) => new Promise((resolve, reject) => { + const dialog = new ProgressDialog({ + propsData: { + title: '新しいバナーをアップロードしています' + } + }).$mount(); + document.body.appendChild(dialog.$el); + + if (folder) data.append('folderId', folder.id); + + const xhr = new XMLHttpRequest(); + xhr.open('POST', apiUrl + '/drive/files/create', true); + xhr.onload = e => { + const file = JSON.parse((e.target as any).response); + (dialog as any).close(); + resolve(file); + }; + xhr.onerror = reject; + + xhr.upload.onprogress = e => { + if (e.lengthComputable) (dialog as any).update(e.loaded, e.total); + }; + + xhr.send(data); + }); + + const setBanner = file => { + return os.api('i/update', { + bannerId: file.id + }).then(i => { + os.i.bannerId = i.bannerId; + os.i.bannerUrl = i.bannerUrl; + + os.apis.dialog({ + title: '%fa:info-circle%バナーを更新しました', + text: '新しいバナーが反映されるまで時間がかかる場合があります。', + actions: [{ + text: 'わかった' + }] + }); + + return i; + }); + }; + + return (file = null) => { + const selectedFile = file + ? Promise.resolve(file) + : os.apis.chooseDriveFile({ + multiple: false, + title: '%fa:image%バナーにする画像を選択' + }); + + return selectedFile + .then(cropImage) + .then(setBanner) + .catch(err => err && console.warn(err)); + }; +}; diff --git a/src/client/app/desktop/assets/grid.svg b/src/client/app/desktop/assets/grid.svg new file mode 100644 index 0000000000..d1d72cd8ce --- /dev/null +++ b/src/client/app/desktop/assets/grid.svg @@ -0,0 +1,150 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="32" + height="32" + viewBox="0 0 8.4666665 8.4666669" + version="1.1" + id="svg8" + inkscape:version="0.92.1 r15371" + sodipodi:docname="grid.svg"> + <defs + id="defs2" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="22.4" + inkscape:cx="14.687499" + inkscape:cy="14.558219" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="true" + units="px" + showguides="true" + inkscape:window-width="1920" + inkscape:window-height="1017" + inkscape:window-x="-8" + inkscape:window-y="1072" + inkscape:window-maximized="1"> + <inkscape:grid + type="xygrid" + id="grid3680" + empspacing="8" + empcolor="#ff3fff" + empopacity="0.41176471" /> + </sodipodi:namedview> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="レイヤー 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(0,-288.53331)"> + <path + style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 0,296.99998 v -8.46667 h 8.4666666 l 10e-8,0.26458 H 0.26458333 l 0,8.20209 z" + id="path3684" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccc" /> + <path + style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 4.2333334,292.23748 h 0.2645833 v 0.52916 h 0.5291667 l 0,0.26459 H 4.4979167 v 0.52917 H 4.2333334 v -0.52917 H 3.7041667 l 0,-0.26459 h 0.5291667 z" + id="path4491" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccccccccc" /> + <path + style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 3.4395833,292.76664 0,0.26459 H 2.38125 l 0,-0.26459 z" + id="path4493" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccc" /> + <path + style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 6.3499999,292.76664 10e-8,0.26459 H 5.2916667 l -1e-7,-0.26459 z" + id="path4493-2" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccc" /> + <path + style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 7.6729167,292.76664 v 0.26459 H 6.6145834 v -0.26459 z" + id="path4493-6" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccc" /> + <path + style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 2.1166666,292.76664 1e-7,0.26459 H 1.0583334 l -1e-7,-0.26459 z" + id="path4493-1" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccc" /> + <path + style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 4.2333333,291.97289 0.2645834,0 v -1.05833 l -0.2645834,0 z" + id="path4522" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccc" /> + <path + style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 4.2333334,290.64997 0.2645833,1e-5 v -1.05833 l -0.2645833,-1e-5 z" + id="path4522-7" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccc" /> + <path + style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 4.2333334,294.88331 h 0.2645833 v -1.05833 H 4.2333334 Z" + id="path4522-5" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccc" /> + <path + style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 4.2333333,296.20622 h 0.2645833 v -1.05833 H 4.2333333 Z" + id="path4522-74" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccc" /> + <path + style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 4.2333334,289.32706 0.2645834,10e-6 -10e-8,-0.52918 -0.2645834,-10e-6 z" + id="path4522-7-4" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccc" /> + <path + style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 4.2333332,296.99998 h 0.2645835 l 0,-0.52917 H 4.2333333 Z" + id="path4522-7-4-4" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccc" /> + <path + style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 0.79375,292.76664 -3e-8,0.26459 -0.52916667,0 3e-8,-0.26459 z" + id="path4493-1-7" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccc" /> + <path + style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 8.4666667,292.76664 v 0.26459 l -0.5291667,0 v -0.26459 z" + id="path4493-1-7-2" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccc" /> + </g> +</svg> diff --git a/src/web/app/desktop/assets/header-logo-white.svg b/src/client/app/desktop/assets/header-logo-white.svg similarity index 100% rename from src/web/app/desktop/assets/header-logo-white.svg rename to src/client/app/desktop/assets/header-logo-white.svg diff --git a/src/web/app/desktop/assets/header-logo.svg b/src/client/app/desktop/assets/header-logo.svg similarity index 100% rename from src/web/app/desktop/assets/header-logo.svg rename to src/client/app/desktop/assets/header-logo.svg diff --git a/src/client/app/desktop/assets/index.jpg b/src/client/app/desktop/assets/index.jpg new file mode 100644 index 0000000000..10c412efe2 Binary files /dev/null and b/src/client/app/desktop/assets/index.jpg differ diff --git a/src/web/app/desktop/assets/remove.png b/src/client/app/desktop/assets/remove.png similarity index 100% rename from src/web/app/desktop/assets/remove.png rename to src/client/app/desktop/assets/remove.png diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts new file mode 100644 index 0000000000..b3152e708b --- /dev/null +++ b/src/client/app/desktop/script.ts @@ -0,0 +1,167 @@ +/** + * Desktop Client + */ + +import VueRouter from 'vue-router'; + +// Style +import './style.styl'; +import '../../element.scss'; + +import init from '../init'; +import fuckAdBlock from '../common/scripts/fuck-ad-block'; +import { HomeStreamManager } from '../common/scripts/streaming/home'; +import composeNotification from '../common/scripts/compose-notification'; + +import chooseDriveFolder from './api/choose-drive-folder'; +import chooseDriveFile from './api/choose-drive-file'; +import dialog from './api/dialog'; +import input from './api/input'; +import post from './api/post'; +import notify from './api/notify'; +import updateAvatar from './api/update-avatar'; +import updateBanner from './api/update-banner'; + +import MkIndex from './views/pages/index.vue'; +import MkUser from './views/pages/user/user.vue'; +import MkSelectDrive from './views/pages/selectdrive.vue'; +import MkDrive from './views/pages/drive.vue'; +import MkHomeCustomize from './views/pages/home-customize.vue'; +import MkMessagingRoom from './views/pages/messaging-room.vue'; +import MkNote from './views/pages/note.vue'; +import MkSearch from './views/pages/search.vue'; +import MkOthello from './views/pages/othello.vue'; + +/** + * init + */ +init(async (launch) => { + // Register directives + require('./views/directives'); + + // Register components + require('./views/components'); + require('./views/widgets'); + + // Init router + const router = new VueRouter({ + mode: 'history', + routes: [ + { path: '/', name: 'index', component: MkIndex }, + { path: '/i/customize-home', component: MkHomeCustomize }, + { path: '/i/messaging/:user', component: MkMessagingRoom }, + { path: '/i/drive', component: MkDrive }, + { path: '/i/drive/folder/:folder', component: MkDrive }, + { path: '/selectdrive', component: MkSelectDrive }, + { path: '/search', component: MkSearch }, + { path: '/othello', component: MkOthello }, + { path: '/othello/:game', component: MkOthello }, + { path: '/@:user', component: MkUser }, + { path: '/notes/:note', component: MkNote } + ] + }); + + // Launch the app + const [, os] = launch(router, os => ({ + chooseDriveFolder, + chooseDriveFile, + dialog, + input, + post, + notify, + updateAvatar: updateAvatar(os), + updateBanner: updateBanner(os) + })); + + /** + * Fuck AD Block + */ + fuckAdBlock(os); + + /** + * Init Notification + */ + if ('Notification' in window) { + // 許可を得ていなかったらリクエスト + if ((Notification as any).permission == 'default') { + await Notification.requestPermission(); + } + + if ((Notification as any).permission == 'granted') { + registerNotifications(os.stream); + } + } +}, true); + +function registerNotifications(stream: HomeStreamManager) { + if (stream == null) return; + + if (stream.hasConnection) { + attach(stream.borrow()); + } + + stream.on('connected', connection => { + attach(connection); + }); + + function attach(connection) { + connection.on('drive_file_created', file => { + const _n = composeNotification('drive_file_created', file); + const n = new Notification(_n.title, { + body: _n.body, + icon: _n.icon + }); + setTimeout(n.close.bind(n), 5000); + }); + + connection.on('mention', note => { + const _n = composeNotification('mention', note); + const n = new Notification(_n.title, { + body: _n.body, + icon: _n.icon + }); + setTimeout(n.close.bind(n), 6000); + }); + + connection.on('reply', note => { + const _n = composeNotification('reply', note); + const n = new Notification(_n.title, { + body: _n.body, + icon: _n.icon + }); + setTimeout(n.close.bind(n), 6000); + }); + + connection.on('quote', note => { + const _n = composeNotification('quote', note); + const n = new Notification(_n.title, { + body: _n.body, + icon: _n.icon + }); + setTimeout(n.close.bind(n), 6000); + }); + + connection.on('unread_messaging_message', message => { + const _n = composeNotification('unread_messaging_message', message); + const n = new Notification(_n.title, { + body: _n.body, + icon: _n.icon + }); + n.onclick = () => { + n.close(); + /*(riot as any).mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), { + user: message.user + });*/ + }; + setTimeout(n.close.bind(n), 7000); + }); + + connection.on('othello_invited', matching => { + const _n = composeNotification('othello_invited', matching); + const n = new Notification(_n.title, { + body: _n.body, + icon: _n.icon + }); + }); + } +} diff --git a/src/client/app/desktop/style.styl b/src/client/app/desktop/style.styl new file mode 100644 index 0000000000..49f71fbde7 --- /dev/null +++ b/src/client/app/desktop/style.styl @@ -0,0 +1,50 @@ +@import "../app" +@import "../reset" + +@import "./ui" + +*::input-placeholder + color #D8CBC5 + +* + &:focus + outline none + + &::scrollbar + width 5px + background transparent + + &:horizontal + height 5px + + &::scrollbar-button + width 0 + height 0 + background rgba(0, 0, 0, 0.2) + + &::scrollbar-piece + background transparent + + &:start + background transparent + + &::scrollbar-thumb + background rgba(0, 0, 0, 0.2) + + &:hover + background rgba(0, 0, 0, 0.4) + + &:active + background $theme-color + + &::scrollbar-corner + background rgba(0, 0, 0, 0.2) + +html + height 100% + background #f7f7f7 + +body + display flex + flex-direction column + min-height 100% diff --git a/src/client/app/desktop/ui.styl b/src/client/app/desktop/ui.styl new file mode 100644 index 0000000000..5a8d1718e2 --- /dev/null +++ b/src/client/app/desktop/ui.styl @@ -0,0 +1,125 @@ +@import "../../const" + +button + font-family sans-serif + + * + pointer-events none + +button.ui +.button.ui + display inline-block + cursor pointer + padding 0 14px + margin 0 + min-width 100px + line-height 38px + font-size 14px + color #888 + text-decoration none + background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) + border solid 1px #e2e2e2 + border-radius 4px + outline none + + &.block + display block + + &:focus + &:after + content "" + pointer-events none + position absolute + top -5px + right -5px + bottom -5px + left -5px + border 2px solid rgba($theme-color, 0.3) + border-radius 8px + + &:disabled + opacity 0.7 + cursor default + + &:hover + background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) + border-color #dcdcdc + + &:active + background #ececec + border-color #dcdcdc + + &.primary + color $theme-color-foreground + background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) + border solid 1px lighten($theme-color, 15%) + + &:not(:disabled) + font-weight bold + + &:hover:not(:disabled) + background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) + border-color $theme-color + + &:active:not(:disabled) + background $theme-color + border-color $theme-color + +input:not([type]).ui +input[type='text'].ui +input[type='password'].ui +input[type='email'].ui +input[type='date'].ui +input[type='number'].ui +textarea.ui + display block + padding 10px + width 100% + height 40px + font-family sans-serif + font-size 16px + color #55595c + border solid 1px #dadada + border-radius 4px + + &:hover + border-color #b0b0b0 + + &:focus + border-color $theme-color + +textarea.ui + min-width 100% + max-width 100% + min-height 64px + +.ui.info + display block + margin 1em 0 + padding 0 1em + font-size 90% + color rgba(#000, 0.87) + background #f8f8f9 + border solid 1px rgba(34, 36, 38, 0.22) + border-radius 4px + + > p + opacity 0.8 + + > [data-fa]:first-child + margin-right 0.25em + + &.warn + color #573a08 + background #FFFAF3 + border-color #C9BA9B + +.ui.from.group + display block + margin 16px 0 + + > p:first-child + margin 0 0 6px 0 + font-size 90% + font-weight bold + color rgba(#373a3c, 0.9) diff --git a/src/client/app/desktop/views/components/activity.calendar.vue b/src/client/app/desktop/views/components/activity.calendar.vue new file mode 100644 index 0000000000..8b43536c2b --- /dev/null +++ b/src/client/app/desktop/views/components/activity.calendar.vue @@ -0,0 +1,66 @@ +<template> +<svg viewBox="0 0 21 7" preserveAspectRatio="none"> + <rect v-for="record in data" class="day" + width="1" height="1" + :x="record.x" :y="record.date.weekday" + rx="1" ry="1" + fill="transparent"> + <title>{{ record.date.year }}/{{ record.date.month }}/{{ record.date.day }}</title> + </rect> + <rect v-for="record in data" class="day" + :width="record.v" :height="record.v" + :x="record.x + ((1 - record.v) / 2)" :y="record.date.weekday + ((1 - record.v) / 2)" + rx="1" ry="1" + :fill="record.color" + style="pointer-events: none;"/> + <rect class="today" + width="1" height="1" + :x="data[data.length - 1].x" :y="data[data.length - 1].date.weekday" + rx="1" ry="1" + fill="none" + stroke-width="0.1" + stroke="#f73520"/> +</svg> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['data'], + created() { + this.data.forEach(d => d.total = d.notes + d.replies + d.renotes); + const peak = Math.max.apply(null, this.data.map(d => d.total)); + + let x = 0; + this.data.reverse().forEach(d => { + d.x = x; + d.date.weekday = (new Date(d.date.year, d.date.month - 1, d.date.day)).getDay(); + + d.v = peak == 0 ? 0 : d.total / (peak / 2); + if (d.v > 1) d.v = 1; + const ch = d.date.weekday == 0 || d.date.weekday == 6 ? 275 : 170; + const cs = d.v * 100; + const cl = 15 + ((1 - d.v) * 80); + d.color = `hsl(${ch}, ${cs}%, ${cl}%)`; + + if (d.date.weekday == 6) x++; + }); + } +}); +</script> + +<style lang="stylus" scoped> +svg + display block + padding 10px + width 100% + + > rect + transform-origin center + + &.day + &:hover + fill rgba(0, 0, 0, 0.05) + +</style> diff --git a/src/client/app/desktop/views/components/activity.chart.vue b/src/client/app/desktop/views/components/activity.chart.vue new file mode 100644 index 0000000000..175c5d37ed --- /dev/null +++ b/src/client/app/desktop/views/components/activity.chart.vue @@ -0,0 +1,103 @@ +<template> +<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none" @mousedown.prevent="onMousedown"> + <title>Black ... Total<br/>Blue ... Notes<br/>Red ... Replies<br/>Green ... Renotes</title> + <polyline + :points="pointsNote" + fill="none" + stroke-width="1" + stroke="#41ddde"/> + <polyline + :points="pointsReply" + fill="none" + stroke-width="1" + stroke="#f7796c"/> + <polyline + :points="pointsRenote" + fill="none" + stroke-width="1" + stroke="#a1de41"/> + <polyline + :points="pointsTotal" + fill="none" + stroke-width="1" + stroke="#555" + stroke-dasharray="2 2"/> +</svg> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +function dragListen(fn) { + window.addEventListener('mousemove', fn); + window.addEventListener('mouseleave', dragClear.bind(null, fn)); + window.addEventListener('mouseup', dragClear.bind(null, fn)); +} + +function dragClear(fn) { + window.removeEventListener('mousemove', fn); + window.removeEventListener('mouseleave', dragClear); + window.removeEventListener('mouseup', dragClear); +} + +export default Vue.extend({ + props: ['data'], + data() { + return { + viewBoxX: 140, + viewBoxY: 60, + zoom: 1, + pos: 0, + pointsNote: null, + pointsReply: null, + pointsRenote: null, + pointsTotal: null + }; + }, + created() { + this.data.reverse(); + this.data.forEach(d => d.total = d.notes + d.replies + d.renotes); + this.render(); + }, + methods: { + render() { + const peak = Math.max.apply(null, this.data.map(d => d.total)); + if (peak != 0) { + this.pointsNote = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' '); + this.pointsReply = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' '); + this.pointsRenote = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' '); + this.pointsTotal = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' '); + } + }, + onMousedown(e) { + const clickX = e.clientX; + const clickY = e.clientY; + const baseZoom = this.zoom; + const basePos = this.pos; + + // 動かした時 + dragListen(me => { + let moveLeft = me.clientX - clickX; + let moveTop = me.clientY - clickY; + + this.zoom = baseZoom + (-moveTop / 20); + this.pos = basePos + moveLeft; + if (this.zoom < 1) this.zoom = 1; + if (this.pos > 0) this.pos = 0; + if (this.pos < -(((this.data.length - 1) * this.zoom) - this.viewBoxX)) this.pos = -(((this.data.length - 1) * this.zoom) - this.viewBoxX); + + this.render(); + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +svg + display block + padding 10px + width 100% + cursor all-scroll + +</style> diff --git a/src/client/app/desktop/views/components/activity.vue b/src/client/app/desktop/views/components/activity.vue new file mode 100644 index 0000000000..480b956ecc --- /dev/null +++ b/src/client/app/desktop/views/components/activity.vue @@ -0,0 +1,116 @@ +<template> +<div class="mk-activity" :data-melt="design == 2"> + <template v-if="design == 0"> + <p class="title">%fa:chart-bar%%i18n:desktop.tags.mk-activity-widget.title%</p> + <button @click="toggle" title="%i18n:desktop.tags.mk-activity-widget.toggle%">%fa:sort%</button> + </template> + <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> + <template v-else> + <x-calendar v-show="view == 0" :data="[].concat(activity)"/> + <x-chart v-show="view == 1" :data="[].concat(activity)"/> + </template> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XCalendar from './activity.calendar.vue'; +import XChart from './activity.chart.vue'; + +export default Vue.extend({ + components: { + XCalendar, + XChart + }, + props: { + design: { + default: 0 + }, + initView: { + default: 0 + }, + user: { + type: Object, + required: true + } + }, + data() { + return { + fetching: true, + activity: null, + view: this.initView + }; + }, + mounted() { + (this as any).api('aggregation/users/activity', { + userId: this.user.id, + limit: 20 * 7 + }).then(activity => { + this.activity = activity; + this.fetching = false; + }); + }, + methods: { + toggle() { + if (this.view == 1) { + this.view = 0; + this.$emit('viewChanged', this.view); + } else { + this.view++; + this.$emit('viewChanged', this.view); + } + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-activity + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + &[data-melt] + background transparent !important + border none !important + + > .title + z-index 1 + margin 0 + padding 0 16px + line-height 42px + font-size 0.9em + font-weight bold + color #888 + box-shadow 0 1px rgba(0, 0, 0, 0.07) + + > [data-fa] + margin-right 4px + + > button + position absolute + z-index 2 + top 0 + right 0 + padding 0 + width 42px + font-size 0.9em + line-height 42px + color #ccc + + &:hover + color #aaa + + &:active + color #999 + + > .fetching + margin 0 + padding 16px + text-align center + color #aaa + + > [data-fa] + margin-right 4px + +</style> diff --git a/src/web/app/desktop/tags/analog-clock.tag b/src/client/app/desktop/views/components/analog-clock.vue similarity index 74% rename from src/web/app/desktop/tags/analog-clock.tag rename to src/client/app/desktop/views/components/analog-clock.vue index 6cd7103c6e..81eec81598 100644 --- a/src/web/app/desktop/tags/analog-clock.tag +++ b/src/client/app/desktop/views/components/analog-clock.vue @@ -1,36 +1,41 @@ -<mk-analog-clock> - <canvas ref="canvas" width="256" height="256"></canvas> - <style> - :scope - > canvas - display block - width 256px - height 256px - </style> - <script> - const Vec2 = function(x, y) { - this.x = x; - this.y = y; +<template> +<canvas class="mk-analog-clock" ref="canvas" width="256" height="256"></canvas> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { themeColor } from '../../../config'; + +const Vec2 = function(this: any, x, y) { + this.x = x; + this.y = y; +}; + +export default Vue.extend({ + data() { + return { + clock: null }; + }, + mounted() { + this.tick(); + this.clock = setInterval(this.tick, 1000); + }, + beforeDestroy() { + clearInterval(this.clock); + }, + methods: { + tick() { + const canv = this.$refs.canvas as any; - this.on('mount', () => { - this.draw() - this.clock = setInterval(this.draw, 1000); - }); - - this.on('unmount', () => { - clearInterval(this.clock); - }); - - this.draw = () => { const now = new Date(); const s = now.getSeconds(); const m = now.getMinutes(); const h = now.getHours(); - const ctx = this.refs.canvas.getContext('2d'); - const canvW = this.refs.canvas.width; - const canvH = this.refs.canvas.height; + const ctx = canv.getContext('2d'); + const canvW = canv.width; + const canvH = canv.height; ctx.clearRect(0, 0, canvW, canvH); { // 背景 @@ -72,7 +77,7 @@ const length = Math.min(canvW, canvH) / 4; const uv = new Vec2(Math.sin(angle), -Math.cos(angle)); ctx.beginPath(); - ctx.strokeStyle = THEME_COLOR; + ctx.strokeStyle = themeColor; ctx.lineWidth = 2; ctx.moveTo(canvW / 2 - uv.x * length / 5, canvH / 2 - uv.y * length / 5); ctx.lineTo(canvW / 2 + uv.x * length, canvH / 2 + uv.y * length); @@ -90,6 +95,14 @@ ctx.lineTo(canvW / 2 + uv.x * length, canvH / 2 + uv.y * length); ctx.stroke(); } - }; - </script> -</mk-analog-clock> + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-analog-clock + display block + width 256px + height 256px +</style> diff --git a/src/client/app/desktop/views/components/calendar.vue b/src/client/app/desktop/views/components/calendar.vue new file mode 100644 index 0000000000..71aab2e8a5 --- /dev/null +++ b/src/client/app/desktop/views/components/calendar.vue @@ -0,0 +1,252 @@ +<template> +<div class="mk-calendar" :data-melt="design == 4 || design == 5"> + <template v-if="design == 0 || design == 1"> + <button @click="prev" title="%i18n:desktop.tags.mk-calendar-widget.prev%">%fa:chevron-circle-left%</button> + <p class="title">{{ '%i18n:desktop.tags.mk-calendar-widget.title%'.replace('{1}', year).replace('{2}', month) }}</p> + <button @click="next" title="%i18n:desktop.tags.mk-calendar-widget.next%">%fa:chevron-circle-right%</button> + </template> + + <div class="calendar"> + <template v-if="design == 0 || design == 2 || design == 4"> + <div class="weekday" + v-for="(day, i) in Array(7).fill(0)" + :data-today="year == today.getFullYear() && month == today.getMonth() + 1 && today.getDay() == i" + :data-is-donichi="i == 0 || i == 6" + >{{ weekdayText[i] }}</div> + </template> + <div v-for="n in paddingDays"></div> + <div class="day" v-for="(day, i) in days" + :data-today="isToday(i + 1)" + :data-selected="isSelected(i + 1)" + :data-is-out-of-range="isOutOfRange(i + 1)" + :data-is-donichi="isDonichi(i + 1)" + @click="go(i + 1)" + :title="isOutOfRange(i + 1) ? null : '%i18n:desktop.tags.mk-calendar-widget.go%'" + > + <div>{{ i + 1 }}</div> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +const eachMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + +function isLeapYear(year) { + return (year % 400 == 0) ? true : + (year % 100 == 0) ? false : + (year % 4 == 0) ? true : + false; +} + +export default Vue.extend({ + props: { + design: { + default: 0 + }, + start: { + type: Date, + required: false + } + }, + data() { + return { + today: new Date(), + year: new Date().getFullYear(), + month: new Date().getMonth() + 1, + selected: new Date(), + weekdayText: [ + '%i18n:common.weekday-short.sunday%', + '%i18n:common.weekday-short.monday%', + '%i18n:common.weekday-short.tuesday%', + '%i18n:common.weekday-short.wednesday%', + '%i18n:common.weekday-short.thursday%', + '%i18n:common.weekday-short.friday%', + '%i18n:common.weekday-short.satruday%' + ] + }; + }, + computed: { + paddingDays(): number { + const date = new Date(this.year, this.month - 1, 1); + return date.getDay(); + }, + days(): number { + let days = eachMonthDays[this.month - 1]; + + // うるう年なら+1日 + if (this.month == 2 && isLeapYear(this.year)) days++; + + return days; + } + }, + methods: { + isToday(day) { + return this.year == this.today.getFullYear() && this.month == this.today.getMonth() + 1 && day == this.today.getDate(); + }, + + isSelected(day) { + return this.year == this.selected.getFullYear() && this.month == this.selected.getMonth() + 1 && day == this.selected.getDate(); + }, + + isOutOfRange(day) { + const test = (new Date(this.year, this.month - 1, day)).getTime(); + return test > this.today.getTime() || + (this.start ? test < (this.start as any).getTime() : false); + }, + + isDonichi(day) { + const weekday = (new Date(this.year, this.month - 1, day)).getDay(); + return weekday == 0 || weekday == 6; + }, + + prev() { + if (this.month == 1) { + this.year = this.year - 1; + this.month = 12; + } else { + this.month--; + } + }, + + next() { + if (this.month == 12) { + this.year = this.year + 1; + this.month = 1; + } else { + this.month++; + } + }, + + go(day) { + if (this.isOutOfRange(day)) return; + const date = new Date(this.year, this.month - 1, day, 23, 59, 59, 999); + this.selected = date; + this.$emit('chosen', this.selected); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-calendar + color #777 + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + &[data-melt] + background transparent !important + border none !important + + > .title + z-index 1 + margin 0 + padding 0 16px + text-align center + line-height 42px + font-size 0.9em + font-weight bold + color #888 + box-shadow 0 1px rgba(0, 0, 0, 0.07) + + > [data-fa] + margin-right 4px + + > button + position absolute + z-index 2 + top 0 + padding 0 + width 42px + font-size 0.9em + line-height 42px + color #ccc + + &:hover + color #aaa + + &:active + color #999 + + &:first-of-type + left 0 + + &:last-of-type + right 0 + + > .calendar + display flex + flex-wrap wrap + padding 16px + + * + user-select none + + > div + width calc(100% * (1/7)) + text-align center + line-height 32px + font-size 14px + + &.weekday + color #19a2a9 + + &[data-is-donichi] + color #ef95a0 + + &[data-today] + box-shadow 0 0 0 1px #19a2a9 inset + border-radius 6px + + &[data-is-donichi] + box-shadow 0 0 0 1px #ef95a0 inset + + &.day + cursor pointer + color #777 + + > div + border-radius 6px + + &:hover > div + background rgba(0, 0, 0, 0.025) + + &:active > div + background rgba(0, 0, 0, 0.05) + + &[data-is-donichi] + color #ef95a0 + + &[data-is-out-of-range] + cursor default + color rgba(#777, 0.5) + + &[data-is-donichi] + color rgba(#ef95a0, 0.5) + + &[data-selected] + font-weight bold + + > div + background rgba(0, 0, 0, 0.025) + + &:active > div + background rgba(0, 0, 0, 0.05) + + &[data-today] + > div + color $theme-color-foreground + background $theme-color + + &:hover > div + background lighten($theme-color, 10%) + + &:active > div + background darken($theme-color, 10%) + +</style> diff --git a/src/client/app/desktop/views/components/choose-file-from-drive-window.vue b/src/client/app/desktop/views/components/choose-file-from-drive-window.vue new file mode 100644 index 0000000000..9a1e9c958a --- /dev/null +++ b/src/client/app/desktop/views/components/choose-file-from-drive-window.vue @@ -0,0 +1,180 @@ +<template> +<mk-window ref="window" is-modal width="800px" height="500px" @closed="$destroy"> + <span slot="header"> + <span v-html="title" :class="$style.title"></span> + <span :class="$style.count" v-if="multiple && files.length > 0">({{ files.length }}ファイル選択中)</span> + </span> + + <mk-drive + ref="browser" + :class="$style.browser" + :multiple="multiple" + @selected="onSelected" + @change-selection="onChangeSelection" + /> + <div :class="$style.footer"> + <button :class="$style.upload" title="PCからドライブにファイルをアップロード" @click="upload">%fa:upload%</button> + <button :class="$style.cancel" @click="cancel">キャンセル</button> + <button :class="$style.ok" :disabled="multiple && files.length == 0" @click="ok">決定</button> + </div> +</mk-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: { + multiple: { + default: false + }, + title: { + default: '%fa:R file%ファイルを選択' + } + }, + data() { + return { + files: [] + }; + }, + methods: { + onSelected(file) { + this.files = [file]; + this.ok(); + }, + onChangeSelection(files) { + this.files = files; + }, + upload() { + (this.$refs.browser as any).selectLocalFile(); + }, + ok() { + this.$emit('selected', this.multiple ? this.files : this.files[0]); + (this.$refs.window as any).close(); + }, + cancel() { + (this.$refs.window as any).close(); + } + } +}); +</script> + +<style lang="stylus" module> +@import '~const.styl' + +.title + > [data-fa] + margin-right 4px + +.count + margin-left 8px + opacity 0.7 + +.browser + height calc(100% - 72px) + +.footer + height 72px + background lighten($theme-color, 95%) + +.upload + display inline-block + position absolute + top 8px + left 16px + cursor pointer + padding 0 + margin 8px 4px 0 0 + width 40px + height 40px + font-size 1em + color rgba($theme-color, 0.5) + background transparent + outline none + border solid 1px transparent + border-radius 4px + + &:hover + background transparent + border-color rgba($theme-color, 0.3) + + &:active + color rgba($theme-color, 0.6) + background transparent + border-color rgba($theme-color, 0.5) + box-shadow 0 2px 4px rgba(darken($theme-color, 50%), 0.15) inset + + &:focus + &:after + content "" + pointer-events none + position absolute + top -5px + right -5px + bottom -5px + left -5px + border 2px solid rgba($theme-color, 0.3) + border-radius 8px + +.ok +.cancel + display block + position absolute + bottom 16px + cursor pointer + padding 0 + margin 0 + width 120px + height 40px + font-size 1em + outline none + border-radius 4px + + &:focus + &:after + content "" + pointer-events none + position absolute + top -5px + right -5px + bottom -5px + left -5px + border 2px solid rgba($theme-color, 0.3) + border-radius 8px + + &:disabled + opacity 0.7 + cursor default + +.ok + right 16px + color $theme-color-foreground + background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) + border solid 1px lighten($theme-color, 15%) + + &:not(:disabled) + font-weight bold + + &:hover:not(:disabled) + background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) + border-color $theme-color + + &:active:not(:disabled) + background $theme-color + border-color $theme-color + +.cancel + right 148px + color #888 + background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) + border solid 1px #e2e2e2 + + &:hover + background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) + border-color #dcdcdc + + &:active + background #ececec + border-color #dcdcdc + +</style> + diff --git a/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue b/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue new file mode 100644 index 0000000000..f99533176d --- /dev/null +++ b/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue @@ -0,0 +1,114 @@ +<template> +<mk-window ref="window" is-modal width="800px" height="500px" @closed="$destroy"> + <span slot="header"> + <span v-html="title" :class="$style.title"></span> + </span> + + <mk-drive + ref="browser" + :class="$style.browser" + :multiple="false" + /> + <div :class="$style.footer"> + <button :class="$style.cancel" @click="cancel">キャンセル</button> + <button :class="$style.ok" @click="ok">決定</button> + </div> +</mk-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: { + title: { + default: '%fa:R folder%フォルダを選択' + } + }, + methods: { + ok() { + this.$emit('selected', (this.$refs.browser as any).folder); + (this.$refs.window as any).close(); + }, + cancel() { + (this.$refs.window as any).close(); + } + } +}); +</script> + +<style lang="stylus" module> +@import '~const.styl' + +.title + > [data-fa] + margin-right 4px + +.browser + height calc(100% - 72px) + +.footer + height 72px + background lighten($theme-color, 95%) + +.ok +.cancel + display block + position absolute + bottom 16px + cursor pointer + padding 0 + margin 0 + width 120px + height 40px + font-size 1em + outline none + border-radius 4px + + &:focus + &:after + content "" + pointer-events none + position absolute + top -5px + right -5px + bottom -5px + left -5px + border 2px solid rgba($theme-color, 0.3) + border-radius 8px + + &:disabled + opacity 0.7 + cursor default + +.ok + right 16px + color $theme-color-foreground + background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) + border solid 1px lighten($theme-color, 15%) + + &:not(:disabled) + font-weight bold + + &:hover:not(:disabled) + background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) + border-color $theme-color + + &:active:not(:disabled) + background $theme-color + border-color $theme-color + +.cancel + right 148px + color #888 + background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) + border solid 1px #e2e2e2 + + &:hover + background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) + border-color #dcdcdc + + &:active + background #ececec + border-color #dcdcdc + +</style> diff --git a/src/client/app/desktop/views/components/context-menu.menu.vue b/src/client/app/desktop/views/components/context-menu.menu.vue new file mode 100644 index 0000000000..6359dbf1b4 --- /dev/null +++ b/src/client/app/desktop/views/components/context-menu.menu.vue @@ -0,0 +1,121 @@ +<template> +<ul class="menu"> + <li v-for="(item, i) in menu" :class="item.type"> + <template v-if="item.type == 'item'"> + <p @click="click(item)"><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</p> + </template> + <template v-if="item.type == 'link'"> + <a :href="item.href" :target="item.target" @click="click(item)"><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</a> + </template> + <template v-else-if="item.type == 'nest'"> + <p><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}...<span class="caret">%fa:caret-right%</span></p> + <me-nu :menu="item.menu" @x="click"/> + </template> + </li> +</ul> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + name: 'me-nu', + props: ['menu'], + methods: { + click(item) { + this.$emit('x', item); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.menu + $width = 240px + $item-height = 38px + $padding = 10px + + margin 0 + padding $padding 0 + list-style none + + li + display block + + &.divider + margin-top $padding + padding-top $padding + border-top solid 1px #eee + + &.nest + > p + cursor default + + > .caret + position absolute + top 0 + right 8px + + > * + line-height $item-height + width 28px + text-align center + + &:hover > ul + visibility visible + + &:active + > p, a + background $theme-color + + > p, a + display block + z-index 1 + margin 0 + padding 0 32px 0 38px + line-height $item-height + color #868C8C + text-decoration none + cursor pointer + + &:hover + text-decoration none + + * + pointer-events none + + &:hover + > p, a + text-decoration none + background $theme-color + color $theme-color-foreground + + &:active + > p, a + text-decoration none + background darken($theme-color, 10%) + color $theme-color-foreground + + li > ul + visibility hidden + position absolute + top 0 + left $width + margin-top -($padding) + width $width + background #fff + border-radius 0 4px 4px 4px + box-shadow 2px 2px 8px rgba(0, 0, 0, 0.2) + transition visibility 0s linear 0.2s + +</style> + +<style lang="stylus" module> +.icon + > * + width 28px + margin-left -28px + text-align center +</style> + diff --git a/src/client/app/desktop/views/components/context-menu.vue b/src/client/app/desktop/views/components/context-menu.vue new file mode 100644 index 0000000000..8bd9945840 --- /dev/null +++ b/src/client/app/desktop/views/components/context-menu.vue @@ -0,0 +1,74 @@ +<template> +<div class="context-menu" :style="{ left: `${x}px`, top: `${y}px` }" @contextmenu.prevent="() => {}"> + <x-menu :menu="menu" @x="click"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as anime from 'animejs'; +import contains from '../../../common/scripts/contains'; +import XMenu from './context-menu.menu.vue'; + +export default Vue.extend({ + components: { + XMenu + }, + props: ['x', 'y', 'menu'], + mounted() { + this.$nextTick(() => { + Array.from(document.querySelectorAll('body *')).forEach(el => { + el.addEventListener('mousedown', this.onMousedown); + }); + + this.$el.style.display = 'block'; + + anime({ + targets: this.$el, + opacity: [0, 1], + duration: 100, + easing: 'linear' + }); + }); + }, + methods: { + onMousedown(e) { + e.preventDefault(); + if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close(); + return false; + }, + click(item) { + if (item.onClick) item.onClick(); + this.close(); + }, + close() { + Array.from(document.querySelectorAll('body *')).forEach(el => { + el.removeEventListener('mousedown', this.onMousedown); + }); + + this.$emit('closed'); + this.$destroy(); + } + } +}); +</script> + +<style lang="stylus" scoped> +.context-menu + $width = 240px + $item-height = 38px + $padding = 10px + + display none + position fixed + top 0 + left 0 + z-index 4096 + width $width + font-size 0.8em + background #fff + border-radius 0 4px 4px 4px + box-shadow 2px 2px 8px rgba(0, 0, 0, 0.2) + opacity 0 + +</style> diff --git a/src/client/app/desktop/views/components/crop-window.vue b/src/client/app/desktop/views/components/crop-window.vue new file mode 100644 index 0000000000..eb6a55d959 --- /dev/null +++ b/src/client/app/desktop/views/components/crop-window.vue @@ -0,0 +1,178 @@ +<template> + <mk-window ref="window" is-modal width="800px" :can-close="false"> + <span slot="header">%fa:crop%{{ title }}</span> + <div class="body"> + <vue-cropper ref="cropper" + :src="image.url" + :view-mode="1" + :aspect-ratio="aspectRatio" + :container-style="{ width: '100%', 'max-height': '400px' }" + /> + </div> + <div :class="$style.actions"> + <button :class="$style.skip" @click="skip">クロップをスキップ</button> + <button :class="$style.cancel" @click="cancel">キャンセル</button> + <button :class="$style.ok" @click="ok">決定</button> + </div> + </mk-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import VueCropper from 'vue-cropperjs'; + +export default Vue.extend({ + components: { + VueCropper + }, + props: { + image: { + type: Object, + required: true + }, + title: { + type: String, + required: true + }, + aspectRatio: { + type: Number, + required: true + } + }, + methods: { + ok() { + (this.$refs.cropper as any).getCroppedCanvas().toBlob(blob => { + this.$emit('cropped', blob); + (this.$refs.window as any).close(); + }); + }, + + skip() { + this.$emit('skipped'); + (this.$refs.window as any).close(); + }, + + cancel() { + this.$emit('canceled'); + (this.$refs.window as any).close(); + } + } +}); +</script> + +<style lang="stylus" module> +@import '~const.styl' + +.header + > [data-fa] + margin-right 4px + +.img + width 100% + max-height 400px + +.actions + height 72px + background lighten($theme-color, 95%) + +.ok +.cancel +.skip + display block + position absolute + bottom 16px + cursor pointer + padding 0 + margin 0 + height 40px + font-size 1em + outline none + border-radius 4px + + &:focus + &:after + content "" + pointer-events none + position absolute + top -5px + right -5px + bottom -5px + left -5px + border 2px solid rgba($theme-color, 0.3) + border-radius 8px + + &:disabled + opacity 0.7 + cursor default + +.ok +.cancel + width 120px + +.ok + right 16px + color $theme-color-foreground + background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) + border solid 1px lighten($theme-color, 15%) + + &:not(:disabled) + font-weight bold + + &:hover:not(:disabled) + background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) + border-color $theme-color + + &:active:not(:disabled) + background $theme-color + border-color $theme-color + +.cancel +.skip + color #888 + background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) + border solid 1px #e2e2e2 + + &:hover + background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) + border-color #dcdcdc + + &:active + background #ececec + border-color #dcdcdc + +.cancel + right 148px + +.skip + left 16px + width 150px + +</style> + +<style lang="stylus"> +.cropper-modal { + opacity: 0.8; +} + +.cropper-view-box { + outline-color: $theme-color; +} + +.cropper-line, .cropper-point { + background-color: $theme-color; +} + +.cropper-bg { + animation: cropper-bg 0.5s linear infinite; +} + +@keyframes cropper-bg { + 0% { + background-position: 0 0; + } + + 100% { + background-position: -8px -8px; + } +} +</style> diff --git a/src/client/app/desktop/views/components/dialog.vue b/src/client/app/desktop/views/components/dialog.vue new file mode 100644 index 0000000000..fa17e4a9d2 --- /dev/null +++ b/src/client/app/desktop/views/components/dialog.vue @@ -0,0 +1,170 @@ +<template> +<div class="mk-dialog"> + <div class="bg" ref="bg" @click="onBgClick"></div> + <div class="main" ref="main"> + <header v-html="title" :class="$style.header"></header> + <div class="body" v-html="text"></div> + <div class="buttons"> + <button v-for="button in buttons" @click="click(button)">{{ button.text }}</button> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as anime from 'animejs'; + +export default Vue.extend({ + props: { + title: { + type: String, + required: false + }, + text: { + type: String, + required: true + }, + buttons: { + type: Array, + default: () => { + return [{ + text: 'OK' + }]; + } + }, + modal: { + type: Boolean, + default: false + } + }, + mounted() { + this.$nextTick(() => { + (this.$refs.bg as any).style.pointerEvents = 'auto'; + anime({ + targets: this.$refs.bg, + opacity: 1, + duration: 100, + easing: 'linear' + }); + + anime({ + targets: this.$refs.main, + opacity: 1, + scale: [1.2, 1], + duration: 300, + easing: [0, 0.5, 0.5, 1] + }); + }); + }, + methods: { + click(button) { + this.$emit('clicked', button.id); + this.close(); + }, + close() { + (this.$refs.bg as any).style.pointerEvents = 'none'; + anime({ + targets: this.$refs.bg, + opacity: 0, + duration: 300, + easing: 'linear' + }); + + (this.$refs.main as any).style.pointerEvents = 'none'; + anime({ + targets: this.$refs.main, + opacity: 0, + scale: 0.8, + duration: 300, + easing: [ 0.5, -0.5, 1, 0.5 ], + complete: () => this.$destroy() + }); + }, + onBgClick() { + if (!this.modal) { + this.close(); + } + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-dialog + > .bg + display block + position fixed + z-index 8192 + top 0 + left 0 + width 100% + height 100% + background rgba(0, 0, 0, 0.7) + opacity 0 + pointer-events none + + > .main + display block + position fixed + z-index 8192 + top 20% + left 0 + right 0 + margin 0 auto 0 auto + padding 32px 42px + width 480px + background #fff + opacity 0 + + > .body + margin 1em 0 + color #888 + + > .buttons + > button + display inline-block + float right + margin 0 + padding 10px 10px + font-size 1.1em + font-weight normal + text-decoration none + color #888 + background transparent + outline none + border none + border-radius 0 + cursor pointer + transition color 0.1s ease + + i + margin 0 0.375em + + &:hover + color $theme-color + + &:active + color darken($theme-color, 10%) + transition color 0s ease + +</style> + +<style lang="stylus" module> +@import '~const.styl' + +.header + margin 1em 0 + color $theme-color + // color #43A4EC + font-weight bold + + &:empty + display none + + > i + margin-right 0.5em + +</style> diff --git a/src/client/app/desktop/views/components/drive-window.vue b/src/client/app/desktop/views/components/drive-window.vue new file mode 100644 index 0000000000..3a072f4794 --- /dev/null +++ b/src/client/app/desktop/views/components/drive-window.vue @@ -0,0 +1,56 @@ +<template> +<mk-window ref="window" @closed="$destroy" width="800px" height="500px" :popout-url="popout"> + <template slot="header"> + <p v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> %i18n:desktop.tags.mk-drive-browser-window.used%</p> + <span :class="$style.title">%fa:cloud%%i18n:desktop.tags.mk-drive-browser-window.drive%</span> + </template> + <mk-drive :class="$style.browser" multiple :init-folder="folder" ref="browser"/> +</mk-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { url } from '../../../config'; + +export default Vue.extend({ + props: ['folder'], + data() { + return { + usage: null + }; + }, + mounted() { + (this as any).api('drive').then(info => { + this.usage = info.usage / info.capacity * 100; + }); + }, + methods: { + popout() { + const folder = (this.$refs.browser as any) ? (this.$refs.browser as any).folder : null; + if (folder) { + return `${url}/i/drive/folder/${folder.id}`; + } else { + return `${url}/i/drive`; + } + } + } +}); +</script> + +<style lang="stylus" module> +.title + > [data-fa] + margin-right 4px + +.info + position absolute + top 0 + left 16px + margin 0 + font-size 80% + +.browser + height 100% + +</style> + diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue new file mode 100644 index 0000000000..85f8361c9f --- /dev/null +++ b/src/client/app/desktop/views/components/drive.file.vue @@ -0,0 +1,317 @@ +<template> +<div class="root file" + :data-is-selected="isSelected" + :data-is-contextmenu-showing="isContextmenuShowing" + @click="onClick" + draggable="true" + @dragstart="onDragstart" + @dragend="onDragend" + @contextmenu.prevent.stop="onContextmenu" + :title="title" +> + <div class="label" v-if="os.i.avatarId == file.id"><img src="/assets/label.svg"/> + <p>%i18n:desktop.tags.mk-drive-browser-file.avatar%</p> + </div> + <div class="label" v-if="os.i.bannerId == file.id"><img src="/assets/label.svg"/> + <p>%i18n:desktop.tags.mk-drive-browser-file.banner%</p> + </div> + <div class="thumbnail" ref="thumbnail" :style="`background-color: ${ background }`"> + <img :src="`${file.url}?thumbnail&size=128`" alt="" @load="onThumbnailLoaded"/> + </div> + <p class="name"> + <span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span> + <span class="ext" v-if="file.name.lastIndexOf('.') != -1">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span> + </p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as anime from 'animejs'; +import contextmenu from '../../api/contextmenu'; +import copyToClipboard from '../../../common/scripts/copy-to-clipboard'; + +export default Vue.extend({ + props: ['file'], + data() { + return { + isContextmenuShowing: false, + isDragging: false + }; + }, + computed: { + browser(): any { + return this.$parent; + }, + isSelected(): boolean { + return this.browser.selectedFiles.some(f => f.id == this.file.id); + }, + title(): string { + return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.datasize)}`; + }, + background(): string { + return this.file.properties.avgColor + ? `rgb(${this.file.properties.avgColor.join(',')})` + : 'transparent'; + } + }, + methods: { + onClick() { + this.browser.chooseFile(this.file); + }, + + onContextmenu(e) { + this.isContextmenuShowing = true; + contextmenu(e, [{ + type: 'item', + text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.rename%', + icon: '%fa:i-cursor%', + onClick: this.rename + }, { + type: 'item', + text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.copy-url%', + icon: '%fa:link%', + onClick: this.copyUrl + }, { + type: 'link', + href: `${this.file.url}?download`, + text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.download%', + icon: '%fa:download%', + }, { + type: 'divider', + }, { + type: 'item', + text: '%i18n:common.delete%', + icon: '%fa:R trash-alt%', + onClick: this.deleteFile + }, { + type: 'divider', + }, { + type: 'nest', + text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.else-files%', + menu: [{ + type: 'item', + text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.set-as-avatar%', + onClick: this.setAsAvatar + }, { + type: 'item', + text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.set-as-banner%', + onClick: this.setAsBanner + }] + }, { + type: 'nest', + text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.open-in-app%', + menu: [{ + type: 'item', + text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.add-app%...', + onClick: this.addApp + }] + }], { + closed: () => { + this.isContextmenuShowing = false; + } + }); + }, + + onDragstart(e) { + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('mk_drive_file', JSON.stringify(this.file)); + this.isDragging = true; + + // 親ブラウザに対して、ドラッグが開始されたフラグを立てる + // (=あなたの子供が、ドラッグを開始しましたよ) + this.browser.isDragSource = true; + }, + + onDragend(e) { + this.isDragging = false; + this.browser.isDragSource = false; + }, + + onThumbnailLoaded() { + if (this.file.properties.avgColor) { + anime({ + targets: this.$refs.thumbnail, + backgroundColor: `rgba(${this.file.properties.avgColor.join(',')}, 0)`, + duration: 100, + easing: 'linear' + }); + } + }, + + rename() { + (this as any).apis.input({ + title: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.rename-file%', + placeholder: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.input-new-file-name%', + default: this.file.name, + allowEmpty: false + }).then(name => { + (this as any).api('drive/files/update', { + fileId: this.file.id, + name: name + }) + }); + }, + + copyUrl() { + copyToClipboard(this.file.url); + (this as any).apis.dialog({ + title: '%fa:check%%i18n:desktop.tags.mk-drive-browser-file-contextmenu.copied%', + text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.copied-url-to-clipboard%', + actions: [{ + text: '%i18n:common.ok%' + }] + }); + }, + + setAsAvatar() { + (this as any).apis.updateAvatar(this.file); + }, + + setAsBanner() { + (this as any).apis.updateBanner(this.file); + }, + + addApp() { + alert('not implemented yet'); + }, + + deleteFile() { + alert('not implemented yet'); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.root.file + padding 8px 0 0 0 + height 180px + border-radius 4px + + &, * + cursor pointer + + &:hover + background rgba(0, 0, 0, 0.05) + + > .label + &:before + &:after + background #0b65a5 + + &:active + background rgba(0, 0, 0, 0.1) + + > .label + &:before + &:after + background #0b588c + + &[data-is-selected] + background $theme-color + + &:hover + background lighten($theme-color, 10%) + + &:active + background darken($theme-color, 10%) + + > .label + &:before + &:after + display none + + > .name + color $theme-color-foreground + + &[data-is-contextmenu-showing] + &:after + content "" + pointer-events none + position absolute + top -4px + right -4px + bottom -4px + left -4px + border 2px dashed rgba($theme-color, 0.3) + border-radius 4px + + > .label + position absolute + top 0 + left 0 + pointer-events none + + &:before + content "" + display block + position absolute + z-index 1 + top 0 + left 57px + width 28px + height 8px + background #0c7ac9 + + &:after + content "" + display block + position absolute + z-index 1 + top 57px + left 0 + width 8px + height 28px + background #0c7ac9 + + > img + position absolute + z-index 2 + top 0 + left 0 + + > p + position absolute + z-index 3 + top 19px + left -28px + width 120px + margin 0 + text-align center + line-height 28px + color #fff + transform rotate(-45deg) + + > .thumbnail + width 128px + height 128px + margin auto + + > img + display block + position absolute + top 0 + left 0 + right 0 + bottom 0 + margin auto + max-width 128px + max-height 128px + pointer-events none + + > .name + display block + margin 4px 0 0 0 + font-size 0.8em + text-align center + word-break break-all + color #444 + overflow hidden + + > .ext + opacity 0.5 + +</style> diff --git a/src/client/app/desktop/views/components/drive.folder.vue b/src/client/app/desktop/views/components/drive.folder.vue new file mode 100644 index 0000000000..a926bf47b2 --- /dev/null +++ b/src/client/app/desktop/views/components/drive.folder.vue @@ -0,0 +1,267 @@ +<template> +<div class="root folder" + :data-is-contextmenu-showing="isContextmenuShowing" + :data-draghover="draghover" + @click="onClick" + @mouseover="onMouseover" + @mouseout="onMouseout" + @dragover.prevent.stop="onDragover" + @dragenter.prevent="onDragenter" + @dragleave="onDragleave" + @drop.prevent.stop="onDrop" + draggable="true" + @dragstart="onDragstart" + @dragend="onDragend" + @contextmenu.prevent.stop="onContextmenu" + :title="title" +> + <p class="name"> + <template v-if="hover">%fa:R folder-open .fw%</template> + <template v-if="!hover">%fa:R folder .fw%</template> + {{ folder.name }} + </p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import contextmenu from '../../api/contextmenu'; + +export default Vue.extend({ + props: ['folder'], + data() { + return { + hover: false, + draghover: false, + isDragging: false, + isContextmenuShowing: false + }; + }, + computed: { + browser(): any { + return this.$parent; + }, + title(): string { + return this.folder.name; + } + }, + methods: { + onClick() { + this.browser.move(this.folder); + }, + + onContextmenu(e) { + this.isContextmenuShowing = true; + contextmenu(e, [{ + type: 'item', + text: '%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.move-to-this-folder%', + icon: '%fa:arrow-right%', + onClick: this.go + }, { + type: 'item', + text: '%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.show-in-new-window%', + icon: '%fa:R window-restore%', + onClick: this.newWindow + }, { + type: 'divider', + }, { + type: 'item', + text: '%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.rename%', + icon: '%fa:i-cursor%', + onClick: this.rename + }, { + type: 'divider', + }, { + type: 'item', + text: '%i18n:common.delete%', + icon: '%fa:R trash-alt%', + onClick: this.deleteFolder + }], { + closed: () => { + this.isContextmenuShowing = false; + } + }); + }, + + onMouseover() { + this.hover = true; + }, + + onMouseout() { + this.hover = false + }, + + onDragover(e) { + // 自分自身がドラッグされている場合 + if (this.isDragging) { + // 自分自身にはドロップさせない + e.dataTransfer.dropEffect = 'none'; + return; + } + + const isFile = e.dataTransfer.items[0].kind == 'file'; + const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; + const isDriveFolder = e.dataTransfer.types[0] == 'mk_drive_folder'; + + if (isFile || isDriveFile || isDriveFolder) { + e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; + } else { + e.dataTransfer.dropEffect = 'none'; + } + }, + + onDragenter() { + if (!this.isDragging) this.draghover = true; + }, + + onDragleave() { + this.draghover = false; + }, + + onDrop(e) { + this.draghover = false; + + // ファイルだったら + if (e.dataTransfer.files.length > 0) { + Array.from(e.dataTransfer.files).forEach(file => { + this.browser.upload(file, this.folder); + }); + return; + } + + //#region ドライブのファイル + const driveFile = e.dataTransfer.getData('mk_drive_file'); + if (driveFile != null && driveFile != '') { + const file = JSON.parse(driveFile); + this.browser.removeFile(file.id); + (this as any).api('drive/files/update', { + fileId: file.id, + folderId: this.folder.id + }); + } + //#endregion + + //#region ドライブのフォルダ + const driveFolder = e.dataTransfer.getData('mk_drive_folder'); + if (driveFolder != null && driveFolder != '') { + const folder = JSON.parse(driveFolder); + + // 移動先が自分自身ならreject + if (folder.id == this.folder.id) return; + + this.browser.removeFolder(folder.id); + (this as any).api('drive/folders/update', { + folderId: folder.id, + parentId: this.folder.id + }).then(() => { + // noop + }).catch(err => { + switch (err) { + case 'detected-circular-definition': + (this as any).apis.dialog({ + title: '%fa:exclamation-triangle%%i18n:desktop.tags.mk-drive-browser-folder.unable-to-process%', + text: '%i18n:desktop.tags.mk-drive-browser-folder.circular-reference-detected%', + actions: [{ + text: '%i18n:common.ok%' + }] + }); + break; + default: + alert('%i18n:desktop.tags.mk-drive-browser-folder.unhandled-error% ' + err); + } + }); + } + //#endregion + }, + + onDragstart(e) { + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('mk_drive_folder', JSON.stringify(this.folder)); + this.isDragging = true; + + // 親ブラウザに対して、ドラッグが開始されたフラグを立てる + // (=あなたの子供が、ドラッグを開始しましたよ) + this.browser.isDragSource = true; + }, + + onDragend() { + this.isDragging = false; + this.browser.isDragSource = false; + }, + + go() { + this.browser.move(this.folder.id); + }, + + newWindow() { + this.browser.newWindow(this.folder); + }, + + rename() { + (this as any).apis.input({ + title: '%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.rename-folder%', + placeholder: '%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.input-new-folder-name%', + default: this.folder.name + }).then(name => { + (this as any).api('drive/folders/update', { + folderId: this.folder.id, + name: name + }); + }); + }, + + deleteFolder() { + alert('not implemented yet'); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.root.folder + padding 8px + height 64px + background lighten($theme-color, 95%) + border-radius 4px + + &, * + cursor pointer + + * + pointer-events none + + &:hover + background lighten($theme-color, 90%) + + &:active + background lighten($theme-color, 85%) + + &[data-is-contextmenu-showing] + &[data-draghover] + &:after + content "" + pointer-events none + position absolute + top -4px + right -4px + bottom -4px + left -4px + border 2px dashed rgba($theme-color, 0.3) + border-radius 4px + + &[data-draghover] + background lighten($theme-color, 90%) + + > .name + margin 0 + font-size 0.9em + color darken($theme-color, 30%) + + > [data-fa] + margin-right 4px + margin-left 2px + text-align left + +</style> diff --git a/src/client/app/desktop/views/components/drive.nav-folder.vue b/src/client/app/desktop/views/components/drive.nav-folder.vue new file mode 100644 index 0000000000..d885a72f7f --- /dev/null +++ b/src/client/app/desktop/views/components/drive.nav-folder.vue @@ -0,0 +1,113 @@ +<template> +<div class="root nav-folder" + :data-draghover="draghover" + @click="onClick" + @dragover.prevent.stop="onDragover" + @dragenter="onDragenter" + @dragleave="onDragleave" + @drop.stop="onDrop" +> + <template v-if="folder == null">%fa:cloud%</template> + <span>{{ folder == null ? '%i18n:desktop.tags.mk-drive-browser-nav-folder.drive%' : folder.name }}</span> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['folder'], + data() { + return { + hover: false, + draghover: false + }; + }, + computed: { + browser(): any { + return this.$parent; + } + }, + methods: { + onClick() { + this.browser.move(this.folder); + }, + onMouseover() { + this.hover = true; + }, + onMouseout() { + this.hover = false; + }, + onDragover(e) { + // このフォルダがルートかつカレントディレクトリならドロップ禁止 + if (this.folder == null && this.browser.folder == null) { + e.dataTransfer.dropEffect = 'none'; + } + + const isFile = e.dataTransfer.items[0].kind == 'file'; + const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; + const isDriveFolder = e.dataTransfer.types[0] == 'mk_drive_folder'; + + if (isFile || isDriveFile || isDriveFolder) { + e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; + } else { + e.dataTransfer.dropEffect = 'none'; + } + + return false; + }, + onDragenter() { + if (this.folder || this.browser.folder) this.draghover = true; + }, + onDragleave() { + if (this.folder || this.browser.folder) this.draghover = false; + }, + onDrop(e) { + this.draghover = false; + + // ファイルだったら + if (e.dataTransfer.files.length > 0) { + Array.from(e.dataTransfer.files).forEach(file => { + this.browser.upload(file, this.folder); + }); + return; + } + + //#region ドライブのファイル + const driveFile = e.dataTransfer.getData('mk_drive_file'); + if (driveFile != null && driveFile != '') { + const file = JSON.parse(driveFile); + this.browser.removeFile(file.id); + (this as any).api('drive/files/update', { + fileId: file.id, + folderId: this.folder ? this.folder.id : null + }); + } + //#endregion + + //#region ドライブのフォルダ + const driveFolder = e.dataTransfer.getData('mk_drive_folder'); + if (driveFolder != null && driveFolder != '') { + const folder = JSON.parse(driveFolder); + // 移動先が自分自身ならreject + if (this.folder && folder.id == this.folder.id) return; + this.browser.removeFolder(folder.id); + (this as any).api('drive/folders/update', { + folderId: folder.id, + parentId: this.folder ? this.folder.id : null + }); + } + //#endregion + } + } +}); +</script> + +<style lang="stylus" scoped> +.root.nav-folder + > * + pointer-events none + + &[data-draghover] + background #eee + +</style> diff --git a/src/client/app/desktop/views/components/drive.vue b/src/client/app/desktop/views/components/drive.vue new file mode 100644 index 0000000000..c766dfec12 --- /dev/null +++ b/src/client/app/desktop/views/components/drive.vue @@ -0,0 +1,773 @@ +<template> +<div class="mk-drive"> + <nav> + <div class="path" @contextmenu.prevent.stop="() => {}"> + <x-nav-folder :class="{ current: folder == null }"/> + <template v-for="folder in hierarchyFolders"> + <span class="separator">%fa:angle-right%</span> + <x-nav-folder :folder="folder" :key="folder.id"/> + </template> + <span class="separator" v-if="folder != null">%fa:angle-right%</span> + <span class="folder current" v-if="folder != null">{{ folder.name }}</span> + </div> + <input class="search" type="search" placeholder=" %i18n:desktop.tags.mk-drive-browser.search%"/> + </nav> + <div class="main" :class="{ uploading: uploadings.length > 0, fetching }" + ref="main" + @mousedown="onMousedown" + @dragover.prevent.stop="onDragover" + @dragenter="onDragenter" + @dragleave="onDragleave" + @drop.prevent.stop="onDrop" + @contextmenu.prevent.stop="onContextmenu" + > + <div class="selection" ref="selection"></div> + <div class="contents" ref="contents"> + <div class="folders" ref="foldersContainer" v-if="folders.length > 0"> + <x-folder v-for="folder in folders" :key="folder.id" class="folder" :folder="folder"/> + <!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> + <div class="padding" v-for="n in 16"></div> + <button v-if="moreFolders">%i18n:desktop.tags.mk-drive-browser.load-more%</button> + </div> + <div class="files" ref="filesContainer" v-if="files.length > 0"> + <x-file v-for="file in files" :key="file.id" class="file" :file="file"/> + <!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> + <div class="padding" v-for="n in 16"></div> + <button v-if="moreFiles" @click="fetchMoreFiles">%i18n:desktop.tags.mk-drive-browser.load-more%</button> + </div> + <div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching"> + <p v-if="draghover">%i18n:desktop.tags.mk-drive-browser.empty-draghover%</p> + <p v-if="!draghover && folder == null"><strong>%i18n:desktop.tags.mk-drive-browser.empty-drive%</strong><br/>%i18n:desktop.tags.mk-drive-browser.empty-drive-description%</p> + <p v-if="!draghover && folder != null">%i18n:desktop.tags.mk-drive-browser.empty-folder%</p> + </div> + </div> + <div class="fetching" v-if="fetching"> + <div class="spinner"> + <div class="dot1"></div> + <div class="dot2"></div> + </div> + </div> + </div> + <div class="dropzone" v-if="draghover"></div> + <mk-uploader ref="uploader" @change="onChangeUploaderUploads" @uploaded="onUploaderUploaded"/> + <input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" @change="onChangeFileInput"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import MkDriveWindow from './drive-window.vue'; +import XNavFolder from './drive.nav-folder.vue'; +import XFolder from './drive.folder.vue'; +import XFile from './drive.file.vue'; +import contains from '../../../common/scripts/contains'; +import contextmenu from '../../api/contextmenu'; +import { url } from '../../../config'; + +export default Vue.extend({ + components: { + XNavFolder, + XFolder, + XFile + }, + props: { + initFolder: { + type: Object, + required: false + }, + multiple: { + type: Boolean, + default: false + } + }, + data() { + return { + /** + * 現在の階層(フォルダ) + * * null でルートを表す + */ + folder: null, + + files: [], + folders: [], + moreFiles: false, + moreFolders: false, + hierarchyFolders: [], + selectedFiles: [], + uploadings: [], + connection: null, + connectionId: null, + + /** + * ドロップされようとしているか + */ + draghover: false, + + /** + * 自信の所有するアイテムがドラッグをスタートさせたか + * (自分自身の階層にドロップできないようにするためのフラグ) + */ + isDragSource: false, + + fetching: true + }; + }, + mounted() { + this.connection = (this as any).os.streams.driveStream.getConnection(); + this.connectionId = (this as any).os.streams.driveStream.use(); + + this.connection.on('file_created', this.onStreamDriveFileCreated); + this.connection.on('file_updated', this.onStreamDriveFileUpdated); + this.connection.on('folder_created', this.onStreamDriveFolderCreated); + this.connection.on('folder_updated', this.onStreamDriveFolderUpdated); + + if (this.initFolder) { + this.move(this.initFolder); + } else { + this.fetch(); + } + }, + beforeDestroy() { + this.connection.off('file_created', this.onStreamDriveFileCreated); + this.connection.off('file_updated', this.onStreamDriveFileUpdated); + this.connection.off('folder_created', this.onStreamDriveFolderCreated); + this.connection.off('folder_updated', this.onStreamDriveFolderUpdated); + (this as any).os.streams.driveStream.dispose(this.connectionId); + }, + methods: { + onContextmenu(e) { + contextmenu(e, [{ + type: 'item', + text: '%i18n:desktop.tags.mk-drive-browser-base-contextmenu.create-folder%', + icon: '%fa:R folder%', + onClick: this.createFolder + }, { + type: 'item', + text: '%i18n:desktop.tags.mk-drive-browser-base-contextmenu.upload%', + icon: '%fa:upload%', + onClick: this.selectLocalFile + }, { + type: 'item', + text: '%i18n:desktop.tags.mk-drive-browser-base-contextmenu.url-upload%', + icon: '%fa:cloud-upload-alt%', + onClick: this.urlUpload + }]); + }, + + onStreamDriveFileCreated(file) { + this.addFile(file, true); + }, + + onStreamDriveFileUpdated(file) { + const current = this.folder ? this.folder.id : null; + if (current != file.folderId) { + this.removeFile(file); + } else { + this.addFile(file, true); + } + }, + + onStreamDriveFolderCreated(folder) { + this.addFolder(folder, true); + }, + + onStreamDriveFolderUpdated(folder) { + const current = this.folder ? this.folder.id : null; + if (current != folder.parentId) { + this.removeFolder(folder); + } else { + this.addFolder(folder, true); + } + }, + + onChangeUploaderUploads(uploads) { + this.uploadings = uploads; + }, + + onUploaderUploaded(file) { + this.addFile(file, true); + }, + + onMousedown(e): any { + if (contains(this.$refs.foldersContainer, e.target) || contains(this.$refs.filesContainer, e.target)) return true; + + const main = this.$refs.main as any; + const selection = this.$refs.selection as any; + + const rect = main.getBoundingClientRect(); + + const left = e.pageX + main.scrollLeft - rect.left - window.pageXOffset + const top = e.pageY + main.scrollTop - rect.top - window.pageYOffset + + const move = e => { + selection.style.display = 'block'; + + const cursorX = e.pageX + main.scrollLeft - rect.left - window.pageXOffset; + const cursorY = e.pageY + main.scrollTop - rect.top - window.pageYOffset; + const w = cursorX - left; + const h = cursorY - top; + + if (w > 0) { + selection.style.width = w + 'px'; + selection.style.left = left + 'px'; + } else { + selection.style.width = -w + 'px'; + selection.style.left = cursorX + 'px'; + } + + if (h > 0) { + selection.style.height = h + 'px'; + selection.style.top = top + 'px'; + } else { + selection.style.height = -h + 'px'; + selection.style.top = cursorY + 'px'; + } + }; + + const up = e => { + document.documentElement.removeEventListener('mousemove', move); + document.documentElement.removeEventListener('mouseup', up); + + selection.style.display = 'none'; + }; + + document.documentElement.addEventListener('mousemove', move); + document.documentElement.addEventListener('mouseup', up); + }, + + onDragover(e): any { + // ドラッグ元が自分自身の所有するアイテムだったら + if (this.isDragSource) { + // 自分自身にはドロップさせない + e.dataTransfer.dropEffect = 'none'; + return; + } + + const isFile = e.dataTransfer.items[0].kind == 'file'; + const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; + const isDriveFolder = e.dataTransfer.types[0] == 'mk_drive_folder'; + + if (isFile || isDriveFile || isDriveFolder) { + e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; + } else { + e.dataTransfer.dropEffect = 'none'; + } + + return false; + }, + + onDragenter(e) { + if (!this.isDragSource) this.draghover = true; + }, + + onDragleave(e) { + this.draghover = false; + }, + + onDrop(e): any { + this.draghover = false; + + // ドロップされてきたものがファイルだったら + if (e.dataTransfer.files.length > 0) { + Array.from(e.dataTransfer.files).forEach(file => { + this.upload(file, this.folder); + }); + return; + } + + //#region ドライブのファイル + const driveFile = e.dataTransfer.getData('mk_drive_file'); + if (driveFile != null && driveFile != '') { + const file = JSON.parse(driveFile); + if (this.files.some(f => f.id == file.id)) return; + this.removeFile(file.id); + (this as any).api('drive/files/update', { + fileId: file.id, + folderId: this.folder ? this.folder.id : null + }); + } + //#endregion + + //#region ドライブのフォルダ + const driveFolder = e.dataTransfer.getData('mk_drive_folder'); + if (driveFolder != null && driveFolder != '') { + const folder = JSON.parse(driveFolder); + + // 移動先が自分自身ならreject + if (this.folder && folder.id == this.folder.id) return false; + if (this.folders.some(f => f.id == folder.id)) return false; + this.removeFolder(folder.id); + (this as any).api('drive/folders/update', { + folderId: folder.id, + parentId: this.folder ? this.folder.id : null + }).then(() => { + // noop + }).catch(err => { + switch (err) { + case 'detected-circular-definition': + (this as any).apis.dialog({ + title: '%fa:exclamation-triangle%%i18n:desktop.tags.mk-drive-browser.unable-to-process%', + text: '%i18n:desktop.tags.mk-drive-browser.circular-reference-detected%', + actions: [{ + text: '%i18n:common.ok%' + }] + }); + break; + default: + alert('%i18n:desktop.tags.mk-drive-browser.unhandled-error% ' + err); + } + }); + } + //#endregion + }, + + selectLocalFile() { + (this.$refs.fileInput as any).click(); + }, + + urlUpload() { + (this as any).apis.input({ + title: '%i18n:desktop.tags.mk-drive-browser.url-upload%', + placeholder: '%i18n:desktop.tags.mk-drive-browser.url-of-file%' + }).then(url => { + (this as any).api('drive/files/upload_from_url', { + url: url, + folderId: this.folder ? this.folder.id : undefined + }); + + (this as any).apis.dialog({ + title: '%fa:check%%i18n:desktop.tags.mk-drive-browser.url-upload-requested%', + text: '%i18n:desktop.tags.mk-drive-browser.may-take-time%', + actions: [{ + text: '%i18n:common.ok%' + }] + }); + }); + }, + + createFolder() { + (this as any).apis.input({ + title: '%i18n:desktop.tags.mk-drive-browser.create-folder%', + placeholder: '%i18n:desktop.tags.mk-drive-browser.folder-name%' + }).then(name => { + (this as any).api('drive/folders/create', { + name: name, + folderId: this.folder ? this.folder.id : undefined + }).then(folder => { + this.addFolder(folder, true); + }); + }); + }, + + onChangeFileInput() { + Array.from((this.$refs.fileInput as any).files).forEach(file => { + this.upload(file, this.folder); + }); + }, + + upload(file, folder) { + if (folder && typeof folder == 'object') folder = folder.id; + (this.$refs.uploader as any).upload(file, folder); + }, + + chooseFile(file) { + const isAlreadySelected = this.selectedFiles.some(f => f.id == file.id); + if (this.multiple) { + if (isAlreadySelected) { + this.selectedFiles = this.selectedFiles.filter(f => f.id != file.id); + } else { + this.selectedFiles.push(file); + } + this.$emit('change-selection', this.selectedFiles); + } else { + if (isAlreadySelected) { + this.$emit('selected', file); + } else { + this.selectedFiles = [file]; + this.$emit('change-selection', [file]); + } + } + }, + + newWindow(folder) { + if (document.body.clientWidth > 800) { + (this as any).os.new(MkDriveWindow, { + folder: folder + }); + } else { + window.open(url + '/i/drive/folder/' + folder.id, + 'drive_window', + 'height=500, width=800'); + } + }, + + move(target) { + if (target == null) { + this.goRoot(); + return; + } else if (typeof target == 'object') { + target = target.id; + } + + this.fetching = true; + + (this as any).api('drive/folders/show', { + folderId: target + }).then(folder => { + this.folder = folder; + this.hierarchyFolders = []; + + const dive = folder => { + this.hierarchyFolders.unshift(folder); + if (folder.parent) dive(folder.parent); + }; + + if (folder.parent) dive(folder.parent); + + this.$emit('open-folder', folder); + this.fetch(); + }); + }, + + addFolder(folder, unshift = false) { + const current = this.folder ? this.folder.id : null; + if (current != folder.parentId) return; + + if (this.folders.some(f => f.id == folder.id)) { + const exist = this.folders.map(f => f.id).indexOf(folder.id); + Vue.set(this.folders, exist, folder); + return; + } + + if (unshift) { + this.folders.unshift(folder); + } else { + this.folders.push(folder); + } + }, + + addFile(file, unshift = false) { + const current = this.folder ? this.folder.id : null; + if (current != file.folderId) return; + + if (this.files.some(f => f.id == file.id)) { + const exist = this.files.map(f => f.id).indexOf(file.id); + Vue.set(this.files, exist, file); + return; + } + + if (unshift) { + this.files.unshift(file); + } else { + this.files.push(file); + } + }, + + removeFolder(folder) { + if (typeof folder == 'object') folder = folder.id; + this.folders = this.folders.filter(f => f.id != folder); + }, + + removeFile(file) { + if (typeof file == 'object') file = file.id; + this.files = this.files.filter(f => f.id != file); + }, + + appendFile(file) { + this.addFile(file); + }, + + appendFolder(folder) { + this.addFolder(folder); + }, + + prependFile(file) { + this.addFile(file, true); + }, + + prependFolder(folder) { + this.addFolder(folder, true); + }, + + goRoot() { + // 既にrootにいるなら何もしない + if (this.folder == null) return; + + this.folder = null; + this.hierarchyFolders = []; + this.$emit('move-root'); + this.fetch(); + }, + + fetch() { + this.folders = []; + this.files = []; + this.moreFolders = false; + this.moreFiles = false; + this.fetching = true; + + let fetchedFolders = null; + let fetchedFiles = null; + + const foldersMax = 30; + const filesMax = 30; + + // フォルダ一覧取得 + (this as any).api('drive/folders', { + folderId: this.folder ? this.folder.id : null, + limit: foldersMax + 1 + }).then(folders => { + if (folders.length == foldersMax + 1) { + this.moreFolders = true; + folders.pop(); + } + fetchedFolders = folders; + complete(); + }); + + // ファイル一覧取得 + (this as any).api('drive/files', { + folderId: this.folder ? this.folder.id : null, + limit: filesMax + 1 + }).then(files => { + if (files.length == filesMax + 1) { + this.moreFiles = true; + files.pop(); + } + fetchedFiles = files; + complete(); + }); + + let flag = false; + const complete = () => { + if (flag) { + fetchedFolders.forEach(this.appendFolder); + fetchedFiles.forEach(this.appendFile); + this.fetching = false; + } else { + flag = true; + } + }; + }, + + fetchMoreFiles() { + this.fetching = true; + + const max = 30; + + // ファイル一覧取得 + (this as any).api('drive/files', { + folderId: this.folder ? this.folder.id : null, + limit: max + 1 + }).then(files => { + if (files.length == max + 1) { + this.moreFiles = true; + files.pop(); + } else { + this.moreFiles = false; + } + files.forEach(this.appendFile); + this.fetching = false; + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-drive + + > nav + display block + z-index 2 + width 100% + overflow auto + font-size 0.9em + color #555 + background #fff + //border-bottom 1px solid #dfdfdf + box-shadow 0 1px 0 rgba(0, 0, 0, 0.05) + + &, * + user-select none + + > .path + display inline-block + vertical-align bottom + margin 0 + padding 0 8px + width calc(100% - 200px) + line-height 38px + white-space nowrap + + > * + display inline-block + margin 0 + padding 0 8px + line-height 38px + cursor pointer + + i + margin-right 4px + + * + pointer-events none + + &:hover + text-decoration underline + + &.current + font-weight bold + cursor default + + &:hover + text-decoration none + + &.separator + margin 0 + padding 0 + opacity 0.5 + cursor default + + > [data-fa] + margin 0 + + > .search + display inline-block + vertical-align bottom + user-select text + cursor auto + margin 0 + padding 0 18px + width 200px + font-size 1em + line-height 38px + background transparent + outline none + //border solid 1px #ddd + border none + border-radius 0 + box-shadow none + transition color 0.5s ease, border 0.5s ease + font-family FontAwesome, sans-serif + + &[data-active='true'] + background #fff + + &::-webkit-input-placeholder, + &:-ms-input-placeholder, + &:-moz-placeholder + color $ui-control-foreground-color + + > .main + padding 8px + height calc(100% - 38px) + overflow auto + + &, * + user-select none + + &.fetching + cursor wait !important + + * + pointer-events none + + > .contents + opacity 0.5 + + &.uploading + height calc(100% - 38px - 100px) + + > .selection + display none + position absolute + z-index 128 + top 0 + left 0 + border solid 1px $theme-color + background rgba($theme-color, 0.5) + pointer-events none + + > .contents + + > .folders + > .files + display flex + flex-wrap wrap + + > .folder + > .file + flex-grow 1 + width 144px + margin 4px + + > .padding + flex-grow 1 + pointer-events none + width 144px + 8px // 8px is margin + + > .empty + padding 16px + text-align center + color #999 + pointer-events none + + > p + margin 0 + + > .fetching + .spinner + margin 100px auto + width 40px + height 40px + text-align center + + animation sk-rotate 2.0s infinite linear + + .dot1, .dot2 + width 60% + height 60% + display inline-block + position absolute + top 0 + background-color rgba(0, 0, 0, 0.3) + border-radius 100% + + animation sk-bounce 2.0s infinite ease-in-out + + .dot2 + top auto + bottom 0 + animation-delay -1.0s + + @keyframes sk-rotate { 100% { transform: rotate(360deg); }} + + @keyframes sk-bounce { + 0%, 100% { + transform: scale(0.0); + } 50% { + transform: scale(1.0); + } + } + + > .dropzone + position absolute + left 0 + top 38px + width 100% + height calc(100% - 38px) + border dashed 2px rgba($theme-color, 0.5) + pointer-events none + + > .mk-uploader + height 100px + padding 16px + background #fff + + > input + display none + +</style> diff --git a/src/client/app/desktop/views/components/ellipsis-icon.vue b/src/client/app/desktop/views/components/ellipsis-icon.vue new file mode 100644 index 0000000000..c54a7db29d --- /dev/null +++ b/src/client/app/desktop/views/components/ellipsis-icon.vue @@ -0,0 +1,37 @@ +<template> +<div class="mk-ellipsis-icon"> + <div></div><div></div><div></div> +</div> +</template> + +<style lang="stylus" scoped> +.mk-ellipsis-icon + width 70px + margin 0 auto + text-align center + + > div + display inline-block + width 18px + height 18px + background-color rgba(0, 0, 0, 0.3) + border-radius 100% + animation bounce 1.4s infinite ease-in-out both + + &:nth-child(1) + animation-delay 0s + + &:nth-child(2) + margin 0 6px + animation-delay 0.16s + + &:nth-child(3) + animation-delay 0.32s + + @keyframes bounce + 0%, 80%, 100% + transform scale(0) + 40% + transform scale(1) + +</style> diff --git a/src/client/app/desktop/views/components/follow-button.vue b/src/client/app/desktop/views/components/follow-button.vue new file mode 100644 index 0000000000..9eb22b0fb8 --- /dev/null +++ b/src/client/app/desktop/views/components/follow-button.vue @@ -0,0 +1,164 @@ +<template> +<button class="mk-follow-button" + :class="{ wait, follow: !user.isFollowing, unfollow: user.isFollowing, big: size == 'big' }" + @click="onClick" + :disabled="wait" + :title="user.isFollowing ? 'フォロー解除' : 'フォローする'" +> + <template v-if="!wait && user.isFollowing"> + <template v-if="size == 'compact'">%fa:minus%</template> + <template v-if="size == 'big'">%fa:minus%フォロー解除</template> + </template> + <template v-if="!wait && !user.isFollowing"> + <template v-if="size == 'compact'">%fa:plus%</template> + <template v-if="size == 'big'">%fa:plus%フォロー</template> + </template> + <template v-if="wait">%fa:spinner .pulse .fw%</template> +</button> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: { + user: { + type: Object, + required: true + }, + size: { + type: String, + default: 'compact' + } + }, + data() { + return { + wait: false, + connection: null, + connectionId: null + }; + }, + mounted() { + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + + this.connection.on('follow', this.onFollow); + this.connection.on('unfollow', this.onUnfollow); + }, + beforeDestroy() { + this.connection.off('follow', this.onFollow); + this.connection.off('unfollow', this.onUnfollow); + (this as any).os.stream.dispose(this.connectionId); + }, + methods: { + + onFollow(user) { + if (user.id == this.user.id) { + this.user.isFollowing = user.isFollowing; + } + }, + + onUnfollow(user) { + if (user.id == this.user.id) { + this.user.isFollowing = user.isFollowing; + } + }, + + onClick() { + this.wait = true; + if (this.user.isFollowing) { + (this as any).api('following/delete', { + userId: this.user.id + }).then(() => { + this.user.isFollowing = false; + }).catch(err => { + console.error(err); + }).then(() => { + this.wait = false; + }); + } else { + (this as any).api('following/create', { + userId: this.user.id + }).then(() => { + this.user.isFollowing = true; + }).catch(err => { + console.error(err); + }).then(() => { + this.wait = false; + }); + } + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-follow-button + display block + cursor pointer + padding 0 + margin 0 + width 32px + height 32px + font-size 1em + outline none + border-radius 4px + + * + pointer-events none + + &:focus + &:after + content "" + pointer-events none + position absolute + top -5px + right -5px + bottom -5px + left -5px + border 2px solid rgba($theme-color, 0.3) + border-radius 8px + + &.follow + color #888 + background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) + border solid 1px #e2e2e2 + + &:hover + background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) + border-color #dcdcdc + + &:active + background #ececec + border-color #dcdcdc + + &.unfollow + color $theme-color-foreground + background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) + border solid 1px lighten($theme-color, 15%) + + &:not(:disabled) + font-weight bold + + &:hover:not(:disabled) + background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) + border-color $theme-color + + &:active:not(:disabled) + background $theme-color + border-color $theme-color + + &.wait + cursor wait !important + opacity 0.7 + + &.big + width 100% + height 38px + line-height 38px + + i + margin-right 8px + +</style> diff --git a/src/client/app/desktop/views/components/followers-window.vue b/src/client/app/desktop/views/components/followers-window.vue new file mode 100644 index 0000000000..16206299d7 --- /dev/null +++ b/src/client/app/desktop/views/components/followers-window.vue @@ -0,0 +1,27 @@ +<template> +<mk-window width="400px" height="550px" @closed="$destroy"> + <span slot="header" :class="$style.header"> + <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ user | userName }}のフォロワー + </span> + <mk-followers :user="user"/> +</mk-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['user'] +}); +</script> + +<style lang="stylus" module> +.header + > img + display inline-block + vertical-align bottom + height calc(100% - 10px) + margin 5px + border-radius 4px + +</style> diff --git a/src/client/app/desktop/views/components/followers.vue b/src/client/app/desktop/views/components/followers.vue new file mode 100644 index 0000000000..a1b98995d8 --- /dev/null +++ b/src/client/app/desktop/views/components/followers.vue @@ -0,0 +1,26 @@ +<template> +<mk-users-list + :fetch="fetch" + :count="user.followersCount" + :you-know-count="user.followersYouKnowCount" +> + フォロワーはいないようです。 +</mk-users-list> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['user'], + methods: { + fetch(iknow, limit, cursor, cb) { + (this as any).api('users/followers', { + userId: this.user.id, + iknow: iknow, + limit: limit, + cursor: cursor ? cursor : undefined + }).then(cb); + } + } +}); +</script> diff --git a/src/client/app/desktop/views/components/following-window.vue b/src/client/app/desktop/views/components/following-window.vue new file mode 100644 index 0000000000..cc3d77198e --- /dev/null +++ b/src/client/app/desktop/views/components/following-window.vue @@ -0,0 +1,27 @@ +<template> +<mk-window width="400px" height="550px" @closed="$destroy"> + <span slot="header" :class="$style.header"> + <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ user | userName }}のフォロー + </span> + <mk-following :user="user"/> +</mk-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['user'] +}); +</script> + +<style lang="stylus" module> +.header + > img + display inline-block + vertical-align bottom + height calc(100% - 10px) + margin 5px + border-radius 4px + +</style> diff --git a/src/client/app/desktop/views/components/following.vue b/src/client/app/desktop/views/components/following.vue new file mode 100644 index 0000000000..b7aedda84f --- /dev/null +++ b/src/client/app/desktop/views/components/following.vue @@ -0,0 +1,26 @@ +<template> +<mk-users-list + :fetch="fetch" + :count="user.followingCount" + :you-know-count="user.followingYouKnowCount" +> + フォロー中のユーザーはいないようです。 +</mk-users-list> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['user'], + methods: { + fetch(iknow, limit, cursor, cb) { + (this as any).api('users/following', { + userId: this.user.id, + iknow: iknow, + limit: limit, + cursor: cursor ? cursor : undefined + }).then(cb); + } + } +}); +</script> diff --git a/src/client/app/desktop/views/components/friends-maker.vue b/src/client/app/desktop/views/components/friends-maker.vue new file mode 100644 index 0000000000..af5bde3ad5 --- /dev/null +++ b/src/client/app/desktop/views/components/friends-maker.vue @@ -0,0 +1,169 @@ +<template> +<div class="mk-friends-maker"> + <p class="title">気になるユーザーをフォロー:</p> + <div class="users" v-if="!fetching && users.length > 0"> + <div class="user" v-for="user in users" :key="user.id"> + <router-link class="avatar-anchor" :to="user | userPage"> + <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="user.id"/> + </router-link> + <div class="body"> + <router-link class="name" :to="user | userPage" v-user-preview="user.id">{{ user | userName }}</router-link> + <p class="username">@{{ user | acct }}</p> + </div> + <mk-follow-button :user="user"/> + </div> + </div> + <p class="empty" v-if="!fetching && users.length == 0">おすすめのユーザーは見つかりませんでした。</p> + <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p> + <a class="refresh" @click="refresh">もっと見る</a> + <button class="close" @click="$destroy()" title="閉じる">%fa:times%</button> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + data() { + return { + users: [], + fetching: true, + limit: 6, + page: 0 + }; + }, + mounted() { + this.fetch(); + }, + methods: { + fetch() { + this.fetching = true; + this.users = []; + + (this as any).api('users/recommendation', { + limit: this.limit, + offset: this.limit * this.page + }).then(users => { + this.users = users; + this.fetching = false; + }); + }, + refresh() { + if (this.users.length < this.limit) { + this.page = 0; + } else { + this.page++; + } + this.fetch(); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-friends-maker + padding 24px + + > .title + margin 0 0 12px 0 + font-size 1em + font-weight bold + color #888 + + > .users + &:after + content "" + display block + clear both + + > .user + padding 16px + width 238px + float left + + &:after + content "" + display block + clear both + + > .avatar-anchor + display block + float left + margin 0 12px 0 0 + + > .avatar + display block + width 42px + height 42px + margin 0 + border-radius 8px + vertical-align bottom + + > .body + float left + width calc(100% - 54px) + + > .name + margin 0 + font-size 16px + line-height 24px + color #555 + + > .username + margin 0 + font-size 15px + line-height 16px + color #ccc + + > .mk-follow-button + position absolute + top 16px + right 16px + + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > .fetching + margin 0 + padding 16px + text-align center + color #aaa + + > [data-fa] + margin-right 4px + + > .refresh + display block + margin 0 8px 0 0 + text-align right + font-size 0.9em + color #999 + + > .close + cursor pointer + display block + position absolute + top 6px + right 6px + z-index 1 + margin 0 + padding 0 + font-size 1.2em + color #999 + border none + outline none + background transparent + + &:hover + color #555 + + &:active + color #222 + + > [data-fa] + padding 14px + +</style> diff --git a/src/client/app/desktop/views/components/game-window.vue b/src/client/app/desktop/views/components/game-window.vue new file mode 100644 index 0000000000..3c8bf40e12 --- /dev/null +++ b/src/client/app/desktop/views/components/game-window.vue @@ -0,0 +1,37 @@ +<template> +<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy"> + <span slot="header" :class="$style.header">%fa:gamepad%オセロ</span> + <mk-othello :class="$style.content" @gamed="g => game = g"/> +</mk-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { url } from '../../../config'; + +export default Vue.extend({ + data() { + return { + game: null + }; + }, + computed: { + popout(): string { + return this.game + ? `${url}/othello/${this.game.id}` + : `${url}/othello`; + } + } +}); +</script> + +<style lang="stylus" module> +.header + > [data-fa] + margin-right 4px + +.content + height 100% + overflow auto + +</style> diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue new file mode 100644 index 0000000000..90e9d1b785 --- /dev/null +++ b/src/client/app/desktop/views/components/home.vue @@ -0,0 +1,360 @@ +<template> +<div class="mk-home" :data-customize="customize"> + <div class="customize" v-if="customize"> + <router-link to="/">%fa:check%完了</router-link> + <div> + <div class="adder"> + <p>ウィジェットを追加:</p> + <select v-model="widgetAdderSelected"> + <option value="profile">プロフィール</option> + <option value="calendar">カレンダー</option> + <option value="timemachine">カレンダー(タイムマシン)</option> + <option value="activity">アクティビティ</option> + <option value="rss">RSSリーダー</option> + <option value="trends">トレンド</option> + <option value="photo-stream">フォトストリーム</option> + <option value="slideshow">スライドショー</option> + <option value="version">バージョン</option> + <option value="broadcast">ブロードキャスト</option> + <option value="notifications">通知</option> + <option value="users">おすすめユーザー</option> + <option value="polls">投票</option> + <option value="post-form">投稿フォーム</option> + <option value="messaging">メッセージ</option> + <option value="channel">チャンネル</option> + <option value="access-log">アクセスログ</option> + <option value="server">サーバー情報</option> + <option value="donation">寄付のお願い</option> + <option value="nav">ナビゲーション</option> + <option value="tips">ヒント</option> + </select> + <button @click="addWidget">追加</button> + </div> + <div class="trash"> + <x-draggable v-model="trash" :options="{ group: 'x' }" @add="onTrash"></x-draggable> + <p>ゴミ箱</p> + </div> + </div> + </div> + <div class="main"> + <template v-if="customize"> + <x-draggable v-for="place in ['left', 'right']" + :list="widgets[place]" + :class="place" + :data-place="place" + :options="{ group: 'x', animation: 150 }" + @sort="onWidgetSort" + :key="place" + > + <div v-for="widget in widgets[place]" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)"> + <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true"/> + </div> + </x-draggable> + <div class="main"> + <a @click="hint">カスタマイズのヒント</a> + <div> + <mk-post-form v-if="os.i.clientSettings.showPostFormOnTopOfTl"/> + <mk-timeline ref="tl" @loaded="onTlLoaded"/> + </div> + </div> + </template> + <template v-else> + <div v-for="place in ['left', 'right']" :class="place"> + <component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" @chosen="warp"/> + </div> + <div class="main"> + <mk-post-form v-if="os.i.clientSettings.showPostFormOnTopOfTl"/> + <mk-timeline ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/> + <mk-mentions @loaded="onTlLoaded" v-if="mode == 'mentions'"/> + </div> + </template> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as XDraggable from 'vuedraggable'; +import * as uuid from 'uuid'; + +export default Vue.extend({ + components: { + XDraggable + }, + props: { + customize: { + type: Boolean, + default: false + }, + mode: { + type: String, + default: 'timeline' + } + }, + data() { + return { + connection: null, + connectionId: null, + widgetAdderSelected: null, + trash: [], + widgets: { + left: [], + right: [] + } + }; + }, + computed: { + home: { + get(): any[] { + //#region 互換性のため + (this as any).os.i.clientSettings.home.forEach(w => { + if (w.name == 'rss-reader') w.name = 'rss'; + if (w.name == 'user-recommendation') w.name = 'users'; + if (w.name == 'recommended-polls') w.name = 'polls'; + }); + //#endregion + return (this as any).os.i.clientSettings.home; + }, + set(value) { + (this as any).os.i.clientSettings.home = value; + } + }, + left(): any[] { + return this.home.filter(w => w.place == 'left'); + }, + right(): any[] { + return this.home.filter(w => w.place == 'right'); + } + }, + created() { + this.widgets.left = this.left; + this.widgets.right = this.right; + this.$watch('os.i.clientSettings', i => { + this.widgets.left = this.left; + this.widgets.right = this.right; + }, { + deep: true + }); + }, + mounted() { + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + + this.connection.on('home_updated', this.onHomeUpdated); + }, + beforeDestroy() { + this.connection.off('home_updated', this.onHomeUpdated); + (this as any).os.stream.dispose(this.connectionId); + }, + methods: { + hint() { + (this as any).apis.dialog({ + title: '%fa:info-circle%カスタマイズのヒント', + text: '<p>ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。</p>' + + '<p>一部のウィジェットは、<strong><strong>右</strong>クリック</strong>することで表示を変更することができます。</p>' + + '<p>ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。</p>' + + '<p>カスタマイズを終了するには、右上の「完了」をクリックします。</p>', + actions: [{ + text: 'Got it!' + }] + }); + }, + onTlLoaded() { + this.$emit('loaded'); + }, + onHomeUpdated(data) { + if (data.home) { + (this as any).os.i.clientSettings.home = data.home; + this.widgets.left = data.home.filter(w => w.place == 'left'); + this.widgets.right = data.home.filter(w => w.place == 'right'); + } else { + const w = (this as any).os.i.clientSettings.home.find(w => w.id == data.id); + if (w != null) { + w.data = data.data; + this.$refs[w.id][0].preventSave = true; + this.$refs[w.id][0].props = w.data; + this.widgets.left = (this as any).os.i.clientSettings.home.filter(w => w.place == 'left'); + this.widgets.right = (this as any).os.i.clientSettings.home.filter(w => w.place == 'right'); + } + } + }, + onWidgetContextmenu(widgetId) { + const w = (this.$refs[widgetId] as any)[0]; + if (w.func) w.func(); + }, + onWidgetSort() { + this.saveHome(); + }, + onTrash(evt) { + this.saveHome(); + }, + addWidget() { + const widget = { + name: this.widgetAdderSelected, + id: uuid(), + place: 'left', + data: {} + }; + + this.widgets.left.unshift(widget); + this.saveHome(); + }, + saveHome() { + const left = this.widgets.left; + const right = this.widgets.right; + this.home = left.concat(right); + left.forEach(w => w.place = 'left'); + right.forEach(w => w.place = 'right'); + (this as any).api('i/update_home', { + home: this.home + }); + }, + warp(date) { + (this.$refs.tl as any).warp(date); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-home + display block + + &[data-customize] + padding-top 48px + background-image url('/assets/desktop/grid.svg') + + > .main > .main + > a + display block + margin-bottom 8px + text-align center + + > div + cursor not-allowed !important + + > * + pointer-events none + + &:not([data-customize]) + > .main > *:empty + display none + + > .customize + position fixed + z-index 1000 + top 0 + left 0 + width 100% + height 48px + background #f7f7f7 + box-shadow 0 1px 1px rgba(0, 0, 0, 0.075) + + > a + display block + position absolute + z-index 1001 + top 0 + right 0 + padding 0 16px + line-height 48px + text-decoration none + color $theme-color-foreground + background $theme-color + transition background 0.1s ease + + &:hover + background lighten($theme-color, 10%) + + &:active + background darken($theme-color, 10%) + transition background 0s ease + + > [data-fa] + margin-right 8px + + > div + display flex + margin 0 auto + max-width 1200px - 32px + + > div + width 50% + + &.adder + > p + display inline + line-height 48px + + &.trash + border-left solid 1px #ddd + + > div + width 100% + height 100% + + > p + position absolute + top 0 + left 0 + width 100% + line-height 48px + margin 0 + text-align center + pointer-events none + + > .main + display flex + justify-content center + margin 0 auto + max-width 1200px + + > * + .customize-container + cursor move + border-radius 6px + + &:hover + box-shadow 0 0 8px rgba(64, 120, 200, 0.3) + + > * + pointer-events none + + > .main + padding 16px + width calc(100% - 275px * 2) + order 2 + + .mk-post-form + margin-bottom 16px + border solid 1px #e5e5e5 + border-radius 4px + + > *:not(.main) + width 275px + padding 16px 0 16px 0 + + > *:not(:last-child) + margin-bottom 16px + + > .left + padding-left 16px + order 1 + + > .right + padding-right 16px + order 3 + + @media (max-width 1100px) + > *:not(.main) + display none + + > .main + float none + width 100% + max-width 700px + margin 0 auto + +</style> diff --git a/src/client/app/desktop/views/components/index.ts b/src/client/app/desktop/views/components/index.ts new file mode 100644 index 0000000000..4f61f43692 --- /dev/null +++ b/src/client/app/desktop/views/components/index.ts @@ -0,0 +1,61 @@ +import Vue from 'vue'; + +import ui from './ui.vue'; +import uiNotification from './ui-notification.vue'; +import home from './home.vue'; +import timeline from './timeline.vue'; +import notes from './notes.vue'; +import subNoteContent from './sub-note-content.vue'; +import window from './window.vue'; +import noteFormWindow from './post-form-window.vue'; +import renoteFormWindow from './renote-form-window.vue'; +import analogClock from './analog-clock.vue'; +import ellipsisIcon from './ellipsis-icon.vue'; +import mediaImage from './media-image.vue'; +import mediaImageDialog from './media-image-dialog.vue'; +import mediaVideo from './media-video.vue'; +import notifications from './notifications.vue'; +import noteForm from './post-form.vue'; +import renoteForm from './renote-form.vue'; +import followButton from './follow-button.vue'; +import notePreview from './note-preview.vue'; +import drive from './drive.vue'; +import noteDetail from './note-detail.vue'; +import settings from './settings.vue'; +import calendar from './calendar.vue'; +import activity from './activity.vue'; +import friendsMaker from './friends-maker.vue'; +import followers from './followers.vue'; +import following from './following.vue'; +import usersList from './users-list.vue'; +import widgetContainer from './widget-container.vue'; + +Vue.component('mk-ui', ui); +Vue.component('mk-ui-notification', uiNotification); +Vue.component('mk-home', home); +Vue.component('mk-timeline', timeline); +Vue.component('mk-notes', notes); +Vue.component('mk-sub-note-content', subNoteContent); +Vue.component('mk-window', window); +Vue.component('mk-post-form-window', noteFormWindow); +Vue.component('mk-renote-form-window', renoteFormWindow); +Vue.component('mk-analog-clock', analogClock); +Vue.component('mk-ellipsis-icon', ellipsisIcon); +Vue.component('mk-media-image', mediaImage); +Vue.component('mk-media-image-dialog', mediaImageDialog); +Vue.component('mk-media-video', mediaVideo); +Vue.component('mk-notifications', notifications); +Vue.component('mk-post-form', noteForm); +Vue.component('mk-renote-form', renoteForm); +Vue.component('mk-follow-button', followButton); +Vue.component('mk-note-preview', notePreview); +Vue.component('mk-drive', drive); +Vue.component('mk-note-detail', noteDetail); +Vue.component('mk-settings', settings); +Vue.component('mk-calendar', calendar); +Vue.component('mk-activity', activity); +Vue.component('mk-friends-maker', friendsMaker); +Vue.component('mk-followers', followers); +Vue.component('mk-following', following); +Vue.component('mk-users-list', usersList); +Vue.component('mk-widget-container', widgetContainer); diff --git a/src/client/app/desktop/views/components/input-dialog.vue b/src/client/app/desktop/views/components/input-dialog.vue new file mode 100644 index 0000000000..e939fc1903 --- /dev/null +++ b/src/client/app/desktop/views/components/input-dialog.vue @@ -0,0 +1,180 @@ +<template> +<mk-window ref="window" is-modal width="500px" @before-close="beforeClose" @closed="$destroy"> + <span slot="header" :class="$style.header"> + %fa:i-cursor%{{ title }} + </span> + + <div :class="$style.body"> + <input ref="text" v-model="text" :type="type" @keydown="onKeydown" :placeholder="placeholder"/> + </div> + <div :class="$style.actions"> + <button :class="$style.cancel" @click="cancel">キャンセル</button> + <button :class="$style.ok" :disabled="!allowEmpty && text.length == 0" @click="ok">決定</button> + </div> +</mk-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: { + title: { + type: String + }, + placeholder: { + type: String + }, + default: { + type: String + }, + allowEmpty: { + default: true + }, + type: { + default: 'text' + } + }, + data() { + return { + done: false, + text: '' + }; + }, + mounted() { + if (this.default) this.text = this.default; + this.$nextTick(() => { + (this.$refs.text as any).focus(); + }); + }, + methods: { + ok() { + if (!this.allowEmpty && this.text == '') return; + this.done = true; + (this.$refs.window as any).close(); + }, + cancel() { + this.done = false; + (this.$refs.window as any).close(); + }, + beforeClose() { + if (this.done) { + this.$emit('done', this.text); + } else { + this.$emit('canceled'); + } + }, + onKeydown(e) { + if (e.which == 13) { // Enter + e.preventDefault(); + e.stopPropagation(); + this.ok(); + } + } + } +}); +</script> + + +<style lang="stylus" module> +@import '~const.styl' + +.header + > [data-fa] + margin-right 4px + +.body + padding 16px + + > input + display block + padding 8px + margin 0 + width 100% + max-width 100% + min-width 100% + font-size 1em + color #333 + background #fff + outline none + border solid 1px rgba($theme-color, 0.1) + border-radius 4px + transition border-color .3s ease + + &:hover + border-color rgba($theme-color, 0.2) + transition border-color .1s ease + + &:focus + color $theme-color + border-color rgba($theme-color, 0.5) + transition border-color 0s ease + + &::-webkit-input-placeholder + color rgba($theme-color, 0.3) + +.actions + height 72px + background lighten($theme-color, 95%) + +.ok +.cancel + display block + position absolute + bottom 16px + cursor pointer + padding 0 + margin 0 + width 120px + height 40px + font-size 1em + outline none + border-radius 4px + + &:focus + &:after + content "" + pointer-events none + position absolute + top -5px + right -5px + bottom -5px + left -5px + border 2px solid rgba($theme-color, 0.3) + border-radius 8px + + &:disabled + opacity 0.7 + cursor default + +.ok + right 16px + color $theme-color-foreground + background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) + border solid 1px lighten($theme-color, 15%) + + &:not(:disabled) + font-weight bold + + &:hover:not(:disabled) + background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) + border-color $theme-color + + &:active:not(:disabled) + background $theme-color + border-color $theme-color + +.cancel + right 148px + color #888 + background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) + border solid 1px #e2e2e2 + + &:hover + background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) + border-color #dcdcdc + + &:active + background #ececec + border-color #dcdcdc + +</style> diff --git a/src/client/app/desktop/views/components/media-image-dialog.vue b/src/client/app/desktop/views/components/media-image-dialog.vue new file mode 100644 index 0000000000..dec140d1c9 --- /dev/null +++ b/src/client/app/desktop/views/components/media-image-dialog.vue @@ -0,0 +1,69 @@ +<template> +<div class="mk-media-image-dialog"> + <div class="bg" @click="close"></div> + <img :src="image.url" :alt="image.name" :title="image.name" @click="close"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as anime from 'animejs'; + +export default Vue.extend({ + props: ['image'], + mounted() { + anime({ + targets: this.$el, + opacity: 1, + duration: 100, + easing: 'linear' + }); + }, + methods: { + close() { + anime({ + targets: this.$el, + opacity: 0, + duration: 100, + easing: 'linear', + complete: () => this.$destroy() + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-media-image-dialog + display block + position fixed + z-index 2048 + top 0 + left 0 + width 100% + height 100% + opacity 0 + + > .bg + display block + position fixed + z-index 1 + top 0 + left 0 + width 100% + height 100% + background rgba(0, 0, 0, 0.7) + + > img + position fixed + z-index 2 + top 0 + right 0 + bottom 0 + left 0 + max-width 100% + max-height 100% + margin auto + cursor zoom-out + +</style> diff --git a/src/client/app/desktop/views/components/media-image.vue b/src/client/app/desktop/views/components/media-image.vue new file mode 100644 index 0000000000..51309a0578 --- /dev/null +++ b/src/client/app/desktop/views/components/media-image.vue @@ -0,0 +1,63 @@ +<template> +<a class="mk-media-image" + :href="image.url" + @mousemove="onMousemove" + @mouseleave="onMouseleave" + @click.prevent="onClick" + :style="style" + :title="image.name" +></a> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import MkMediaImageDialog from './media-image-dialog.vue'; + +export default Vue.extend({ + props: ['image'], + computed: { + style(): any { + return { + 'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent', + 'background-image': `url(${this.image.url}?thumbnail&size=512)` + }; + } + }, + methods: { + onMousemove(e) { + const rect = this.$el.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + const xp = mouseX / this.$el.offsetWidth * 100; + const yp = mouseY / this.$el.offsetHeight * 100; + this.$el.style.backgroundPosition = xp + '% ' + yp + '%'; + this.$el.style.backgroundImage = 'url("' + this.image.url + '?thumbnail")'; + }, + + onMouseleave() { + this.$el.style.backgroundPosition = ''; + }, + + onClick() { + (this as any).os.new(MkMediaImageDialog, { + image: this.image + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-media-image + display block + cursor zoom-in + overflow hidden + width 100% + height 100% + background-position center + border-radius 4px + + &:not(:hover) + background-size cover + +</style> diff --git a/src/client/app/desktop/views/components/media-video-dialog.vue b/src/client/app/desktop/views/components/media-video-dialog.vue new file mode 100644 index 0000000000..cbf862cd1c --- /dev/null +++ b/src/client/app/desktop/views/components/media-video-dialog.vue @@ -0,0 +1,70 @@ +<template> +<div class="mk-media-video-dialog"> + <div class="bg" @click="close"></div> + <video :src="video.url" :title="video.name" controls autoplay ref="video"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as anime from 'animejs'; + +export default Vue.extend({ + props: ['video', 'start'], + mounted() { + anime({ + targets: this.$el, + opacity: 1, + duration: 100, + easing: 'linear' + }); + const videoTag = this.$refs.video as HTMLVideoElement + if (this.start) videoTag.currentTime = this.start + }, + methods: { + close() { + anime({ + targets: this.$el, + opacity: 0, + duration: 100, + easing: 'linear', + complete: () => this.$destroy() + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-media-video-dialog + display block + position fixed + z-index 2048 + top 0 + left 0 + width 100% + height 100% + opacity 0 + + > .bg + display block + position fixed + z-index 1 + top 0 + left 0 + width 100% + height 100% + background rgba(0, 0, 0, 0.7) + + > video + position fixed + z-index 2 + top 0 + right 0 + bottom 0 + left 0 + max-width 80vw + max-height 80vh + margin auto + +</style> diff --git a/src/client/app/desktop/views/components/media-video.vue b/src/client/app/desktop/views/components/media-video.vue new file mode 100644 index 0000000000..4fd955a821 --- /dev/null +++ b/src/client/app/desktop/views/components/media-video.vue @@ -0,0 +1,67 @@ +<template> + <video class="mk-media-video" + :src="video.url" + :title="video.name" + controls + @dblclick.prevent="onClick" + ref="video" + v-if="inlinePlayable" /> + <a class="mk-media-video-thumbnail" + :href="video.url" + :style="imageStyle" + @click.prevent="onClick" + :title="video.name" + v-else> + %fa:R play-circle% + </a> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import MkMediaVideoDialog from './media-video-dialog.vue'; + +export default Vue.extend({ + props: ['video', 'inlinePlayable'], + computed: { + imageStyle(): any { + return { + 'background-image': `url(${this.video.url}?thumbnail&size=512)` + }; + } + }, + methods: { + onClick() { + const videoTag = this.$refs.video as (HTMLVideoElement | null) + var start = 0 + if (videoTag) { + start = videoTag.currentTime + videoTag.pause() + } + (this as any).os.new(MkMediaVideoDialog, { + video: this.video, + start, + }) + } + } +}) +</script> + +<style lang="stylus" scoped> +.mk-media-video + display block + width 100% + height 100% + border-radius 4px +.mk-media-video-thumbnail + display flex + justify-content center + align-items center + font-size 3.5em + + cursor zoom-in + overflow hidden + background-position center + background-size cover + width 100% + height 100% +</style> diff --git a/src/client/app/desktop/views/components/mentions.vue b/src/client/app/desktop/views/components/mentions.vue new file mode 100644 index 0000000000..fc3a7af75d --- /dev/null +++ b/src/client/app/desktop/views/components/mentions.vue @@ -0,0 +1,125 @@ +<template> +<div class="mk-mentions"> + <header> + <span :data-is-active="mode == 'all'" @click="mode = 'all'">すべて</span> + <span :data-is-active="mode == 'following'" @click="mode = 'following'">フォロー中</span> + </header> + <div class="fetching" v-if="fetching"> + <mk-ellipsis-icon/> + </div> + <p class="empty" v-if="notes.length == 0 && !fetching"> + %fa:R comments% + <span v-if="mode == 'all'">あなた宛ての投稿はありません。</span> + <span v-if="mode == 'following'">あなたがフォローしているユーザーからの言及はありません。</span> + </p> + <mk-notes :notes="notes" ref="timeline"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + data() { + return { + fetching: true, + moreFetching: false, + mode: 'all', + notes: [] + }; + }, + watch: { + mode() { + this.fetch(); + } + }, + mounted() { + document.addEventListener('keydown', this.onDocumentKeydown); + window.addEventListener('scroll', this.onScroll); + + this.fetch(() => this.$emit('loaded')); + }, + beforeDestroy() { + document.removeEventListener('keydown', this.onDocumentKeydown); + window.removeEventListener('scroll', this.onScroll); + }, + methods: { + onDocumentKeydown(e) { + if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { + if (e.which == 84) { // t + (this.$refs.timeline as any).focus(); + } + } + }, + onScroll() { + const current = window.scrollY + window.innerHeight; + if (current > document.body.offsetHeight - 8) this.more(); + }, + fetch(cb?) { + this.fetching = true; + this.notes = []; + (this as any).api('notes/mentions', { + following: this.mode == 'following' + }).then(notes => { + this.notes = notes; + this.fetching = false; + if (cb) cb(); + }); + }, + more() { + if (this.moreFetching || this.fetching || this.notes.length == 0) return; + this.moreFetching = true; + (this as any).api('notes/mentions', { + following: this.mode == 'following', + untilId: this.notes[this.notes.length - 1].id + }).then(notes => { + this.notes = this.notes.concat(notes); + this.moreFetching = false; + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-mentions + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + > header + padding 8px 16px + border-bottom solid 1px #eee + + > span + margin-right 16px + line-height 27px + font-size 18px + color #555 + + &:not([data-is-active]) + color $theme-color + cursor pointer + + &:hover + text-decoration underline + + > .fetching + padding 64px 0 + + > .empty + display block + margin 0 auto + padding 32px + max-width 400px + text-align center + color #999 + + > [data-fa] + display block + margin-bottom 16px + font-size 3em + color #ccc + +</style> diff --git a/src/client/app/desktop/views/components/messaging-room-window.vue b/src/client/app/desktop/views/components/messaging-room-window.vue new file mode 100644 index 0000000000..dbe3266734 --- /dev/null +++ b/src/client/app/desktop/views/components/messaging-room-window.vue @@ -0,0 +1,32 @@ +<template> +<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy"> + <span slot="header" :class="$style.header">%fa:comments%メッセージ: {{ user | userName }}</span> + <mk-messaging-room :user="user" :class="$style.content"/> +</mk-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { url } from '../../../config'; +import getAcct from '../../../../../acct/render'; + +export default Vue.extend({ + props: ['user'], + computed: { + popout(): string { + return `${url}/i/messaging/${getAcct(this.user)}`; + } + } +}); +</script> + +<style lang="stylus" module> +.header + > [data-fa] + margin-right 4px + +.content + height 100% + overflow auto + +</style> diff --git a/src/client/app/desktop/views/components/messaging-window.vue b/src/client/app/desktop/views/components/messaging-window.vue new file mode 100644 index 0000000000..ac27465987 --- /dev/null +++ b/src/client/app/desktop/views/components/messaging-window.vue @@ -0,0 +1,32 @@ +<template> +<mk-window ref="window" width="500px" height="560px" @closed="$destroy"> + <span slot="header" :class="$style.header">%fa:comments%メッセージ</span> + <mk-messaging :class="$style.content" @navigate="navigate"/> +</mk-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import MkMessagingRoomWindow from './messaging-room-window.vue'; + +export default Vue.extend({ + methods: { + navigate(user) { + (this as any).os.new(MkMessagingRoomWindow, { + user: user + }); + } + } +}); +</script> + +<style lang="stylus" module> +.header + > [data-fa] + margin-right 4px + +.content + height 100% + overflow auto + +</style> diff --git a/src/client/app/desktop/views/components/note-detail.sub.vue b/src/client/app/desktop/views/components/note-detail.sub.vue new file mode 100644 index 0000000000..16bc2a1d98 --- /dev/null +++ b/src/client/app/desktop/views/components/note-detail.sub.vue @@ -0,0 +1,122 @@ +<template> +<div class="sub" :title="title"> + <router-link class="avatar-anchor" :to="note.user | userPage"> + <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/> + </router-link> + <div class="main"> + <header> + <div class="left"> + <router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link> + <span class="username">@{{ note.user | acct }}</span> + </div> + <div class="right"> + <router-link class="time" :to="note | notePage"> + <mk-time :time="note.createdAt"/> + </router-link> + </div> + </header> + <div class="body"> + <mk-note-html v-if="note.text" :text="note.text" :i="os.i" :class="$style.text"/> + <div class="media" v-if="note.media > 0"> + <mk-media-list :media-list="note.media"/> + </div> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import dateStringify from '../../../common/scripts/date-stringify'; + +export default Vue.extend({ + props: ['note'], + computed: { + title(): string { + return dateStringify(this.note.createdAt); + } + } +}); +</script> + +<style lang="stylus" scoped> +.sub + margin 0 + padding 20px 32px + background #fdfdfd + + &:after + content "" + display block + clear both + + &:hover + > .main > footer > button + color #888 + + > .avatar-anchor + display block + float left + margin 0 16px 0 0 + + > .avatar + display block + width 44px + height 44px + margin 0 + border-radius 4px + vertical-align bottom + + > .main + float left + width calc(100% - 60px) + + > header + margin-bottom 4px + white-space nowrap + + &:after + content "" + display block + clear both + + > .left + float left + + > .name + display inline + margin 0 + padding 0 + color #777 + font-size 1em + font-weight 700 + text-align left + text-decoration none + + &:hover + text-decoration underline + + > .username + text-align left + margin 0 0 0 8px + color #ccc + + > .right + float right + + > .time + font-size 0.9em + color #c0c0c0 + +</style> + +<style lang="stylus" module> +.text + cursor default + display block + margin 0 + padding 0 + overflow-wrap break-word + font-size 1em + color #717171 +</style> diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue new file mode 100644 index 0000000000..50bbb76988 --- /dev/null +++ b/src/client/app/desktop/views/components/note-detail.vue @@ -0,0 +1,434 @@ +<template> +<div class="mk-note-detail" :title="title"> + <button + class="read-more" + v-if="p.reply && p.reply.replyId && context == null" + title="会話をもっと読み込む" + @click="fetchContext" + :disabled="contextFetching" + > + <template v-if="!contextFetching">%fa:ellipsis-v%</template> + <template v-if="contextFetching">%fa:spinner .pulse%</template> + </button> + <div class="context"> + <x-sub v-for="note in context" :key="note.id" :note="note"/> + </div> + <div class="reply-to" v-if="p.reply"> + <x-sub :note="p.reply"/> + </div> + <div class="renote" v-if="isRenote"> + <p> + <router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId"> + <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/> + </router-link> + %fa:retweet% + <router-link class="name" :href="note.user | userPage">{{ note.user | userName }}</router-link> + がRenote + </p> + </div> + <article> + <router-link class="avatar-anchor" :to="p.user | userPage"> + <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/> + </router-link> + <header> + <router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link> + <span class="username">@{{ p.user | acct }}</span> + <router-link class="time" :to="p | notePage"> + <mk-time :time="p.createdAt"/> + </router-link> + </header> + <div class="body"> + <mk-note-html :class="$style.text" v-if="p.text" :text="p.text" :i="os.i"/> + <div class="media" v-if="p.media.length > 0"> + <mk-media-list :media-list="p.media"/> + </div> + <mk-poll v-if="p.poll" :note="p"/> + <mk-url-preview v-for="url in urls" :url="url" :key="url"/> + <div class="tags" v-if="p.tags && p.tags.length > 0"> + <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link> + </div> + <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a> + <div class="map" v-if="p.geo" ref="map"></div> + <div class="renote" v-if="p.renote"> + <mk-note-preview :note="p.renote"/> + </div> + </div> + <footer> + <mk-reactions-viewer :note="p"/> + <button @click="reply" title="返信"> + %fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p> + </button> + <button @click="renote" title="Renote"> + %fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p> + </button> + <button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="リアクション"> + %fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p> + </button> + <button @click="menu" ref="menuButton"> + %fa:ellipsis-h% + </button> + </footer> + </article> + <div class="replies" v-if="!compact"> + <x-sub v-for="note in replies" :key="note.id" :note="note"/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import dateStringify from '../../../common/scripts/date-stringify'; +import parse from '../../../../../text/parse'; + +import MkPostFormWindow from './post-form-window.vue'; +import MkRenoteFormWindow from './renote-form-window.vue'; +import MkNoteMenu from '../../../common/views/components/note-menu.vue'; +import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; +import XSub from './note-detail.sub.vue'; + +export default Vue.extend({ + components: { + XSub + }, + + props: { + note: { + type: Object, + required: true + }, + compact: { + default: false + } + }, + + data() { + return { + context: [], + contextFetching: false, + replies: [] + }; + }, + + computed: { + isRenote(): boolean { + return (this.note.renote && + this.note.text == null && + this.note.mediaIds.length == 0 && + this.note.poll == null); + }, + p(): any { + return this.isRenote ? this.note.renote : this.note; + }, + reactionsCount(): number { + return this.p.reactionCounts + ? Object.keys(this.p.reactionCounts) + .map(key => this.p.reactionCounts[key]) + .reduce((a, b) => a + b) + : 0; + }, + title(): string { + return dateStringify(this.p.createdAt); + }, + urls(): string[] { + if (this.p.text) { + const ast = parse(this.p.text); + return ast + .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) + .map(t => t.url); + } else { + return null; + } + } + }, + + mounted() { + // Get replies + if (!this.compact) { + (this as any).api('notes/replies', { + noteId: this.p.id, + limit: 8 + }).then(replies => { + this.replies = replies; + }); + } + + // Draw map + if (this.p.geo) { + const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true; + if (shouldShowMap) { + (this as any).os.getGoogleMaps().then(maps => { + const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]); + const map = new maps.Map(this.$refs.map, { + center: uluru, + zoom: 15 + }); + new maps.Marker({ + position: uluru, + map: map + }); + }); + } + } + }, + + methods: { + fetchContext() { + this.contextFetching = true; + + // Fetch context + (this as any).api('notes/context', { + noteId: this.p.replyId + }).then(context => { + this.contextFetching = false; + this.context = context.reverse(); + }); + }, + reply() { + (this as any).os.new(MkPostFormWindow, { + reply: this.p + }); + }, + renote() { + (this as any).os.new(MkRenoteFormWindow, { + note: this.p + }); + }, + react() { + (this as any).os.new(MkReactionPicker, { + source: this.$refs.reactButton, + note: this.p + }); + }, + menu() { + (this as any).os.new(MkNoteMenu, { + source: this.$refs.menuButton, + note: this.p + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-note-detail + margin 0 + padding 0 + overflow hidden + text-align left + background #fff + border solid 1px rgba(0, 0, 0, 0.1) + border-radius 8px + + > .read-more + display block + margin 0 + padding 10px 0 + width 100% + font-size 1em + text-align center + color #999 + cursor pointer + background #fafafa + outline none + border none + border-bottom solid 1px #eef0f2 + border-radius 6px 6px 0 0 + + &:hover + background #f6f6f6 + + &:active + background #f0f0f0 + + &:disabled + color #ccc + + > .context + > * + border-bottom 1px solid #eef0f2 + + > .renote + color #9dbb00 + background linear-gradient(to bottom, #edfde2 0%, #fff 100%) + + > p + margin 0 + padding 16px 32px + + .avatar-anchor + display inline-block + + .avatar + vertical-align bottom + min-width 28px + min-height 28px + max-width 28px + max-height 28px + margin 0 8px 0 0 + border-radius 6px + + [data-fa] + margin-right 4px + + .name + font-weight bold + + & + article + padding-top 8px + + > .reply-to + border-bottom 1px solid #eef0f2 + + > article + padding 28px 32px 18px 32px + + &:after + content "" + display block + clear both + + &:hover + > .main > footer > button + color #888 + + > .avatar-anchor + display block + width 60px + height 60px + + > .avatar + display block + width 60px + height 60px + margin 0 + border-radius 8px + vertical-align bottom + + > header + position absolute + top 28px + left 108px + width calc(100% - 108px) + + > .name + display inline-block + margin 0 + line-height 24px + color #777 + font-size 18px + font-weight 700 + text-align left + text-decoration none + + &:hover + text-decoration underline + + > .username + display block + text-align left + margin 0 + color #ccc + + > .time + position absolute + top 0 + right 32px + font-size 1em + color #c0c0c0 + + > .body + padding 8px 0 + + > .renote + margin 8px 0 + + > .mk-note-preview + padding 16px + border dashed 1px #c0dac6 + border-radius 8px + + > .location + margin 4px 0 + font-size 12px + color #ccc + + > .map + width 100% + height 300px + + &:empty + display none + + > .mk-url-preview + margin-top 8px + + > .tags + margin 4px 0 0 0 + + > * + display inline-block + margin 0 8px 0 0 + padding 2px 8px 2px 16px + font-size 90% + color #8d969e + background #edf0f3 + border-radius 4px + + &:before + content "" + display block + position absolute + top 0 + bottom 0 + left 4px + width 8px + height 8px + margin auto 0 + background #fff + border-radius 100% + + &:hover + text-decoration none + background #e2e7ec + + > footer + font-size 1.2em + + > button + margin 0 28px 0 0 + padding 8px + background transparent + border none + font-size 1em + color #ddd + cursor pointer + + &:hover + color #666 + + > .count + display inline + margin 0 0 0 8px + color #999 + + &.reacted + color $theme-color + + > .replies + > * + border-top 1px solid #eef0f2 + +</style> + +<style lang="stylus" module> +.text + cursor default + display block + margin 0 + padding 0 + overflow-wrap break-word + font-size 1.5em + color #717171 +</style> diff --git a/src/client/app/desktop/views/components/note-preview.vue b/src/client/app/desktop/views/components/note-preview.vue new file mode 100644 index 0000000000..ff3ecadc20 --- /dev/null +++ b/src/client/app/desktop/views/components/note-preview.vue @@ -0,0 +1,99 @@ +<template> +<div class="mk-note-preview" :title="title"> + <router-link class="avatar-anchor" :to="note.user | userPage"> + <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/> + </router-link> + <div class="main"> + <header> + <router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link> + <span class="username">@{{ note.user | acct }}</span> + <router-link class="time" :to="note | notePage"> + <mk-time :time="note.createdAt"/> + </router-link> + </header> + <div class="body"> + <mk-sub-note-content class="text" :note="note"/> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import dateStringify from '../../../common/scripts/date-stringify'; + +export default Vue.extend({ + props: ['note'], + computed: { + title(): string { + return dateStringify(this.note.createdAt); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-note-preview + font-size 0.9em + background #fff + + &:after + content "" + display block + clear both + + &:hover + > .main > footer > button + color #888 + + > .avatar-anchor + display block + float left + margin 0 16px 0 0 + + > .avatar + display block + width 52px + height 52px + margin 0 + border-radius 8px + vertical-align bottom + + > .main + float left + width calc(100% - 68px) + + > header + display flex + white-space nowrap + + > .name + margin 0 .5em 0 0 + padding 0 + color #607073 + font-size 1em + font-weight bold + text-decoration none + white-space normal + + &:hover + text-decoration underline + + > .username + margin 0 .5em 0 0 + color #d1d8da + + > .time + margin-left auto + color #b2b8bb + + > .body + + > .text + cursor default + margin 0 + padding 0 + font-size 1.1em + color #717171 + +</style> diff --git a/src/client/app/desktop/views/components/notes.note.sub.vue b/src/client/app/desktop/views/components/notes.note.sub.vue new file mode 100644 index 0000000000..e854785783 --- /dev/null +++ b/src/client/app/desktop/views/components/notes.note.sub.vue @@ -0,0 +1,108 @@ +<template> +<div class="sub" :title="title"> + <router-link class="avatar-anchor" :to="note.user | userPage"> + <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/> + </router-link> + <div class="main"> + <header> + <router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link> + <span class="username">@{{ note.user | acct }}</span> + <router-link class="created-at" :to="note | notePage"> + <mk-time :time="note.createdAt"/> + </router-link> + </header> + <div class="body"> + <mk-sub-note-content class="text" :note="note"/> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import dateStringify from '../../../common/scripts/date-stringify'; + +export default Vue.extend({ + props: ['note'], + computed: { + title(): string { + return dateStringify(this.note.createdAt); + } + } +}); +</script> + +<style lang="stylus" scoped> +.sub + margin 0 + padding 16px + font-size 0.9em + + &:after + content "" + display block + clear both + + &:hover + > .main > footer > button + color #888 + + > .avatar-anchor + display block + float left + margin 0 14px 0 0 + + > .avatar + display block + width 52px + height 52px + margin 0 + border-radius 8px + vertical-align bottom + + > .main + float left + width calc(100% - 66px) + + > header + display flex + margin-bottom 2px + white-space nowrap + line-height 21px + + > .name + display block + margin 0 .5em 0 0 + padding 0 + overflow hidden + color #607073 + font-size 1em + font-weight bold + text-decoration none + text-overflow ellipsis + + &:hover + text-decoration underline + + > .username + margin 0 .5em 0 0 + color #d1d8da + + > .created-at + margin-left auto + color #b2b8bb + + > .body + + > .text + cursor default + margin 0 + padding 0 + font-size 1.1em + color #717171 + + pre + max-height 120px + font-size 80% + +</style> diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue new file mode 100644 index 0000000000..8561643c9c --- /dev/null +++ b/src/client/app/desktop/views/components/notes.note.vue @@ -0,0 +1,586 @@ +<template> +<div class="note" tabindex="-1" :title="title" @keydown="onKeydown"> + <div class="reply-to" v-if="p.reply"> + <x-sub :note="p.reply"/> + </div> + <div class="renote" v-if="isRenote"> + <p> + <router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId"> + <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/> + </router-link> + %fa:retweet% + <span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span> + <a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a> + <span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span> + </p> + <mk-time :time="note.createdAt"/> + </div> + <article> + <router-link class="avatar-anchor" :to="p.user | userPage"> + <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/> + </router-link> + <div class="main"> + <header> + <router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link> + <span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span> + <span class="username">@{{ p.user | acct }}</span> + <div class="info"> + <span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span> + <span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span> + <router-link class="created-at" :to="p | notePage"> + <mk-time :time="p.createdAt"/> + </router-link> + </div> + </header> + <div class="body"> + <p class="channel" v-if="p.channel"> + <a :href="`${_CH_URL_}/${p.channel.id}`" target="_blank">{{ p.channel.title }}</a>: + </p> + <div class="text"> + <a class="reply" v-if="p.reply">%fa:reply%</a> + <mk-note-html v-if="p.textHtml" :text="p.text" :i="os.i" :class="$style.text"/> + <a class="rp" v-if="p.renote">RP:</a> + </div> + <div class="media" v-if="p.media.length > 0"> + <mk-media-list :media-list="p.media"/> + </div> + <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> + <div class="tags" v-if="p.tags && p.tags.length > 0"> + <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link> + </div> + <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a> + <div class="map" v-if="p.geo" ref="map"></div> + <div class="renote" v-if="p.renote"> + <mk-note-preview :note="p.renote"/> + </div> + <mk-url-preview v-for="url in urls" :url="url" :key="url"/> + </div> + <footer> + <mk-reactions-viewer :note="p" ref="reactionsViewer"/> + <button @click="reply" title="%i18n:desktop.tags.mk-timeline-note.reply%"> + %fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p> + </button> + <button @click="renote" title="%i18n:desktop.tags.mk-timeline-note.renote%"> + %fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p> + </button> + <button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:desktop.tags.mk-timeline-note.add-reaction%"> + %fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p> + </button> + <button @click="menu" ref="menuButton"> + %fa:ellipsis-h% + </button> + <button title="%i18n:desktop.tags.mk-timeline-note.detail"> + <template v-if="!isDetailOpened">%fa:caret-down%</template> + <template v-if="isDetailOpened">%fa:caret-up%</template> + </button> + </footer> + </div> + </article> + <div class="detail" v-if="isDetailOpened"> + <mk-note-status-graph width="462" height="130" :note="p"/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import dateStringify from '../../../common/scripts/date-stringify'; +import parse from '../../../../../text/parse'; + +import MkPostFormWindow from './post-form-window.vue'; +import MkRenoteFormWindow from './renote-form-window.vue'; +import MkNoteMenu from '../../../common/views/components/note-menu.vue'; +import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; +import XSub from './notes.note.sub.vue'; + +function focus(el, fn) { + const target = fn(el); + if (target) { + if (target.hasAttribute('tabindex')) { + target.focus(); + } else { + focus(target, fn); + } + } +} + +export default Vue.extend({ + components: { + XSub + }, + + props: ['note'], + + data() { + return { + isDetailOpened: false, + connection: null, + connectionId: null + }; + }, + + computed: { + + isRenote(): boolean { + return (this.note.renote && + this.note.text == null && + this.note.mediaIds.length == 0 && + this.note.poll == null); + }, + p(): any { + return this.isRenote ? this.note.renote : this.note; + }, + reactionsCount(): number { + return this.p.reactionCounts + ? Object.keys(this.p.reactionCounts) + .map(key => this.p.reactionCounts[key]) + .reduce((a, b) => a + b) + : 0; + }, + title(): string { + return dateStringify(this.p.createdAt); + }, + urls(): string[] { + if (this.p.text) { + const ast = parse(this.p.text); + return ast + .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) + .map(t => t.url); + } else { + return null; + } + } + }, + + created() { + if ((this as any).os.isSignedIn) { + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + } + }, + + mounted() { + this.capture(true); + + if ((this as any).os.isSignedIn) { + this.connection.on('_connected_', this.onStreamConnected); + } + + // Draw map + if (this.p.geo) { + const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true; + if (shouldShowMap) { + (this as any).os.getGoogleMaps().then(maps => { + const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]); + const map = new maps.Map(this.$refs.map, { + center: uluru, + zoom: 15 + }); + new maps.Marker({ + position: uluru, + map: map + }); + }); + } + } + }, + + beforeDestroy() { + this.decapture(true); + + if ((this as any).os.isSignedIn) { + this.connection.off('_connected_', this.onStreamConnected); + (this as any).os.stream.dispose(this.connectionId); + } + }, + + methods: { + capture(withHandler = false) { + if ((this as any).os.isSignedIn) { + this.connection.send({ + type: 'capture', + id: this.p.id + }); + if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated); + } + }, + decapture(withHandler = false) { + if ((this as any).os.isSignedIn) { + this.connection.send({ + type: 'decapture', + id: this.p.id + }); + if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated); + } + }, + onStreamConnected() { + this.capture(); + }, + onStreamNoteUpdated(data) { + const note = data.note; + if (note.id == this.note.id) { + this.$emit('update:note', note); + } else if (note.id == this.note.renoteId) { + this.note.renote = note; + } + }, + reply() { + (this as any).os.new(MkPostFormWindow, { + reply: this.p + }); + }, + renote() { + (this as any).os.new(MkRenoteFormWindow, { + note: this.p + }); + }, + react() { + (this as any).os.new(MkReactionPicker, { + source: this.$refs.reactButton, + note: this.p + }); + }, + menu() { + (this as any).os.new(MkNoteMenu, { + source: this.$refs.menuButton, + note: this.p + }); + }, + onKeydown(e) { + let shouldBeCancel = true; + + switch (true) { + case e.which == 38: // [↑] + case e.which == 74: // [j] + case e.which == 9 && e.shiftKey: // [Shift] + [Tab] + focus(this.$el, e => e.previousElementSibling); + break; + + case e.which == 40: // [↓] + case e.which == 75: // [k] + case e.which == 9: // [Tab] + focus(this.$el, e => e.nextElementSibling); + break; + + case e.which == 81: // [q] + case e.which == 69: // [e] + this.renote(); + break; + + case e.which == 70: // [f] + case e.which == 76: // [l] + //this.like(); + break; + + case e.which == 82: // [r] + this.reply(); + break; + + default: + shouldBeCancel = false; + } + + if (shouldBeCancel) e.preventDefault(); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.note + margin 0 + padding 0 + background #fff + border-bottom solid 1px #eaeaea + + &:first-child + border-top-left-radius 6px + border-top-right-radius 6px + + > .renote + border-top-left-radius 6px + border-top-right-radius 6px + + &:last-of-type + border-bottom none + + &:focus + z-index 1 + + &:after + content "" + pointer-events none + position absolute + top 2px + right 2px + bottom 2px + left 2px + border 2px solid rgba($theme-color, 0.3) + border-radius 4px + + > .renote + color #9dbb00 + background linear-gradient(to bottom, #edfde2 0%, #fff 100%) + + > p + margin 0 + padding 16px 32px + line-height 28px + + .avatar-anchor + display inline-block + + .avatar + vertical-align bottom + width 28px + height 28px + margin 0 8px 0 0 + border-radius 6px + + [data-fa] + margin-right 4px + + .name + font-weight bold + + > .mk-time + position absolute + top 16px + right 32px + font-size 0.9em + line-height 28px + + & + article + padding-top 8px + + > .reply-to + padding 0 16px + background rgba(0, 0, 0, 0.0125) + + > .mk-note-preview + background transparent + + > article + padding 28px 32px 18px 32px + + &:after + content "" + display block + clear both + + &:hover + > .main > footer > button + color #888 + + > .avatar-anchor + display block + float left + margin 0 16px 10px 0 + //position -webkit-sticky + //position sticky + //top 74px + + > .avatar + display block + width 58px + height 58px + margin 0 + border-radius 8px + vertical-align bottom + + > .main + float left + width calc(100% - 74px) + + > header + display flex + align-items center + margin-bottom 4px + white-space nowrap + + > .name + display block + margin 0 .5em 0 0 + padding 0 + overflow hidden + color #627079 + font-size 1em + font-weight bold + text-decoration none + text-overflow ellipsis + + &:hover + text-decoration underline + + > .is-bot + margin 0 .5em 0 0 + padding 1px 6px + font-size 12px + color #aaa + border solid 1px #ddd + border-radius 3px + + > .username + margin 0 .5em 0 0 + color #ccc + + > .info + margin-left auto + font-size 0.9em + + > .mobile + margin-right 8px + color #ccc + + > .app + margin-right 8px + padding-right 8px + color #ccc + border-right solid 1px #eaeaea + + > .created-at + color #c0c0c0 + + > .body + + > .text + cursor default + display block + margin 0 + padding 0 + overflow-wrap break-word + font-size 1.1em + color #717171 + + >>> .quote + margin 8px + padding 6px 12px + color #aaa + border-left solid 3px #eee + + > .reply + margin-right 8px + color #717171 + + > .rp + margin-left 4px + font-style oblique + color #a0bf46 + + > .location + margin 4px 0 + font-size 12px + color #ccc + + > .map + width 100% + height 300px + + &:empty + display none + + > .tags + margin 4px 0 0 0 + + > * + display inline-block + margin 0 8px 0 0 + padding 2px 8px 2px 16px + font-size 90% + color #8d969e + background #edf0f3 + border-radius 4px + + &:before + content "" + display block + position absolute + top 0 + bottom 0 + left 4px + width 8px + height 8px + margin auto 0 + background #fff + border-radius 100% + + &:hover + text-decoration none + background #e2e7ec + + .mk-url-preview + margin-top 8px + + > .channel + margin 0 + + > .mk-poll + font-size 80% + + > .renote + margin 8px 0 + + > .mk-note-preview + padding 16px + border dashed 1px #c0dac6 + border-radius 8px + + > footer + > button + margin 0 28px 0 0 + padding 0 8px + line-height 32px + font-size 1em + color #ddd + background transparent + border none + cursor pointer + + &:hover + color #666 + + > .count + display inline + margin 0 0 0 8px + color #999 + + &.reacted + color $theme-color + + &:last-child + position absolute + right 0 + margin 0 + + > .detail + padding-top 4px + background rgba(0, 0, 0, 0.0125) + +</style> + +<style lang="stylus" module> +.text + + code + padding 4px 8px + margin 0 0.5em + font-size 80% + color #525252 + background #f8f8f8 + border-radius 2px + + pre > code + padding 16px + margin 0 + + [data-is-me]:after + content "you" + padding 0 4px + margin-left 4px + font-size 80% + color $theme-color-foreground + background $theme-color + border-radius 4px +</style> diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue new file mode 100644 index 0000000000..b5f6957a16 --- /dev/null +++ b/src/client/app/desktop/views/components/notes.vue @@ -0,0 +1,89 @@ +<template> +<div class="mk-notes"> + <template v-for="(note, i) in _notes"> + <x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> + <p class="date" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> + <span>%fa:angle-up%{{ note._datetext }}</span> + <span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span> + </p> + </template> + <footer> + <slot name="footer"></slot> + </footer> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XNote from './notes.note.vue'; + +export default Vue.extend({ + components: { + XNote + }, + props: { + notes: { + type: Array, + default: () => [] + } + }, + computed: { + _notes(): any[] { + return (this.notes as any).map(note => { + const date = new Date(note.createdAt).getDate(); + const month = new Date(note.createdAt).getMonth() + 1; + note._date = date; + note._datetext = `${month}月 ${date}日`; + return note; + }); + } + }, + methods: { + focus() { + (this.$el as any).children[0].focus(); + }, + onNoteUpdated(i, note) { + Vue.set((this as any).notes, i, note); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-notes + + > .date + display block + margin 0 + line-height 32px + font-size 14px + text-align center + color #aaa + background #fdfdfd + border-bottom solid 1px #eaeaea + + span + margin 0 16px + + [data-fa] + margin-right 8px + + > footer + > * + display block + margin 0 + padding 16px + width 100% + text-align center + color #ccc + border-top solid 1px #eaeaea + border-bottom-left-radius 4px + border-bottom-right-radius 4px + + > button + &:hover + background #f5f5f5 + + &:active + background #eee +</style> diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue new file mode 100644 index 0000000000..598c2ad2fa --- /dev/null +++ b/src/client/app/desktop/views/components/notifications.vue @@ -0,0 +1,315 @@ +<template> +<div class="mk-notifications"> + <div class="notifications" v-if="notifications.length != 0"> + <template v-for="(notification, i) in _notifications"> + <div class="notification" :class="notification.type" :key="notification.id"> + <mk-time :time="notification.createdAt"/> + <template v-if="notification.type == 'reaction'"> + <router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id"> + <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> + </router-link> + <div class="text"> + <p> + <mk-reaction-icon :reaction="notification.reaction"/> + <router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link> + </p> + <router-link class="note-ref" :to="notification.note | notePage"> + %fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right% + </router-link> + </div> + </template> + <template v-if="notification.type == 'renote'"> + <router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId"> + <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> + </router-link> + <div class="text"> + <p>%fa:retweet% + <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> + </p> + <router-link class="note-ref" :to="notification.note | notePage"> + %fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right% + </router-link> + </div> + </template> + <template v-if="notification.type == 'quote'"> + <router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId"> + <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> + </router-link> + <div class="text"> + <p>%fa:quote-left% + <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> + </p> + <router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link> + </div> + </template> + <template v-if="notification.type == 'follow'"> + <router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id"> + <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> + </router-link> + <div class="text"> + <p>%fa:user-plus% + <router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link> + </p> + </div> + </template> + <template v-if="notification.type == 'reply'"> + <router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId"> + <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> + </router-link> + <div class="text"> + <p>%fa:reply% + <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> + </p> + <router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link> + </div> + </template> + <template v-if="notification.type == 'mention'"> + <router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId"> + <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> + </router-link> + <div class="text"> + <p>%fa:at% + <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> + </p> + <a class="note-preview" :href="notification.note | notePage">{{ getNoteSummary(notification.note) }}</a> + </div> + </template> + <template v-if="notification.type == 'poll_vote'"> + <router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id"> + <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> + </router-link> + <div class="text"> + <p>%fa:chart-pie%<a :href="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</a></p> + <router-link class="note-ref" :to="notification.note | notePage"> + %fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right% + </router-link> + </div> + </template> + </div> + <p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'"> + <span>%fa:angle-up%{{ notification._datetext }}</span> + <span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span> + </p> + </template> + </div> + <button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications"> + <template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:desktop.tags.mk-notifications.more%' }} + </button> + <p class="empty" v-if="notifications.length == 0 && !fetching">ありません!</p> + <p class="loading" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import getNoteSummary from '../../../../../renderers/get-note-summary'; + +export default Vue.extend({ + data() { + return { + fetching: true, + fetchingMoreNotifications: false, + notifications: [], + moreNotifications: false, + connection: null, + connectionId: null, + getNoteSummary + }; + }, + computed: { + _notifications(): any[] { + return (this.notifications as any).map(notification => { + const date = new Date(notification.createdAt).getDate(); + const month = new Date(notification.createdAt).getMonth() + 1; + notification._date = date; + notification._datetext = `${month}月 ${date}日`; + return notification; + }); + } + }, + mounted() { + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + + this.connection.on('notification', this.onNotification); + + const max = 10; + + (this as any).api('i/notifications', { + limit: max + 1 + }).then(notifications => { + if (notifications.length == max + 1) { + this.moreNotifications = true; + notifications.pop(); + } + + this.notifications = notifications; + this.fetching = false; + }); + }, + beforeDestroy() { + this.connection.off('notification', this.onNotification); + (this as any).os.stream.dispose(this.connectionId); + }, + methods: { + fetchMoreNotifications() { + this.fetchingMoreNotifications = true; + + const max = 30; + + (this as any).api('i/notifications', { + limit: max + 1, + untilId: this.notifications[this.notifications.length - 1].id + }).then(notifications => { + if (notifications.length == max + 1) { + this.moreNotifications = true; + notifications.pop(); + } else { + this.moreNotifications = false; + } + this.notifications = this.notifications.concat(notifications); + this.fetchingMoreNotifications = false; + }); + }, + onNotification(notification) { + // TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない + this.connection.send({ + type: 'read_notification', + id: notification.id + }); + + this.notifications.unshift(notification); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-notifications + > .notifications + > .notification + margin 0 + padding 16px + overflow-wrap break-word + font-size 0.9em + border-bottom solid 1px rgba(0, 0, 0, 0.05) + + &:last-child + border-bottom none + + > .mk-time + display inline + position absolute + top 16px + right 12px + vertical-align top + color rgba(0, 0, 0, 0.6) + font-size small + + &:after + content "" + display block + clear both + + > .avatar-anchor + display block + float left + position -webkit-sticky + position sticky + top 16px + + > img + display block + min-width 36px + min-height 36px + max-width 36px + max-height 36px + border-radius 6px + + > .text + float right + width calc(100% - 36px) + padding-left 8px + + p + margin 0 + + i, .mk-reaction-icon + margin-right 4px + + .note-preview + color rgba(0, 0, 0, 0.7) + + .note-ref + color rgba(0, 0, 0, 0.7) + + [data-fa] + font-size 1em + font-weight normal + font-style normal + display inline-block + margin-right 3px + + &.renote, &.quote + .text p i + color #77B255 + + &.follow + .text p i + color #53c7ce + + &.reply, &.mention + .text p i + color #555 + + > .date + display block + margin 0 + line-height 32px + text-align center + font-size 0.8em + color #aaa + background #fdfdfd + border-bottom solid 1px rgba(0, 0, 0, 0.05) + + span + margin 0 16px + + [data-fa] + margin-right 8px + + > .more + display block + width 100% + padding 16px + color #555 + border-top solid 1px rgba(0, 0, 0, 0.05) + + &:hover + background rgba(0, 0, 0, 0.025) + + &:active + background rgba(0, 0, 0, 0.05) + + &.fetching + cursor wait + + > [data-fa] + margin-right 4px + + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > .loading + margin 0 + padding 16px + text-align center + color #aaa + + > [data-fa] + margin-right 4px + +</style> diff --git a/src/client/app/desktop/views/components/post-form-window.vue b/src/client/app/desktop/views/components/post-form-window.vue new file mode 100644 index 0000000000..c890592a5e --- /dev/null +++ b/src/client/app/desktop/views/components/post-form-window.vue @@ -0,0 +1,76 @@ +<template> +<mk-window ref="window" is-modal @closed="$destroy"> + <span slot="header"> + <span :class="$style.icon" v-if="geo">%fa:map-marker-alt%</span> + <span v-if="!reply">%i18n:desktop.tags.mk-post-form-window.note%</span> + <span v-if="reply">%i18n:desktop.tags.mk-post-form-window.reply%</span> + <span :class="$style.count" v-if="media.length != 0">{{ '%i18n:desktop.tags.mk-post-form-window.attaches%'.replace('{}', media.length) }}</span> + <span :class="$style.count" v-if="uploadings.length != 0">{{ '%i18n:desktop.tags.mk-post-form-window.uploading-media%'.replace('{}', uploadings.length) }}<mk-ellipsis/></span> + </span> + + <mk-note-preview v-if="reply" :class="$style.notePreview" :note="reply"/> + <mk-post-form ref="form" + :reply="reply" + @posted="onPosted" + @change-uploadings="onChangeUploadings" + @change-attached-media="onChangeMedia" + @geo-attached="onGeoAttached" + @geo-dettached="onGeoDettached"/> +</mk-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['reply'], + data() { + return { + uploadings: [], + media: [], + geo: null + }; + }, + mounted() { + this.$nextTick(() => { + (this.$refs.form as any).focus(); + }); + }, + methods: { + onChangeUploadings(files) { + this.uploadings = files; + }, + onChangeMedia(media) { + this.media = media; + }, + onGeoAttached(geo) { + this.geo = geo; + }, + onGeoDettached() { + this.geo = null; + }, + onPosted() { + (this.$refs.window as any).close(); + } + } +}); +</script> + +<style lang="stylus" module> +.icon + margin-right 8px + +.count + margin-left 8px + opacity 0.8 + + &:before + content '(' + + &:after + content ')' + +.notePreview + margin 16px 22px + +</style> diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue new file mode 100644 index 0000000000..8e3ec24bc8 --- /dev/null +++ b/src/client/app/desktop/views/components/post-form.vue @@ -0,0 +1,536 @@ +<template> +<div class="mk-post-form" + @dragover.stop="onDragover" + @dragenter="onDragenter" + @dragleave="onDragleave" + @drop.stop="onDrop" +> + <div class="content"> + <textarea :class="{ with: (files.length != 0 || poll) }" + ref="text" v-model="text" :disabled="posting" + @keydown="onKeydown" @paste="onPaste" :placeholder="placeholder" + v-autocomplete="'text'" + ></textarea> + <div class="medias" :class="{ with: poll }" v-show="files.length != 0"> + <x-draggable :list="files" :options="{ animation: 150 }"> + <div v-for="file in files" :key="file.id"> + <div class="img" :style="{ backgroundImage: `url(${file.url}?thumbnail&size=64)` }" :title="file.name"></div> + <img class="remove" @click="detachMedia(file.id)" src="/assets/desktop/remove.png" title="%i18n:desktop.tags.mk-post-form.attach-cancel%" alt=""/> + </div> + </x-draggable> + <p class="remain">{{ 4 - files.length }}/4</p> + </div> + <mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="saveDraft()"/> + </div> + <mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/> + <button class="upload" title="%i18n:desktop.tags.mk-post-form.attach-media-from-local%" @click="chooseFile">%fa:upload%</button> + <button class="drive" title="%i18n:desktop.tags.mk-post-form.attach-media-from-drive%" @click="chooseFileFromDrive">%fa:cloud%</button> + <button class="kao" title="%i18n:desktop.tags.mk-post-form.insert-a-kao%" @click="kao">%fa:R smile%</button> + <button class="poll" title="%i18n:desktop.tags.mk-post-form.create-poll%" @click="poll = true">%fa:chart-pie%</button> + <button class="geo" title="位置情報を添付する" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button> + <p class="text-count" :class="{ over: text.length > 1000 }">{{ '%i18n:desktop.tags.mk-post-form.text-remain%'.replace('{}', 1000 - text.length) }}</p> + <button :class="{ posting }" class="submit" :disabled="!canPost" @click="post"> + {{ posting ? '%i18n:desktop.tags.mk-post-form.posting%' : submitText }}<mk-ellipsis v-if="posting"/> + </button> + <input ref="file" type="file" accept="image/*" multiple="multiple" tabindex="-1" @change="onChangeFile"/> + <div class="dropzone" v-if="draghover"></div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as XDraggable from 'vuedraggable'; +import getKao from '../../../common/scripts/get-kao'; + +export default Vue.extend({ + components: { + XDraggable + }, + props: ['reply', 'renote'], + data() { + return { + posting: false, + text: '', + files: [], + uploadings: [], + poll: false, + geo: null, + autocomplete: null, + draghover: false + }; + }, + computed: { + draftId(): string { + return this.renote + ? 'renote:' + this.renote.id + : this.reply + ? 'reply:' + this.reply.id + : 'note'; + }, + placeholder(): string { + return this.renote + ? '%i18n:desktop.tags.mk-post-form.quote-placeholder%' + : this.reply + ? '%i18n:desktop.tags.mk-post-form.reply-placeholder%' + : '%i18n:desktop.tags.mk-post-form.note-placeholder%'; + }, + submitText(): string { + return this.renote + ? '%i18n:desktop.tags.mk-post-form.renote%' + : this.reply + ? '%i18n:desktop.tags.mk-post-form.reply%' + : '%i18n:desktop.tags.mk-post-form.note%'; + }, + canPost(): boolean { + return !this.posting && (this.text.length != 0 || this.files.length != 0 || this.poll || this.renote); + } + }, + watch: { + text() { + this.saveDraft(); + }, + poll() { + this.saveDraft(); + }, + files() { + this.saveDraft(); + } + }, + mounted() { + this.$nextTick(() => { + // 書きかけの投稿を復元 + const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftId]; + if (draft) { + this.text = draft.data.text; + this.files = draft.data.files; + if (draft.data.poll) { + this.poll = true; + this.$nextTick(() => { + (this.$refs.poll as any).set(draft.data.poll); + }); + } + this.$emit('change-attached-media', this.files); + } + }); + }, + methods: { + focus() { + (this.$refs.text as any).focus(); + }, + chooseFile() { + (this.$refs.file as any).click(); + }, + chooseFileFromDrive() { + (this as any).apis.chooseDriveFile({ + multiple: true + }).then(files => { + files.forEach(this.attachMedia); + }); + }, + attachMedia(driveFile) { + this.files.push(driveFile); + this.$emit('change-attached-media', this.files); + }, + detachMedia(id) { + this.files = this.files.filter(x => x.id != id); + this.$emit('change-attached-media', this.files); + }, + onChangeFile() { + Array.from((this.$refs.file as any).files).forEach(this.upload); + }, + upload(file) { + (this.$refs.uploader as any).upload(file); + }, + onChangeUploadings(uploads) { + this.$emit('change-uploadings', uploads); + }, + clear() { + this.text = ''; + this.files = []; + this.poll = false; + this.$emit('change-attached-media', this.files); + }, + onKeydown(e) { + if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post(); + }, + onPaste(e) { + Array.from(e.clipboardData.items).forEach((item: any) => { + if (item.kind == 'file') { + this.upload(item.getAsFile()); + } + }); + }, + onDragover(e) { + const isFile = e.dataTransfer.items[0].kind == 'file'; + const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; + if (isFile || isDriveFile) { + e.preventDefault(); + this.draghover = true; + e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; + } + }, + onDragenter(e) { + this.draghover = true; + }, + onDragleave(e) { + this.draghover = false; + }, + onDrop(e): void { + this.draghover = false; + + // ファイルだったら + if (e.dataTransfer.files.length > 0) { + e.preventDefault(); + Array.from(e.dataTransfer.files).forEach(this.upload); + return; + } + + //#region ドライブのファイル + const driveFile = e.dataTransfer.getData('mk_drive_file'); + if (driveFile != null && driveFile != '') { + const file = JSON.parse(driveFile); + this.files.push(file); + this.$emit('change-attached-media', this.files); + e.preventDefault(); + } + //#endregion + }, + setGeo() { + if (navigator.geolocation == null) { + alert('お使いの端末は位置情報に対応していません'); + return; + } + + navigator.geolocation.getCurrentPosition(pos => { + this.geo = pos.coords; + this.$emit('geo-attached', this.geo); + }, err => { + alert('エラー: ' + err.message); + }, { + enableHighAccuracy: true + }); + }, + removeGeo() { + this.geo = null; + this.$emit('geo-dettached'); + }, + post() { + this.posting = true; + + (this as any).api('notes/create', { + text: this.text == '' ? undefined : this.text, + mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined, + replyId: this.reply ? this.reply.id : undefined, + renoteId: this.renote ? this.renote.id : undefined, + poll: this.poll ? (this.$refs.poll as any).get() : undefined, + geo: this.geo ? { + coordinates: [this.geo.longitude, this.geo.latitude], + altitude: this.geo.altitude, + accuracy: this.geo.accuracy, + altitudeAccuracy: this.geo.altitudeAccuracy, + heading: isNaN(this.geo.heading) ? null : this.geo.heading, + speed: this.geo.speed, + } : null + }).then(data => { + this.clear(); + this.deleteDraft(); + this.$emit('posted'); + (this as any).apis.notify(this.renote + ? '%i18n:desktop.tags.mk-post-form.reposted%' + : this.reply + ? '%i18n:desktop.tags.mk-post-form.replied%' + : '%i18n:desktop.tags.mk-post-form.posted%'); + }).catch(err => { + (this as any).apis.notify(this.renote + ? '%i18n:desktop.tags.mk-post-form.renote-failed%' + : this.reply + ? '%i18n:desktop.tags.mk-post-form.reply-failed%' + : '%i18n:desktop.tags.mk-post-form.note-failed%'); + }).then(() => { + this.posting = false; + }); + }, + saveDraft() { + const data = JSON.parse(localStorage.getItem('drafts') || '{}'); + + data[this.draftId] = { + updatedAt: new Date(), + data: { + text: this.text, + files: this.files, + poll: this.poll && this.$refs.poll ? (this.$refs.poll as any).get() : undefined + } + } + + localStorage.setItem('drafts', JSON.stringify(data)); + }, + deleteDraft() { + const data = JSON.parse(localStorage.getItem('drafts') || '{}'); + + delete data[this.draftId]; + + localStorage.setItem('drafts', JSON.stringify(data)); + }, + kao() { + this.text += getKao(); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-post-form + display block + padding 16px + background lighten($theme-color, 95%) + + &:after + content "" + display block + clear both + + > .content + + textarea + display block + padding 12px + margin 0 + width 100% + max-width 100% + min-width 100% + min-height calc(16px + 12px + 12px) + font-size 16px + color #333 + background #fff + outline none + border solid 1px rgba($theme-color, 0.1) + border-radius 4px + transition border-color .3s ease + + &:hover + border-color rgba($theme-color, 0.2) + transition border-color .1s ease + + & + * + & + * + * + border-color rgba($theme-color, 0.2) + transition border-color .1s ease + + &:focus + color $theme-color + border-color rgba($theme-color, 0.5) + transition border-color 0s ease + + & + * + & + * + * + border-color rgba($theme-color, 0.5) + transition border-color 0s ease + + &:disabled + opacity 0.5 + + &::-webkit-input-placeholder + color rgba($theme-color, 0.3) + + &.with + border-bottom solid 1px rgba($theme-color, 0.1) !important + border-radius 4px 4px 0 0 + + > .medias + margin 0 + padding 0 + background lighten($theme-color, 98%) + border solid 1px rgba($theme-color, 0.1) + border-top none + border-radius 0 0 4px 4px + transition border-color .3s ease + + &.with + border-bottom solid 1px rgba($theme-color, 0.1) !important + border-radius 0 + + > .remain + display block + position absolute + top 8px + right 8px + margin 0 + padding 0 + color rgba($theme-color, 0.4) + + > div + padding 4px + + &:after + content "" + display block + clear both + + > div + float left + border solid 4px transparent + cursor move + + &:hover > .remove + display block + + > .img + width 64px + height 64px + background-size cover + background-position center center + + > .remove + display none + position absolute + top -6px + right -6px + width 16px + height 16px + cursor pointer + + > .mk-poll-editor + background lighten($theme-color, 98%) + border solid 1px rgba($theme-color, 0.1) + border-top none + border-radius 0 0 4px 4px + transition border-color .3s ease + + > .mk-uploader + margin 8px 0 0 0 + padding 8px + border solid 1px rgba($theme-color, 0.2) + border-radius 4px + + input[type='file'] + display none + + .text-count + pointer-events none + display block + position absolute + bottom 16px + right 138px + margin 0 + line-height 40px + color rgba($theme-color, 0.5) + + &.over + color #ec3828 + + .submit + display block + position absolute + bottom 16px + right 16px + cursor pointer + padding 0 + margin 0 + width 110px + height 40px + font-size 1em + color $theme-color-foreground + background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) + outline none + border solid 1px lighten($theme-color, 15%) + border-radius 4px + + &:not(:disabled) + font-weight bold + + &:hover:not(:disabled) + background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) + border-color $theme-color + + &:active:not(:disabled) + background $theme-color + border-color $theme-color + + &:focus + &:after + content "" + pointer-events none + position absolute + top -5px + right -5px + bottom -5px + left -5px + border 2px solid rgba($theme-color, 0.3) + border-radius 8px + + &:disabled + opacity 0.7 + cursor default + + &.wait + background linear-gradient( + 45deg, + darken($theme-color, 10%) 25%, + $theme-color 25%, + $theme-color 50%, + darken($theme-color, 10%) 50%, + darken($theme-color, 10%) 75%, + $theme-color 75%, + $theme-color + ) + background-size 32px 32px + animation stripe-bg 1.5s linear infinite + opacity 0.7 + cursor wait + + @keyframes stripe-bg + from {background-position: 0 0;} + to {background-position: -64px 32px;} + + > .upload + > .drive + > .kao + > .poll + > .geo + display inline-block + cursor pointer + padding 0 + margin 8px 4px 0 0 + width 40px + height 40px + font-size 1em + color rgba($theme-color, 0.5) + background transparent + outline none + border solid 1px transparent + border-radius 4px + + &:hover + background transparent + border-color rgba($theme-color, 0.3) + + &:active + color rgba($theme-color, 0.6) + background linear-gradient(to bottom, lighten($theme-color, 80%) 0%, lighten($theme-color, 90%) 100%) + border-color rgba($theme-color, 0.5) + box-shadow 0 2px 4px rgba(0, 0, 0, 0.15) inset + + &:focus + &:after + content "" + pointer-events none + position absolute + top -5px + right -5px + bottom -5px + left -5px + border 2px solid rgba($theme-color, 0.3) + border-radius 8px + + > .dropzone + position absolute + left 0 + top 0 + width 100% + height 100% + border dashed 2px rgba($theme-color, 0.5) + pointer-events none + +</style> diff --git a/src/client/app/desktop/views/components/progress-dialog.vue b/src/client/app/desktop/views/components/progress-dialog.vue new file mode 100644 index 0000000000..a4292e1aec --- /dev/null +++ b/src/client/app/desktop/views/components/progress-dialog.vue @@ -0,0 +1,95 @@ +<template> +<mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="$destroy"> + <span slot="header">{{ title }}<mk-ellipsis/></span> + <div :class="$style.body"> + <p :class="$style.init" v-if="isNaN(value)">待機中<mk-ellipsis/></p> + <p :class="$style.percentage" v-if="!isNaN(value)">{{ Math.floor((value / max) * 100) }}</p> + <progress :class="$style.progress" + v-if="!isNaN(value) && value < max" + :value="isNaN(value) ? 0 : value" + :max="max" + ></progress> + <div :class="[$style.progress, $style.waiting]" v-if="value >= max"></div> + </div> +</mk-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['title', 'initValue', 'initMax'], + data() { + return { + value: this.initValue, + max: this.initMax + }; + }, + methods: { + update(value, max) { + this.value = parseInt(value, 10); + this.max = parseInt(max, 10); + }, + close() { + (this.$refs.window as any).close(); + } + } +}); +</script> + +<style lang="stylus" module> +@import '~const.styl' + +.body + padding 18px 24px 24px 24px + +.init + display block + margin 0 + text-align center + color rgba(#000, 0.7) + +.percentage + display block + margin 0 0 4px 0 + text-align center + line-height 16px + color rgba($theme-color, 0.7) + + &:after + content '%' + +.progress + display block + margin 0 + width 100% + height 10px + background transparent + border none + border-radius 4px + overflow hidden + + &::-webkit-progress-value + background $theme-color + + &::-webkit-progress-bar + background rgba($theme-color, 0.1) + +.waiting + background linear-gradient( + 45deg, + lighten($theme-color, 30%) 25%, + $theme-color 25%, + $theme-color 50%, + lighten($theme-color, 30%) 50%, + lighten($theme-color, 30%) 75%, + $theme-color 75%, + $theme-color + ) + background-size 32px 32px + animation progress-dialog-tag-progress-waiting 1.5s linear infinite + + @keyframes progress-dialog-tag-progress-waiting + from {background-position: 0 0;} + to {background-position: -64px 32px;} + +</style> diff --git a/src/client/app/desktop/views/components/renote-form-window.vue b/src/client/app/desktop/views/components/renote-form-window.vue new file mode 100644 index 0000000000..09176b5ba7 --- /dev/null +++ b/src/client/app/desktop/views/components/renote-form-window.vue @@ -0,0 +1,42 @@ +<template> +<mk-window ref="window" is-modal @closed="$destroy"> + <span slot="header" :class="$style.header">%fa:retweet%%i18n:desktop.tags.mk-renote-form-window.title%</span> + <mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled"/> +</mk-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['note'], + mounted() { + document.addEventListener('keydown', this.onDocumentKeydown); + }, + beforeDestroy() { + document.removeEventListener('keydown', this.onDocumentKeydown); + }, + methods: { + onDocumentKeydown(e) { + if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { + if (e.which == 27) { // Esc + (this.$refs.window as any).close(); + } + } + }, + onPosted() { + (this.$refs.window as any).close(); + }, + onCanceled() { + (this.$refs.window as any).close(); + } + } +}); +</script> + +<style lang="stylus" module> +.header + > [data-fa] + margin-right 4px + +</style> diff --git a/src/client/app/desktop/views/components/renote-form.vue b/src/client/app/desktop/views/components/renote-form.vue new file mode 100644 index 0000000000..c6b9074156 --- /dev/null +++ b/src/client/app/desktop/views/components/renote-form.vue @@ -0,0 +1,131 @@ +<template> +<div class="mk-renote-form"> + <mk-note-preview :note="note"/> + <template v-if="!quote"> + <footer> + <a class="quote" v-if="!quote" @click="onQuote">%i18n:desktop.tags.mk-renote-form.quote%</a> + <button class="cancel" @click="cancel">%i18n:desktop.tags.mk-renote-form.cancel%</button> + <button class="ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:desktop.tags.mk-renote-form.reposting%' : '%i18n:desktop.tags.mk-renote-form.renote%' }}</button> + </footer> + </template> + <template v-if="quote"> + <mk-post-form ref="form" :renote="note" @posted="onChildFormPosted"/> + </template> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['note'], + data() { + return { + wait: false, + quote: false + }; + }, + methods: { + ok() { + this.wait = true; + (this as any).api('notes/create', { + renoteId: this.note.id + }).then(data => { + this.$emit('posted'); + (this as any).apis.notify('%i18n:desktop.tags.mk-renote-form.success%'); + }).catch(err => { + (this as any).apis.notify('%i18n:desktop.tags.mk-renote-form.failure%'); + }).then(() => { + this.wait = false; + }); + }, + cancel() { + this.$emit('canceled'); + }, + onQuote() { + this.quote = true; + + this.$nextTick(() => { + (this.$refs.form as any).focus(); + }); + }, + onChildFormPosted() { + this.$emit('posted'); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-renote-form + + > .mk-note-preview + margin 16px 22px + + > footer + height 72px + background lighten($theme-color, 95%) + + > .quote + position absolute + bottom 16px + left 28px + line-height 40px + + button + display block + position absolute + bottom 16px + cursor pointer + padding 0 + margin 0 + width 120px + height 40px + font-size 1em + outline none + border-radius 4px + + &:focus + &:after + content "" + pointer-events none + position absolute + top -5px + right -5px + bottom -5px + left -5px + border 2px solid rgba($theme-color, 0.3) + border-radius 8px + + > .cancel + right 148px + color #888 + background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) + border solid 1px #e2e2e2 + + &:hover + background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) + border-color #dcdcdc + + &:active + background #ececec + border-color #dcdcdc + + > .ok + right 16px + font-weight bold + color $theme-color-foreground + background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) + border solid 1px lighten($theme-color, 15%) + + &:hover + background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) + border-color $theme-color + + &:active + background $theme-color + border-color $theme-color + +</style> diff --git a/src/client/app/desktop/views/components/repost-form-window.vue b/src/client/app/desktop/views/components/repost-form-window.vue new file mode 100644 index 0000000000..09176b5ba7 --- /dev/null +++ b/src/client/app/desktop/views/components/repost-form-window.vue @@ -0,0 +1,42 @@ +<template> +<mk-window ref="window" is-modal @closed="$destroy"> + <span slot="header" :class="$style.header">%fa:retweet%%i18n:desktop.tags.mk-renote-form-window.title%</span> + <mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled"/> +</mk-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['note'], + mounted() { + document.addEventListener('keydown', this.onDocumentKeydown); + }, + beforeDestroy() { + document.removeEventListener('keydown', this.onDocumentKeydown); + }, + methods: { + onDocumentKeydown(e) { + if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { + if (e.which == 27) { // Esc + (this.$refs.window as any).close(); + } + } + }, + onPosted() { + (this.$refs.window as any).close(); + }, + onCanceled() { + (this.$refs.window as any).close(); + } + } +}); +</script> + +<style lang="stylus" module> +.header + > [data-fa] + margin-right 4px + +</style> diff --git a/src/client/app/desktop/views/components/repost-form.vue b/src/client/app/desktop/views/components/repost-form.vue new file mode 100644 index 0000000000..c6b9074156 --- /dev/null +++ b/src/client/app/desktop/views/components/repost-form.vue @@ -0,0 +1,131 @@ +<template> +<div class="mk-renote-form"> + <mk-note-preview :note="note"/> + <template v-if="!quote"> + <footer> + <a class="quote" v-if="!quote" @click="onQuote">%i18n:desktop.tags.mk-renote-form.quote%</a> + <button class="cancel" @click="cancel">%i18n:desktop.tags.mk-renote-form.cancel%</button> + <button class="ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:desktop.tags.mk-renote-form.reposting%' : '%i18n:desktop.tags.mk-renote-form.renote%' }}</button> + </footer> + </template> + <template v-if="quote"> + <mk-post-form ref="form" :renote="note" @posted="onChildFormPosted"/> + </template> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['note'], + data() { + return { + wait: false, + quote: false + }; + }, + methods: { + ok() { + this.wait = true; + (this as any).api('notes/create', { + renoteId: this.note.id + }).then(data => { + this.$emit('posted'); + (this as any).apis.notify('%i18n:desktop.tags.mk-renote-form.success%'); + }).catch(err => { + (this as any).apis.notify('%i18n:desktop.tags.mk-renote-form.failure%'); + }).then(() => { + this.wait = false; + }); + }, + cancel() { + this.$emit('canceled'); + }, + onQuote() { + this.quote = true; + + this.$nextTick(() => { + (this.$refs.form as any).focus(); + }); + }, + onChildFormPosted() { + this.$emit('posted'); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-renote-form + + > .mk-note-preview + margin 16px 22px + + > footer + height 72px + background lighten($theme-color, 95%) + + > .quote + position absolute + bottom 16px + left 28px + line-height 40px + + button + display block + position absolute + bottom 16px + cursor pointer + padding 0 + margin 0 + width 120px + height 40px + font-size 1em + outline none + border-radius 4px + + &:focus + &:after + content "" + pointer-events none + position absolute + top -5px + right -5px + bottom -5px + left -5px + border 2px solid rgba($theme-color, 0.3) + border-radius 8px + + > .cancel + right 148px + color #888 + background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) + border solid 1px #e2e2e2 + + &:hover + background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) + border-color #dcdcdc + + &:active + background #ececec + border-color #dcdcdc + + > .ok + right 16px + font-weight bold + color $theme-color-foreground + background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) + border solid 1px lighten($theme-color, 15%) + + &:hover + background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) + border-color $theme-color + + &:active + background $theme-color + border-color $theme-color + +</style> diff --git a/src/client/app/desktop/views/components/settings-window.vue b/src/client/app/desktop/views/components/settings-window.vue new file mode 100644 index 0000000000..d5be177dcc --- /dev/null +++ b/src/client/app/desktop/views/components/settings-window.vue @@ -0,0 +1,24 @@ +<template> +<mk-window ref="window" is-modal width="700px" height="550px" @closed="$destroy"> + <span slot="header" :class="$style.header">%fa:cog%設定</span> + <mk-settings @done="close"/> +</mk-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + methods: { + close() { + (this as any).$refs.window.close(); + } + } +}); +</script> + +<style lang="stylus" module> +.header + > [data-fa] + margin-right 4px + +</style> diff --git a/src/client/app/desktop/views/components/settings.2fa.vue b/src/client/app/desktop/views/components/settings.2fa.vue new file mode 100644 index 0000000000..fb2dc30f2a --- /dev/null +++ b/src/client/app/desktop/views/components/settings.2fa.vue @@ -0,0 +1,80 @@ +<template> +<div class="2fa"> + <p>%i18n:desktop.tags.mk-2fa-setting.intro%<a href="%i18n:desktop.tags.mk-2fa-setting.url%" target="_blank">%i18n:desktop.tags.mk-2fa-setting.detail%</a></p> + <div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-2fa-setting.caution%</p></div> + <p v-if="!data && !os.i.twoFactorEnabled"><button @click="register" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p> + <template v-if="os.i.twoFactorEnabled"> + <p>%i18n:desktop.tags.mk-2fa-setting.already-registered%</p> + <button @click="unregister" class="ui">%i18n:desktop.tags.mk-2fa-setting.unregister%</button> + </template> + <div v-if="data"> + <ol> + <li>%i18n:desktop.tags.mk-2fa-setting.authenticator% <a href="https://support.google.com/accounts/answer/1066447" target="_blank">%i18n:desktop.tags.mk-2fa-setting.howtoinstall%</a></li> + <li>%i18n:desktop.tags.mk-2fa-setting.scan%<br><img :src="data.qr"></li> + <li>%i18n:desktop.tags.mk-2fa-setting.done%<br> + <input type="number" v-model="token" class="ui"> + <button @click="submit" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.submit%</button> + </li> + </ol> + <div class="ui info"><p>%fa:info-circle%%i18n:desktop.tags.mk-2fa-setting.info%</p></div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + data() { + return { + data: null, + token: null + }; + }, + methods: { + register() { + (this as any).apis.input({ + title: '%i18n:desktop.tags.mk-2fa-setting.enter-password%', + type: 'password' + }).then(password => { + (this as any).api('i/2fa/register', { + password: password + }).then(data => { + this.data = data; + }); + }); + }, + + unregister() { + (this as any).apis.input({ + title: '%i18n:desktop.tags.mk-2fa-setting.enter-password%', + type: 'password' + }).then(password => { + (this as any).api('i/2fa/unregister', { + password: password + }).then(() => { + (this as any).apis.notify('%i18n:desktop.tags.mk-2fa-setting.unregistered%'); + (this as any).os.i.twoFactorEnabled = false; + }); + }); + }, + + submit() { + (this as any).api('i/2fa/done', { + token: this.token + }).then(() => { + (this as any).apis.notify('%i18n:desktop.tags.mk-2fa-setting.success%'); + (this as any).os.i.twoFactorEnabled = true; + }).catch(() => { + (this as any).apis.notify('%i18n:desktop.tags.mk-2fa-setting.failed%'); + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.2fa + color #4a535a + +</style> diff --git a/src/client/app/desktop/views/components/settings.api.vue b/src/client/app/desktop/views/components/settings.api.vue new file mode 100644 index 0000000000..5831f82075 --- /dev/null +++ b/src/client/app/desktop/views/components/settings.api.vue @@ -0,0 +1,40 @@ +<template> +<div class="root api"> + <p>Token: <code>{{ os.i.token }}</code></p> + <p>%i18n:desktop.tags.mk-api-info.intro%</p> + <div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-api-info.caution%</p></div> + <p>%i18n:desktop.tags.mk-api-info.regeneration-of-token%</p> + <button class="ui" @click="regenerateToken">%i18n:desktop.tags.mk-api-info.regenerate-token%</button> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + methods: { + regenerateToken() { + (this as any).apis.input({ + title: '%i18n:desktop.tags.mk-api-info.enter-password%', + type: 'password' + }).then(password => { + (this as any).api('i/regenerate_token', { + password: password + }); + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.root.api + color #4a535a + + code + display inline-block + padding 4px 6px + color #555 + background #eee + border-radius 2px +</style> diff --git a/src/client/app/desktop/views/components/settings.apps.vue b/src/client/app/desktop/views/components/settings.apps.vue new file mode 100644 index 0000000000..0503b03abd --- /dev/null +++ b/src/client/app/desktop/views/components/settings.apps.vue @@ -0,0 +1,39 @@ +<template> +<div class="root"> + <div class="none ui info" v-if="!fetching && apps.length == 0"> + <p>%fa:info-circle%%i18n:common.tags.mk-authorized-apps.no-apps%</p> + </div> + <div class="apps" v-if="apps.length != 0"> + <div v-for="app in apps"> + <p><b>{{ app.name }}</b></p> + <p>{{ app.description }}</p> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + data() { + return { + fetching: true, + apps: [] + }; + }, + mounted() { + (this as any).api('i/authorized_apps').then(apps => { + this.apps = apps; + this.fetching = false; + }); + } +}); +</script> + +<style lang="stylus" scoped> +.root + > .apps + > div + padding 16px 0 0 0 + border-bottom solid 1px #eee +</style> diff --git a/src/client/app/desktop/views/components/settings.drive.vue b/src/client/app/desktop/views/components/settings.drive.vue new file mode 100644 index 0000000000..8bb0c760a7 --- /dev/null +++ b/src/client/app/desktop/views/components/settings.drive.vue @@ -0,0 +1,35 @@ +<template> +<div class="root"> + <template v-if="!fetching"> + <el-progress :text-inside="true" :stroke-width="18" :percentage="Math.floor((usage / capacity) * 100)"/> + <p><b>{{ capacity | bytes }}</b>中<b>{{ usage | bytes }}</b>使用中</p> + </template> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + data() { + return { + fetching: true, + usage: null, + capacity: null + }; + }, + mounted() { + (this as any).api('drive').then(info => { + this.capacity = info.capacity; + this.usage = info.usage; + this.fetching = false; + }); + } +}); +</script> + +<style lang="stylus" scoped> +.root + > p + > b + margin 0 8px +</style> diff --git a/src/client/app/desktop/views/components/settings.mute.vue b/src/client/app/desktop/views/components/settings.mute.vue new file mode 100644 index 0000000000..94492ad262 --- /dev/null +++ b/src/client/app/desktop/views/components/settings.mute.vue @@ -0,0 +1,31 @@ +<template> +<div> + <div class="none ui info" v-if="!fetching && users.length == 0"> + <p>%fa:info-circle%%i18n:desktop.tags.mk-mute-setting.no-users%</p> + </div> + <div class="users" v-if="users.length != 0"> + <div v-for="user in users" :key="user.id"> + <p><b>{{ user | userName }}</b> @{{ user | acct }}</p> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + data() { + return { + fetching: true, + users: [] + }; + }, + mounted() { + (this as any).api('mute/list').then(x => { + this.users = x.users; + this.fetching = false; + }); + } +}); +</script> diff --git a/src/client/app/desktop/views/components/settings.password.vue b/src/client/app/desktop/views/components/settings.password.vue new file mode 100644 index 0000000000..f883b54065 --- /dev/null +++ b/src/client/app/desktop/views/components/settings.password.vue @@ -0,0 +1,47 @@ +<template> +<div> + <button @click="reset" class="ui primary">%i18n:desktop.tags.mk-password-setting.reset%</button> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + methods: { + reset() { + (this as any).apis.input({ + title: '%i18n:desktop.tags.mk-password-setting.enter-current-password%', + type: 'password' + }).then(currentPassword => { + (this as any).apis.input({ + title: '%i18n:desktop.tags.mk-password-setting.enter-new-password%', + type: 'password' + }).then(newPassword => { + (this as any).apis.input({ + title: '%i18n:desktop.tags.mk-password-setting.enter-new-password-again%', + type: 'password' + }).then(newPassword2 => { + if (newPassword !== newPassword2) { + (this as any).apis.dialog({ + title: null, + text: '%i18n:desktop.tags.mk-password-setting.not-match%', + actions: [{ + text: 'OK' + }] + }); + return; + } + (this as any).api('i/change_password', { + currentPasword: currentPassword, + newPassword: newPassword + }).then(() => { + (this as any).apis.notify('%i18n:desktop.tags.mk-password-setting.changed%'); + }); + }); + }); + }); + } + } +}); +</script> diff --git a/src/client/app/desktop/views/components/settings.profile.vue b/src/client/app/desktop/views/components/settings.profile.vue new file mode 100644 index 0000000000..324a939f95 --- /dev/null +++ b/src/client/app/desktop/views/components/settings.profile.vue @@ -0,0 +1,87 @@ +<template> +<div class="profile"> + <label class="avatar ui from group"> + <p>%i18n:desktop.tags.mk-profile-setting.avatar%</p> + <img class="avatar" :src="`${os.i.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + <button class="ui" @click="updateAvatar">%i18n:desktop.tags.mk-profile-setting.choice-avatar%</button> + </label> + <label class="ui from group"> + <p>%i18n:desktop.tags.mk-profile-setting.name%</p> + <input v-model="name" type="text" class="ui"/> + </label> + <label class="ui from group"> + <p>%i18n:desktop.tags.mk-profile-setting.location%</p> + <input v-model="location" type="text" class="ui"/> + </label> + <label class="ui from group"> + <p>%i18n:desktop.tags.mk-profile-setting.description%</p> + <textarea v-model="description" class="ui"></textarea> + </label> + <label class="ui from group"> + <p>%i18n:desktop.tags.mk-profile-setting.birthday%</p> + <el-date-picker v-model="birthday" type="date" value-format="yyyy-MM-dd"/> + </label> + <button class="ui primary" @click="save">%i18n:desktop.tags.mk-profile-setting.save%</button> + <section> + <h2>その他</h2> + <mk-switch v-model="os.i.isBot" @change="onChangeIsBot" text="このアカウントはbotです"/> + </section> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + data() { + return { + name: null, + location: null, + description: null, + birthday: null, + }; + }, + created() { + this.name = (this as any).os.i.name || ''; + this.location = (this as any).os.i.profile.location; + this.description = (this as any).os.i.description; + this.birthday = (this as any).os.i.profile.birthday; + }, + methods: { + updateAvatar() { + (this as any).apis.updateAvatar(); + }, + save() { + (this as any).api('i/update', { + name: this.name || null, + location: this.location || null, + description: this.description || null, + birthday: this.birthday || null + }).then(() => { + (this as any).apis.notify('プロフィールを更新しました'); + }); + }, + onChangeIsBot() { + (this as any).api('i/update', { + isBot: (this as any).os.i.isBot + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.profile + > .avatar + > img + display inline-block + vertical-align top + width 64px + height 64px + border-radius 4px + + > button + margin-left 8px + +</style> + diff --git a/src/client/app/desktop/views/components/settings.signins.vue b/src/client/app/desktop/views/components/settings.signins.vue new file mode 100644 index 0000000000..a414c95c27 --- /dev/null +++ b/src/client/app/desktop/views/components/settings.signins.vue @@ -0,0 +1,98 @@ +<template> +<div class="root"> +<div class="signins" v-if="signins.length != 0"> + <div v-for="signin in signins"> + <header @click="signin._show = !signin._show"> + <template v-if="signin.success">%fa:check%</template> + <template v-else>%fa:times%</template> + <span class="ip">{{ signin.ip }}</span> + <mk-time :time="signin.createdAt"/> + </header> + <div class="headers" v-show="signin._show"> + <tree-view :data="signin.headers"/> + </div> + </div> +</div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + data() { + return { + fetching: true, + signins: [], + connection: null, + connectionId: null + }; + }, + mounted() { + (this as any).api('i/signin_history').then(signins => { + this.signins = signins; + this.fetching = false; + }); + + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + + this.connection.on('signin', this.onSignin); + }, + beforeDestroy() { + this.connection.off('signin', this.onSignin); + (this as any).os.stream.dispose(this.connectionId); + }, + methods: { + onSignin(signin) { + this.signins.unshift(signin); + } + } +}); +</script> + +<style lang="stylus" scoped> +.root + > .signins + > div + border-bottom solid 1px #eee + + > header + display flex + padding 8px 0 + line-height 32px + cursor pointer + + > [data-fa] + margin-right 8px + text-align left + + &.check + color #0fda82 + + &.times + color #ff3100 + + > .ip + display inline-block + text-align left + padding 8px + line-height 16px + font-family monospace + font-size 14px + color #444 + background #f8f8f8 + border-radius 4px + + > .mk-time + margin-left auto + text-align right + color #777 + + > .headers + overflow auto + margin 0 0 16px 0 + max-height 100px + white-space pre-wrap + word-break break-all + +</style> diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue new file mode 100644 index 0000000000..4184ae82c7 --- /dev/null +++ b/src/client/app/desktop/views/components/settings.vue @@ -0,0 +1,419 @@ +<template> +<div class="mk-settings"> + <div class="nav"> + <p :class="{ active: page == 'profile' }" @mousedown="page = 'profile'">%fa:user .fw%%i18n:desktop.tags.mk-settings.profile%</p> + <p :class="{ active: page == 'web' }" @mousedown="page = 'web'">%fa:desktop .fw%Web</p> + <p :class="{ active: page == 'notification' }" @mousedown="page = 'notification'">%fa:R bell .fw%通知</p> + <p :class="{ active: page == 'drive' }" @mousedown="page = 'drive'">%fa:cloud .fw%%i18n:desktop.tags.mk-settings.drive%</p> + <p :class="{ active: page == 'mute' }" @mousedown="page = 'mute'">%fa:ban .fw%%i18n:desktop.tags.mk-settings.mute%</p> + <p :class="{ active: page == 'apps' }" @mousedown="page = 'apps'">%fa:puzzle-piece .fw%アプリ</p> + <p :class="{ active: page == 'twitter' }" @mousedown="page = 'twitter'">%fa:B twitter .fw%Twitter</p> + <p :class="{ active: page == 'security' }" @mousedown="page = 'security'">%fa:unlock-alt .fw%%i18n:desktop.tags.mk-settings.security%</p> + <p :class="{ active: page == 'api' }" @mousedown="page = 'api'">%fa:key .fw%API</p> + <p :class="{ active: page == 'other' }" @mousedown="page = 'other'">%fa:cogs .fw%%i18n:desktop.tags.mk-settings.other%</p> + </div> + <div class="pages"> + <section class="profile" v-show="page == 'profile'"> + <h1>%i18n:desktop.tags.mk-settings.profile%</h1> + <x-profile/> + </section> + + <section class="web" v-show="page == 'web'"> + <h1>動作</h1> + <mk-switch v-model="os.i.clientSettings.fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み"> + <span>ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。</span> + </mk-switch> + <mk-switch v-model="autoPopout" text="ウィンドウの自動ポップアウト"> + <span>ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。</span> + </mk-switch> + </section> + + <section class="web" v-show="page == 'web'"> + <h1>デザインと表示</h1> + <div class="div"> + <button class="ui button" @click="customizeHome">ホームをカスタマイズ</button> + </div> + <mk-switch v-model="os.i.clientSettings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/> + <mk-switch v-model="os.i.clientSettings.showMaps" @change="onChangeShowMaps" text="マップの自動展開"> + <span>位置情報が添付された投稿のマップを自動的に展開します。</span> + </mk-switch> + <mk-switch v-model="os.i.clientSettings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="ウィンドウのタイトルバーにグラデーションを使用"/> + </section> + + <section class="web" v-show="page == 'web'"> + <h1>サウンド</h1> + <mk-switch v-model="enableSounds" text="サウンドを有効にする"> + <span>投稿やメッセージを送受信したときなどにサウンドを再生します。この設定はブラウザに記憶されます。</span> + </mk-switch> + <label>ボリューム</label> + <el-slider + v-model="soundVolume" + :show-input="true" + :format-tooltip="v => `${v}%`" + :disabled="!enableSounds" + /> + <button class="ui button" @click="soundTest">%fa:volume-up% テスト</button> + </section> + + <section class="web" v-show="page == 'web'"> + <h1>モバイル</h1> + <mk-switch v-model="os.i.clientSettings.disableViaMobile" @change="onChangeDisableViaMobile" text="「モバイルからの投稿」フラグを付けない"/> + </section> + + <section class="web" v-show="page == 'web'"> + <h1>言語</h1> + <el-select v-model="lang" placeholder="言語を選択"> + <el-option-group label="推奨"> + <el-option label="自動" value=""/> + </el-option-group> + <el-option-group label="言語を指定"> + <el-option label="ja-JP" value="ja"/> + <el-option label="en-US" value="en"/> + </el-option-group> + </el-select> + <div class="none ui info"> + <p>%fa:info-circle%変更はページの再度読み込み後に反映されます。</p> + </div> + </section> + + <section class="web" v-show="page == 'web'"> + <h1>キャッシュ</h1> + <button class="ui button" @click="clean">クリーンアップ</button> + <div class="none ui info warn"> + <p>%fa:exclamation-triangle%クリーンアップを行うと、ブラウザに記憶されたアカウント情報のキャッシュ、書きかけの投稿・返信・メッセージ、およびその他のデータ(設定情報含む)が削除されます。クリーンアップを行った後はページを再度読み込みする必要があります。</p> + </div> + </section> + + <section class="notification" v-show="page == 'notification'"> + <h1>通知</h1> + <mk-switch v-model="os.i.settings.autoWatch" @change="onChangeAutoWatch" text="投稿の自動ウォッチ"> + <span>リアクションしたり返信したりした投稿に関する通知を自動的に受け取るようにします。</span> + </mk-switch> + </section> + + <section class="drive" v-show="page == 'drive'"> + <h1>%i18n:desktop.tags.mk-settings.drive%</h1> + <x-drive/> + </section> + + <section class="mute" v-show="page == 'mute'"> + <h1>%i18n:desktop.tags.mk-settings.mute%</h1> + <x-mute/> + </section> + + <section class="apps" v-show="page == 'apps'"> + <h1>アプリケーション</h1> + <x-apps/> + </section> + + <section class="twitter" v-show="page == 'twitter'"> + <h1>Twitter</h1> + <mk-twitter-setting/> + </section> + + <section class="password" v-show="page == 'security'"> + <h1>%i18n:desktop.tags.mk-settings.password%</h1> + <x-password/> + </section> + + <section class="2fa" v-show="page == 'security'"> + <h1>%i18n:desktop.tags.mk-settings.2fa%</h1> + <x-2fa/> + </section> + + <section class="signin" v-show="page == 'security'"> + <h1>サインイン履歴</h1> + <x-signins/> + </section> + + <section class="api" v-show="page == 'api'"> + <h1>API</h1> + <x-api/> + </section> + + <section class="other" v-show="page == 'other'"> + <h1>Misskeyについて</h1> + <p v-if="meta">このサーバーの運営者: <i><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></i></p> + </section> + + <section class="other" v-show="page == 'other'"> + <h1>Misskey Update</h1> + <p> + <span>バージョン: <i>{{ version }}</i></span> + <template v-if="latestVersion !== undefined"> + <br> + <span>最新のバージョン: <i>{{ latestVersion ? latestVersion : version }}</i></span> + </template> + </p> + <button class="ui button block" @click="checkForUpdate" :disabled="checkingForUpdate"> + <template v-if="checkingForUpdate">アップデートを確認中<mk-ellipsis/></template> + <template v-else>アップデートを確認</template> + </button> + <details> + <summary>詳細設定</summary> + <mk-switch v-model="preventUpdate" text="アップデートを延期する(非推奨)"> + <span>この設定をオンにしてもアップデートが反映される場合があります。この設定はこのデバイスのみ有効です。</span> + </mk-switch> + </details> + </section> + + <section class="other" v-show="page == 'other'"> + <h1>高度な設定</h1> + <mk-switch v-model="debug" text="デバッグモードを有効にする"> + <span>この設定はブラウザに記憶されます。</span> + </mk-switch> + <template v-if="debug"> + <mk-switch v-model="useRawScript" text="生のスクリプトを読み込む"> + <span>圧縮されていない「生の」スクリプトを使用します。サイズが大きいため、読み込みに時間がかかる場合があります。この設定はブラウザに記憶されます。</span> + </mk-switch> + <div class="none ui info"> + <p>%fa:info-circle%Misskeyはソースマップも提供しています。</p> + </div> + </template> + <mk-switch v-model="enableExperimental" text="実験的機能を有効にする"> + <span>実験的機能を有効にするとMisskeyの動作が不安定になる可能性があります。この設定はブラウザに記憶されます。</span> + </mk-switch> + <details v-if="debug"> + <summary>ツール</summary> + <button class="ui button block" @click="taskmngr">タスクマネージャ</button> + </details> + </section> + + <section class="other" v-show="page == 'other'"> + <h1>%i18n:desktop.tags.mk-settings.license%</h1> + <div v-html="license"></div> + <a :href="licenseUrl" target="_blank">サードパーティ</a> + </section> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XProfile from './settings.profile.vue'; +import XMute from './settings.mute.vue'; +import XPassword from './settings.password.vue'; +import X2fa from './settings.2fa.vue'; +import XApi from './settings.api.vue'; +import XApps from './settings.apps.vue'; +import XSignins from './settings.signins.vue'; +import XDrive from './settings.drive.vue'; +import { url, docsUrl, license, lang, version } from '../../../config'; +import checkForUpdate from '../../../common/scripts/check-for-update'; +import MkTaskManager from './taskmanager.vue'; + +export default Vue.extend({ + components: { + XProfile, + XMute, + XPassword, + X2fa, + XApi, + XApps, + XSignins, + XDrive + }, + data() { + return { + page: 'profile', + meta: null, + license, + version, + latestVersion: undefined, + checkingForUpdate: false, + enableSounds: localStorage.getItem('enableSounds') == 'true', + autoPopout: localStorage.getItem('autoPopout') == 'true', + soundVolume: localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) : 100, + lang: localStorage.getItem('lang') || '', + preventUpdate: localStorage.getItem('preventUpdate') == 'true', + debug: localStorage.getItem('debug') == 'true', + useRawScript: localStorage.getItem('useRawScript') == 'true', + enableExperimental: localStorage.getItem('enableExperimental') == 'true' + }; + }, + computed: { + licenseUrl(): string { + return `${docsUrl}/${lang}/license`; + } + }, + watch: { + autoPopout() { + localStorage.setItem('autoPopout', this.autoPopout ? 'true' : 'false'); + }, + enableSounds() { + localStorage.setItem('enableSounds', this.enableSounds ? 'true' : 'false'); + }, + soundVolume() { + localStorage.setItem('soundVolume', this.soundVolume.toString()); + }, + lang() { + localStorage.setItem('lang', this.lang); + }, + preventUpdate() { + localStorage.setItem('preventUpdate', this.preventUpdate ? 'true' : 'false'); + }, + debug() { + localStorage.setItem('debug', this.debug ? 'true' : 'false'); + }, + useRawScript() { + localStorage.setItem('useRawScript', this.useRawScript ? 'true' : 'false'); + }, + enableExperimental() { + localStorage.setItem('enableExperimental', this.enableExperimental ? 'true' : 'false'); + } + }, + created() { + (this as any).os.getMeta().then(meta => { + this.meta = meta; + }); + }, + methods: { + taskmngr() { + (this as any).os.new(MkTaskManager); + }, + customizeHome() { + this.$router.push('/i/customize-home'); + this.$emit('done'); + }, + onChangeFetchOnScroll(v) { + (this as any).api('i/update_client_setting', { + name: 'fetchOnScroll', + value: v + }); + }, + onChangeAutoWatch(v) { + (this as any).api('i/update', { + autoWatch: v + }); + }, + onChangeShowPostFormOnTopOfTl(v) { + (this as any).api('i/update_client_setting', { + name: 'showPostFormOnTopOfTl', + value: v + }); + }, + onChangeShowMaps(v) { + (this as any).api('i/update_client_setting', { + name: 'showMaps', + value: v + }); + }, + onChangeGradientWindowHeader(v) { + (this as any).api('i/update_client_setting', { + name: 'gradientWindowHeader', + value: v + }); + }, + onChangeDisableViaMobile(v) { + (this as any).api('i/update_client_setting', { + name: 'disableViaMobile', + value: v + }); + }, + checkForUpdate() { + this.checkingForUpdate = true; + checkForUpdate((this as any).os, true, true).then(newer => { + this.checkingForUpdate = false; + this.latestVersion = newer; + if (newer == null) { + (this as any).apis.dialog({ + title: '利用可能な更新はありません', + text: 'お使いのMisskeyは最新です。' + }); + } else { + (this as any).apis.dialog({ + title: '新しいバージョンが利用可能です', + text: 'ページを再度読み込みすると更新が適用されます。' + }); + } + }); + }, + clean() { + localStorage.clear(); + (this as any).apis.dialog({ + title: 'キャッシュを削除しました', + text: 'ページを再度読み込みしてください。' + }); + }, + soundTest() { + const sound = new Audio(`${url}/assets/message.mp3`); + sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1; + sound.play(); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-settings + display flex + width 100% + height 100% + + > .nav + flex 0 0 200px + width 100% + height 100% + padding 16px 0 0 0 + overflow auto + border-right solid 1px #ddd + + > p + display block + padding 10px 16px + margin 0 + color #666 + cursor pointer + user-select none + transition margin-left 0.2s ease + + > [data-fa] + margin-right 4px + + &:hover + color #555 + + &.active + margin-left 8px + color $theme-color !important + + > .pages + width 100% + height 100% + flex auto + overflow auto + + > section + margin 32px + color #4a535a + + > h1 + margin 0 0 1em 0 + padding 0 0 8px 0 + font-size 1em + color #555 + border-bottom solid 1px #eee + + &, >>> * + .ui.button.block + margin 16px 0 + + > section + margin 32px 0 + + > h2 + margin 0 0 1em 0 + padding 0 0 8px 0 + font-size 1em + color #555 + border-bottom solid 1px #eee + + > .web + > .div + border-bottom solid 1px #eee + padding 0 0 16px 0 + margin 0 0 16px 0 + +</style> diff --git a/src/client/app/desktop/views/components/sub-note-content.vue b/src/client/app/desktop/views/components/sub-note-content.vue new file mode 100644 index 0000000000..51ee93cba6 --- /dev/null +++ b/src/client/app/desktop/views/components/sub-note-content.vue @@ -0,0 +1,44 @@ +<template> +<div class="mk-sub-note-content"> + <div class="body"> + <a class="reply" v-if="note.replyId">%fa:reply%</a> + <mk-note-html :text="note.text" :i="os.i"/> + <a class="rp" v-if="note.renoteId" :href="`/note:${note.renoteId}`">RP: ...</a> + </div> + <details v-if="note.media.length > 0"> + <summary>({{ note.media.length }}つのメディア)</summary> + <mk-media-list :media-list="note.media"/> + </details> + <details v-if="note.poll"> + <summary>投票</summary> + <mk-poll :note="note"/> + </details> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['note'] +}); +</script> + +<style lang="stylus" scoped> +.mk-sub-note-content + overflow-wrap break-word + + > .body + > .reply + margin-right 6px + color #717171 + + > .rp + margin-left 4px + font-style oblique + color #a0bf46 + + mk-poll + font-size 80% + +</style> diff --git a/src/client/app/desktop/views/components/taskmanager.vue b/src/client/app/desktop/views/components/taskmanager.vue new file mode 100644 index 0000000000..a00fabb047 --- /dev/null +++ b/src/client/app/desktop/views/components/taskmanager.vue @@ -0,0 +1,219 @@ +<template> +<mk-window ref="window" width="750px" height="500px" @closed="$destroy" name="TaskManager"> + <span slot="header" :class="$style.header">%fa:stethoscope%タスクマネージャ</span> + <el-tabs :class="$style.content"> + <el-tab-pane label="Requests"> + <el-table + :data="os.requests" + style="width: 100%" + :default-sort="{prop: 'date', order: 'descending'}" + > + <el-table-column type="expand"> + <template slot-scope="props"> + <pre>{{ props.row.data }}</pre> + <pre>{{ props.row.res }}</pre> + </template> + </el-table-column> + + <el-table-column + label="Requested at" + prop="date" + sortable + > + <template slot-scope="scope"> + <b style="margin-right: 8px">{{ scope.row.date.getTime() }}</b> + <span>(<mk-time :time="scope.row.date"/>)</span> + </template> + </el-table-column> + + <el-table-column + label="Name" + > + <template slot-scope="scope"> + <b>{{ scope.row.name }}</b> + </template> + </el-table-column> + + <el-table-column + label="Status" + > + <template slot-scope="scope"> + <span>{{ scope.row.status || '(pending)' }}</span> + </template> + </el-table-column> + </el-table> + </el-tab-pane> + + <el-tab-pane label="Streams"> + <el-table + :data="os.connections" + style="width: 100%" + > + <el-table-column + label="Uptime" + > + <template slot-scope="scope"> + <mk-timer v-if="scope.row.connectedAt" :time="scope.row.connectedAt"/> + <span v-else>-</span> + </template> + </el-table-column> + + <el-table-column + label="Name" + > + <template slot-scope="scope"> + <b>{{ scope.row.name == '' ? '[Home]' : scope.row.name }}</b> + </template> + </el-table-column> + + <el-table-column + label="User" + > + <template slot-scope="scope"> + <span>{{ scope.row.user || '(anonymous)' }}</span> + </template> + </el-table-column> + + <el-table-column + prop="state" + label="State" + /> + + <el-table-column + prop="in" + label="In" + /> + + <el-table-column + prop="out" + label="Out" + /> + </el-table> + </el-tab-pane> + + <el-tab-pane label="Streams (Inspect)"> + <el-tabs type="card" style="height:50%"> + <el-tab-pane v-for="c in os.connections" :label="c.name == '' ? '[Home]' : c.name" :key="c.id" :name="c.id" ref="connectionsTab"> + <div style="padding: 12px 0 0 12px"> + <el-button size="mini" @click="send(c)">Send</el-button> + <el-button size="mini" type="warning" @click="c.isSuspended = true" v-if="!c.isSuspended">Suspend</el-button> + <el-button size="mini" type="success" @click="c.isSuspended = false" v-else>Resume</el-button> + <el-button size="mini" type="danger" @click="c.close">Disconnect</el-button> + </div> + + <el-table + :data="c.inout" + style="width: 100%" + :default-sort="{prop: 'at', order: 'descending'}" + > + <el-table-column type="expand"> + <template slot-scope="props"> + <pre>{{ props.row.data }}</pre> + </template> + </el-table-column> + + <el-table-column + label="Date" + prop="at" + sortable + > + <template slot-scope="scope"> + <b style="margin-right: 8px">{{ scope.row.at.getTime() }}</b> + <span>(<mk-time :time="scope.row.at"/>)</span> + </template> + </el-table-column> + + <el-table-column + label="Type" + > + <template slot-scope="scope"> + <span>{{ getMessageType(scope.row.data) }}</span> + </template> + </el-table-column> + + <el-table-column + label="Incoming / Outgoing" + prop="type" + /> + </el-table> + </el-tab-pane> + </el-tabs> + </el-tab-pane> + + <el-tab-pane label="Windows"> + <el-table + :data="Array.from(os.windows.windows)" + style="width: 100%" + > + <el-table-column + label="Name" + > + <template slot-scope="scope"> + <b>{{ scope.row.name || '(unknown)' }}</b> + </template> + </el-table-column> + + <el-table-column + label="Operations" + > + <template slot-scope="scope"> + <el-button size="mini" type="danger" @click="scope.row.close">Close</el-button> + </template> + </el-table-column> + </el-table> + </el-tab-pane> + </el-tabs> +</mk-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + mounted() { + (this as any).os.windows.on('added', this.onWindowsChanged); + (this as any).os.windows.on('removed', this.onWindowsChanged); + }, + beforeDestroy() { + (this as any).os.windows.off('added', this.onWindowsChanged); + (this as any).os.windows.off('removed', this.onWindowsChanged); + }, + methods: { + getMessageType(data): string { + return data.type ? data.type : '-'; + }, + onWindowsChanged() { + this.$forceUpdate(); + }, + send(c) { + (this as any).apis.input({ + title: 'Send a JSON message', + allowEmpty: false + }).then(json => { + c.send(JSON.parse(json)); + }); + } + } +}); +</script> + +<style lang="stylus" module> +.header + > [data-fa] + margin-right 4px + +.content + height 100% + overflow auto + +</style> + +<style> +.el-tabs__header { + margin-bottom: 0 !important; +} + +.el-tabs__item { + padding: 0 20px !important; +} +</style> diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue new file mode 100644 index 0000000000..e1f88b62f3 --- /dev/null +++ b/src/client/app/desktop/views/components/timeline.vue @@ -0,0 +1,156 @@ +<template> +<div class="mk-timeline"> + <mk-friends-maker v-if="alone"/> + <div class="fetching" v-if="fetching"> + <mk-ellipsis-icon/> + </div> + <p class="empty" v-if="notes.length == 0 && !fetching"> + %fa:R comments%自分の投稿や、自分がフォローしているユーザーの投稿が表示されます。 + </p> + <mk-notes :notes="notes" ref="timeline"> + <button slot="footer" @click="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> + <template v-if="!moreFetching">もっと見る</template> + <template v-if="moreFetching">%fa:spinner .pulse .fw%</template> + </button> + </mk-notes> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { url } from '../../../config'; + +export default Vue.extend({ + data() { + return { + fetching: true, + moreFetching: false, + existMore: false, + notes: [], + connection: null, + connectionId: null, + date: null + }; + }, + computed: { + alone(): boolean { + return (this as any).os.i.followingCount == 0; + } + }, + mounted() { + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + + this.connection.on('note', this.onNote); + this.connection.on('follow', this.onChangeFollowing); + this.connection.on('unfollow', this.onChangeFollowing); + + document.addEventListener('keydown', this.onKeydown); + window.addEventListener('scroll', this.onScroll); + + this.fetch(); + }, + beforeDestroy() { + this.connection.off('note', this.onNote); + this.connection.off('follow', this.onChangeFollowing); + this.connection.off('unfollow', this.onChangeFollowing); + (this as any).os.stream.dispose(this.connectionId); + + document.removeEventListener('keydown', this.onKeydown); + window.removeEventListener('scroll', this.onScroll); + }, + methods: { + fetch(cb?) { + this.fetching = true; + + (this as any).api('notes/timeline', { + limit: 11, + untilDate: this.date ? this.date.getTime() : undefined + }).then(notes => { + if (notes.length == 11) { + notes.pop(); + this.existMore = true; + } + this.notes = notes; + this.fetching = false; + this.$emit('loaded'); + if (cb) cb(); + }); + }, + more() { + if (this.moreFetching || this.fetching || this.notes.length == 0 || !this.existMore) return; + this.moreFetching = true; + (this as any).api('notes/timeline', { + limit: 11, + untilId: this.notes[this.notes.length - 1].id + }).then(notes => { + if (notes.length == 11) { + notes.pop(); + } else { + this.existMore = false; + } + this.notes = this.notes.concat(notes); + this.moreFetching = false; + }); + }, + onNote(note) { + // サウンドを再生する + if ((this as any).os.isEnableSounds) { + const sound = new Audio(`${url}/assets/post.mp3`); + sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1; + sound.play(); + } + + this.notes.unshift(note); + }, + onChangeFollowing() { + this.fetch(); + }, + onScroll() { + if ((this as any).os.i.clientSettings.fetchOnScroll !== false) { + const current = window.scrollY + window.innerHeight; + if (current > document.body.offsetHeight - 8) this.more(); + } + }, + onKeydown(e) { + if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { + if (e.which == 84) { // t + (this.$refs.timeline as any).focus(); + } + } + }, + warp(date) { + this.date = date; + this.fetch(); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-timeline + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + > .mk-friends-maker + border-bottom solid 1px #eee + + > .fetching + padding 64px 0 + + > .empty + display block + margin 0 auto + padding 32px + max-width 400px + text-align center + color #999 + + > [data-fa] + display block + margin-bottom 16px + font-size 3em + color #ccc + +</style> diff --git a/src/client/app/desktop/views/components/ui-notification.vue b/src/client/app/desktop/views/components/ui-notification.vue new file mode 100644 index 0000000000..9983f02c5e --- /dev/null +++ b/src/client/app/desktop/views/components/ui-notification.vue @@ -0,0 +1,61 @@ +<template> +<div class="mk-ui-notification"> + <p>{{ message }}</p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as anime from 'animejs'; + +export default Vue.extend({ + props: ['message'], + mounted() { + this.$nextTick(() => { + anime({ + targets: this.$el, + opacity: 1, + translateY: [-64, 0], + easing: 'easeOutElastic', + duration: 500 + }); + + setTimeout(() => { + anime({ + targets: this.$el, + opacity: 0, + translateY: -64, + duration: 500, + easing: 'easeInElastic', + complete: () => this.$destroy() + }); + }, 6000); + }); + } +}); +</script> + +<style lang="stylus" scoped> +.mk-ui-notification + display block + position fixed + z-index 10000 + top -128px + left 0 + right 0 + margin 0 auto + padding 128px 0 0 0 + width 500px + color rgba(#000, 0.6) + background rgba(#fff, 0.9) + border-radius 0 0 8px 8px + box-shadow 0 2px 4px rgba(#000, 0.2) + transform translateY(-64px) + opacity 0 + + > p + margin 0 + line-height 64px + text-align center + +</style> diff --git a/src/client/app/desktop/views/components/ui.header.account.vue b/src/client/app/desktop/views/components/ui.header.account.vue new file mode 100644 index 0000000000..ec4635f338 --- /dev/null +++ b/src/client/app/desktop/views/components/ui.header.account.vue @@ -0,0 +1,225 @@ +<template> +<div class="account"> + <button class="header" :data-active="isOpen" @click="toggle"> + <span class="username">{{ os.i.username }}<template v-if="!isOpen">%fa:angle-down%</template><template v-if="isOpen">%fa:angle-up%</template></span> + <img class="avatar" :src="`${ os.i.avatarUrl }?thumbnail&size=64`" alt="avatar"/> + </button> + <transition name="zoom-in-top"> + <div class="menu" v-if="isOpen"> + <ul> + <li> + <router-link :to="`/@${ os.i.username }`">%fa:user%%i18n:desktop.tags.mk-ui-header-account.profile%%fa:angle-right%</router-link> + </li> + <li @click="drive"> + <p>%fa:cloud%%i18n:desktop.tags.mk-ui-header-account.drive%%fa:angle-right%</p> + </li> + <li> + <a href="/i/mentions">%fa:at%%i18n:desktop.tags.mk-ui-header-account.mentions%%fa:angle-right%</a> + </li> + </ul> + <ul> + <li @click="settings"> + <p>%fa:cog%%i18n:desktop.tags.mk-ui-header-account.settings%%fa:angle-right%</p> + </li> + </ul> + <ul> + <li @click="signout"> + <p>%fa:power-off%%i18n:desktop.tags.mk-ui-header-account.signout%%fa:angle-right%</p> + </li> + </ul> + </div> + </transition> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import MkSettingsWindow from './settings-window.vue'; +import MkDriveWindow from './drive-window.vue'; +import contains from '../../../common/scripts/contains'; + +export default Vue.extend({ + data() { + return { + isOpen: false + }; + }, + beforeDestroy() { + this.close(); + }, + methods: { + toggle() { + this.isOpen ? this.close() : this.open(); + }, + open() { + this.isOpen = true; + Array.from(document.querySelectorAll('body *')).forEach(el => { + el.addEventListener('mousedown', this.onMousedown); + }); + }, + close() { + this.isOpen = false; + Array.from(document.querySelectorAll('body *')).forEach(el => { + el.removeEventListener('mousedown', this.onMousedown); + }); + }, + onMousedown(e) { + e.preventDefault(); + if (!contains(this.$el, e.target) && this.$el != e.target) this.close(); + return false; + }, + drive() { + this.close(); + (this as any).os.new(MkDriveWindow); + }, + settings() { + this.close(); + (this as any).os.new(MkSettingsWindow); + }, + signout() { + (this as any).os.signout(); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.account + > .header + display block + margin 0 + padding 0 + color #9eaba8 + border none + background transparent + cursor pointer + + * + pointer-events none + + &:hover + &[data-active='true'] + color darken(#9eaba8, 20%) + + > .avatar + filter saturate(150%) + + &:active + color darken(#9eaba8, 30%) + + > .username + display block + float left + margin 0 12px 0 16px + max-width 16em + line-height 48px + font-weight bold + font-family Meiryo, sans-serif + text-decoration none + + [data-fa] + margin-left 8px + + > .avatar + display block + float left + min-width 32px + max-width 32px + min-height 32px + max-height 32px + margin 8px 8px 8px 0 + border-radius 4px + transition filter 100ms ease + + > .menu + display block + position absolute + top 56px + right -2px + width 230px + font-size 0.8em + background #fff + border-radius 4px + box-shadow 0 1px 4px rgba(0, 0, 0, 0.25) + + &:before + content "" + pointer-events none + display block + position absolute + top -28px + right 12px + border-top solid 14px transparent + border-right solid 14px transparent + border-bottom solid 14px rgba(0, 0, 0, 0.1) + border-left solid 14px transparent + + &:after + content "" + pointer-events none + display block + position absolute + top -27px + right 12px + border-top solid 14px transparent + border-right solid 14px transparent + border-bottom solid 14px #fff + border-left solid 14px transparent + + ul + display block + margin 10px 0 + padding 0 + list-style none + + & + ul + padding-top 10px + border-top solid 1px #eee + + > li + display block + margin 0 + padding 0 + + > a + > p + display block + z-index 1 + padding 0 28px + margin 0 + line-height 40px + color #868C8C + cursor pointer + + * + pointer-events none + + > [data-fa]:first-of-type + margin-right 6px + + > [data-fa]:last-of-type + display block + position absolute + top 0 + right 8px + z-index 1 + padding 0 20px + font-size 1.2em + line-height 40px + + &:hover, &:active + text-decoration none + background $theme-color + color $theme-color-foreground + + &:active + background darken($theme-color, 10%) + +.zoom-in-top-enter-active, +.zoom-in-top-leave-active { + transform-origin: center -16px; +} + +</style> diff --git a/src/client/app/desktop/views/components/ui.header.clock.vue b/src/client/app/desktop/views/components/ui.header.clock.vue new file mode 100644 index 0000000000..cd23a67506 --- /dev/null +++ b/src/client/app/desktop/views/components/ui.header.clock.vue @@ -0,0 +1,109 @@ +<template> +<div class="clock"> + <div class="header"> + <time ref="time"> + <span class="yyyymmdd">{{ yyyy }}/{{ mm }}/{{ dd }}</span> + <br> + <span class="hhnn">{{ hh }}<span :style="{ visibility: now.getSeconds() % 2 == 0 ? 'visible' : 'hidden' }">:</span>{{ nn }}</span> + </time> + </div> + <div class="content"> + <mk-analog-clock/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + data() { + return { + now: new Date(), + clock: null + }; + }, + computed: { + yyyy(): number { + return this.now.getFullYear(); + }, + mm(): string { + return ('0' + (this.now.getMonth() + 1)).slice(-2); + }, + dd(): string { + return ('0' + this.now.getDate()).slice(-2); + }, + hh(): string { + return ('0' + this.now.getHours()).slice(-2); + }, + nn(): string { + return ('0' + this.now.getMinutes()).slice(-2); + } + }, + mounted() { + this.tick(); + this.clock = setInterval(this.tick, 1000); + }, + beforeDestroy() { + clearInterval(this.clock); + }, + methods: { + tick() { + this.now = new Date(); + } + } +}); +</script> + +<style lang="stylus" scoped> +.clock + display inline-block + overflow visible + + > .header + padding 0 12px + text-align center + font-size 10px + + &, * + cursor: default + + &:hover + background #899492 + + & + .content + visibility visible + + > time + color #fff !important + + * + color #fff !important + + &:after + content "" + display block + clear both + + > time + display table-cell + vertical-align middle + height 48px + color #9eaba8 + + > .yyyymmdd + opacity 0.7 + + > .content + visibility hidden + display block + position absolute + top auto + right 0 + z-index 3 + margin 0 + padding 0 + width 256px + background #899492 + +</style> diff --git a/src/client/app/desktop/views/components/ui.header.nav.vue b/src/client/app/desktop/views/components/ui.header.nav.vue new file mode 100644 index 0000000000..5d7ae0a4e6 --- /dev/null +++ b/src/client/app/desktop/views/components/ui.header.nav.vue @@ -0,0 +1,167 @@ +<template> +<div class="nav"> + <ul> + <template v-if="os.isSignedIn"> + <li class="home" :class="{ active: $route.name == 'index' }"> + <router-link to="/"> + %fa:home% + <p>%i18n:desktop.tags.mk-ui-header-nav.home%</p> + </router-link> + </li> + <li class="messaging"> + <a @click="messaging"> + %fa:comments% + <p>%i18n:desktop.tags.mk-ui-header-nav.messaging%</p> + <template v-if="hasUnreadMessagingMessages">%fa:circle%</template> + </a> + </li> + <li class="game"> + <a @click="game"> + %fa:gamepad% + <p>ゲーム</p> + <template v-if="hasGameInvitations">%fa:circle%</template> + </a> + </li> + </template> + </ul> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import MkMessagingWindow from './messaging-window.vue'; +import MkGameWindow from './game-window.vue'; + +export default Vue.extend({ + data() { + return { + hasUnreadMessagingMessages: false, + hasGameInvitations: false, + connection: null, + connectionId: null + }; + }, + mounted() { + if ((this as any).os.isSignedIn) { + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + + this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages); + this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage); + this.connection.on('othello_invited', this.onOthelloInvited); + this.connection.on('othello_no_invites', this.onOthelloNoInvites); + + // Fetch count of unread messaging messages + (this as any).api('messaging/unread').then(res => { + if (res.count > 0) { + this.hasUnreadMessagingMessages = true; + } + }); + } + }, + beforeDestroy() { + if ((this as any).os.isSignedIn) { + this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages); + this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage); + this.connection.off('othello_invited', this.onOthelloInvited); + this.connection.off('othello_no_invites', this.onOthelloNoInvites); + (this as any).os.stream.dispose(this.connectionId); + } + }, + methods: { + onUnreadMessagingMessage() { + this.hasUnreadMessagingMessages = true; + }, + + onReadAllMessagingMessages() { + this.hasUnreadMessagingMessages = false; + }, + + onOthelloInvited() { + this.hasGameInvitations = true; + }, + + onOthelloNoInvites() { + this.hasGameInvitations = false; + }, + + messaging() { + (this as any).os.new(MkMessagingWindow); + }, + + game() { + (this as any).os.new(MkGameWindow); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.nav + display inline-block + margin 0 + padding 0 + line-height 3rem + vertical-align top + + > ul + display inline-block + margin 0 + padding 0 + vertical-align top + line-height 3rem + list-style none + + > li + display inline-block + vertical-align top + height 48px + line-height 48px + + &.active + > a + border-bottom solid 3px $theme-color + + > a + display inline-block + z-index 1 + height 100% + padding 0 24px + font-size 13px + font-variant small-caps + color #9eaba8 + text-decoration none + transition none + cursor pointer + + * + pointer-events none + + &:hover + color darken(#9eaba8, 20%) + text-decoration none + + > [data-fa]:first-child + margin-right 8px + + > [data-fa]:last-child + margin-left 5px + font-size 10px + color $theme-color + + @media (max-width 1100px) + margin-left -5px + + > p + display inline + margin 0 + + @media (max-width 1100px) + display none + + @media (max-width 700px) + padding 0 12px + +</style> diff --git a/src/client/app/desktop/views/components/ui.header.notifications.vue b/src/client/app/desktop/views/components/ui.header.notifications.vue new file mode 100644 index 0000000000..e829418d18 --- /dev/null +++ b/src/client/app/desktop/views/components/ui.header.notifications.vue @@ -0,0 +1,158 @@ +<template> +<div class="notifications"> + <button :data-active="isOpen" @click="toggle" title="%i18n:desktop.tags.mk-ui-header-notifications.title%"> + %fa:R bell%<template v-if="hasUnreadNotifications">%fa:circle%</template> + </button> + <div class="pop" v-if="isOpen"> + <mk-notifications/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import contains from '../../../common/scripts/contains'; + +export default Vue.extend({ + data() { + return { + isOpen: false, + hasUnreadNotifications: false, + connection: null, + connectionId: null + }; + }, + mounted() { + if ((this as any).os.isSignedIn) { + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + + this.connection.on('read_all_notifications', this.onReadAllNotifications); + this.connection.on('unread_notification', this.onUnreadNotification); + + // Fetch count of unread notifications + (this as any).api('notifications/get_unread_count').then(res => { + if (res.count > 0) { + this.hasUnreadNotifications = true; + } + }); + } + }, + beforeDestroy() { + if ((this as any).os.isSignedIn) { + this.connection.off('read_all_notifications', this.onReadAllNotifications); + this.connection.off('unread_notification', this.onUnreadNotification); + (this as any).os.stream.dispose(this.connectionId); + } + }, + methods: { + onReadAllNotifications() { + this.hasUnreadNotifications = false; + }, + + onUnreadNotification() { + this.hasUnreadNotifications = true; + }, + + toggle() { + this.isOpen ? this.close() : this.open(); + }, + + open() { + this.isOpen = true; + Array.from(document.querySelectorAll('body *')).forEach(el => { + el.addEventListener('mousedown', this.onMousedown); + }); + }, + + close() { + this.isOpen = false; + Array.from(document.querySelectorAll('body *')).forEach(el => { + el.removeEventListener('mousedown', this.onMousedown); + }); + }, + + onMousedown(e) { + e.preventDefault(); + if (!contains(this.$el, e.target) && this.$el != e.target) this.close(); + return false; + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.notifications + + > button + display block + margin 0 + padding 0 + width 32px + color #9eaba8 + border none + background transparent + cursor pointer + + * + pointer-events none + + &:hover + &[data-active='true'] + color darken(#9eaba8, 20%) + + &:active + color darken(#9eaba8, 30%) + + > [data-fa].bell + font-size 1.2em + line-height 48px + + > [data-fa].circle + margin-left -5px + vertical-align super + font-size 10px + color $theme-color + + > .pop + display block + position absolute + top 56px + right -72px + width 300px + background #fff + border-radius 4px + box-shadow 0 1px 4px rgba(0, 0, 0, 0.25) + + &:before + content "" + pointer-events none + display block + position absolute + top -28px + right 74px + border-top solid 14px transparent + border-right solid 14px transparent + border-bottom solid 14px rgba(0, 0, 0, 0.1) + border-left solid 14px transparent + + &:after + content "" + pointer-events none + display block + position absolute + top -27px + right 74px + border-top solid 14px transparent + border-right solid 14px transparent + border-bottom solid 14px #fff + border-left solid 14px transparent + + > .mk-notifications + max-height 350px + font-size 1rem + overflow auto + +</style> diff --git a/src/client/app/desktop/views/components/ui.header.post.vue b/src/client/app/desktop/views/components/ui.header.post.vue new file mode 100644 index 0000000000..5c1756b756 --- /dev/null +++ b/src/client/app/desktop/views/components/ui.header.post.vue @@ -0,0 +1,54 @@ +<template> +<div class="note"> + <button @click="post" title="%i18n:desktop.tags.mk-ui-header-note-button.note%">%fa:pencil-alt%</button> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + methods: { + post() { + (this as any).apis.post(); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.note + display inline-block + padding 8px + height 100% + vertical-align top + + > button + display inline-block + margin 0 + padding 0 10px + height 100% + font-size 1.2em + font-weight normal + text-decoration none + color $theme-color-foreground + background $theme-color !important + outline none + border none + border-radius 4px + transition background 0.1s ease + cursor pointer + + * + pointer-events none + + &:hover + background lighten($theme-color, 10%) !important + + &:active + background darken($theme-color, 10%) !important + transition background 0s ease + +</style> diff --git a/src/client/app/desktop/views/components/ui.header.search.vue b/src/client/app/desktop/views/components/ui.header.search.vue new file mode 100644 index 0000000000..86215556ad --- /dev/null +++ b/src/client/app/desktop/views/components/ui.header.search.vue @@ -0,0 +1,70 @@ +<template> +<form class="search" @submit.prevent="onSubmit"> + %fa:search% + <input v-model="q" type="search" placeholder="%i18n:desktop.tags.mk-ui-header-search.placeholder%"/> + <div class="result"></div> +</form> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + data() { + return { + q: '' + }; + }, + methods: { + onSubmit() { + location.href = `/search?q=${encodeURIComponent(this.q)}`; + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.search + + > [data-fa] + display block + position absolute + top 0 + left 0 + width 48px + text-align center + line-height 48px + color #9eaba8 + pointer-events none + + > * + vertical-align middle + + > input + user-select text + cursor auto + margin 8px 0 0 0 + padding 6px 18px 6px 36px + width 14em + height 32px + font-size 1em + background rgba(0, 0, 0, 0.05) + outline none + //border solid 1px #ddd + border none + border-radius 16px + transition color 0.5s ease, border 0.5s ease + font-family FontAwesome, sans-serif + + &::placeholder + color #9eaba8 + + &:hover + background rgba(0, 0, 0, 0.08) + + &:focus + box-shadow 0 0 0 2px rgba($theme-color, 0.5) !important + +</style> diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue new file mode 100644 index 0000000000..2b63030cd2 --- /dev/null +++ b/src/client/app/desktop/views/components/ui.header.vue @@ -0,0 +1,178 @@ +<template> +<div class="header"> + <mk-special-message/> + <div class="main" ref="main"> + <div class="backdrop"></div> + <div class="main"> + <p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ os.i | userName }}</b>さん</p> + <div class="container" ref="mainContainer"> + <div class="left"> + <x-nav/> + </div> + <div class="right"> + <x-search/> + <x-account v-if="os.isSignedIn"/> + <x-notifications v-if="os.isSignedIn"/> + <x-post v-if="os.isSignedIn"/> + <x-clock/> + </div> + </div> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as anime from 'animejs'; + +import XNav from './ui.header.nav.vue'; +import XSearch from './ui.header.search.vue'; +import XAccount from './ui.header.account.vue'; +import XNotifications from './ui.header.notifications.vue'; +import XPost from './ui.header.post.vue'; +import XClock from './ui.header.clock.vue'; + +export default Vue.extend({ + components: { + XNav, + XSearch, + XAccount, + XNotifications, + XPost, + XClock, + }, + mounted() { + if ((this as any).os.isSignedIn) { + const ago = (new Date().getTime() - new Date((this as any).os.i.lastUsedAt).getTime()) / 1000 + const isHisasiburi = ago >= 3600; + (this as any).os.i.lastUsedAt = new Date(); + if (isHisasiburi) { + (this.$refs.welcomeback as any).style.display = 'block'; + (this.$refs.main as any).style.overflow = 'hidden'; + + anime({ + targets: this.$refs.welcomeback, + top: '0', + opacity: 1, + delay: 1000, + duration: 500, + easing: 'easeOutQuad' + }); + + anime({ + targets: this.$refs.mainContainer, + opacity: 0, + delay: 1000, + duration: 500, + easing: 'easeOutQuad' + }); + + setTimeout(() => { + anime({ + targets: this.$refs.welcomeback, + top: '-48px', + opacity: 0, + duration: 500, + complete: () => { + (this.$refs.welcomeback as any).style.display = 'none'; + (this.$refs.main as any).style.overflow = 'initial'; + }, + easing: 'easeInQuad' + }); + + anime({ + targets: this.$refs.mainContainer, + opacity: 1, + duration: 500, + easing: 'easeInQuad' + }); + }, 2500); + } + } + } +}); +</script> + +<style lang="stylus" scoped> +root(isDark) + position -webkit-sticky + position sticky + top 0 + z-index 1000 + width 100% + box-shadow 0 1px 1px rgba(0, 0, 0, 0.075) + + > .main + height 48px + + > .backdrop + position absolute + top 0 + z-index 1000 + width 100% + height 48px + background isDark ? #313543 : #f7f7f7 + + > .main + z-index 1001 + margin 0 + padding 0 + background-clip content-box + font-size 0.9rem + user-select none + + > p + display none + position absolute + top 48px + width 100% + line-height 48px + margin 0 + text-align center + color #888 + opacity 0 + + > .container + display flex + width 100% + max-width 1300px + margin 0 auto + + &:before + content "" + position absolute + top 0 + left 0 + display block + width 100% + height 48px + background-image url(/assets/desktop/header-logo.svg) + background-size 46px + background-position center + background-repeat no-repeat + opacity 0.3 + + > .left + margin 0 auto 0 0 + height 48px + + > .right + margin 0 0 0 auto + height 48px + + > * + display inline-block + vertical-align top + + @media (max-width 1100px) + > .mk-ui-header-search + display none + +.header[data-is-darkmode] + root(true) + +.header + root(false) + +</style> diff --git a/src/client/app/desktop/views/components/ui.vue b/src/client/app/desktop/views/components/ui.vue new file mode 100644 index 0000000000..87f932ff14 --- /dev/null +++ b/src/client/app/desktop/views/components/ui.vue @@ -0,0 +1,37 @@ +<template> +<div> + <x-header/> + <div class="content"> + <slot></slot> + </div> + <mk-stream-indicator v-if="os.isSignedIn"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XHeader from './ui.header.vue'; + +export default Vue.extend({ + components: { + XHeader + }, + mounted() { + document.addEventListener('keydown', this.onKeydown); + }, + beforeDestroy() { + document.removeEventListener('keydown', this.onKeydown); + }, + methods: { + onKeydown(e) { + if (e.target.tagName == 'INPUT' || e.target.tagName == 'TEXTAREA') return; + + if (e.which == 80 || e.which == 78) { // p or n + e.preventDefault(); + (this as any).apis.post(); + } + } + } +}); +</script> + diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue new file mode 100644 index 0000000000..bcd79dc2af --- /dev/null +++ b/src/client/app/desktop/views/components/user-preview.vue @@ -0,0 +1,167 @@ +<template> +<div class="mk-user-preview"> + <template v-if="u != null"> + <div class="banner" :style="u.bannerUrl ? `background-image: url(${u.bannerUrl}?thumbnail&size=512)` : ''"></div> + <router-link class="avatar" :to="u | userPage"> + <img :src="`${u.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + </router-link> + <div class="title"> + <router-link class="name" :to="u | userPage">{{ u.name }}</router-link> + <p class="username">@{{ u | acct }}</p> + </div> + <div class="description">{{ u.description }}</div> + <div class="status"> + <div> + <p>投稿</p><a>{{ u.notesCount }}</a> + </div> + <div> + <p>フォロー</p><a>{{ u.followingCount }}</a> + </div> + <div> + <p>フォロワー</p><a>{{ u.followersCount }}</a> + </div> + </div> + <mk-follow-button v-if="os.isSignedIn && user.id != os.i.id" :user="u"/> + </template> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as anime from 'animejs'; +import parseAcct from '../../../../../acct/parse'; + +export default Vue.extend({ + props: { + user: { + type: [Object, String], + required: true + } + }, + data() { + return { + u: null + }; + }, + mounted() { + if (typeof this.user == 'object') { + this.u = this.user; + this.$nextTick(() => { + this.open(); + }); + } else { + const query = this.user[0] == '@' ? + parseAcct(this.user.substr(1)) : + { userId: this.user }; + + (this as any).api('users/show', query).then(user => { + this.u = user; + this.open(); + }); + } + }, + methods: { + open() { + anime({ + targets: this.$el, + opacity: 1, + 'margin-top': 0, + duration: 200, + easing: 'easeOutQuad' + }); + }, + close() { + anime({ + targets: this.$el, + opacity: 0, + 'margin-top': '-8px', + duration: 200, + easing: 'easeOutQuad', + complete: () => this.$destroy() + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-user-preview + position absolute + z-index 2048 + margin-top -8px + width 250px + background #fff + background-clip content-box + border solid 1px rgba(0, 0, 0, 0.1) + border-radius 4px + overflow hidden + opacity 0 + + > .banner + height 84px + background-color #f5f5f5 + background-size cover + background-position center + + > .avatar + display block + position absolute + top 62px + left 13px + z-index 2 + + > img + display block + width 58px + height 58px + margin 0 + border solid 3px #fff + border-radius 8px + + > .title + display block + padding 8px 0 8px 82px + + > .name + display inline-block + margin 0 + font-weight bold + line-height 16px + color #656565 + + > .username + display block + margin 0 + line-height 16px + font-size 0.8em + color #999 + + > .description + padding 0 16px + font-size 0.7em + color #555 + + > .status + padding 8px 16px + + > div + display inline-block + width 33% + + > p + margin 0 + font-size 0.7em + color #aaa + + > a + font-size 1em + color $theme-color + + > .mk-follow-button + position absolute + top 92px + right 8px + +</style> diff --git a/src/client/app/desktop/views/components/users-list.item.vue b/src/client/app/desktop/views/components/users-list.item.vue new file mode 100644 index 0000000000..005c9cd6d3 --- /dev/null +++ b/src/client/app/desktop/views/components/users-list.item.vue @@ -0,0 +1,101 @@ +<template> +<div class="root item"> + <router-link class="avatar-anchor" :to="user | userPage" v-user-preview="user.id"> + <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + </router-link> + <div class="main"> + <header> + <router-link class="name" :to="user | userPage" v-user-preview="user.id">{{ user | userName }}</router-link> + <span class="username">@{{ user | acct }}</span> + </header> + <div class="body"> + <p class="followed" v-if="user.isFollowed">フォローされています</p> + <div class="description">{{ user.description }}</div> + </div> + </div> + <mk-follow-button :user="user"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['user'] +}); +</script> + +<style lang="stylus" scoped> +.root.item + padding 16px + font-size 16px + + &:after + content "" + display block + clear both + + > .avatar-anchor + display block + float left + margin 0 16px 0 0 + + > .avatar + display block + width 58px + height 58px + margin 0 + border-radius 8px + vertical-align bottom + + > .main + float left + width calc(100% - 74px) + + > header + margin-bottom 2px + + > .name + display inline + margin 0 + padding 0 + color #777 + font-size 1em + font-weight 700 + text-align left + text-decoration none + + &:hover + text-decoration underline + + > .username + text-align left + margin 0 0 0 8px + color #ccc + + > .body + > .followed + display inline-block + margin 0 0 4px 0 + padding 2px 8px + vertical-align top + font-size 10px + color #71afc7 + background #eefaff + border-radius 4px + + > .description + cursor default + display block + margin 0 + padding 0 + overflow-wrap break-word + font-size 1.1em + color #717171 + + > .mk-follow-button + position absolute + top 16px + right 16px + +</style> diff --git a/src/client/app/desktop/views/components/users-list.vue b/src/client/app/desktop/views/components/users-list.vue new file mode 100644 index 0000000000..a08e76f573 --- /dev/null +++ b/src/client/app/desktop/views/components/users-list.vue @@ -0,0 +1,143 @@ +<template> +<div class="mk-users-list"> + <nav> + <div> + <span :data-is-active="mode == 'all'" @click="mode = 'all'">すべて<span>{{ count }}</span></span> + <span v-if="os.isSignedIn && youKnowCount" :data-is-active="mode == 'iknow'" @click="mode = 'iknow'">知り合い<span>{{ youKnowCount }}</span></span> + </div> + </nav> + <div class="users" v-if="!fetching && users.length != 0"> + <div v-for="u in users" :key="u.id"> + <x-item :user="u"/> + </div> + </div> + <button class="more" v-if="!fetching && next != null" @click="more" :disabled="moreFetching"> + <span v-if="!moreFetching">もっと</span> + <span v-if="moreFetching">読み込み中<mk-ellipsis/></span> + </button> + <p class="no" v-if="!fetching && users.length == 0"> + <slot></slot> + </p> + <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XItem from './users-list.item.vue'; + +export default Vue.extend({ + components: { + XItem + }, + props: ['fetch', 'count', 'youKnowCount'], + data() { + return { + limit: 30, + mode: 'all', + fetching: true, + moreFetching: false, + users: [], + next: null + }; + }, + mounted() { + this._fetch(() => { + this.$emit('loaded'); + }); + }, + methods: { + _fetch(cb) { + this.fetching = true; + this.fetch(this.mode == 'iknow', this.limit, null, obj => { + this.users = obj.users; + this.next = obj.next; + this.fetching = false; + if (cb) cb(); + }); + }, + more() { + this.moreFetching = true; + this.fetch(this.mode == 'iknow', this.limit, this.next, obj => { + this.moreFetching = false; + this.users = this.users.concat(obj.users); + this.next = obj.next; + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-users-list + height 100% + background #fff + + > nav + z-index 1 + box-shadow 0 1px 0 rgba(#000, 0.1) + + > div + display flex + justify-content center + margin 0 auto + max-width 600px + + > span + display block + flex 1 1 + text-align center + line-height 52px + font-size 14px + color #657786 + border-bottom solid 2px transparent + cursor pointer + + * + pointer-events none + + &[data-is-active] + font-weight bold + color $theme-color + border-color $theme-color + cursor default + + > span + display inline-block + margin-left 4px + padding 2px 5px + font-size 12px + line-height 1 + color #888 + background #eee + border-radius 20px + + > .users + height calc(100% - 54px) + overflow auto + + > * + border-bottom solid 1px rgba(0, 0, 0, 0.05) + + > * + max-width 600px + margin 0 auto + + > .no + margin 0 + padding 16px + text-align center + color #aaa + + > .fetching + margin 0 + padding 16px + text-align center + color #aaa + + > [data-fa] + margin-right 4px + +</style> diff --git a/src/client/app/desktop/views/components/widget-container.vue b/src/client/app/desktop/views/components/widget-container.vue new file mode 100644 index 0000000000..188a67313e --- /dev/null +++ b/src/client/app/desktop/views/components/widget-container.vue @@ -0,0 +1,85 @@ +<template> +<div class="mk-widget-container" :class="{ naked }"> + <header :class="{ withGradient }" v-if="showHeader"> + <div class="title"><slot name="header"></slot></div> + <slot name="func"></slot> + </header> + <slot></slot> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: { + showHeader: { + type: Boolean, + default: true + }, + naked: { + type: Boolean, + default: false + } + }, + computed: { + withGradient(): boolean { + return (this as any).os.isSignedIn + ? (this as any).os.i.clientSettings.gradientWindowHeader != null + ? (this as any).os.i.clientSettings.gradientWindowHeader + : false + : false; + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-widget-container + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + overflow hidden + + &.naked + background transparent !important + border none !important + + > header + > .title + z-index 1 + margin 0 + padding 0 16px + line-height 42px + font-size 0.9em + font-weight bold + color #888 + box-shadow 0 1px rgba(0, 0, 0, 0.07) + + > [data-fa] + margin-right 4px + + &:empty + display none + + > button + position absolute + z-index 2 + top 0 + right 0 + padding 0 + width 42px + font-size 0.9em + line-height 42px + color #ccc + + &:hover + color #aaa + + &:active + color #999 + + &.withGradient + > .title + background linear-gradient(to bottom, #fff, #ececec) + box-shadow 0 1px rgba(#000, 0.11) +</style> diff --git a/src/client/app/desktop/views/components/window.vue b/src/client/app/desktop/views/components/window.vue new file mode 100644 index 0000000000..e2cab21799 --- /dev/null +++ b/src/client/app/desktop/views/components/window.vue @@ -0,0 +1,635 @@ +<template> +<div class="mk-window" :data-flexible="isFlexible" @dragover="onDragover"> + <div class="bg" ref="bg" v-show="isModal" @click="onBgClick"></div> + <div class="main" ref="main" tabindex="-1" :data-is-modal="isModal" @mousedown="onBodyMousedown" @keydown="onKeydown" :style="{ width, height }"> + <div class="body"> + <header ref="header" + :class="{ withGradient }" + @contextmenu.prevent="() => {}" @mousedown.prevent="onHeaderMousedown" + > + <h1><slot name="header"></slot></h1> + <div> + <button class="popout" v-if="popoutUrl" @mousedown.stop="() => {}" @click="popout" title="ポップアウト">%fa:R window-restore%</button> + <button class="close" v-if="canClose" @mousedown.stop="() => {}" @click="close" title="閉じる">%fa:times%</button> + </div> + </header> + <div class="content"> + <slot></slot> + </div> + </div> + <div class="handle top" v-if="canResize" @mousedown.prevent="onTopHandleMousedown"></div> + <div class="handle right" v-if="canResize" @mousedown.prevent="onRightHandleMousedown"></div> + <div class="handle bottom" v-if="canResize" @mousedown.prevent="onBottomHandleMousedown"></div> + <div class="handle left" v-if="canResize" @mousedown.prevent="onLeftHandleMousedown"></div> + <div class="handle top-left" v-if="canResize" @mousedown.prevent="onTopLeftHandleMousedown"></div> + <div class="handle top-right" v-if="canResize" @mousedown.prevent="onTopRightHandleMousedown"></div> + <div class="handle bottom-right" v-if="canResize" @mousedown.prevent="onBottomRightHandleMousedown"></div> + <div class="handle bottom-left" v-if="canResize" @mousedown.prevent="onBottomLeftHandleMousedown"></div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as anime from 'animejs'; +import contains from '../../../common/scripts/contains'; + +const minHeight = 40; +const minWidth = 200; + +function dragListen(fn) { + window.addEventListener('mousemove', fn); + window.addEventListener('mouseleave', dragClear.bind(null, fn)); + window.addEventListener('mouseup', dragClear.bind(null, fn)); +} + +function dragClear(fn) { + window.removeEventListener('mousemove', fn); + window.removeEventListener('mouseleave', dragClear); + window.removeEventListener('mouseup', dragClear); +} + +export default Vue.extend({ + props: { + isModal: { + type: Boolean, + default: false + }, + canClose: { + type: Boolean, + default: true + }, + width: { + type: String, + default: '530px' + }, + height: { + type: String, + default: 'auto' + }, + popoutUrl: { + type: [String, Function], + default: null + }, + name: { + type: String, + default: null + } + }, + + data() { + return { + preventMount: false + }; + }, + + computed: { + isFlexible(): boolean { + return this.height == null; + }, + canResize(): boolean { + return !this.isFlexible; + }, + withGradient(): boolean { + return (this as any).os.isSignedIn + ? (this as any).os.i.clientSettings.gradientWindowHeader != null + ? (this as any).os.i.clientSettings.gradientWindowHeader + : false + : false; + } + }, + + created() { + if (localStorage.getItem('autoPopout') == 'true' && this.popoutUrl) { + this.popout(); + this.preventMount = true; + } else { + // ウィンドウをウィンドウシステムに登録 + (this as any).os.windows.add(this); + } + }, + + mounted() { + if (this.preventMount) { + this.$destroy(); + return; + } + + this.$nextTick(() => { + const main = this.$refs.main as any; + main.style.top = '15%'; + main.style.left = (window.innerWidth / 2) - (main.offsetWidth / 2) + 'px'; + + window.addEventListener('resize', this.onBrowserResize); + + this.open(); + }); + }, + + destroyed() { + // ウィンドウをウィンドウシステムから削除 + (this as any).os.windows.remove(this); + + window.removeEventListener('resize', this.onBrowserResize); + }, + + methods: { + open() { + this.$emit('opening'); + + this.top(); + + const bg = this.$refs.bg as any; + const main = this.$refs.main as any; + + if (this.isModal) { + bg.style.pointerEvents = 'auto'; + anime({ + targets: bg, + opacity: 1, + duration: 100, + easing: 'linear' + }); + } + + main.style.pointerEvents = 'auto'; + anime({ + targets: main, + opacity: 1, + scale: [1.1, 1], + duration: 200, + easing: 'easeOutQuad' + }); + + if (focus) main.focus(); + + setTimeout(() => { + this.$emit('opened'); + }, 300); + }, + + close() { + this.$emit('before-close'); + + const bg = this.$refs.bg as any; + const main = this.$refs.main as any; + + if (this.isModal) { + bg.style.pointerEvents = 'none'; + anime({ + targets: bg, + opacity: 0, + duration: 300, + easing: 'linear' + }); + } + + main.style.pointerEvents = 'none'; + + anime({ + targets: main, + opacity: 0, + scale: 0.8, + duration: 300, + easing: [0.5, -0.5, 1, 0.5] + }); + + setTimeout(() => { + this.$destroy(); + this.$emit('closed'); + }, 300); + }, + + popout() { + const url = typeof this.popoutUrl == 'function' ? this.popoutUrl() : this.popoutUrl; + + const main = this.$refs.main as any; + + if (main) { + const position = main.getBoundingClientRect(); + + const width = parseInt(getComputedStyle(main, '').width, 10); + const height = parseInt(getComputedStyle(main, '').height, 10); + const x = window.screenX + position.left; + const y = window.screenY + position.top; + + window.open(url, url, + `width=${width}, height=${height}, top=${y}, left=${x}`); + + this.close(); + } else { + const x = window.top.outerHeight / 2 + window.top.screenY - (parseInt(this.height, 10) / 2); + const y = window.top.outerWidth / 2 + window.top.screenX - (parseInt(this.width, 10) / 2); + window.open(url, url, + `width=${this.width}, height=${this.height}, top=${x}, left=${y}`); + } + }, + + // 最前面へ移動 + top() { + let z = 0; + + (this as any).os.windows.getAll().forEach(w => { + if (w == this) return; + const m = w.$refs.main; + const mz = Number(document.defaultView.getComputedStyle(m, null).zIndex); + if (mz > z) z = mz; + }); + + if (z > 0) { + (this.$refs.main as any).style.zIndex = z + 1; + if (this.isModal) (this.$refs.bg as any).style.zIndex = z + 1; + } + }, + + onBgClick() { + if (this.canClose) this.close(); + }, + + onBodyMousedown() { + this.top(); + }, + + onHeaderMousedown(e) { + const main = this.$refs.main as any; + + if (!contains(main, document.activeElement)) main.focus(); + + const position = main.getBoundingClientRect(); + + const clickX = e.clientX; + const clickY = e.clientY; + const moveBaseX = clickX - position.left; + const moveBaseY = clickY - position.top; + const browserWidth = window.innerWidth; + const browserHeight = window.innerHeight; + const windowWidth = main.offsetWidth; + const windowHeight = main.offsetHeight; + + // 動かした時 + dragListen(me => { + let moveLeft = me.clientX - moveBaseX; + let moveTop = me.clientY - moveBaseY; + + // 上はみ出し + if (moveTop < 0) moveTop = 0; + + // 左はみ出し + if (moveLeft < 0) moveLeft = 0; + + // 下はみ出し + if (moveTop + windowHeight > browserHeight) moveTop = browserHeight - windowHeight; + + // 右はみ出し + if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth; + + main.style.left = moveLeft + 'px'; + main.style.top = moveTop + 'px'; + }); + }, + + // 上ハンドル掴み時 + onTopHandleMousedown(e) { + const main = this.$refs.main as any; + + const base = e.clientY; + const height = parseInt(getComputedStyle(main, '').height, 10); + const top = parseInt(getComputedStyle(main, '').top, 10); + + // 動かした時 + dragListen(me => { + const move = me.clientY - base; + if (top + move > 0) { + if (height + -move > minHeight) { + this.applyTransformHeight(height + -move); + this.applyTransformTop(top + move); + } else { // 最小の高さより小さくなろうとした時 + this.applyTransformHeight(minHeight); + this.applyTransformTop(top + (height - minHeight)); + } + } else { // 上のはみ出し時 + this.applyTransformHeight(top + height); + this.applyTransformTop(0); + } + }); + }, + + // 右ハンドル掴み時 + onRightHandleMousedown(e) { + const main = this.$refs.main as any; + + const base = e.clientX; + const width = parseInt(getComputedStyle(main, '').width, 10); + const left = parseInt(getComputedStyle(main, '').left, 10); + const browserWidth = window.innerWidth; + + // 動かした時 + dragListen(me => { + const move = me.clientX - base; + if (left + width + move < browserWidth) { + if (width + move > minWidth) { + this.applyTransformWidth(width + move); + } else { // 最小の幅より小さくなろうとした時 + this.applyTransformWidth(minWidth); + } + } else { // 右のはみ出し時 + this.applyTransformWidth(browserWidth - left); + } + }); + }, + + // 下ハンドル掴み時 + onBottomHandleMousedown(e) { + const main = this.$refs.main as any; + + const base = e.clientY; + const height = parseInt(getComputedStyle(main, '').height, 10); + const top = parseInt(getComputedStyle(main, '').top, 10); + const browserHeight = window.innerHeight; + + // 動かした時 + dragListen(me => { + const move = me.clientY - base; + if (top + height + move < browserHeight) { + if (height + move > minHeight) { + this.applyTransformHeight(height + move); + } else { // 最小の高さより小さくなろうとした時 + this.applyTransformHeight(minHeight); + } + } else { // 下のはみ出し時 + this.applyTransformHeight(browserHeight - top); + } + }); + }, + + // 左ハンドル掴み時 + onLeftHandleMousedown(e) { + const main = this.$refs.main as any; + + const base = e.clientX; + const width = parseInt(getComputedStyle(main, '').width, 10); + const left = parseInt(getComputedStyle(main, '').left, 10); + + // 動かした時 + dragListen(me => { + const move = me.clientX - base; + if (left + move > 0) { + if (width + -move > minWidth) { + this.applyTransformWidth(width + -move); + this.applyTransformLeft(left + move); + } else { // 最小の幅より小さくなろうとした時 + this.applyTransformWidth(minWidth); + this.applyTransformLeft(left + (width - minWidth)); + } + } else { // 左のはみ出し時 + this.applyTransformWidth(left + width); + this.applyTransformLeft(0); + } + }); + }, + + // 左上ハンドル掴み時 + onTopLeftHandleMousedown(e) { + this.onTopHandleMousedown(e); + this.onLeftHandleMousedown(e); + }, + + // 右上ハンドル掴み時 + onTopRightHandleMousedown(e) { + this.onTopHandleMousedown(e); + this.onRightHandleMousedown(e); + }, + + // 右下ハンドル掴み時 + onBottomRightHandleMousedown(e) { + this.onBottomHandleMousedown(e); + this.onRightHandleMousedown(e); + }, + + // 左下ハンドル掴み時 + onBottomLeftHandleMousedown(e) { + this.onBottomHandleMousedown(e); + this.onLeftHandleMousedown(e); + }, + + // 高さを適用 + applyTransformHeight(height) { + (this.$refs.main as any).style.height = height + 'px'; + }, + + // 幅を適用 + applyTransformWidth(width) { + (this.$refs.main as any).style.width = width + 'px'; + }, + + // Y座標を適用 + applyTransformTop(top) { + (this.$refs.main as any).style.top = top + 'px'; + }, + + // X座標を適用 + applyTransformLeft(left) { + (this.$refs.main as any).style.left = left + 'px'; + }, + + onDragover(e) { + e.dataTransfer.dropEffect = 'none'; + }, + + onKeydown(e) { + if (e.which == 27) { // Esc + if (this.canClose) { + e.preventDefault(); + e.stopPropagation(); + this.close(); + } + } + }, + + onBrowserResize() { + const main = this.$refs.main as any; + const position = main.getBoundingClientRect(); + const browserWidth = window.innerWidth; + const browserHeight = window.innerHeight; + const windowWidth = main.offsetWidth; + const windowHeight = main.offsetHeight; + if (position.left < 0) main.style.left = 0; + if (position.top < 0) main.style.top = 0; + if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px'; + if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px'; + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-window + display block + + > .bg + display block + position fixed + z-index 2000 + top 0 + left 0 + width 100% + height 100% + background rgba(0, 0, 0, 0.7) + opacity 0 + pointer-events none + + > .main + display block + position fixed + z-index 2000 + top 15% + left 0 + margin 0 + opacity 0 + pointer-events none + + &:focus + &:not([data-is-modal]) + > .body + box-shadow 0 0 0px 1px rgba($theme-color, 0.5), 0 2px 6px 0 rgba(0, 0, 0, 0.2) + + > .handle + $size = 8px + + position absolute + + &.top + top -($size) + left 0 + width 100% + height $size + cursor ns-resize + + &.right + top 0 + right -($size) + width $size + height 100% + cursor ew-resize + + &.bottom + bottom -($size) + left 0 + width 100% + height $size + cursor ns-resize + + &.left + top 0 + left -($size) + width $size + height 100% + cursor ew-resize + + &.top-left + top -($size) + left -($size) + width $size * 2 + height $size * 2 + cursor nwse-resize + + &.top-right + top -($size) + right -($size) + width $size * 2 + height $size * 2 + cursor nesw-resize + + &.bottom-right + bottom -($size) + right -($size) + width $size * 2 + height $size * 2 + cursor nwse-resize + + &.bottom-left + bottom -($size) + left -($size) + width $size * 2 + height $size * 2 + cursor nesw-resize + + > .body + height 100% + overflow hidden + background #fff + border-radius 6px + box-shadow 0 2px 6px 0 rgba(0, 0, 0, 0.2) + + > header + $header-height = 40px + + z-index 1001 + height $header-height + overflow hidden + white-space nowrap + cursor move + background #fff + border-radius 6px 6px 0 0 + box-shadow 0 1px 0 rgba(#000, 0.1) + + &.withGradient + background linear-gradient(to bottom, #fff, #ececec) + box-shadow 0 1px 0 rgba(#000, 0.15) + + &, * + user-select none + + > h1 + pointer-events none + display block + margin 0 auto + overflow hidden + height $header-height + text-overflow ellipsis + text-align center + font-size 1em + line-height $header-height + font-weight normal + color #666 + + > div:last-child + position absolute + top 0 + right 0 + display block + z-index 1 + + > * + display inline-block + margin 0 + padding 0 + cursor pointer + font-size 1em + color rgba(#000, 0.4) + border none + outline none + background transparent + + &:hover + color rgba(#000, 0.6) + + &:active + color darken(#000, 30%) + + > [data-fa] + padding 0 + width $header-height + line-height $header-height + text-align center + + > .content + height 100% + + &:not([flexible]) + > .main > .body > .content + height calc(100% - 40px) + +</style> diff --git a/src/client/app/desktop/views/directives/index.ts b/src/client/app/desktop/views/directives/index.ts new file mode 100644 index 0000000000..324e07596d --- /dev/null +++ b/src/client/app/desktop/views/directives/index.ts @@ -0,0 +1,6 @@ +import Vue from 'vue'; + +import userPreview from './user-preview'; + +Vue.directive('userPreview', userPreview); +Vue.directive('user-preview', userPreview); diff --git a/src/client/app/desktop/views/directives/user-preview.ts b/src/client/app/desktop/views/directives/user-preview.ts new file mode 100644 index 0000000000..8a4035881a --- /dev/null +++ b/src/client/app/desktop/views/directives/user-preview.ts @@ -0,0 +1,72 @@ +/** + * マウスオーバーするとユーザーがプレビューされる要素を設定します + */ + +import MkUserPreview from '../components/user-preview.vue'; + +export default { + bind(el, binding, vn) { + const self = el._userPreviewDirective_ = {} as any; + + self.user = binding.value; + self.tag = null; + self.showTimer = null; + self.hideTimer = null; + + self.close = () => { + if (self.tag) { + self.tag.close(); + self.tag = null; + } + }; + + const show = () => { + if (self.tag) return; + + self.tag = new MkUserPreview({ + parent: vn.context, + propsData: { + user: self.user + } + }).$mount(); + + const preview = self.tag.$el; + const rect = el.getBoundingClientRect(); + const x = rect.left + el.offsetWidth + window.pageXOffset; + const y = rect.top + window.pageYOffset; + + preview.style.top = y + 'px'; + preview.style.left = x + 'px'; + + preview.addEventListener('mouseover', () => { + clearTimeout(self.hideTimer); + }); + + preview.addEventListener('mouseleave', () => { + clearTimeout(self.showTimer); + self.hideTimer = setTimeout(self.close, 500); + }); + + document.body.appendChild(preview); + }; + + el.addEventListener('mouseover', () => { + clearTimeout(self.showTimer); + clearTimeout(self.hideTimer); + self.showTimer = setTimeout(show, 500); + }); + + el.addEventListener('mouseleave', () => { + clearTimeout(self.showTimer); + clearTimeout(self.hideTimer); + self.hideTimer = setTimeout(self.close, 500); + }); + }, + + unbind(el, binding, vn) { + const self = el._userPreviewDirective_; + clearTimeout(self.showTimer); + clearTimeout(self.hideTimer); + self.close(); + } +}; diff --git a/src/client/app/desktop/views/pages/drive.vue b/src/client/app/desktop/views/pages/drive.vue new file mode 100644 index 0000000000..353f59b703 --- /dev/null +++ b/src/client/app/desktop/views/pages/drive.vue @@ -0,0 +1,52 @@ +<template> +<div class="mk-drive-page"> + <mk-drive :init-folder="folder" @move-root="onMoveRoot" @open-folder="onOpenFolder"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + data() { + return { + folder: null + }; + }, + created() { + this.folder = this.$route.params.folder; + }, + mounted() { + document.title = 'Misskey Drive'; + }, + methods: { + onMoveRoot() { + const title = 'Misskey Drive'; + + // Rewrite URL + history.pushState(null, title, '/i/drive'); + + document.title = title; + }, + onOpenFolder(folder) { + const title = folder.name + ' | Misskey Drive'; + + // Rewrite URL + history.pushState(null, title, '/i/drive/folder/' + folder.id); + + document.title = title; + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-drive-page + position fixed + width 100% + height 100% + background #fff + + > .mk-drive + height 100% +</style> + diff --git a/src/client/app/desktop/views/pages/home-customize.vue b/src/client/app/desktop/views/pages/home-customize.vue new file mode 100644 index 0000000000..8aa06be57f --- /dev/null +++ b/src/client/app/desktop/views/pages/home-customize.vue @@ -0,0 +1,12 @@ +<template> +<mk-home customize/> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + mounted() { + document.title = 'Misskey - ホームのカスタマイズ'; + } +}); +</script> diff --git a/src/client/app/desktop/views/pages/home.vue b/src/client/app/desktop/views/pages/home.vue new file mode 100644 index 0000000000..e4caa2022e --- /dev/null +++ b/src/client/app/desktop/views/pages/home.vue @@ -0,0 +1,62 @@ +<template> +<mk-ui> + <mk-home :mode="mode" @loaded="loaded"/> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import Progress from '../../../common/scripts/loading'; +import getNoteSummary from '../../../../../renderers/get-note-summary'; + +export default Vue.extend({ + props: { + mode: { + type: String, + default: 'timeline' + } + }, + data() { + return { + connection: null, + connectionId: null, + unreadCount: 0 + }; + }, + mounted() { + document.title = 'Misskey'; + + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + + this.connection.on('note', this.onStreamNote); + document.addEventListener('visibilitychange', this.onVisibilitychange, false); + + Progress.start(); + }, + beforeDestroy() { + this.connection.off('note', this.onStreamNote); + (this as any).os.stream.dispose(this.connectionId); + document.removeEventListener('visibilitychange', this.onVisibilitychange); + }, + methods: { + loaded() { + Progress.done(); + }, + + onStreamNote(note) { + if (document.hidden && note.userId != (this as any).os.i.id) { + this.unreadCount++; + document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`; + } + }, + + onVisibilitychange() { + if (!document.hidden) { + this.unreadCount = 0; + document.title = 'Misskey'; + } + } + } +}); +</script> diff --git a/src/client/app/desktop/views/pages/index.vue b/src/client/app/desktop/views/pages/index.vue new file mode 100644 index 0000000000..0ea47d913b --- /dev/null +++ b/src/client/app/desktop/views/pages/index.vue @@ -0,0 +1,16 @@ +<template> +<component :is="os.isSignedIn ? 'home' : 'welcome'"></component> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import Home from './home.vue'; +import Welcome from './welcome.vue'; + +export default Vue.extend({ + components: { + Home, + Welcome + } +}); +</script> diff --git a/src/client/app/desktop/views/pages/messaging-room.vue b/src/client/app/desktop/views/pages/messaging-room.vue new file mode 100644 index 0000000000..1cc8d8a778 --- /dev/null +++ b/src/client/app/desktop/views/pages/messaging-room.vue @@ -0,0 +1,55 @@ +<template> +<div class="mk-messaging-room-page"> + <mk-messaging-room v-if="user" :user="user" :is-naked="true"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import Progress from '../../../common/scripts/loading'; +import parseAcct from '../../../../../acct/parse'; +import getUserName from '../../../../../renderers/get-user-name'; + +export default Vue.extend({ + data() { + return { + fetching: true, + user: null + }; + }, + watch: { + $route: 'fetch' + }, + created() { + this.fetch(); + }, + mounted() { + document.documentElement.style.background = '#fff'; + }, + methods: { + fetch() { + Progress.start(); + this.fetching = true; + + (this as any).api('users/show', parseAcct(this.$route.params.user)).then(user => { + this.user = user; + this.fetching = false; + + document.title = 'メッセージ: ' + getUserName(this.user); + + Progress.done(); + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-messaging-room-page + display flex + flex 1 + flex-direction column + min-height 100% + background #fff + +</style> diff --git a/src/client/app/desktop/views/pages/note.vue b/src/client/app/desktop/views/pages/note.vue new file mode 100644 index 0000000000..17c2b1e954 --- /dev/null +++ b/src/client/app/desktop/views/pages/note.vue @@ -0,0 +1,67 @@ +<template> +<mk-ui> + <main v-if="!fetching"> + <a v-if="note.next" :href="note.next">%fa:angle-up%%i18n:desktop.tags.mk-note-page.next%</a> + <mk-note-detail :note="note"/> + <a v-if="note.prev" :href="note.prev">%fa:angle-down%%i18n:desktop.tags.mk-note-page.prev%</a> + </main> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import Progress from '../../../common/scripts/loading'; + +export default Vue.extend({ + data() { + return { + fetching: true, + note: null + }; + }, + watch: { + $route: 'fetch' + }, + created() { + this.fetch(); + }, + methods: { + fetch() { + Progress.start(); + this.fetching = true; + + (this as any).api('notes/show', { + noteId: this.$route.params.note + }).then(note => { + this.note = note; + this.fetching = false; + + Progress.done(); + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +main + padding 16px + text-align center + + > a + display inline-block + + &:first-child + margin-bottom 4px + + &:last-child + margin-top 4px + + > [data-fa] + margin-right 4px + + > .mk-note-detail + margin 0 auto + width 640px + +</style> diff --git a/src/client/app/desktop/views/pages/othello.vue b/src/client/app/desktop/views/pages/othello.vue new file mode 100644 index 0000000000..0d8e987dd9 --- /dev/null +++ b/src/client/app/desktop/views/pages/othello.vue @@ -0,0 +1,50 @@ +<template> +<component :is="ui ? 'mk-ui' : 'div'"> + <mk-othello v-if="!fetching" :init-game="game" @gamed="onGamed"/> +</component> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import Progress from '../../../common/scripts/loading'; + +export default Vue.extend({ + props: { + ui: { + default: false + } + }, + data() { + return { + fetching: false, + game: null + }; + }, + watch: { + $route: 'fetch' + }, + created() { + this.fetch(); + }, + methods: { + fetch() { + if (this.$route.params.game == null) return; + + Progress.start(); + this.fetching = true; + + (this as any).api('othello/games/show', { + gameId: this.$route.params.game + }).then(game => { + this.game = game; + this.fetching = false; + + Progress.done(); + }); + }, + onGamed(game) { + history.pushState(null, null, '/othello/' + game.id); + } + } +}); +</script> diff --git a/src/client/app/desktop/views/pages/search.vue b/src/client/app/desktop/views/pages/search.vue new file mode 100644 index 0000000000..698154e667 --- /dev/null +++ b/src/client/app/desktop/views/pages/search.vue @@ -0,0 +1,138 @@ +<template> +<mk-ui> + <header :class="$style.header"> + <h1>{{ q }}</h1> + </header> + <div :class="$style.loading" v-if="fetching"> + <mk-ellipsis-icon/> + </div> + <p :class="$style.empty" v-if="!fetching && empty">%fa:search%「{{ q }}」に関する投稿は見つかりませんでした。</p> + <mk-notes ref="timeline" :class="$style.notes" :notes="notes"> + <div slot="footer"> + <template v-if="!moreFetching">%fa:search%</template> + <template v-if="moreFetching">%fa:spinner .pulse .fw%</template> + </div> + </mk-notes> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import Progress from '../../../common/scripts/loading'; +import parse from '../../../common/scripts/parse-search-query'; + +const limit = 20; + +export default Vue.extend({ + data() { + return { + fetching: true, + moreFetching: false, + existMore: false, + offset: 0, + notes: [] + }; + }, + watch: { + $route: 'fetch' + }, + computed: { + empty(): boolean { + return this.notes.length == 0; + }, + q(): string { + return this.$route.query.q; + } + }, + mounted() { + document.addEventListener('keydown', this.onDocumentKeydown); + window.addEventListener('scroll', this.onScroll); + + this.fetch(); + }, + beforeDestroy() { + document.removeEventListener('keydown', this.onDocumentKeydown); + window.removeEventListener('scroll', this.onScroll); + }, + methods: { + onDocumentKeydown(e) { + if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { + if (e.which == 84) { // t + (this.$refs.timeline as any).focus(); + } + } + }, + fetch() { + this.fetching = true; + Progress.start(); + + (this as any).api('notes/search', Object.assign({ + limit: limit + 1, + offset: this.offset + }, parse(this.q))).then(notes => { + if (notes.length == limit + 1) { + notes.pop(); + this.existMore = true; + } + this.notes = notes; + this.fetching = false; + Progress.done(); + }); + }, + more() { + if (this.moreFetching || this.fetching || this.notes.length == 0 || !this.existMore) return; + this.offset += limit; + this.moreFetching = true; + return (this as any).api('notes/search', Object.assign({ + limit: limit + 1, + offset: this.offset + }, parse(this.q))).then(notes => { + if (notes.length == limit + 1) { + notes.pop(); + } else { + this.existMore = false; + } + this.notes = this.notes.concat(notes); + this.moreFetching = false; + }); + }, + onScroll() { + const current = window.scrollY + window.innerHeight; + if (current > document.body.offsetHeight - 16) this.more(); + } + } +}); +</script> + +<style lang="stylus" module> +.header + width 100% + max-width 600px + margin 0 auto + color #555 + +.notes + max-width 600px + margin 0 auto + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + overflow hidden + +.loading + padding 64px 0 + +.empty + display block + margin 0 auto + padding 32px + max-width 400px + text-align center + color #999 + + > [data-fa] + display block + margin-bottom 16px + font-size 3em + color #ccc + +</style> diff --git a/src/client/app/desktop/views/pages/selectdrive.vue b/src/client/app/desktop/views/pages/selectdrive.vue new file mode 100644 index 0000000000..4f0b86014b --- /dev/null +++ b/src/client/app/desktop/views/pages/selectdrive.vue @@ -0,0 +1,177 @@ +<template> +<div class="mkp-selectdrive"> + <mk-drive ref="browser" + :multiple="multiple" + @selected="onSelected" + @change-selection="onChangeSelection" + /> + <footer> + <button class="upload" title="%i18n:desktop.tags.mk-selectdrive-page.upload%" @click="upload">%fa:upload%</button> + <button class="cancel" @click="close">%i18n:desktop.tags.mk-selectdrive-page.cancel%</button> + <button class="ok" @click="ok">%i18n:desktop.tags.mk-selectdrive-page.ok%</button> + </footer> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + data() { + return { + files: [] + }; + }, + computed: { + multiple(): boolean { + const q = (new URL(location.toString())).searchParams; + return q.get('multiple') == 'true'; + } + }, + mounted() { + document.title = '%i18n:desktop.tags.mk-selectdrive-page.title%'; + }, + methods: { + onSelected(file) { + this.files = [file]; + this.ok(); + }, + onChangeSelection(files) { + this.files = files; + }, + upload() { + (this.$refs.browser as any).selectLocalFile(); + }, + close() { + window.close(); + }, + ok() { + window.opener.cb(this.multiple ? this.files : this.files[0]); + this.close(); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mkp-selectdrive + display block + position fixed + width 100% + height 100% + background #fff + + > .mk-drive + height calc(100% - 72px) + + > footer + position fixed + bottom 0 + left 0 + width 100% + height 72px + background lighten($theme-color, 95%) + + .upload + display inline-block + position absolute + top 8px + left 16px + cursor pointer + padding 0 + margin 8px 4px 0 0 + width 40px + height 40px + font-size 1em + color rgba($theme-color, 0.5) + background transparent + outline none + border solid 1px transparent + border-radius 4px + + &:hover + background transparent + border-color rgba($theme-color, 0.3) + + &:active + color rgba($theme-color, 0.6) + background transparent + border-color rgba($theme-color, 0.5) + box-shadow 0 2px 4px rgba(darken($theme-color, 50%), 0.15) inset + + &:focus + &:after + content "" + pointer-events none + position absolute + top -5px + right -5px + bottom -5px + left -5px + border 2px solid rgba($theme-color, 0.3) + border-radius 8px + + .ok + .cancel + display block + position absolute + bottom 16px + cursor pointer + padding 0 + margin 0 + width 120px + height 40px + font-size 1em + outline none + border-radius 4px + + &:focus + &:after + content "" + pointer-events none + position absolute + top -5px + right -5px + bottom -5px + left -5px + border 2px solid rgba($theme-color, 0.3) + border-radius 8px + + &:disabled + opacity 0.7 + cursor default + + .ok + right 16px + color $theme-color-foreground + background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) + border solid 1px lighten($theme-color, 15%) + + &:not(:disabled) + font-weight bold + + &:hover:not(:disabled) + background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) + border-color $theme-color + + &:active:not(:disabled) + background $theme-color + border-color $theme-color + + .cancel + right 148px + color #888 + background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) + border solid 1px #e2e2e2 + + &:hover + background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) + border-color #dcdcdc + + &:active + background #ececec + border-color #dcdcdc + +</style> diff --git a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue new file mode 100644 index 0000000000..4113ef13ab --- /dev/null +++ b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue @@ -0,0 +1,80 @@ +<template> +<div class="followers-you-know"> + <p class="title">%fa:users%%i18n:desktop.tags.mk-user.followers-you-know.title%</p> + <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.followers-you-know.loading%<mk-ellipsis/></p> + <div v-if="!fetching && users.length > 0"> + <router-link v-for="user in users" :to="user | userPage" :key="user.id"> + <img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user | userName" v-user-preview="user.id"/> + </router-link> + </div> + <p class="empty" v-if="!fetching && users.length == 0">%i18n:desktop.tags.mk-user.followers-you-know.no-users%</p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['user'], + data() { + return { + users: [], + fetching: true + }; + }, + mounted() { + (this as any).api('users/followers', { + userId: this.user.id, + iknow: true, + limit: 16 + }).then(x => { + this.users = x.users; + this.fetching = false; + }); + } +}); +</script> + +<style lang="stylus" scoped> +.followers-you-know + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + > .title + z-index 1 + margin 0 + padding 0 16px + line-height 42px + font-size 0.9em + font-weight bold + color #888 + box-shadow 0 1px rgba(0, 0, 0, 0.07) + + > i + margin-right 4px + + > div + padding 8px + + > a + display inline-block + margin 4px + + > img + width 48px + height 48px + vertical-align bottom + border-radius 100% + + > .initializing + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > i + margin-right 4px + +</style> diff --git a/src/client/app/desktop/views/pages/user/user.friends.vue b/src/client/app/desktop/views/pages/user/user.friends.vue new file mode 100644 index 0000000000..4b5ec88d52 --- /dev/null +++ b/src/client/app/desktop/views/pages/user/user.friends.vue @@ -0,0 +1,120 @@ +<template> +<div class="friends"> + <p class="title">%fa:users%%i18n:desktop.tags.mk-user.frequently-replied-users.title%</p> + <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.frequently-replied-users.loading%<mk-ellipsis/></p> + <template v-if="!fetching && users.length != 0"> + <div class="user" v-for="friend in users"> + <router-link class="avatar-anchor" :to="friend | userPage"> + <img class="avatar" :src="`${friend.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="friend.id"/> + </router-link> + <div class="body"> + <router-link class="name" :to="friend | userPage" v-user-preview="friend.id">{{ friend.name }}</router-link> + <p class="username">@{{ friend | acct }}</p> + </div> + <mk-follow-button :user="friend"/> + </div> + </template> + <p class="empty" v-if="!fetching && users.length == 0">%i18n:desktop.tags.mk-user.frequently-replied-users.no-users%</p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['user'], + data() { + return { + users: [], + fetching: true + }; + }, + mounted() { + (this as any).api('users/get_frequently_replied_users', { + userId: this.user.id, + limit: 4 + }).then(docs => { + this.users = docs.map(doc => doc.user); + this.fetching = false; + }); + } +}); +</script> + +<style lang="stylus" scoped> +.friends + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + > .title + z-index 1 + margin 0 + padding 0 16px + line-height 42px + font-size 0.9em + font-weight bold + color #888 + box-shadow 0 1px rgba(0, 0, 0, 0.07) + + > i + margin-right 4px + + > .initializing + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > i + margin-right 4px + + > .user + padding 16px + border-bottom solid 1px #eee + + &:last-child + border-bottom none + + &:after + content "" + display block + clear both + + > .avatar-anchor + display block + float left + margin 0 12px 0 0 + + > .avatar + display block + width 42px + height 42px + margin 0 + border-radius 8px + vertical-align bottom + + > .body + float left + width calc(100% - 54px) + + > .name + margin 0 + font-size 16px + line-height 24px + color #555 + + > .username + display block + margin 0 + font-size 15px + line-height 16px + color #ccc + + > .mk-follow-button + position absolute + top 16px + right 16px + +</style> diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue new file mode 100644 index 0000000000..e026820b39 --- /dev/null +++ b/src/client/app/desktop/views/pages/user/user.header.vue @@ -0,0 +1,190 @@ +<template> +<div class="header" :data-is-dark-background="user.bannerUrl != null"> + <div class="banner-container" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=2048)` : ''"> + <div class="banner" ref="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=2048)` : ''" @click="onBannerClick"></div> + </div> + <div class="fade"></div> + <div class="container"> + <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=150`" alt="avatar"/> + <div class="title"> + <p class="name">{{ user | userName }}</p> + <p class="username">@{{ user | acct }}</p> + <p class="location" v-if="user.host === null && user.profile.location">%fa:map-marker%{{ user.profile.location }}</p> + </div> + <footer> + <router-link :to="user | userPage" :data-active="$parent.page == 'home'">%fa:home%概要</router-link> + <router-link :to="user | userPage('media')" :data-active="$parent.page == 'media'">%fa:image%メディア</router-link> + <router-link :to="user | userPage('graphs')" :data-active="$parent.page == 'graphs'">%fa:chart-bar%グラフ</router-link> + </footer> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['user'], + mounted() { + window.addEventListener('load', this.onScroll); + window.addEventListener('scroll', this.onScroll); + window.addEventListener('resize', this.onScroll); + }, + beforeDestroy() { + window.removeEventListener('load', this.onScroll); + window.removeEventListener('scroll', this.onScroll); + window.removeEventListener('resize', this.onScroll); + }, + methods: { + onScroll() { + const banner = this.$refs.banner as any; + + const top = window.scrollY; + + const z = 1.25; // 奥行き(小さいほど奥) + const pos = -(top / z); + banner.style.backgroundPosition = `center calc(50% - ${pos}px)`; + + const blur = top / 32 + if (blur <= 10) banner.style.filter = `blur(${blur}px)`; + }, + + onBannerClick() { + if (!(this as any).os.isSignedIn || (this as any).os.i.id != this.user.id) return; + + (this as any).apis.updateBanner().then(i => { + this.user.bannerUrl = i.bannerUrl; + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.header + $banner-height = 320px + $footer-height = 58px + + overflow hidden + background #f7f7f7 + box-shadow 0 1px 1px rgba(0, 0, 0, 0.075) + + &[data-is-dark-background] + > .banner-container + > .banner + background-color #383838 + + > .fade + background linear-gradient(transparent, rgba(0, 0, 0, 0.7)) + + > .container + > .title + color #fff + + > .name + text-shadow 0 0 8px #000 + + > .banner-container + height $banner-height + overflow hidden + background-size cover + background-position center + + > .banner + height 100% + background-color #f5f5f5 + background-size cover + background-position center + + > .fade + $fade-hight = 78px + + position absolute + top ($banner-height - $fade-hight) + left 0 + width 100% + height $fade-hight + + > .container + max-width 1200px + margin 0 auto + + > .avatar + display block + position absolute + bottom 16px + left 16px + z-index 2 + width 160px + height 160px + margin 0 + border solid 3px #fff + border-radius 8px + box-shadow 1px 1px 3px rgba(0, 0, 0, 0.2) + + > .title + position absolute + bottom $footer-height + left 0 + width 100% + padding 0 0 8px 195px + color #656565 + font-family '游ゴシック', 'YuGothic', 'ヒラギノ角ゴ ProN W3', 'Hiragino Kaku Gothic ProN', 'Meiryo', 'メイリオ', sans-serif + + > .name + display block + margin 0 + line-height 40px + font-weight bold + font-size 2em + + > .username + > .location + display inline-block + margin 0 16px 0 0 + line-height 20px + opacity 0.8 + + > i + margin-right 4px + + > footer + z-index 1 + height $footer-height + padding-left 195px + + > a + display inline-block + margin 0 + padding 0 16px + height $footer-height + line-height $footer-height + color #555 + + &[data-active] + border-bottom solid 4px $theme-color + + > i + margin-right 6px + + > button + display block + position absolute + top 0 + right 0 + margin 8px + padding 0 + width $footer-height - 16px + line-height $footer-height - 16px - 2px + font-size 1.2em + color #777 + border solid 1px #eee + border-radius 4px + + &:hover + color #555 + border solid 1px #ddd + +</style> diff --git a/src/client/app/desktop/views/pages/user/user.home.vue b/src/client/app/desktop/views/pages/user/user.home.vue new file mode 100644 index 0000000000..c254a320ce --- /dev/null +++ b/src/client/app/desktop/views/pages/user/user.home.vue @@ -0,0 +1,103 @@ +<template> +<div class="home"> + <div> + <div ref="left"> + <x-profile :user="user"/> + <x-photos :user="user"/> + <x-followers-you-know v-if="os.isSignedIn && os.i.id != user.id" :user="user"/> + <p v-if="user.host === null">%i18n:desktop.tags.mk-user.last-used-at%: <b><mk-time :time="user.lastUsedAt"/></b></p> + </div> + </div> + <main> + <mk-note-detail v-if="user.pinnedNote" :note="user.pinnedNote" :compact="true"/> + <x-timeline class="timeline" ref="tl" :user="user"/> + </main> + <div> + <div ref="right"> + <mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/> + <mk-activity :user="user"/> + <x-friends :user="user"/> + <div class="nav"><mk-nav/></div> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XTimeline from './user.timeline.vue'; +import XProfile from './user.profile.vue'; +import XPhotos from './user.photos.vue'; +import XFollowersYouKnow from './user.followers-you-know.vue'; +import XFriends from './user.friends.vue'; + +export default Vue.extend({ + components: { + XTimeline, + XProfile, + XPhotos, + XFollowersYouKnow, + XFriends + }, + props: ['user'], + methods: { + warp(date) { + (this.$refs.tl as any).warp(date); + } + } +}); +</script> + +<style lang="stylus" scoped> +.home + display flex + justify-content center + margin 0 auto + max-width 1200px + + > main + > div > div + > *:not(:last-child) + margin-bottom 16px + + > main + padding 16px + width calc(100% - 275px * 2) + + > .timeline + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + > div + width 275px + margin 0 + + &:first-child > div + padding 16px 0 16px 16px + + > p + display block + margin 0 + padding 0 12px + text-align center + font-size 0.8em + color #aaa + + &:last-child > div + padding 16px 16px 16px 0 + + > .nav + padding 16px + font-size 12px + color #aaa + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + a + color #999 + + i + color #ccc + +</style> diff --git a/src/client/app/desktop/views/pages/user/user.photos.vue b/src/client/app/desktop/views/pages/user/user.photos.vue new file mode 100644 index 0000000000..99a1a8d707 --- /dev/null +++ b/src/client/app/desktop/views/pages/user/user.photos.vue @@ -0,0 +1,88 @@ +<template> +<div class="photos"> + <p class="title">%fa:camera%%i18n:desktop.tags.mk-user.photos.title%</p> + <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.photos.loading%<mk-ellipsis/></p> + <div class="stream" v-if="!fetching && images.length > 0"> + <div v-for="image in images" class="img" + :style="`background-image: url(${image.url}?thumbnail&size=256)`" + ></div> + </div> + <p class="empty" v-if="!fetching && images.length == 0">%i18n:desktop.tags.mk-user.photos.no-photos%</p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['user'], + data() { + return { + images: [], + fetching: true + }; + }, + mounted() { + (this as any).api('users/notes', { + userId: this.user.id, + withMedia: true, + limit: 9 + }).then(notes => { + notes.forEach(note => { + note.media.forEach(media => { + if (this.images.length < 9) this.images.push(media); + }); + }); + this.fetching = false; + }); + } +}); +</script> + +<style lang="stylus" scoped> +.photos + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + > .title + z-index 1 + margin 0 + padding 0 16px + line-height 42px + font-size 0.9em + font-weight bold + color #888 + box-shadow 0 1px rgba(0, 0, 0, 0.07) + + > i + margin-right 4px + + > .stream + display -webkit-flex + display -moz-flex + display -ms-flex + display flex + justify-content center + flex-wrap wrap + padding 8px + + > .img + flex 1 1 33% + width 33% + height 80px + background-position center center + background-size cover + background-clip content-box + border solid 2px transparent + + > .initializing + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > i + margin-right 4px + +</style> diff --git a/src/client/app/desktop/views/pages/user/user.profile.vue b/src/client/app/desktop/views/pages/user/user.profile.vue new file mode 100644 index 0000000000..44f20c2bc0 --- /dev/null +++ b/src/client/app/desktop/views/pages/user/user.profile.vue @@ -0,0 +1,138 @@ +<template> +<div class="profile"> + <div class="friend-form" v-if="os.isSignedIn && os.i.id != user.id"> + <mk-follow-button :user="user" size="big"/> + <p class="followed" v-if="user.isFollowed">%i18n:desktop.tags.mk-user.follows-you%</p> + <p v-if="user.isMuted">%i18n:desktop.tags.mk-user.muted% <a @click="unmute">%i18n:desktop.tags.mk-user.unmute%</a></p> + <p v-if="!user.isMuted"><a @click="mute">%i18n:desktop.tags.mk-user.mute%</a></p> + </div> + <div class="description" v-if="user.description">{{ user.description }}</div> + <div class="birthday" v-if="user.host === null && user.profile.birthday"> + <p>%fa:birthday-cake%{{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)</p> + </div> + <div class="twitter" v-if="user.host === null && user.twitter"> + <p>%fa:B twitter%<a :href="`https://twitter.com/${user.twitter.screenName}`" target="_blank">@{{ user.twitter.screenName }}</a></p> + </div> + <div class="status"> + <p class="notes-count">%fa:angle-right%<a>{{ user.notesCount }}</a><b>投稿</b></p> + <p class="following">%fa:angle-right%<a @click="showFollowing">{{ user.followingCount }}</a>人を<b>フォロー</b></p> + <p class="followers">%fa:angle-right%<a @click="showFollowers">{{ user.followersCount }}</a>人の<b>フォロワー</b></p> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as age from 's-age'; +import MkFollowingWindow from '../../components/following-window.vue'; +import MkFollowersWindow from '../../components/followers-window.vue'; + +export default Vue.extend({ + props: ['user'], + computed: { + age(): number { + return age(this.user.profile.birthday); + } + }, + methods: { + showFollowing() { + (this as any).os.new(MkFollowingWindow, { + user: this.user + }); + }, + + showFollowers() { + (this as any).os.new(MkFollowersWindow, { + user: this.user + }); + }, + + mute() { + (this as any).api('mute/create', { + userId: this.user.id + }).then(() => { + this.user.isMuted = true; + }, () => { + alert('error'); + }); + }, + + unmute() { + (this as any).api('mute/delete', { + userId: this.user.id + }).then(() => { + this.user.isMuted = false; + }, () => { + alert('error'); + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.profile + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + > *:first-child + border-top none !important + + > .friend-form + padding 16px + border-top solid 1px #eee + + > .mk-big-follow-button + width 100% + + > .followed + margin 12px 0 0 0 + padding 0 + text-align center + line-height 24px + font-size 0.8em + color #71afc7 + background #eefaff + border-radius 4px + + > .description + padding 16px + color #555 + border-top solid 1px #eee + + > .birthday + padding 16px + color #555 + border-top solid 1px #eee + + > p + margin 0 + + > i + margin-right 8px + + > .twitter + padding 16px + color #555 + border-top solid 1px #eee + + > p + margin 0 + + > i + margin-right 8px + + > .status + padding 16px + color #555 + border-top solid 1px #eee + + > p + margin 8px 0 + + > i + margin-left 8px + margin-right 8px + +</style> diff --git a/src/client/app/desktop/views/pages/user/user.timeline.vue b/src/client/app/desktop/views/pages/user/user.timeline.vue new file mode 100644 index 0000000000..87d133174b --- /dev/null +++ b/src/client/app/desktop/views/pages/user/user.timeline.vue @@ -0,0 +1,139 @@ +<template> +<div class="timeline"> + <header> + <span :data-is-active="mode == 'default'" @click="mode = 'default'">投稿</span> + <span :data-is-active="mode == 'with-replies'" @click="mode = 'with-replies'">投稿と返信</span> + </header> + <div class="loading" v-if="fetching"> + <mk-ellipsis-icon/> + </div> + <p class="empty" v-if="empty">%fa:R comments%このユーザーはまだ何も投稿していないようです。</p> + <mk-notes ref="timeline" :notes="notes"> + <div slot="footer"> + <template v-if="!moreFetching">%fa:moon%</template> + <template v-if="moreFetching">%fa:spinner .pulse .fw%</template> + </div> + </mk-notes> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['user'], + data() { + return { + fetching: true, + moreFetching: false, + mode: 'default', + unreadCount: 0, + notes: [], + date: null + }; + }, + watch: { + mode() { + this.fetch(); + } + }, + computed: { + empty(): boolean { + return this.notes.length == 0; + } + }, + mounted() { + document.addEventListener('keydown', this.onDocumentKeydown); + window.addEventListener('scroll', this.onScroll); + + this.fetch(() => this.$emit('loaded')); + }, + beforeDestroy() { + document.removeEventListener('keydown', this.onDocumentKeydown); + window.removeEventListener('scroll', this.onScroll); + }, + methods: { + onDocumentKeydown(e) { + if (e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') { + if (e.which == 84) { // [t] + (this.$refs.timeline as any).focus(); + } + } + }, + fetch(cb?) { + (this as any).api('users/notes', { + userId: this.user.id, + untilDate: this.date ? this.date.getTime() : undefined, + with_replies: this.mode == 'with-replies' + }).then(notes => { + this.notes = notes; + this.fetching = false; + if (cb) cb(); + }); + }, + more() { + if (this.moreFetching || this.fetching || this.notes.length == 0) return; + this.moreFetching = true; + (this as any).api('users/notes', { + userId: this.user.id, + with_replies: this.mode == 'with-replies', + untilId: this.notes[this.notes.length - 1].id + }).then(notes => { + this.moreFetching = false; + this.notes = this.notes.concat(notes); + }); + }, + onScroll() { + const current = window.scrollY + window.innerHeight; + if (current > document.body.offsetHeight - 16/*遊び*/) { + this.more(); + } + }, + warp(date) { + this.date = date; + this.fetch(); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.timeline + background #fff + + > header + padding 8px 16px + border-bottom solid 1px #eee + + > span + margin-right 16px + line-height 27px + font-size 18px + color #555 + + &:not([data-is-active]) + color $theme-color + cursor pointer + + &:hover + text-decoration underline + + > .loading + padding 64px 0 + + > .empty + display block + margin 0 auto + padding 32px + max-width 400px + text-align center + color #999 + + > [data-fa] + display block + margin-bottom 16px + font-size 3em + color #ccc + +</style> diff --git a/src/client/app/desktop/views/pages/user/user.vue b/src/client/app/desktop/views/pages/user/user.vue new file mode 100644 index 0000000000..3644286fbc --- /dev/null +++ b/src/client/app/desktop/views/pages/user/user.vue @@ -0,0 +1,54 @@ +<template> +<mk-ui> + <div class="user" v-if="!fetching"> + <x-header :user="user"/> + <x-home v-if="page == 'home'" :user="user"/> + </div> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import parseAcct from '../../../../../../acct/parse'; +import getUserName from '../../../../../../renderers/get-user-name'; +import Progress from '../../../../common/scripts/loading'; +import XHeader from './user.header.vue'; +import XHome from './user.home.vue'; + +export default Vue.extend({ + components: { + XHeader, + XHome + }, + props: { + page: { + default: 'home' + } + }, + data() { + return { + fetching: true, + user: null + }; + }, + watch: { + $route: 'fetch' + }, + created() { + this.fetch(); + }, + methods: { + fetch() { + this.fetching = true; + Progress.start(); + (this as any).api('users/show', parseAcct(this.$route.params.user)).then(user => { + this.user = user; + this.fetching = false; + Progress.done(); + document.title = getUserName(this.user) + ' | Misskey'; + }); + } + } +}); +</script> + diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue new file mode 100644 index 0000000000..93d17b58fe --- /dev/null +++ b/src/client/app/desktop/views/pages/welcome.vue @@ -0,0 +1,317 @@ +<template> +<div class="mk-welcome"> + <main> + <div class="top"> + <div> + <div> + <h1>Share<br><span ref="share">Everything!</span><span class="cursor">_</span></h1> + <p>ようこそ! <b>Misskey</b>はTwitter風ミニブログSNSです。思ったことや皆と共有したいことを投稿しましょう。タイムラインを見れば、皆の関心事をすぐにチェックすることもできます。<a :href="aboutUrl">詳しく...</a></p> + <p><button class="signup" @click="signup">はじめる</button><button class="signin" @click="signin">ログイン</button></p> + <div class="users"> + <router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="user | userPage" v-user-preview="user.id"> + <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + </router-link> + </div> + </div> + <div> + <div> + <header>%fa:comments R% タイムライン<div><span></span><span></span><span></span></div></header> + <mk-welcome-timeline/> + </div> + </div> + </div> + </div> + </main> + <mk-forkit/> + <footer> + <div> + <mk-nav :class="$style.nav"/> + <p class="c">{{ copyright }}</p> + </div> + </footer> + <modal name="signup" width="500px" height="auto" scrollable> + <header :class="$style.signupFormHeader">新規登録</header> + <mk-signup :class="$style.signupForm"/> + </modal> + <modal name="signin" width="500px" height="auto" scrollable> + <header :class="$style.signinFormHeader">ログイン</header> + <mk-signin :class="$style.signinForm"/> + </modal> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { docsUrl, copyright, lang } from '../../../config'; + +const shares = [ + 'Everything!', + 'Webpages', + 'Photos', + 'Interests', + 'Favorites' +]; + +export default Vue.extend({ + data() { + return { + aboutUrl: `${docsUrl}/${lang}/about`, + copyright, + users: [], + clock: null, + i: 0 + }; + }, + mounted() { + (this as any).api('users', { + sort: '+follower', + limit: 20 + }).then(users => { + this.users = users; + }); + + this.clock = setInterval(() => { + if (++this.i == shares.length) this.i = 0; + const speed = 70; + const text = (this.$refs.share as any).innerText; + for (let i = 0; i < text.length; i++) { + setTimeout(() => { + if (this.$refs.share) { + (this.$refs.share as any).innerText = text.substr(0, text.length - i); + } + }, i * speed) + } + setTimeout(() => { + const newText = shares[this.i]; + for (let i = 0; i <= newText.length; i++) { + setTimeout(() => { + if (this.$refs.share) { + (this.$refs.share as any).innerText = newText.substr(0, i); + } + }, i * speed) + } + }, text.length * speed); + }, 4000); + }, + beforeDestroy() { + clearInterval(this.clock); + }, + methods: { + signup() { + this.$modal.show('signup'); + }, + signin() { + this.$modal.show('signin'); + } + } +}); +</script> + +<style> +#wait { + right: auto; + left: 15px; +} +</style> + +<style lang="stylus" scoped> +@import '~const.styl' + +@import url('https://fonts.googleapis.com/css?family=Sarpanch:700') + +.mk-welcome + display flex + flex-direction column + flex 1 + $width = 1000px + + background-image url('/assets/welcome-bg.svg') + background-size cover + background-position top center + + &:before + content "" + display block + position fixed + bottom 0 + left 0 + width 100% + height 100% + background-image url('/assets/welcome-fg.svg') + background-size cover + background-position bottom center + + > main + display flex + flex 1 + + > .top + display flex + width 100% + + > div + display flex + max-width $width + 64px + margin 0 auto + padding 80px 32px 0 32px + + > * + margin-bottom 48px + + > div:first-child + margin-right 48px + color #fff + text-shadow 0 0 12px #172062 + + > h1 + margin 0 + font-weight bold + //font-variant small-caps + letter-spacing 12px + font-family 'Sarpanch', sans-serif + font-size 42px + line-height 48px + + > .cursor + animation cursor 1s infinite linear both + + @keyframes cursor + 0% + opacity 1 + 50% + opacity 0 + + > p + margin 1em 0 + line-height 2em + + button + padding 8px 16px + font-size inherit + + .signup + color $theme-color + border solid 2px $theme-color + border-radius 4px + + &:focus + box-shadow 0 0 0 3px rgba($theme-color, 0.2) + + &:hover + color $theme-color-foreground + background $theme-color + + &:active + color $theme-color-foreground + background darken($theme-color, 10%) + border-color darken($theme-color, 10%) + + .signin + &:hover + color #fff + + > .users + margin 16px 0 0 0 + + > * + display inline-block + margin 4px + + > * + display inline-block + width 38px + height 38px + vertical-align top + border-radius 6px + + > div:last-child + + > div + width 410px + background #fff + border-radius 8px + box-shadow 0 0 0 12px rgba(0, 0, 0, 0.1) + overflow hidden + + > header + z-index 1 + padding 12px 16px + color #888d94 + box-shadow 0 1px 0px rgba(0, 0, 0, 0.1) + + > div + position absolute + top 0 + right 0 + padding inherit + + > span + display inline-block + height 11px + width 11px + margin-left 6px + background #ccc + border-radius 100% + vertical-align middle + + &:nth-child(1) + background #5BCC8B + + &:nth-child(2) + background #E6BB46 + + &:nth-child(3) + background #DF7065 + + > .mk-welcome-timeline + max-height 350px + overflow auto + + > footer + font-size 12px + color #949ea5 + + > div + max-width $width + margin 0 auto + padding 0 0 42px 0 + text-align center + + > .c + margin 16px 0 0 0 + font-size 10px + opacity 0.7 + +</style> + +<style lang="stylus" module> +.signupForm + padding 24px 48px 48px 48px + +.signupFormHeader + padding 48px 0 12px 0 + margin: 0 48px + font-size 1.5em + color #777 + border-bottom solid 1px #eee + +.signinForm + padding 24px 48px 48px 48px + +.signinFormHeader + padding 48px 0 12px 0 + margin: 0 48px + font-size 1.5em + color #777 + border-bottom solid 1px #eee + +.nav + a + color #666 +</style> + +<style lang="stylus"> +html +body + background linear-gradient(to bottom, #1e1d65, #bd6659) +</style> diff --git a/src/client/app/desktop/views/widgets/activity.vue b/src/client/app/desktop/views/widgets/activity.vue new file mode 100644 index 0000000000..0bdf4622af --- /dev/null +++ b/src/client/app/desktop/views/widgets/activity.vue @@ -0,0 +1,31 @@ +<template> +<mk-activity + :design="props.design" + :init-view="props.view" + :user="os.i" + @view-changed="viewChanged"/> +</template> + +<script lang="ts"> +import define from '../../../common/define-widget'; +export default define({ + name: 'activity', + props: () => ({ + design: 0, + view: 0 + }) +}).extend({ + methods: { + func() { + if (this.props.design == 2) { + this.props.design = 0; + } else { + this.props.design++; + } + }, + viewChanged(view) { + this.props.view = view; + } + } +}); +</script> diff --git a/src/client/app/desktop/views/widgets/channel.channel.form.vue b/src/client/app/desktop/views/widgets/channel.channel.form.vue new file mode 100644 index 0000000000..f2744268bb --- /dev/null +++ b/src/client/app/desktop/views/widgets/channel.channel.form.vue @@ -0,0 +1,67 @@ +<template> +<div class="form"> + <input v-model="text" :disabled="wait" @keydown="onKeydown" placeholder="書いて"> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + data() { + return { + text: '', + wait: false + }; + }, + methods: { + onKeydown(e) { + if (e.which == 10 || e.which == 13) this.post(); + }, + post() { + this.wait = true; + + let reply = null; + + if (/^>>([0-9]+) /.test(this.text)) { + const index = this.text.match(/^>>([0-9]+) /)[1]; + reply = (this.$parent as any).notes.find(p => p.index.toString() == index); + this.text = this.text.replace(/^>>([0-9]+) /, ''); + } + + (this as any).api('notes/create', { + text: this.text, + replyId: reply ? reply.id : undefined, + channelId: (this.$parent as any).channel.id + }).then(data => { + this.text = ''; + }).catch(err => { + alert('失敗した'); + }).then(() => { + this.wait = false; + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.form + width 100% + height 38px + padding 4px + border-top solid 1px #ddd + + > input + padding 0 8px + width 100% + height 100% + font-size 14px + color #55595c + border solid 1px #dadada + border-radius 4px + + &:hover + &:focus + border-color #aeaeae + +</style> diff --git a/src/client/app/desktop/views/widgets/channel.channel.note.vue b/src/client/app/desktop/views/widgets/channel.channel.note.vue new file mode 100644 index 0000000000..7767919066 --- /dev/null +++ b/src/client/app/desktop/views/widgets/channel.channel.note.vue @@ -0,0 +1,65 @@ +<template> +<div class="note"> + <header> + <a class="index" @click="reply">{{ note.index }}:</a> + <router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id"><b>{{ note.user | userName }}</b></router-link> + <span>ID:<i>{{ note.user | acct }}</i></span> + </header> + <div> + <a v-if="note.reply">>>{{ note.reply.index }}</a> + {{ note.text }} + <div class="media" v-if="note.media"> + <a v-for="file in note.media" :href="file.url" target="_blank"> + <img :src="`${file.url}?thumbnail&size=512`" :alt="file.name" :title="file.name"/> + </a> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['note'], + methods: { + reply() { + this.$emit('reply', this.note); + } + } +}); +</script> + +<style lang="stylus" scoped> +.note + margin 0 + padding 0 + color #444 + + > header + position -webkit-sticky + position sticky + z-index 1 + top 0 + padding 8px 4px 4px 16px + background rgba(255, 255, 255, 0.9) + + > .index + margin-right 0.25em + + > .name + margin-right 0.5em + color #008000 + + > div + padding 0 16px 16px 16px + + > .media + > a + display inline-block + + > img + max-width 100% + vertical-align bottom + +</style> diff --git a/src/client/app/desktop/views/widgets/channel.channel.vue b/src/client/app/desktop/views/widgets/channel.channel.vue new file mode 100644 index 0000000000..ea4d8f8454 --- /dev/null +++ b/src/client/app/desktop/views/widgets/channel.channel.vue @@ -0,0 +1,106 @@ +<template> +<div class="channel"> + <p v-if="fetching">読み込み中<mk-ellipsis/></p> + <div v-if="!fetching" ref="notes" class="notes"> + <p v-if="notes.length == 0">まだ投稿がありません</p> + <x-note class="note" v-for="note in notes.slice().reverse()" :note="note" :key="note.id" @reply="reply"/> + </div> + <x-form class="form" ref="form"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import ChannelStream from '../../../common/scripts/streaming/channel'; +import XForm from './channel.channel.form.vue'; +import XNote from './channel.channel.note.vue'; + +export default Vue.extend({ + components: { + XForm, + XNote + }, + props: ['channel'], + data() { + return { + fetching: true, + notes: [], + connection: null + }; + }, + watch: { + channel() { + this.zap(); + } + }, + mounted() { + this.zap(); + }, + beforeDestroy() { + this.disconnect(); + }, + methods: { + zap() { + this.fetching = true; + + (this as any).api('channels/notes', { + channelId: this.channel.id + }).then(notes => { + this.notes = notes; + this.fetching = false; + + this.$nextTick(() => { + this.scrollToBottom(); + }); + + this.disconnect(); + this.connection = new ChannelStream((this as any).os, this.channel.id); + this.connection.on('note', this.onNote); + }); + }, + disconnect() { + if (this.connection) { + this.connection.off('note', this.onNote); + this.connection.close(); + } + }, + onNote(note) { + this.notes.unshift(note); + this.scrollToBottom(); + }, + scrollToBottom() { + (this.$refs.notes as any).scrollTop = (this.$refs.notes as any).scrollHeight; + }, + reply(note) { + (this.$refs.form as any).text = `>>${ note.index } `; + } + } +}); +</script> + +<style lang="stylus" scoped> +.channel + + > p + margin 0 + padding 16px + text-align center + color #aaa + + > .notes + height calc(100% - 38px) + overflow auto + font-size 0.9em + + > .note + border-bottom solid 1px #eee + + &:last-child + border-bottom none + + > .form + position absolute + left 0 + bottom 0 + +</style> diff --git a/src/client/app/desktop/views/widgets/channel.vue b/src/client/app/desktop/views/widgets/channel.vue new file mode 100644 index 0000000000..c9b62dfeab --- /dev/null +++ b/src/client/app/desktop/views/widgets/channel.vue @@ -0,0 +1,107 @@ +<template> +<div class="mkw-channel"> + <template v-if="!props.compact"> + <p class="title">%fa:tv%{{ channel ? channel.title : '%i18n:desktop.tags.mk-channel-home-widget.title%' }}</p> + <button @click="settings" title="%i18n:desktop.tags.mk-channel-home-widget.settings%">%fa:cog%</button> + </template> + <p class="get-started" v-if="props.channel == null">%i18n:desktop.tags.mk-channel-home-widget.get-started%</p> + <x-channel class="channel" :channel="channel" v-if="channel != null"/> +</div> +</template> + +<script lang="ts"> +import define from '../../../common/define-widget'; +import XChannel from './channel.channel.vue'; + +export default define({ + name: 'server', + props: () => ({ + channel: null, + compact: false + }) +}).extend({ + components: { + XChannel + }, + data() { + return { + fetching: true, + channel: null + }; + }, + mounted() { + if (this.props.channel) { + this.zap(); + } + }, + methods: { + func() { + this.props.compact = !this.props.compact; + }, + settings() { + const id = window.prompt('チャンネルID'); + if (!id) return; + this.props.channel = id; + this.zap(); + }, + zap() { + this.fetching = true; + + (this as any).api('channels/show', { + channelId: this.props.channel + }).then(channel => { + this.channel = channel; + this.fetching = false; + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mkw-channel + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + overflow hidden + + > .title + z-index 2 + margin 0 + padding 0 16px + line-height 42px + font-size 0.9em + font-weight bold + color #888 + box-shadow 0 1px rgba(0, 0, 0, 0.07) + + > [data-fa] + margin-right 4px + + > button + position absolute + z-index 2 + top 0 + right 0 + padding 0 + width 42px + font-size 0.9em + line-height 42px + color #ccc + + &:hover + color #aaa + + &:active + color #999 + + > .get-started + margin 0 + padding 16px + text-align center + color #aaa + + > .channel + height 200px + +</style> diff --git a/src/client/app/desktop/views/widgets/index.ts b/src/client/app/desktop/views/widgets/index.ts new file mode 100644 index 0000000000..77d771d6b3 --- /dev/null +++ b/src/client/app/desktop/views/widgets/index.ts @@ -0,0 +1,23 @@ +import Vue from 'vue'; + +import wNotifications from './notifications.vue'; +import wTimemachine from './timemachine.vue'; +import wActivity from './activity.vue'; +import wTrends from './trends.vue'; +import wUsers from './users.vue'; +import wPolls from './polls.vue'; +import wPostForm from './post-form.vue'; +import wMessaging from './messaging.vue'; +import wChannel from './channel.vue'; +import wProfile from './profile.vue'; + +Vue.component('mkw-notifications', wNotifications); +Vue.component('mkw-timemachine', wTimemachine); +Vue.component('mkw-activity', wActivity); +Vue.component('mkw-trends', wTrends); +Vue.component('mkw-users', wUsers); +Vue.component('mkw-polls', wPolls); +Vue.component('mkw-post-form', wPostForm); +Vue.component('mkw-messaging', wMessaging); +Vue.component('mkw-channel', wChannel); +Vue.component('mkw-profile', wProfile); diff --git a/src/client/app/desktop/views/widgets/messaging.vue b/src/client/app/desktop/views/widgets/messaging.vue new file mode 100644 index 0000000000..2c9f473bd1 --- /dev/null +++ b/src/client/app/desktop/views/widgets/messaging.vue @@ -0,0 +1,59 @@ +<template> +<div class="mkw-messaging"> + <p class="title" v-if="props.design == 0">%fa:comments%%i18n:desktop.tags.mk-messaging-home-widget.title%</p> + <mk-messaging ref="index" compact @navigate="navigate"/> +</div> +</template> + +<script lang="ts"> +import define from '../../../common/define-widget'; +import MkMessagingRoomWindow from '../components/messaging-room-window.vue'; + +export default define({ + name: 'messaging', + props: () => ({ + design: 0 + }) +}).extend({ + methods: { + navigate(user) { + (this as any).os.new(MkMessagingRoomWindow, { + user: user + }); + }, + func() { + if (this.props.design == 1) { + this.props.design = 0; + } else { + this.props.design++; + } + } + } +}); +</script> + +<style lang="stylus" scoped> +.mkw-messaging + overflow hidden + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + > .title + z-index 2 + margin 0 + padding 0 16px + line-height 42px + font-size 0.9em + font-weight bold + color #888 + box-shadow 0 1px rgba(0, 0, 0, 0.07) + + > [data-fa] + margin-right 4px + + > .mk-messaging + max-height 250px + overflow auto + +</style> diff --git a/src/client/app/desktop/views/widgets/notifications.vue b/src/client/app/desktop/views/widgets/notifications.vue new file mode 100644 index 0000000000..1a2b3d3f89 --- /dev/null +++ b/src/client/app/desktop/views/widgets/notifications.vue @@ -0,0 +1,70 @@ +<template> +<div class="mkw-notifications"> + <template v-if="!props.compact"> + <p class="title">%fa:R bell%%i18n:desktop.tags.mk-notifications-home-widget.title%</p> + <button @click="settings" title="%i18n:desktop.tags.mk-notifications-home-widget.settings%">%fa:cog%</button> + </template> + <mk-notifications/> +</div> +</template> + +<script lang="ts"> +import define from '../../../common/define-widget'; +export default define({ + name: 'notifications', + props: () => ({ + compact: false + }) +}).extend({ + methods: { + settings() { + alert('not implemented yet'); + }, + func() { + this.props.compact = !this.props.compact; + } + } +}); +</script> + +<style lang="stylus" scoped> +.mkw-notifications + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + > .title + z-index 1 + margin 0 + padding 0 16px + line-height 42px + font-size 0.9em + font-weight bold + color #888 + box-shadow 0 1px rgba(0, 0, 0, 0.07) + + > [data-fa] + margin-right 4px + + > button + position absolute + z-index 2 + top 0 + right 0 + padding 0 + width 42px + font-size 0.9em + line-height 42px + color #ccc + + &:hover + color #aaa + + &:active + color #999 + + > .mk-notifications + max-height 300px + overflow auto + +</style> diff --git a/src/client/app/desktop/views/widgets/polls.vue b/src/client/app/desktop/views/widgets/polls.vue new file mode 100644 index 0000000000..6ce980821a --- /dev/null +++ b/src/client/app/desktop/views/widgets/polls.vue @@ -0,0 +1,123 @@ +<template> +<div class="mkw-polls"> + <template v-if="!props.compact"> + <p class="title">%fa:chart-pie%%i18n:desktop.tags.mk-recommended-polls-home-widget.title%</p> + <button @click="fetch" title="%i18n:desktop.tags.mk-recommended-polls-home-widget.refresh%">%fa:sync%</button> + </template> + <div class="poll" v-if="!fetching && poll != null"> + <p v-if="poll.text"><router-link to="poll | notePage">{{ poll.text }}</router-link></p> + <p v-if="!poll.text"><router-link to="poll | notePage">%fa:link%</router-link></p> + <mk-poll :note="poll"/> + </div> + <p class="empty" v-if="!fetching && poll == null">%i18n:desktop.tags.mk-recommended-polls-home-widget.nothing%</p> + <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> +</div> +</template> + +<script lang="ts"> +import define from '../../../common/define-widget'; + +export default define({ + name: 'polls', + props: () => ({ + compact: false + }) +}).extend({ + data() { + return { + poll: null, + fetching: true, + offset: 0 + }; + }, + mounted() { + this.fetch(); + }, + methods: { + func() { + this.props.compact = !this.props.compact; + }, + fetch() { + this.fetching = true; + this.poll = null; + + (this as any).api('notes/polls/recommendation', { + limit: 1, + offset: this.offset + }).then(notes => { + const poll = notes ? notes[0] : null; + if (poll == null) { + this.offset = 0; + } else { + this.offset++; + } + this.poll = poll; + this.fetching = false; + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mkw-polls + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + > .title + margin 0 + padding 0 16px + line-height 42px + font-size 0.9em + font-weight bold + color #888 + border-bottom solid 1px #eee + + > [data-fa] + margin-right 4px + + > button + position absolute + z-index 2 + top 0 + right 0 + padding 0 + width 42px + font-size 0.9em + line-height 42px + color #ccc + + &:hover + color #aaa + + &:active + color #999 + + > .poll + padding 16px + font-size 12px + color #555 + + > p + margin 0 0 8px 0 + + > a + color inherit + + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > .fetching + margin 0 + padding 16px + text-align center + color #aaa + + > [data-fa] + margin-right 4px + +</style> diff --git a/src/client/app/desktop/views/widgets/post-form.vue b/src/client/app/desktop/views/widgets/post-form.vue new file mode 100644 index 0000000000..5e59582a0f --- /dev/null +++ b/src/client/app/desktop/views/widgets/post-form.vue @@ -0,0 +1,111 @@ +<template> +<div class="mkw-post-form"> + <template v-if="props.design == 0"> + <p class="title">%fa:pencil-alt%%i18n:desktop.tags.mk-post-form-home-widget.title%</p> + </template> + <textarea :disabled="posting" v-model="text" @keydown="onKeydown" placeholder="%i18n:desktop.tags.mk-post-form-home-widget.placeholder%"></textarea> + <button @click="post" :disabled="posting">%i18n:desktop.tags.mk-post-form-home-widget.note%</button> +</div> +</template> + +<script lang="ts"> +import define from '../../../common/define-widget'; +export default define({ + name: 'post-form', + props: () => ({ + design: 0 + }) +}).extend({ + data() { + return { + posting: false, + text: '' + }; + }, + methods: { + func() { + if (this.props.design == 1) { + this.props.design = 0; + } else { + this.props.design++; + } + }, + onKeydown(e) { + if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post(); + }, + post() { + this.posting = true; + + (this as any).api('notes/create', { + text: this.text + }).then(data => { + this.clear(); + }).catch(err => { + alert('失敗した'); + }).then(() => { + this.posting = false; + }); + }, + clear() { + this.text = ''; + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mkw-post-form + background #fff + overflow hidden + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + > .title + z-index 1 + margin 0 + padding 0 16px + line-height 42px + font-size 0.9em + font-weight bold + color #888 + box-shadow 0 1px rgba(0, 0, 0, 0.07) + + > [data-fa] + margin-right 4px + + > textarea + display block + width 100% + max-width 100% + min-width 100% + padding 16px + margin-bottom 28px + 16px + border none + border-bottom solid 1px #eee + + > button + display block + position absolute + bottom 8px + right 8px + margin 0 + padding 0 10px + height 28px + color $theme-color-foreground + background $theme-color !important + outline none + border none + border-radius 4px + transition background 0.1s ease + cursor pointer + + &:hover + background lighten($theme-color, 10%) !important + + &:active + background darken($theme-color, 10%) !important + transition background 0s ease + +</style> diff --git a/src/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue new file mode 100644 index 0000000000..1b4b11de3c --- /dev/null +++ b/src/client/app/desktop/views/widgets/profile.vue @@ -0,0 +1,126 @@ +<template> +<div class="mkw-profile" + :data-compact="props.design == 1 || props.design == 2" + :data-melt="props.design == 2" +> + <div class="banner" + :style="os.i.bannerUrl ? `background-image: url(${os.i.bannerUrl}?thumbnail&size=256)` : ''" + title="クリックでバナー編集" + @click="os.apis.updateBanner" + ></div> + <img class="avatar" + :src="`${os.i.avatarUrl}?thumbnail&size=96`" + @click="os.apis.updateAvatar" + alt="avatar" + title="クリックでアバター編集" + v-user-preview="os.i.id" + /> + <router-link class="name" :to="os.i | userPage">{{ os.i | userName }}</router-link> + <p class="username">@{{ os.i | acct }}</p> +</div> +</template> + +<script lang="ts"> +import define from '../../../common/define-widget'; + +export default define({ + name: 'profile', + props: () => ({ + design: 0 + }) +}).extend({ + methods: { + func() { + if (this.props.design == 2) { + this.props.design = 0; + } else { + this.props.design++; + } + } + } +}); +</script> + +<style lang="stylus" scoped> +.mkw-profile + overflow hidden + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + &[data-compact] + > .banner:before + content "" + display block + width 100% + height 100% + background rgba(0, 0, 0, 0.5) + + > .avatar + top ((100px - 58px) / 2) + left ((100px - 58px) / 2) + border none + border-radius 100% + box-shadow 0 0 16px rgba(0, 0, 0, 0.5) + + > .name + position absolute + top 0 + left 92px + margin 0 + line-height 100px + color #fff + text-shadow 0 0 8px rgba(0, 0, 0, 0.5) + + > .username + display none + + &[data-melt] + background transparent !important + border none !important + + > .banner + visibility hidden + + > .avatar + box-shadow none + + > .name + color #666 + text-shadow none + + > .banner + height 100px + background-color #f5f5f5 + background-size cover + background-position center + cursor pointer + + > .avatar + display block + position absolute + top 76px + left 16px + width 58px + height 58px + margin 0 + border solid 3px #fff + border-radius 8px + vertical-align bottom + cursor pointer + + > .name + display block + margin 10px 0 0 84px + line-height 16px + font-weight bold + color #555 + + > .username + display block + margin 4px 0 8px 84px + line-height 16px + font-size 0.9em + color #999 + +</style> diff --git a/src/client/app/desktop/views/widgets/timemachine.vue b/src/client/app/desktop/views/widgets/timemachine.vue new file mode 100644 index 0000000000..6db3b14c62 --- /dev/null +++ b/src/client/app/desktop/views/widgets/timemachine.vue @@ -0,0 +1,28 @@ +<template> +<div class="mkw-timemachine"> + <mk-calendar :design="props.design" @chosen="chosen"/> +</div> +</template> + +<script lang="ts"> +import define from '../../../common/define-widget'; +export default define({ + name: 'timemachine', + props: () => ({ + design: 0 + }) +}).extend({ + methods: { + chosen(date) { + this.$emit('chosen', date); + }, + func() { + if (this.props.design == 5) { + this.props.design = 0; + } else { + this.props.design++; + } + } + } +}); +</script> diff --git a/src/client/app/desktop/views/widgets/trends.vue b/src/client/app/desktop/views/widgets/trends.vue new file mode 100644 index 0000000000..20e298730f --- /dev/null +++ b/src/client/app/desktop/views/widgets/trends.vue @@ -0,0 +1,129 @@ +<template> +<div class="mkw-trends"> + <template v-if="!props.compact"> + <p class="title">%fa:fire%%i18n:desktop.tags.mk-trends-home-widget.title%</p> + <button @click="fetch" title="%i18n:desktop.tags.mk-trends-home-widget.refresh%">%fa:sync%</button> + </template> + <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> + <div class="note" v-else-if="note != null"> + <p class="text"><router-link :to="note | notePage">{{ note.text }}</router-link></p> + <p class="author">―<router-link :to="note.user | userPage">@{{ note.user | acct }}</router-link></p> + </div> + <p class="empty" v-else>%i18n:desktop.tags.mk-trends-home-widget.nothing%</p> +</div> +</template> + +<script lang="ts"> +import define from '../../../common/define-widget'; + +export default define({ + name: 'trends', + props: () => ({ + compact: false + }) +}).extend({ + data() { + return { + note: null, + fetching: true, + offset: 0 + }; + }, + mounted() { + this.fetch(); + }, + methods: { + func() { + this.props.compact = !this.props.compact; + }, + fetch() { + this.fetching = true; + this.note = null; + + (this as any).api('notes/trend', { + limit: 1, + offset: this.offset, + renote: false, + reply: false, + media: false, + poll: false + }).then(notes => { + const note = notes ? notes[0] : null; + if (note == null) { + this.offset = 0; + } else { + this.offset++; + } + this.note = note; + this.fetching = false; + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mkw-trends + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + > .title + margin 0 + padding 0 16px + line-height 42px + font-size 0.9em + font-weight bold + color #888 + border-bottom solid 1px #eee + + > [data-fa] + margin-right 4px + + > button + position absolute + z-index 2 + top 0 + right 0 + padding 0 + width 42px + font-size 0.9em + line-height 42px + color #ccc + + &:hover + color #aaa + + &:active + color #999 + + > .note + padding 16px + font-size 12px + font-style oblique + color #555 + + > p + margin 0 + + > .text, + > .author + > a + color inherit + + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > .fetching + margin 0 + padding 16px + text-align center + color #aaa + + > [data-fa] + margin-right 4px + +</style> diff --git a/src/client/app/desktop/views/widgets/users.vue b/src/client/app/desktop/views/widgets/users.vue new file mode 100644 index 0000000000..c7075d9a57 --- /dev/null +++ b/src/client/app/desktop/views/widgets/users.vue @@ -0,0 +1,170 @@ +<template> +<div class="mkw-users"> + <template v-if="!props.compact"> + <p class="title">%fa:users%%i18n:desktop.tags.mk-user-recommendation-home-widget.title%</p> + <button @click="refresh" title="%i18n:desktop.tags.mk-user-recommendation-home-widget.refresh%">%fa:sync%</button> + </template> + <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> + <template v-else-if="users.length != 0"> + <div class="user" v-for="_user in users"> + <router-link class="avatar-anchor" :to="_user | userPage"> + <img class="avatar" :src="`${_user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/> + </router-link> + <div class="body"> + <router-link class="name" :to="_user | userPage" v-user-preview="_user.id">{{ _user | userName }}</router-link> + <p class="username">@{{ _user | acct }}</p> + </div> + <mk-follow-button :user="_user"/> + </div> + </template> + <p class="empty" v-else>%i18n:desktop.tags.mk-user-recommendation-home-widget.no-one%</p> +</div> +</template> + +<script lang="ts"> +import define from '../../../common/define-widget'; + +const limit = 3; + +export default define({ + name: 'users', + props: () => ({ + compact: false + }) +}).extend({ + data() { + return { + users: [], + fetching: true, + page: 0 + }; + }, + mounted() { + this.fetch(); + }, + methods: { + func() { + this.props.compact = !this.props.compact; + }, + fetch() { + this.fetching = true; + this.users = []; + + (this as any).api('users/recommendation', { + limit: limit, + offset: limit * this.page + }).then(users => { + this.users = users; + this.fetching = false; + }); + }, + refresh() { + if (this.users.length < limit) { + this.page = 0; + } else { + this.page++; + } + this.fetch(); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mkw-users + background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + > .title + margin 0 + padding 0 16px + line-height 42px + font-size 0.9em + font-weight bold + color #888 + border-bottom solid 1px #eee + + > [data-fa] + margin-right 4px + + > button + position absolute + z-index 2 + top 0 + right 0 + padding 0 + width 42px + font-size 0.9em + line-height 42px + color #ccc + + &:hover + color #aaa + + &:active + color #999 + + > .user + padding 16px + border-bottom solid 1px #eee + + &:last-child + border-bottom none + + &:after + content "" + display block + clear both + + > .avatar-anchor + display block + float left + margin 0 12px 0 0 + + > .avatar + display block + width 42px + height 42px + margin 0 + border-radius 8px + vertical-align bottom + + > .body + float left + width calc(100% - 54px) + + > .name + margin 0 + font-size 16px + line-height 24px + color #555 + + > .username + display block + margin 0 + font-size 15px + line-height 16px + color #ccc + + > .mk-follow-button + position absolute + top 16px + right 16px + + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > .fetching + margin 0 + padding 16px + text-align center + color #aaa + + > [data-fa] + margin-right 4px + +</style> diff --git a/src/client/app/dev/script.ts b/src/client/app/dev/script.ts new file mode 100644 index 0000000000..c043813b40 --- /dev/null +++ b/src/client/app/dev/script.ts @@ -0,0 +1,44 @@ +/** + * Developer Center + */ + +import Vue from 'vue'; +import VueRouter from 'vue-router'; +import BootstrapVue from 'bootstrap-vue'; +import 'bootstrap/dist/css/bootstrap.css'; +import 'bootstrap-vue/dist/bootstrap-vue.css'; + +// Style +import './style.styl'; + +import init from '../init'; + +import Index from './views/index.vue'; +import Apps from './views/apps.vue'; +import AppNew from './views/new-app.vue'; +import App from './views/app.vue'; +import ui from './views/ui.vue'; + +Vue.use(BootstrapVue); + +Vue.component('mk-ui', ui); + +/** + * init + */ +init(launch => { + // Init router + const router = new VueRouter({ + mode: 'history', + base: '/dev/', + routes: [ + { path: '/', component: Index }, + { path: '/apps', component: Apps }, + { path: '/app/new', component: AppNew }, + { path: '/app/:id', component: App }, + ] + }); + + // Launch the app + launch(router); +}); diff --git a/src/client/app/dev/style.styl b/src/client/app/dev/style.styl new file mode 100644 index 0000000000..e635897b17 --- /dev/null +++ b/src/client/app/dev/style.styl @@ -0,0 +1,10 @@ +@import "../app" +@import "../reset" + +// Bootstrapのデザインを崩すので: +* + position initial + background-clip initial !important + +html + background-color #fff diff --git a/src/client/app/dev/views/app.vue b/src/client/app/dev/views/app.vue new file mode 100644 index 0000000000..a35b032b73 --- /dev/null +++ b/src/client/app/dev/views/app.vue @@ -0,0 +1,39 @@ +<template> +<mk-ui> + <p v-if="fetching">読み込み中</p> + <b-card v-if="!fetching" :header="app.name"> + <b-form-group label="App Secret"> + <b-input :value="app.secret" readonly/> + </b-form-group> + </b-card> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + data() { + return { + fetching: true, + app: null + }; + }, + watch: { + $route: 'fetch' + }, + mounted() { + this.fetch(); + }, + methods: { + fetch() { + this.fetching = true; + (this as any).api('app/show', { + appId: this.$route.params.id + }).then(app => { + this.app = app; + this.fetching = false; + }); + } + } +}); +</script> diff --git a/src/client/app/dev/views/apps.vue b/src/client/app/dev/views/apps.vue new file mode 100644 index 0000000000..7e0b107a30 --- /dev/null +++ b/src/client/app/dev/views/apps.vue @@ -0,0 +1,37 @@ +<template> +<mk-ui> + <b-card header="アプリを管理"> + <b-button to="/app/new" variant="primary">アプリ作成</b-button> + <hr> + <div class="apps"> + <p v-if="fetching">読み込み中</p> + <template v-if="!fetching"> + <b-alert v-if="apps.length == 0">アプリなし</b-alert> + <b-list-group v-else> + <b-list-group-item v-for="app in apps" :key="app.id" :to="`/app/${app.id}`"> + {{ app.name }} + </b-list-group-item> + </b-list-group> + </template> + </div> + </b-card> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + data() { + return { + fetching: true, + apps: [] + }; + }, + mounted() { + (this as any).api('my/apps').then(apps => { + this.apps = apps; + this.fetching = false; + }); + } +}); +</script> diff --git a/src/client/app/dev/views/index.vue b/src/client/app/dev/views/index.vue new file mode 100644 index 0000000000..3f572b3907 --- /dev/null +++ b/src/client/app/dev/views/index.vue @@ -0,0 +1,10 @@ +<template> +<mk-ui> + <b-button to="/apps" variant="primary">アプリの管理</b-button> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend(); +</script> diff --git a/src/client/app/dev/views/new-app.vue b/src/client/app/dev/views/new-app.vue new file mode 100644 index 0000000000..a3d7af4b97 --- /dev/null +++ b/src/client/app/dev/views/new-app.vue @@ -0,0 +1,105 @@ +<template> +<mk-ui> + <b-card header="アプリケーションの作成"> + <b-form @submit.prevent="onSubmit" autocomplete="off"> + <b-form-group label="アプリケーション名" description="あなたのアプリの名称。"> + <b-form-input v-model="name" type="text" placeholder="ex) Misskey for iOS" autocomplete="off" required/> + </b-form-group> + <b-form-group label="ID" description="あなたのアプリのID。"> + <b-input v-model="nid" type="text" pattern="^[a-zA-Z0-9_]{1,30}$" placeholder="ex) misskey-for-ios" autocomplete="off" required/> + <p class="info" v-if="nidState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%確認しています...</p> + <p class="info" v-if="nidState == 'ok'" style="color:#3CB7B5">%fa:fw check%利用できます</p> + <p class="info" v-if="nidState == 'unavailable'" style="color:#FF1161">%fa:fw exclamation-triangle%既に利用されています</p> + <p class="info" v-if="nidState == 'error'" style="color:#FF1161">%fa:fw exclamation-triangle%通信エラー</p> + <p class="info" v-if="nidState == 'invalid-format'" style="color:#FF1161">%fa:fw exclamation-triangle%a~z、A~Z、0~9、_が使えます</p> + <p class="info" v-if="nidState == 'min-range'" style="color:#FF1161">%fa:fw exclamation-triangle%1文字以上でお願いします!</p> + <p class="info" v-if="nidState == 'max-range'" style="color:#FF1161">%fa:fw exclamation-triangle%30文字以内でお願いします</p> + </b-form-group> + <b-form-group label="アプリの概要" description="あなたのアプリの簡単な説明や紹介。"> + <b-textarea v-model="description" placeholder="ex) Misskey iOSクライアント。" autocomplete="off" required></b-textarea> + </b-form-group> + <b-form-group label="コールバックURL (オプション)" description="ユーザーが認証フォームで認証した際にリダイレクトするURLを設定できます。"> + <b-input v-model="cb" type="url" placeholder="ex) https://your.app.example.com/callback.php" autocomplete="off"/> + </b-form-group> + <b-card header="権限"> + <b-form-group description="ここで要求した機能だけがAPIからアクセスできます。"> + <b-alert show variant="warning">%fa:exclamation-triangle%アプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーキーはすべて無効になります。</b-alert> + <b-form-checkbox-group v-model="permission" stacked> + <b-form-checkbox value="account-read">アカウントの情報を見る。</b-form-checkbox> + <b-form-checkbox value="account-write">アカウントの情報を操作する。</b-form-checkbox> + <b-form-checkbox value="note-write">投稿する。</b-form-checkbox> + <b-form-checkbox value="reaction-write">リアクションしたりリアクションをキャンセルする。</b-form-checkbox> + <b-form-checkbox value="following-write">フォローしたりフォロー解除する。</b-form-checkbox> + <b-form-checkbox value="drive-read">ドライブを見る。</b-form-checkbox> + <b-form-checkbox value="drive-write">ドライブを操作する。</b-form-checkbox> + <b-form-checkbox value="notification-read">通知を見る。</b-form-checkbox> + <b-form-checkbox value="notification-write">通知を操作する。</b-form-checkbox> + </b-form-checkbox-group> + </b-form-group> + </b-card> + <hr> + <b-button type="submit" variant="primary">アプリ作成</b-button> + </b-form> + </b-card> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + data() { + return { + name: '', + nid: '', + description: '', + cb: '', + nidState: null, + permission: [] + }; + }, + watch: { + nid() { + if (this.nid == null || this.nid == '') { + this.nidState = null; + return; + } + + const err = + !this.nid.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' : + this.nid.length < 3 ? 'min-range' : + this.nid.length > 30 ? 'max-range' : + null; + + if (err) { + this.nidState = err; + return; + } + + this.nidState = 'wait'; + + (this as any).api('app/nameId/available', { + nameId: this.nid + }).then(result => { + this.nidState = result.available ? 'ok' : 'unavailable'; + }).catch(err => { + this.nidState = 'error'; + }); + } + }, + methods: { + onSubmit() { + (this as any).api('app/create', { + name: this.name, + nameId: this.nid, + description: this.description, + callbackUrl: this.cb, + permission: this.permission + }).then(() => { + location.href = '/apps'; + }).catch(() => { + alert('アプリの作成に失敗しました。再度お試しください。'); + }); + } + } +}); +</script> diff --git a/src/client/app/dev/views/ui.vue b/src/client/app/dev/views/ui.vue new file mode 100644 index 0000000000..4a0fcee635 --- /dev/null +++ b/src/client/app/dev/views/ui.vue @@ -0,0 +1,20 @@ +<template> +<div> + <b-navbar toggleable="md" type="dark" variant="info"> + <b-navbar-brand>Misskey Developers</b-navbar-brand> + <b-navbar-nav> + <b-nav-item to="/">Home</b-nav-item> + <b-nav-item to="/apps">Apps</b-nav-item> + </b-navbar-nav> + </b-navbar> + <main> + <slot></slot> + </main> +</div> +</template> + +<style lang="stylus" scoped> +main + padding 32px + max-width 700px +</style> diff --git a/src/web/app/init.css b/src/client/app/init.css similarity index 100% rename from src/web/app/init.css rename to src/client/app/init.css diff --git a/src/client/app/init.ts b/src/client/app/init.ts new file mode 100644 index 0000000000..2fb8f15cf3 --- /dev/null +++ b/src/client/app/init.ts @@ -0,0 +1,168 @@ +/** + * App initializer + */ + +import Vue from 'vue'; +import VueRouter from 'vue-router'; +import VModal from 'vue-js-modal'; +import * as TreeView from 'vue-json-tree-view'; +import VAnimateCss from 'v-animate-css'; +import Element from 'element-ui'; +import ElementLocaleEn from 'element-ui/lib/locale/lang/en'; +import ElementLocaleJa from 'element-ui/lib/locale/lang/ja'; + +import App from './app.vue'; +import checkForUpdate from './common/scripts/check-for-update'; +import MiOS, { API } from './common/mios'; +import { version, codename, lang } from './config'; + +let elementLocale; +switch (lang) { + case 'ja': elementLocale = ElementLocaleJa; break; + case 'en': elementLocale = ElementLocaleEn; break; + default: elementLocale = ElementLocaleEn; break; +} + +Vue.use(VueRouter); +Vue.use(VModal); +Vue.use(TreeView); +Vue.use(VAnimateCss); +Vue.use(Element, { locale: elementLocale }); + +// Register global directives +require('./common/views/directives'); + +// Register global components +require('./common/views/components'); +require('./common/views/widgets'); + +// Register global filters +require('./common/views/filters'); + +Vue.mixin({ + destroyed(this: any) { + if (this.$el.parentNode) { + this.$el.parentNode.removeChild(this.$el); + } + } +}); + +/** + * APP ENTRY POINT! + */ + +console.info(`Misskey v${version} (${codename})`); +console.info( + '%cここにコードを入力したり張り付けたりしないでください。アカウントが不正利用される可能性があります。', + 'color: red; background: yellow; font-size: 16px; font-weight: bold;'); + +// BootTimer解除 +window.clearTimeout((window as any).mkBootTimer); +delete (window as any).mkBootTimer; + +//#region Set lang attr +const html = document.documentElement; +html.setAttribute('lang', lang); +//#endregion + +//#region Set description meta tag +const head = document.getElementsByTagName('head')[0]; +const meta = document.createElement('meta'); +meta.setAttribute('name', 'description'); +meta.setAttribute('content', '%i18n:common.misskey%'); +head.appendChild(meta); +//#endregion + +// iOSでプライベートモードだとlocalStorageが使えないので既存のメソッドを上書きする +try { + localStorage.setItem('kyoppie', 'yuppie'); +} catch (e) { + Storage.prototype.setItem = () => { }; // noop +} + +// クライアントを更新すべきならする +if (localStorage.getItem('should-refresh') == 'true') { + localStorage.removeItem('should-refresh'); + location.reload(true); +} + +// MiOSを初期化してコールバックする +export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API) => [Vue, MiOS]) => void, sw = false) => { + const os = new MiOS(sw); + + os.init(() => { + // アプリ基底要素マウント + document.body.innerHTML = '<div id="app"></div>'; + + const launch = (router: VueRouter, api?: (os: MiOS) => API) => { + os.apis = api ? api(os) : null; + + Vue.mixin({ + data() { + return { + os, + api: os.api, + apis: os.apis + }; + } + }); + + const app = new Vue({ + router, + created() { + this.$watch('os.i', i => { + // キャッシュ更新 + localStorage.setItem('me', JSON.stringify(i)); + }, { + deep: true + }); + }, + render: createEl => createEl(App) + }); + + os.app = app; + + // マウント + app.$mount('#app'); + + return [app, os] as [Vue, MiOS]; + }; + + try { + callback(launch); + } catch (e) { + panic(e); + } + + //#region 更新チェック + const preventUpdate = localStorage.getItem('preventUpdate') == 'true'; + if (!preventUpdate) { + setTimeout(() => { + checkForUpdate(os); + }, 3000); + } + //#endregion + }); +}; + +// BSoD +function panic(e) { + console.error(e); + + // Display blue screen + document.documentElement.style.background = '#1269e2'; + document.body.innerHTML = + '<div id="error">' + + '<h1>:( 致命的な問題が発生しました。</h1>' + + '<p>お使いのブラウザ(またはOS)のバージョンを更新すると解決する可能性があります。</p>' + + '<hr>' + + `<p>エラーコード: ${e.toString()}</p>` + + `<p>ブラウザ バージョン: ${navigator.userAgent}</p>` + + `<p>クライアント バージョン: ${version}</p>` + + '<hr>' + + '<p>問題が解決しない場合は、上記の情報をお書き添えの上 syuilotan@yahoo.co.jp までご連絡ください。</p>' + + '<p>Thank you for using Misskey.</p>' + + '</div>'; + + // TODO: Report the bug +} diff --git a/src/client/app/mobile/api/choose-drive-file.ts b/src/client/app/mobile/api/choose-drive-file.ts new file mode 100644 index 0000000000..b1a78f2364 --- /dev/null +++ b/src/client/app/mobile/api/choose-drive-file.ts @@ -0,0 +1,18 @@ +import Chooser from '../views/components/drive-file-chooser.vue'; + +export default function(opts) { + return new Promise((res, rej) => { + const o = opts || {}; + const w = new Chooser({ + propsData: { + title: o.title, + multiple: o.multiple, + initFolder: o.currentFolder + } + }).$mount(); + w.$once('selected', file => { + res(file); + }); + document.body.appendChild(w.$el); + }); +} diff --git a/src/client/app/mobile/api/choose-drive-folder.ts b/src/client/app/mobile/api/choose-drive-folder.ts new file mode 100644 index 0000000000..d1f97d1487 --- /dev/null +++ b/src/client/app/mobile/api/choose-drive-folder.ts @@ -0,0 +1,17 @@ +import Chooser from '../views/components/drive-folder-chooser.vue'; + +export default function(opts) { + return new Promise((res, rej) => { + const o = opts || {}; + const w = new Chooser({ + propsData: { + title: o.title, + initFolder: o.currentFolder + } + }).$mount(); + w.$once('selected', folder => { + res(folder); + }); + document.body.appendChild(w.$el); + }); +} diff --git a/src/client/app/mobile/api/dialog.ts b/src/client/app/mobile/api/dialog.ts new file mode 100644 index 0000000000..a2378767be --- /dev/null +++ b/src/client/app/mobile/api/dialog.ts @@ -0,0 +1,5 @@ +export default function(opts) { + return new Promise<string>((res, rej) => { + alert('dialog not implemented yet'); + }); +} diff --git a/src/client/app/mobile/api/input.ts b/src/client/app/mobile/api/input.ts new file mode 100644 index 0000000000..38d0fb61eb --- /dev/null +++ b/src/client/app/mobile/api/input.ts @@ -0,0 +1,8 @@ +export default function(opts) { + return new Promise<string>((res, rej) => { + const x = window.prompt(opts.title); + if (x) { + res(x); + } + }); +} diff --git a/src/client/app/mobile/api/notify.ts b/src/client/app/mobile/api/notify.ts new file mode 100644 index 0000000000..82780d196f --- /dev/null +++ b/src/client/app/mobile/api/notify.ts @@ -0,0 +1,3 @@ +export default function(message) { + alert(message); +} diff --git a/src/client/app/mobile/api/post.ts b/src/client/app/mobile/api/post.ts new file mode 100644 index 0000000000..72919c6505 --- /dev/null +++ b/src/client/app/mobile/api/post.ts @@ -0,0 +1,43 @@ +import PostForm from '../views/components/post-form.vue'; +//import RenoteForm from '../views/components/renote-form.vue'; +import getNoteSummary from '../../../../renderers/get-note-summary'; + +export default (os) => (opts) => { + const o = opts || {}; + + if (o.renote) { + /*const vm = new RenoteForm({ + propsData: { + renote: o.renote + } + }).$mount(); + vm.$once('cancel', recover); + vm.$once('note', recover); + document.body.appendChild(vm.$el);*/ + + const text = window.prompt(`「${getNoteSummary(o.renote)}」をRenote`); + if (text == null) return; + os.api('notes/create', { + renoteId: o.renote.id, + text: text == '' ? undefined : text + }); + } else { + const app = document.getElementById('app'); + app.style.display = 'none'; + + function recover() { + app.style.display = 'block'; + } + + const vm = new PostForm({ + parent: os.app, + propsData: { + reply: o.reply + } + }).$mount(); + vm.$once('cancel', recover); + vm.$once('note', recover); + document.body.appendChild(vm.$el); + (vm as any).focus(); + } +}; diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts new file mode 100644 index 0000000000..1de4891973 --- /dev/null +++ b/src/client/app/mobile/script.ts @@ -0,0 +1,84 @@ +/** + * Mobile Client + */ + +import VueRouter from 'vue-router'; + +// Style +import './style.styl'; +import '../../element.scss'; + +import init from '../init'; + +import chooseDriveFolder from './api/choose-drive-folder'; +import chooseDriveFile from './api/choose-drive-file'; +import dialog from './api/dialog'; +import input from './api/input'; +import post from './api/post'; +import notify from './api/notify'; + +import MkIndex from './views/pages/index.vue'; +import MkSignup from './views/pages/signup.vue'; +import MkUser from './views/pages/user.vue'; +import MkSelectDrive from './views/pages/selectdrive.vue'; +import MkDrive from './views/pages/drive.vue'; +import MkNotifications from './views/pages/notifications.vue'; +import MkMessaging from './views/pages/messaging.vue'; +import MkMessagingRoom from './views/pages/messaging-room.vue'; +import MkNote from './views/pages/note.vue'; +import MkSearch from './views/pages/search.vue'; +import MkFollowers from './views/pages/followers.vue'; +import MkFollowing from './views/pages/following.vue'; +import MkSettings from './views/pages/settings.vue'; +import MkProfileSetting from './views/pages/profile-setting.vue'; +import MkOthello from './views/pages/othello.vue'; + +/** + * init + */ +init((launch) => { + // Register directives + require('./views/directives'); + + // Register components + require('./views/components'); + require('./views/widgets'); + + // http://qiita.com/junya/items/3ff380878f26ca447f85 + document.body.setAttribute('ontouchstart', ''); + + // Init router + const router = new VueRouter({ + mode: 'history', + routes: [ + { path: '/', name: 'index', component: MkIndex }, + { path: '/signup', name: 'signup', component: MkSignup }, + { path: '/i/settings', component: MkSettings }, + { path: '/i/settings/profile', component: MkProfileSetting }, + { path: '/i/notifications', component: MkNotifications }, + { path: '/i/messaging', component: MkMessaging }, + { path: '/i/messaging/:user', component: MkMessagingRoom }, + { path: '/i/drive', component: MkDrive }, + { path: '/i/drive/folder/:folder', component: MkDrive }, + { path: '/i/drive/file/:file', component: MkDrive }, + { path: '/selectdrive', component: MkSelectDrive }, + { path: '/search', component: MkSearch }, + { path: '/othello', component: MkOthello }, + { path: '/othello/:game', component: MkOthello }, + { path: '/@:user', component: MkUser }, + { path: '/@:user/followers', component: MkFollowers }, + { path: '/@:user/following', component: MkFollowing }, + { path: '/notes/:note', component: MkNote } + ] + }); + + // Launch the app + launch(router, os => ({ + chooseDriveFolder, + chooseDriveFile, + dialog, + input, + post: post(os), + notify + })); +}, true); diff --git a/src/client/app/mobile/style.styl b/src/client/app/mobile/style.styl new file mode 100644 index 0000000000..81912a2483 --- /dev/null +++ b/src/client/app/mobile/style.styl @@ -0,0 +1,15 @@ +@import "../app" +@import "../reset" + +#wait + top auto + bottom 15px + left 15px + +html + height 100% + +body + display flex + flex-direction column + min-height 100% diff --git a/src/client/app/mobile/views/components/activity.vue b/src/client/app/mobile/views/components/activity.vue new file mode 100644 index 0000000000..dcd319cb69 --- /dev/null +++ b/src/client/app/mobile/views/components/activity.vue @@ -0,0 +1,62 @@ +<template> +<div class="mk-activity"> + <svg v-if="data" ref="canvas" viewBox="0 0 30 1" preserveAspectRatio="none"> + <g v-for="(d, i) in data"> + <rect width="0.8" :height="d.notesH" + :x="i + 0.1" :y="1 - d.notesH - d.repliesH - d.renotesH" + fill="#41ddde"/> + <rect width="0.8" :height="d.repliesH" + :x="i + 0.1" :y="1 - d.repliesH - d.renotesH" + fill="#f7796c"/> + <rect width="0.8" :height="d.renotesH" + :x="i + 0.1" :y="1 - d.renotesH" + fill="#a1de41"/> + </g> + </svg> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['user'], + data() { + return { + fetching: true, + data: [], + peak: null + }; + }, + mounted() { + (this as any).api('aggregation/users/activity', { + userId: this.user.id, + limit: 30 + }).then(data => { + data.forEach(d => d.total = d.notes + d.replies + d.renotes); + this.peak = Math.max.apply(null, data.map(d => d.total)); + data.forEach(d => { + d.notesH = d.notes / this.peak; + d.repliesH = d.replies / this.peak; + d.renotesH = d.renotes / this.peak; + }); + data.reverse(); + this.data = data; + }); + } +}); +</script> + +<style lang="stylus" scoped> +.mk-activity + max-width 600px + margin 0 auto + + > svg + display block + width 100% + height 80px + + > rect + transform-origin center + +</style> diff --git a/src/client/app/mobile/views/components/drive-file-chooser.vue b/src/client/app/mobile/views/components/drive-file-chooser.vue new file mode 100644 index 0000000000..6806af0f1e --- /dev/null +++ b/src/client/app/mobile/views/components/drive-file-chooser.vue @@ -0,0 +1,98 @@ +<template> +<div class="mk-drive-file-chooser"> + <div class="body"> + <header> + <h1>%i18n:mobile.tags.mk-drive-selector.select-file%<span class="count" v-if="files.length > 0">({{ files.length }})</span></h1> + <button class="close" @click="cancel">%fa:times%</button> + <button v-if="multiple" class="ok" @click="ok">%fa:check%</button> + </header> + <mk-drive ref="browser" + :select-file="true" + :multiple="multiple" + @change-selection="onChangeSelection" + @selected="onSelected" + /> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['multiple'], + data() { + return { + files: [] + }; + }, + methods: { + onChangeSelection(files) { + this.files = files; + }, + onSelected(file) { + this.$emit('selected', file); + this.$destroy(); + }, + cancel() { + this.$emit('canceled'); + this.$destroy(); + }, + ok() { + this.$emit('selected', this.files); + this.$destroy(); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-drive-file-chooser + position fixed + z-index 2048 + top 0 + left 0 + width 100% + height 100% + padding 8px + background rgba(0, 0, 0, 0.2) + + > .body + width 100% + height 100% + background #fff + + > header + border-bottom solid 1px #eee + + > h1 + margin 0 + padding 0 + text-align center + line-height 42px + font-size 1em + font-weight normal + + > .count + margin-left 4px + opacity 0.5 + + > .close + position absolute + top 0 + left 0 + line-height 42px + width 42px + + > .ok + position absolute + top 0 + right 0 + line-height 42px + width 42px + + > .mk-drive + height calc(100% - 42px) + overflow scroll + -webkit-overflow-scrolling touch + +</style> diff --git a/src/client/app/mobile/views/components/drive-folder-chooser.vue b/src/client/app/mobile/views/components/drive-folder-chooser.vue new file mode 100644 index 0000000000..853078664f --- /dev/null +++ b/src/client/app/mobile/views/components/drive-folder-chooser.vue @@ -0,0 +1,78 @@ +<template> +<div class="mk-drive-folder-chooser"> + <div class="body"> + <header> + <h1>%i18n:mobile.tags.mk-drive-folder-selector.select-folder%</h1> + <button class="close" @click="cancel">%fa:times%</button> + <button class="ok" @click="ok">%fa:check%</button> + </header> + <mk-drive ref="browser" + select-folder + /> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + methods: { + cancel() { + this.$emit('canceled'); + this.$destroy(); + }, + ok() { + this.$emit('selected', (this.$refs.browser as any).folder); + this.$destroy(); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-drive-folder-chooser + position fixed + z-index 2048 + top 0 + left 0 + width 100% + height 100% + padding 8px + background rgba(0, 0, 0, 0.2) + + > .body + width 100% + height 100% + background #fff + + > header + border-bottom solid 1px #eee + + > h1 + margin 0 + padding 0 + text-align center + line-height 42px + font-size 1em + font-weight normal + + > .close + position absolute + top 0 + left 0 + line-height 42px + width 42px + + > .ok + position absolute + top 0 + right 0 + line-height 42px + width 42px + + > .mk-drive + height calc(100% - 42px) + overflow scroll + -webkit-overflow-scrolling touch + +</style> diff --git a/src/client/app/mobile/views/components/drive.file-detail.vue b/src/client/app/mobile/views/components/drive.file-detail.vue new file mode 100644 index 0000000000..f3274f677f --- /dev/null +++ b/src/client/app/mobile/views/components/drive.file-detail.vue @@ -0,0 +1,295 @@ +<template> +<div class="file-detail"> + <div class="preview"> + <img v-if="kind == 'image'" ref="img" + :src="file.url" + :alt="file.name" + :title="file.name" + @load="onImageLoaded" + :style="style"> + <template v-if="kind != 'image'">%fa:file%</template> + <footer v-if="kind == 'image' && file.properties && file.properties.width && file.properties.height"> + <span class="size"> + <span class="width">{{ file.properties.width }}</span> + <span class="time">×</span> + <span class="height">{{ file.properties.height }}</span> + <span class="px">px</span> + </span> + <span class="separator"></span> + <span class="aspect-ratio"> + <span class="width">{{ file.properties.width / gcd(file.properties.width, file.properties.height) }}</span> + <span class="colon">:</span> + <span class="height">{{ file.properties.height / gcd(file.properties.width, file.properties.height) }}</span> + </span> + </footer> + </div> + <div class="info"> + <div> + <span class="type"><mk-file-type-icon :type="file.type"/>{{ file.type }}</span> + <span class="separator"></span> + <span class="data-size">{{ file.datasize | bytes }}</span> + <span class="separator"></span> + <span class="created-at" @click="showCreatedAt">%fa:R clock%<mk-time :time="file.createdAt"/></span> + </div> + </div> + <div class="menu"> + <div> + <a :href="`${file.url}?download`" :download="file.name"> + %fa:download%%i18n:mobile.tags.mk-drive-file-viewer.download% + </a> + <button @click="rename"> + %fa:pencil-alt%%i18n:mobile.tags.mk-drive-file-viewer.rename% + </button> + <button @click="move"> + %fa:R folder-open%%i18n:mobile.tags.mk-drive-file-viewer.move% + </button> + </div> + </div> + <div class="exif" v-show="exif"> + <div> + <p> + %fa:camera%%i18n:mobile.tags.mk-drive-file-viewer.exif% + </p> + <pre ref="exif" class="json">{{ exif ? JSON.stringify(exif, null, 2) : '' }}</pre> + </div> + </div> + <div class="hash"> + <div> + <p> + %fa:hashtag%%i18n:mobile.tags.mk-drive-file-viewer.hash% + </p> + <code>{{ file.md5 }}</code> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as EXIF from 'exif-js'; +import * as hljs from 'highlight.js'; +import gcd from '../../../common/scripts/gcd'; + +export default Vue.extend({ + props: ['file'], + data() { + return { + gcd, + exif: null + }; + }, + computed: { + browser(): any { + return this.$parent; + }, + kind(): string { + return this.file.type.split('/')[0]; + }, + style(): any { + return this.file.properties.avgColor ? { + 'background-color': `rgb(${ this.file.properties.avgColor.join(',') })` + } : {}; + } + }, + methods: { + rename() { + const name = window.prompt('名前を変更', this.file.name); + if (name == null || name == '' || name == this.file.name) return; + (this as any).api('drive/files/update', { + fileId: this.file.id, + name: name + }).then(() => { + this.browser.cf(this.file, true); + }); + }, + move() { + (this as any).apis.chooseDriveFolder().then(folder => { + (this as any).api('drive/files/update', { + fileId: this.file.id, + folderId: folder == null ? null : folder.id + }).then(() => { + this.browser.cf(this.file, true); + }); + }); + }, + showCreatedAt() { + alert(new Date(this.file.createdAt).toLocaleString()); + }, + onImageLoaded() { + const self = this; + EXIF.getData(this.$refs.img, function(this: any) { + const allMetaData = EXIF.getAllTags(this); + self.exif = allMetaData; + hljs.highlightBlock(self.$refs.exif); + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.file-detail + + > .preview + padding 8px + background #f0f0f0 + + > img + display block + max-width 100% + max-height 300px + margin 0 auto + box-shadow 1px 1px 4px rgba(0, 0, 0, 0.2) + + > footer + padding 8px 8px 0 8px + font-size 0.8em + color #888 + text-align center + + > .separator + display inline + padding 0 4px + + > .size + display inline + + .time + margin 0 2px + + .px + margin-left 4px + + > .aspect-ratio + display inline + opacity 0.7 + + &:before + content "(" + + &:after + content ")" + + > .info + padding 14px + font-size 0.8em + border-top solid 1px #dfdfdf + + > div + max-width 500px + margin 0 auto + + > .separator + padding 0 4px + color #cdcdcd + + > .type + > .data-size + color #9d9d9d + + > mk-file-type-icon + margin-right 4px + + > .created-at + color #bdbdbd + + > [data-fa] + margin-right 2px + + > .menu + padding 14px + border-top solid 1px #dfdfdf + + > div + max-width 500px + margin 0 auto + + > * + display block + width 100% + padding 10px 16px + margin 0 0 12px 0 + color #333 + font-size 0.9em + text-align center + text-decoration none + text-shadow 0 1px 0 rgba(255, 255, 255, 0.9) + background-image linear-gradient(#fafafa, #eaeaea) + border 1px solid #ddd + border-bottom-color #cecece + border-radius 3px + + &:last-child + margin-bottom 0 + + &:active + background-color #767676 + background-image none + border-color #444 + box-shadow 0 1px 3px rgba(0, 0, 0, 0.075), inset 0 0 5px rgba(0, 0, 0, 0.2) + + > [data-fa] + margin-right 4px + + > .hash + padding 14px + border-top solid 1px #dfdfdf + + > div + max-width 500px + margin 0 auto + + > p + display block + margin 0 + padding 0 + color #555 + font-size 0.9em + + > [data-fa] + margin-right 4px + + > code + display block + width 100% + margin 6px 0 0 0 + padding 8px + white-space nowrap + overflow auto + font-size 0.8em + color #222 + border solid 1px #dfdfdf + border-radius 2px + background #f5f5f5 + + > .exif + padding 14px + border-top solid 1px #dfdfdf + + > div + max-width 500px + margin 0 auto + + > p + display block + margin 0 + padding 0 + color #555 + font-size 0.9em + + > [data-fa] + margin-right 4px + + > pre + display block + width 100% + margin 6px 0 0 0 + padding 8px + height 128px + overflow auto + font-size 0.9em + border solid 1px #dfdfdf + border-radius 2px + background #f5f5f5 + +</style> diff --git a/src/client/app/mobile/views/components/drive.file.vue b/src/client/app/mobile/views/components/drive.file.vue new file mode 100644 index 0000000000..7d1957042b --- /dev/null +++ b/src/client/app/mobile/views/components/drive.file.vue @@ -0,0 +1,171 @@ +<template> +<a class="file" @click.prevent="onClick" :href="`/i/drive/file/${ file.id }`" :data-is-selected="isSelected"> + <div class="container"> + <div class="thumbnail" :style="thumbnail"></div> + <div class="body"> + <p class="name"> + <span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span> + <span class="ext" v-if="file.name.lastIndexOf('.') != -1">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span> + </p> + <!-- + if file.tags.length > 0 + ul.tags + each tag in file.tags + li.tag(style={background: tag.color, color: contrast(tag.color)})= tag.name + --> + <footer> + <p class="type"><mk-file-type-icon :type="file.type"/>{{ file.type }}</p> + <p class="separator"></p> + <p class="data-size">{{ file.datasize | bytes }}</p> + <p class="separator"></p> + <p class="created-at"> + %fa:R clock%<mk-time :time="file.createdAt"/> + </p> + </footer> + </div> + </div> +</a> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['file'], + data() { + return { + isSelected: false + }; + }, + computed: { + browser(): any { + return this.$parent; + }, + thumbnail(): any { + return { + 'background-color': this.file.properties.avgColor ? `rgb(${this.file.properties.avgColor.join(',')})` : 'transparent', + 'background-image': `url(${this.file.url}?thumbnail&size=128)` + }; + } + }, + created() { + this.isSelected = this.browser.selectedFiles.some(f => f.id == this.file.id) + + this.browser.$on('change-selection', this.onBrowserChangeSelection); + }, + beforeDestroy() { + this.browser.$off('change-selection', this.onBrowserChangeSelection); + }, + methods: { + onBrowserChangeSelection(selections) { + this.isSelected = selections.some(f => f.id == this.file.id); + }, + onClick() { + this.browser.chooseFile(this.file); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.file + display block + text-decoration none !important + + * + user-select none + pointer-events none + + > .container + max-width 500px + margin 0 auto + padding 16px + + &:after + content "" + display block + clear both + + > .thumbnail + display block + float left + width 64px + height 64px + background-size cover + background-position center center + + > .body + display block + float left + width calc(100% - 74px) + margin-left 10px + + > .name + display block + margin 0 + padding 0 + font-size 0.9em + font-weight bold + color #555 + text-overflow ellipsis + overflow-wrap break-word + + > .ext + opacity 0.5 + + > .tags + display block + margin 4px 0 0 0 + padding 0 + list-style none + font-size 0.5em + + > .tag + display inline-block + margin 0 5px 0 0 + padding 1px 5px + border-radius 2px + + > footer + display block + margin 4px 0 0 0 + font-size 0.7em + + > .separator + display inline + margin 0 + padding 0 4px + color #CDCDCD + + > .type + display inline + margin 0 + padding 0 + color #9D9D9D + + > .mk-file-type-icon + margin-right 4px + + > .data-size + display inline + margin 0 + padding 0 + color #9D9D9D + + > .created-at + display inline + margin 0 + padding 0 + color #BDBDBD + + > [data-fa] + margin-right 2px + + &[data-is-selected] + background $theme-color + + &, * + color #fff !important + +</style> diff --git a/src/client/app/mobile/views/components/drive.folder.vue b/src/client/app/mobile/views/components/drive.folder.vue new file mode 100644 index 0000000000..22ff38fecb --- /dev/null +++ b/src/client/app/mobile/views/components/drive.folder.vue @@ -0,0 +1,58 @@ +<template> +<a class="root folder" @click.prevent="onClick" :href="`/i/drive/folder/${ folder.id }`"> + <div class="container"> + <p class="name">%fa:folder%{{ folder.name }}</p>%fa:angle-right% + </div> +</a> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['folder'], + computed: { + browser(): any { + return this.$parent; + } + }, + methods: { + onClick() { + this.browser.cd(this.folder); + } + } +}); +</script> + +<style lang="stylus" scoped> +.root.folder + display block + color #777 + text-decoration none !important + + * + user-select none + pointer-events none + + > .container + max-width 500px + margin 0 auto + padding 16px + + > .name + display block + margin 0 + padding 0 + + > [data-fa] + margin-right 6px + + > [data-fa] + position absolute + top 0 + bottom 0 + right 20px + + > * + height 100% + +</style> diff --git a/src/client/app/mobile/views/components/drive.vue b/src/client/app/mobile/views/components/drive.vue new file mode 100644 index 0000000000..ff5366a0ad --- /dev/null +++ b/src/client/app/mobile/views/components/drive.vue @@ -0,0 +1,581 @@ +<template> +<div class="mk-drive"> + <nav ref="nav"> + <a @click.prevent="goRoot()" href="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-drive.drive%</a> + <template v-for="folder in hierarchyFolders"> + <span :key="folder.id + '>'">%fa:angle-right%</span> + <a :key="folder.id" @click.prevent="cd(folder)" :href="`/i/drive/folder/${folder.id}`">{{ folder.name }}</a> + </template> + <template v-if="folder != null"> + <span>%fa:angle-right%</span> + <p>{{ folder.name }}</p> + </template> + <template v-if="file != null"> + <span>%fa:angle-right%</span> + <p>{{ file.name }}</p> + </template> + </nav> + <mk-uploader ref="uploader"/> + <div class="browser" :class="{ fetching }" v-if="file == null"> + <div class="info" v-if="info"> + <p v-if="folder == null">{{ (info.usage / info.capacity * 100).toFixed(1) }}% %i18n:mobile.tags.mk-drive.used%</p> + <p v-if="folder != null && (folder.foldersCount > 0 || folder.filesCount > 0)"> + <template v-if="folder.foldersCount > 0">{{ folder.foldersCount }} %i18n:mobile.tags.mk-drive.folder-count%</template> + <template v-if="folder.foldersCount > 0 && folder.filesCount > 0">%i18n:mobile.tags.mk-drive.count-separator%</template> + <template v-if="folder.filesCount > 0">{{ folder.filesCount }} %i18n:mobile.tags.mk-drive.file-count%</template> + </p> + </div> + <div class="folders" v-if="folders.length > 0"> + <x-folder v-for="folder in folders" :key="folder.id" :folder="folder"/> + <p v-if="moreFolders">%i18n:mobile.tags.mk-drive.load-more%</p> + </div> + <div class="files" v-if="files.length > 0"> + <x-file v-for="file in files" :key="file.id" :file="file"/> + <button class="more" v-if="moreFiles" @click="fetchMoreFiles"> + {{ fetchingMoreFiles ? '%i18n:common.loading%' : '%i18n:mobile.tags.mk-drive.load-more%' }} + </button> + </div> + <div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching"> + <p v-if="folder == null">%i18n:mobile.tags.mk-drive.nothing-in-drive%</p> + <p v-if="folder != null">%i18n:mobile.tags.mk-drive.folder-is-empty%</p> + </div> + </div> + <div class="fetching" v-if="fetching && file == null && files.length == 0 && folders.length == 0"> + <div class="spinner"> + <div class="dot1"></div> + <div class="dot2"></div> + </div> + </div> + <input ref="file" class="file" type="file" multiple="multiple" @change="onChangeLocalFile"/> + <x-file-detail v-if="file != null" :file="file"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XFolder from './drive.folder.vue'; +import XFile from './drive.file.vue'; +import XFileDetail from './drive.file-detail.vue'; + +export default Vue.extend({ + components: { + XFolder, + XFile, + XFileDetail + }, + props: ['initFolder', 'initFile', 'selectFile', 'multiple', 'isNaked', 'top'], + data() { + return { + /** + * 現在の階層(フォルダ) + * * null でルートを表す + */ + folder: null, + + file: null, + + files: [], + folders: [], + moreFiles: false, + moreFolders: false, + hierarchyFolders: [], + selectedFiles: [], + info: null, + connection: null, + connectionId: null, + + fetching: true, + fetchingMoreFiles: false, + fetchingMoreFolders: false + }; + }, + computed: { + isFileSelectMode(): boolean { + return this.selectFile; + } + }, + mounted() { + this.connection = (this as any).os.streams.driveStream.getConnection(); + this.connectionId = (this as any).os.streams.driveStream.use(); + + this.connection.on('file_created', this.onStreamDriveFileCreated); + this.connection.on('file_updated', this.onStreamDriveFileUpdated); + this.connection.on('folder_created', this.onStreamDriveFolderCreated); + this.connection.on('folder_updated', this.onStreamDriveFolderUpdated); + + if (this.initFolder) { + this.cd(this.initFolder, true); + } else if (this.initFile) { + this.cf(this.initFile, true); + } else { + this.fetch(); + } + + if (this.isNaked) { + (this.$refs.nav as any).style.top = `${this.top}px`; + } + }, + beforeDestroy() { + this.connection.off('file_created', this.onStreamDriveFileCreated); + this.connection.off('file_updated', this.onStreamDriveFileUpdated); + this.connection.off('folder_created', this.onStreamDriveFolderCreated); + this.connection.off('folder_updated', this.onStreamDriveFolderUpdated); + (this as any).os.streams.driveStream.dispose(this.connectionId); + }, + methods: { + onStreamDriveFileCreated(file) { + this.addFile(file, true); + }, + + onStreamDriveFileUpdated(file) { + const current = this.folder ? this.folder.id : null; + if (current != file.folderId) { + this.removeFile(file); + } else { + this.addFile(file, true); + } + }, + + onStreamDriveFolderCreated(folder) { + this.addFolder(folder, true); + }, + + onStreamDriveFolderUpdated(folder) { + const current = this.folder ? this.folder.id : null; + if (current != folder.parentId) { + this.removeFolder(folder); + } else { + this.addFolder(folder, true); + } + }, + + dive(folder) { + this.hierarchyFolders.unshift(folder); + if (folder.parent) this.dive(folder.parent); + }, + + cd(target, silent = false) { + this.file = null; + + if (target == null) { + this.goRoot(silent); + return; + } else if (typeof target == 'object') { + target = target.id; + } + + this.fetching = true; + + (this as any).api('drive/folders/show', { + folderId: target + }).then(folder => { + this.folder = folder; + this.hierarchyFolders = []; + + if (folder.parent) this.dive(folder.parent); + + this.$emit('open-folder', this.folder, silent); + this.fetch(); + }); + }, + + addFolder(folder, unshift = false) { + const current = this.folder ? this.folder.id : null; + // 追加しようとしているフォルダが、今居る階層とは違う階層のものだったら中断 + if (current != folder.parentId) return; + + // 追加しようとしているフォルダを既に所有してたら中断 + if (this.folders.some(f => f.id == folder.id)) return; + + if (unshift) { + this.folders.unshift(folder); + } else { + this.folders.push(folder); + } + }, + + addFile(file, unshift = false) { + const current = this.folder ? this.folder.id : null; + // 追加しようとしているファイルが、今居る階層とは違う階層のものだったら中断 + if (current != file.folderId) return; + + if (this.files.some(f => f.id == file.id)) { + const exist = this.files.map(f => f.id).indexOf(file.id); + Vue.set(this.files, exist, file); + return; + } + + if (unshift) { + this.files.unshift(file); + } else { + this.files.push(file); + } + }, + + removeFolder(folder) { + if (typeof folder == 'object') folder = folder.id; + this.folders = this.folders.filter(f => f.id != folder); + }, + + removeFile(file) { + if (typeof file == 'object') file = file.id; + this.files = this.files.filter(f => f.id != file); + }, + + appendFile(file) { + this.addFile(file); + }, + appendFolder(folder) { + this.addFolder(folder); + }, + prependFile(file) { + this.addFile(file, true); + }, + prependFolder(folder) { + this.addFolder(folder, true); + }, + + goRoot(silent = false) { + if (this.folder || this.file) { + this.file = null; + this.folder = null; + this.hierarchyFolders = []; + this.$emit('move-root', silent); + this.fetch(); + } + }, + + fetch() { + this.folders = []; + this.files = []; + this.moreFolders = false; + this.moreFiles = false; + this.fetching = true; + + this.$emit('begin-fetch'); + + let fetchedFolders = null; + let fetchedFiles = null; + + const foldersMax = 20; + const filesMax = 20; + + // フォルダ一覧取得 + (this as any).api('drive/folders', { + folderId: this.folder ? this.folder.id : null, + limit: foldersMax + 1 + }).then(folders => { + if (folders.length == foldersMax + 1) { + this.moreFolders = true; + folders.pop(); + } + fetchedFolders = folders; + complete(); + }); + + // ファイル一覧取得 + (this as any).api('drive/files', { + folderId: this.folder ? this.folder.id : null, + limit: filesMax + 1 + }).then(files => { + if (files.length == filesMax + 1) { + this.moreFiles = true; + files.pop(); + } + fetchedFiles = files; + complete(); + }); + + let flag = false; + const complete = () => { + if (flag) { + fetchedFolders.forEach(this.appendFolder); + fetchedFiles.forEach(this.appendFile); + this.fetching = false; + + // 一連の読み込みが完了したイベントを発行 + this.$emit('fetched'); + } else { + flag = true; + // 一連の読み込みが半分完了したイベントを発行 + this.$emit('fetch-mid'); + } + }; + + if (this.folder == null) { + // Fetch addtional drive info + (this as any).api('drive').then(info => { + this.info = info; + }); + } + }, + + fetchMoreFiles() { + this.fetching = true; + this.fetchingMoreFiles = true; + + const max = 30; + + // ファイル一覧取得 + (this as any).api('drive/files', { + folderId: this.folder ? this.folder.id : null, + limit: max + 1, + untilId: this.files[this.files.length - 1].id + }).then(files => { + if (files.length == max + 1) { + this.moreFiles = true; + files.pop(); + } else { + this.moreFiles = false; + } + files.forEach(this.appendFile); + this.fetching = false; + this.fetchingMoreFiles = false; + }); + }, + + chooseFile(file) { + if (this.isFileSelectMode) { + if (this.multiple) { + if (this.selectedFiles.some(f => f.id == file.id)) { + this.selectedFiles = this.selectedFiles.filter(f => f.id != file.id); + } else { + this.selectedFiles.push(file); + } + this.$emit('change-selection', this.selectedFiles); + } else { + this.$emit('selected', file); + } + } else { + this.cf(file); + } + }, + + cf(file, silent = false) { + if (typeof file == 'object') file = file.id; + + this.fetching = true; + + (this as any).api('drive/files/show', { + fileId: file + }).then(file => { + this.file = file; + this.folder = null; + this.hierarchyFolders = []; + + if (file.folder) this.dive(file.folder); + + this.fetching = false; + + this.$emit('open-file', this.file, silent); + }); + }, + + openContextMenu() { + const fn = window.prompt('何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>'); + if (fn == null || fn == '') return; + switch (fn) { + case '1': + this.selectLocalFile(); + break; + case '2': + this.urlUpload(); + break; + case '3': + this.createFolder(); + break; + case '4': + this.renameFolder(); + break; + case '5': + this.moveFolder(); + break; + case '6': + alert('ごめんなさい!フォルダの削除は未実装です...。'); + break; + } + }, + + selectLocalFile() { + (this.$refs.file as any).click(); + }, + + createFolder() { + const name = window.prompt('フォルダー名'); + if (name == null || name == '') return; + (this as any).api('drive/folders/create', { + name: name, + parentId: this.folder ? this.folder.id : undefined + }).then(folder => { + this.addFolder(folder, true); + }); + }, + + renameFolder() { + if (this.folder == null) { + alert('現在いる場所はルートで、フォルダではないため名前の変更はできません。名前を変更したいフォルダに移動してからやってください。'); + return; + } + const name = window.prompt('フォルダー名', this.folder.name); + if (name == null || name == '') return; + (this as any).api('drive/folders/update', { + name: name, + folderId: this.folder.id + }).then(folder => { + this.cd(folder); + }); + }, + + moveFolder() { + if (this.folder == null) { + alert('現在いる場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。'); + return; + } + (this as any).apis.chooseDriveFolder().then(folder => { + (this as any).api('drive/folders/update', { + parentId: folder ? folder.id : null, + folderId: this.folder.id + }).then(folder => { + this.cd(folder); + }); + }); + }, + + urlUpload() { + const url = window.prompt('アップロードしたいファイルのURL'); + if (url == null || url == '') return; + (this as any).api('drive/files/upload_from_url', { + url: url, + folderId: this.folder ? this.folder.id : undefined + }); + alert('アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。'); + }, + + onChangeLocalFile() { + Array.from((this.$refs.file as any).files) + .forEach(f => (this.$refs.uploader as any).upload(f, this.folder)); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-drive + background #fff + + > nav + display block + position sticky + position -webkit-sticky + top 0 + z-index 1 + width 100% + padding 10px 12px + overflow auto + white-space nowrap + font-size 0.9em + color rgba(0, 0, 0, 0.67) + -webkit-backdrop-filter blur(12px) + backdrop-filter blur(12px) + background-color rgba(#fff, 0.75) + border-bottom solid 1px rgba(0, 0, 0, 0.13) + + > p + > a + display inline + margin 0 + padding 0 + text-decoration none !important + color inherit + + &:last-child + font-weight bold + + > [data-fa] + margin-right 4px + + > span + margin 0 8px + opacity 0.5 + + > .browser + &.fetching + opacity 0.5 + + > .info + border-bottom solid 1px #eee + + &:empty + display none + + > p + display block + max-width 500px + margin 0 auto + padding 4px 16px + font-size 10px + color #777 + + > .folders + > .folder + border-bottom solid 1px #eee + + > .files + > .file + border-bottom solid 1px #eee + + > .more + display block + width 100% + padding 16px + font-size 16px + color #555 + + > .empty + padding 16px + text-align center + color #999 + pointer-events none + + > p + margin 0 + + > .fetching + .spinner + margin 100px auto + width 40px + height 40px + text-align center + + animation sk-rotate 2.0s infinite linear + + .dot1, .dot2 + width 60% + height 60% + display inline-block + position absolute + top 0 + background rgba(0, 0, 0, 0.2) + border-radius 100% + + animation sk-bounce 2.0s infinite ease-in-out + + .dot2 + top auto + bottom 0 + animation-delay -1.0s + + @keyframes sk-rotate { 100% { transform: rotate(360deg); }} + + @keyframes sk-bounce { + 0%, 100% { + transform: scale(0.0); + } 50% { + transform: scale(1.0); + } + } + + > .file + display none + +</style> diff --git a/src/client/app/mobile/views/components/follow-button.vue b/src/client/app/mobile/views/components/follow-button.vue new file mode 100644 index 0000000000..43c69d4e02 --- /dev/null +++ b/src/client/app/mobile/views/components/follow-button.vue @@ -0,0 +1,123 @@ +<template> +<button class="mk-follow-button" + :class="{ wait: wait, follow: !user.isFollowing, unfollow: user.isFollowing }" + @click="onClick" + :disabled="wait" +> + <template v-if="!wait && user.isFollowing">%fa:minus%</template> + <template v-if="!wait && !user.isFollowing">%fa:plus%</template> + <template v-if="wait">%fa:spinner .pulse .fw%</template> + {{ user.isFollowing ? '%i18n:mobile.tags.mk-follow-button.unfollow%' : '%i18n:mobile.tags.mk-follow-button.follow%' }} +</button> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: { + user: { + type: Object, + required: true + } + }, + data() { + return { + wait: false, + connection: null, + connectionId: null + }; + }, + mounted() { + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + + this.connection.on('follow', this.onFollow); + this.connection.on('unfollow', this.onUnfollow); + }, + beforeDestroy() { + this.connection.off('follow', this.onFollow); + this.connection.off('unfollow', this.onUnfollow); + (this as any).os.stream.dispose(this.connectionId); + }, + methods: { + + onFollow(user) { + if (user.id == this.user.id) { + this.user.isFollowing = user.isFollowing; + } + }, + + onUnfollow(user) { + if (user.id == this.user.id) { + this.user.isFollowing = user.isFollowing; + } + }, + + onClick() { + this.wait = true; + if (this.user.isFollowing) { + (this as any).api('following/delete', { + userId: this.user.id + }).then(() => { + this.user.isFollowing = false; + }).catch(err => { + console.error(err); + }).then(() => { + this.wait = false; + }); + } else { + (this as any).api('following/create', { + userId: this.user.id + }).then(() => { + this.user.isFollowing = true; + }).catch(err => { + console.error(err); + }).then(() => { + this.wait = false; + }); + } + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-follow-button + display block + user-select none + cursor pointer + padding 0 16px + margin 0 + height inherit + font-size 16px + outline none + border solid 1px $theme-color + border-radius 4px + + * + pointer-events none + + &.follow + color $theme-color + background transparent + + &:hover + background rgba($theme-color, 0.1) + + &:active + background rgba($theme-color, 0.2) + + &.unfollow + color $theme-color-foreground + background $theme-color + + &.wait + cursor wait !important + opacity 0.7 + + > [data-fa] + margin-right 4px + +</style> diff --git a/src/client/app/mobile/views/components/friends-maker.vue b/src/client/app/mobile/views/components/friends-maker.vue new file mode 100644 index 0000000000..961a5f568a --- /dev/null +++ b/src/client/app/mobile/views/components/friends-maker.vue @@ -0,0 +1,127 @@ +<template> +<div class="mk-friends-maker"> + <p class="title">気になるユーザーをフォロー:</p> + <div class="users" v-if="!fetching && users.length > 0"> + <mk-user-card v-for="user in users" :key="user.id" :user="user"/> + </div> + <p class="empty" v-if="!fetching && users.length == 0">おすすめのユーザーは見つかりませんでした。</p> + <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p> + <a class="refresh" @click="refresh">もっと見る</a> + <button class="close" @click="close" title="閉じる">%fa:times%</button> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + data() { + return { + users: [], + fetching: true, + limit: 6, + page: 0 + }; + }, + mounted() { + this.fetch(); + }, + methods: { + fetch() { + this.fetching = true; + this.users = []; + + (this as any).api('users/recommendation', { + limit: this.limit, + offset: this.limit * this.page + }).then(users => { + this.users = users; + this.fetching = false; + }); + }, + refresh() { + if (this.users.length < this.limit) { + this.page = 0; + } else { + this.page++; + } + this.fetch(); + }, + close() { + this.$destroy(); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-friends-maker + background #fff + border-radius 8px + box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + + > .title + margin 0 + padding 8px 16px + font-size 1em + font-weight bold + color #888 + + > .users + overflow-x scroll + -webkit-overflow-scrolling touch + white-space nowrap + padding 16px + background #eee + + > .mk-user-card + &:not(:last-child) + margin-right 16px + + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > .fetching + margin 0 + padding 16px + text-align center + color #aaa + + > [data-fa] + margin-right 4px + + > .refresh + display block + margin 0 + padding 8px 16px + text-align right + font-size 0.9em + color #999 + + > .close + cursor pointer + display block + position absolute + top 0 + right 0 + z-index 1 + margin 0 + padding 0 + font-size 1.2em + color #999 + border none + outline none + background transparent + + &:hover + color #555 + + &:active + color #222 + + > [data-fa] + padding 10px + +</style> diff --git a/src/client/app/mobile/views/components/index.ts b/src/client/app/mobile/views/components/index.ts new file mode 100644 index 0000000000..9346700304 --- /dev/null +++ b/src/client/app/mobile/views/components/index.ts @@ -0,0 +1,47 @@ +import Vue from 'vue'; + +import ui from './ui.vue'; +import timeline from './timeline.vue'; +import note from './note.vue'; +import notes from './notes.vue'; +import mediaImage from './media-image.vue'; +import mediaVideo from './media-video.vue'; +import drive from './drive.vue'; +import notePreview from './note-preview.vue'; +import subNoteContent from './sub-note-content.vue'; +import noteCard from './note-card.vue'; +import userCard from './user-card.vue'; +import noteDetail from './note-detail.vue'; +import followButton from './follow-button.vue'; +import friendsMaker from './friends-maker.vue'; +import notification from './notification.vue'; +import notifications from './notifications.vue'; +import notificationPreview from './notification-preview.vue'; +import usersList from './users-list.vue'; +import userPreview from './user-preview.vue'; +import userTimeline from './user-timeline.vue'; +import activity from './activity.vue'; +import widgetContainer from './widget-container.vue'; + +Vue.component('mk-ui', ui); +Vue.component('mk-timeline', timeline); +Vue.component('mk-note', note); +Vue.component('mk-notes', notes); +Vue.component('mk-media-image', mediaImage); +Vue.component('mk-media-video', mediaVideo); +Vue.component('mk-drive', drive); +Vue.component('mk-note-preview', notePreview); +Vue.component('mk-sub-note-content', subNoteContent); +Vue.component('mk-note-card', noteCard); +Vue.component('mk-user-card', userCard); +Vue.component('mk-note-detail', noteDetail); +Vue.component('mk-follow-button', followButton); +Vue.component('mk-friends-maker', friendsMaker); +Vue.component('mk-notification', notification); +Vue.component('mk-notifications', notifications); +Vue.component('mk-notification-preview', notificationPreview); +Vue.component('mk-users-list', usersList); +Vue.component('mk-user-preview', userPreview); +Vue.component('mk-user-timeline', userTimeline); +Vue.component('mk-activity', activity); +Vue.component('mk-widget-container', widgetContainer); diff --git a/src/client/app/mobile/views/components/media-image.vue b/src/client/app/mobile/views/components/media-image.vue new file mode 100644 index 0000000000..cfc2134988 --- /dev/null +++ b/src/client/app/mobile/views/components/media-image.vue @@ -0,0 +1,31 @@ +<template> +<a class="mk-media-image" :href="image.url" target="_blank" :style="style" :title="image.name"></a> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['image'], + computed: { + style(): any { + return { + 'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent', + 'background-image': `url(${this.image.url}?thumbnail&size=512)` + }; + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-media-image + display block + overflow hidden + width 100% + height 100% + background-position center + background-size cover + border-radius 4px + +</style> diff --git a/src/client/app/mobile/views/components/media-video.vue b/src/client/app/mobile/views/components/media-video.vue new file mode 100644 index 0000000000..68cd48587a --- /dev/null +++ b/src/client/app/mobile/views/components/media-video.vue @@ -0,0 +1,36 @@ +<template> + <a class="mk-media-video" + :href="video.url" + target="_blank" + :style="imageStyle" + :title="video.name"> + %fa:R play-circle% + </a> +</template> + +<script lang="ts"> +import Vue from 'vue' +export default Vue.extend({ + props: ['video'], + computed: { + imageStyle(): any { + return { + 'background-image': `url(${this.video.url}?thumbnail&size=512)` + }; + } + },}) +</script> + +<style lang="stylus" scoped> +.mk-media-video + display flex + justify-content center + align-items center + + font-size 3.5em + overflow hidden + background-position center + background-size cover + width 100% + height 100% +</style> diff --git a/src/client/app/mobile/views/components/note-card.vue b/src/client/app/mobile/views/components/note-card.vue new file mode 100644 index 0000000000..393fa9b831 --- /dev/null +++ b/src/client/app/mobile/views/components/note-card.vue @@ -0,0 +1,85 @@ +<template> +<div class="mk-note-card"> + <a :href="note | notePage"> + <header> + <img :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/><h3>{{ note.user | userName }}</h3> + </header> + <div> + {{ text }} + </div> + <mk-time :time="note.createdAt"/> + </a> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import summary from '../../../../../renderers/get-note-summary'; + +export default Vue.extend({ + props: ['note'], + computed: { + text(): string { + return summary(this.note); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-note-card + display inline-block + width 150px + //height 120px + font-size 12px + background #fff + border-radius 4px + + > a + display block + color #2c3940 + + &:hover + text-decoration none + + > header + > img + position absolute + top 8px + left 8px + width 28px + height 28px + border-radius 6px + + > h3 + display inline-block + overflow hidden + width calc(100% - 45px) + margin 8px 0 0 42px + line-height 28px + white-space nowrap + text-overflow ellipsis + font-size 12px + + > div + padding 2px 8px 8px 8px + height 60px + overflow hidden + white-space normal + + &:after + content "" + display block + position absolute + top 40px + left 0 + width 100% + height 20px + background linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, #fff 100%) + + > .mk-time + display inline-block + padding 8px + color #aaa + +</style> diff --git a/src/client/app/mobile/views/components/note-detail.sub.vue b/src/client/app/mobile/views/components/note-detail.sub.vue new file mode 100644 index 0000000000..06f442d308 --- /dev/null +++ b/src/client/app/mobile/views/components/note-detail.sub.vue @@ -0,0 +1,103 @@ +<template> +<div class="root sub"> + <router-link class="avatar-anchor" :to="note.user | userPage"> + <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + </router-link> + <div class="main"> + <header> + <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> + <span class="username">@{{ note.user | acct }}</span> + <router-link class="time" :to="note | notePage"> + <mk-time :time="note.createdAt"/> + </router-link> + </header> + <div class="body"> + <mk-sub-note-content class="text" :note="note"/> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['note'] +}); +</script> + +<style lang="stylus" scoped> +.root.sub + padding 8px + font-size 0.9em + background #fdfdfd + + @media (min-width 500px) + padding 12px + + &:after + content "" + display block + clear both + + &:hover + > .main > footer > button + color #888 + + > .avatar-anchor + display block + float left + margin 0 12px 0 0 + + > .avatar + display block + width 48px + height 48px + margin 0 + border-radius 8px + vertical-align bottom + + > .main + float left + width calc(100% - 60px) + + > header + display flex + margin-bottom 4px + white-space nowrap + + > .name + display block + margin 0 .5em 0 0 + padding 0 + overflow hidden + color #607073 + font-size 1em + font-weight 700 + text-align left + text-decoration none + text-overflow ellipsis + + &:hover + text-decoration underline + + > .username + text-align left + margin 0 .5em 0 0 + color #d1d8da + + > .time + margin-left auto + color #b2b8bb + + > .body + + > .text + cursor default + margin 0 + padding 0 + font-size 1.1em + color #717171 + +</style> + diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue new file mode 100644 index 0000000000..63e28b7f54 --- /dev/null +++ b/src/client/app/mobile/views/components/note-detail.vue @@ -0,0 +1,444 @@ +<template> +<div class="mk-note-detail"> + <button + class="more" + v-if="p.reply && p.reply.replyId && context == null" + @click="fetchContext" + :disabled="fetchingContext" + > + <template v-if="!contextFetching">%fa:ellipsis-v%</template> + <template v-if="contextFetching">%fa:spinner .pulse%</template> + </button> + <div class="context"> + <x-sub v-for="note in context" :key="note.id" :note="note"/> + </div> + <div class="reply-to" v-if="p.reply"> + <x-sub :note="p.reply"/> + </div> + <div class="renote" v-if="isRenote"> + <p> + <router-link class="avatar-anchor" :to="note.user | userPage"> + <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/> + </router-link> + %fa:retweet%<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>がRenote + </p> + </div> + <article> + <header> + <router-link class="avatar-anchor" :to="p.user | userPage"> + <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + </router-link> + <div> + <router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link> + <span class="username">@{{ p.user | acct }}</span> + </div> + </header> + <div class="body"> + <mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/> + <div class="tags" v-if="p.tags && p.tags.length > 0"> + <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link> + </div> + <div class="media" v-if="p.media.length > 0"> + <mk-media-list :media-list="p.media"/> + </div> + <mk-poll v-if="p.poll" :note="p"/> + <mk-url-preview v-for="url in urls" :url="url" :key="url"/> + <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a> + <div class="map" v-if="p.geo" ref="map"></div> + <div class="renote" v-if="p.renote"> + <mk-note-preview :note="p.renote"/> + </div> + </div> + <router-link class="time" :to="p | notePage"> + <mk-time :time="p.createdAt" mode="detail"/> + </router-link> + <footer> + <mk-reactions-viewer :note="p"/> + <button @click="reply" title="%i18n:mobile.tags.mk-note-detail.reply%"> + %fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p> + </button> + <button @click="renote" title="Renote"> + %fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p> + </button> + <button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:mobile.tags.mk-note-detail.reaction%"> + %fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p> + </button> + <button @click="menu" ref="menuButton"> + %fa:ellipsis-h% + </button> + </footer> + </article> + <div class="replies" v-if="!compact"> + <x-sub v-for="note in replies" :key="note.id" :note="note"/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import parse from '../../../../../text/parse'; + +import MkNoteMenu from '../../../common/views/components/note-menu.vue'; +import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; +import XSub from './note-detail.sub.vue'; + +export default Vue.extend({ + components: { + XSub + }, + + props: { + note: { + type: Object, + required: true + }, + compact: { + default: false + } + }, + + data() { + return { + context: [], + contextFetching: false, + replies: [] + }; + }, + + computed: { + isRenote(): boolean { + return (this.note.renote && + this.note.text == null && + this.note.mediaIds.length == 0 && + this.note.poll == null); + }, + p(): any { + return this.isRenote ? this.note.renote : this.note; + }, + reactionsCount(): number { + return this.p.reactionCounts + ? Object.keys(this.p.reactionCounts) + .map(key => this.p.reactionCounts[key]) + .reduce((a, b) => a + b) + : 0; + }, + urls(): string[] { + if (this.p.text) { + const ast = parse(this.p.text); + return ast + .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) + .map(t => t.url); + } else { + return null; + } + } + }, + + mounted() { + // Get replies + if (!this.compact) { + (this as any).api('notes/replies', { + noteId: this.p.id, + limit: 8 + }).then(replies => { + this.replies = replies; + }); + } + + // Draw map + if (this.p.geo) { + const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true; + if (shouldShowMap) { + (this as any).os.getGoogleMaps().then(maps => { + const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]); + const map = new maps.Map(this.$refs.map, { + center: uluru, + zoom: 15 + }); + new maps.Marker({ + position: uluru, + map: map + }); + }); + } + } + }, + + methods: { + fetchContext() { + this.contextFetching = true; + + // Fetch context + (this as any).api('notes/context', { + noteId: this.p.replyId + }).then(context => { + this.contextFetching = false; + this.context = context.reverse(); + }); + }, + reply() { + (this as any).apis.post({ + reply: this.p + }); + }, + renote() { + (this as any).apis.post({ + renote: this.p + }); + }, + react() { + (this as any).os.new(MkReactionPicker, { + source: this.$refs.reactButton, + note: this.p, + compact: true + }); + }, + menu() { + (this as any).os.new(MkNoteMenu, { + source: this.$refs.menuButton, + note: this.p, + compact: true + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-note-detail + overflow hidden + margin 0 auto + padding 0 + width 100% + text-align left + background #fff + border-radius 8px + box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + + > .fetching + padding 64px 0 + + > .more + display block + margin 0 + padding 10px 0 + width 100% + font-size 1em + text-align center + color #999 + cursor pointer + background #fafafa + outline none + border none + border-bottom solid 1px #eef0f2 + border-radius 6px 6px 0 0 + box-shadow none + + &:hover + background #f6f6f6 + + &:active + background #f0f0f0 + + &:disabled + color #ccc + + > .context + > * + border-bottom 1px solid #eef0f2 + + > .renote + color #9dbb00 + background linear-gradient(to bottom, #edfde2 0%, #fff 100%) + + > p + margin 0 + padding 16px 32px + + .avatar-anchor + display inline-block + + .avatar + vertical-align bottom + min-width 28px + min-height 28px + max-width 28px + max-height 28px + margin 0 8px 0 0 + border-radius 6px + + [data-fa] + margin-right 4px + + .name + font-weight bold + + & + article + padding-top 8px + + > .reply-to + border-bottom 1px solid #eef0f2 + + > article + padding 14px 16px 9px 16px + + @media (min-width 500px) + padding 28px 32px 18px 32px + + &:after + content "" + display block + clear both + + &:hover + > .main > footer > button + color #888 + + > header + display flex + line-height 1.1 + + > .avatar-anchor + display block + padding 0 .5em 0 0 + + > .avatar + display block + width 54px + height 54px + margin 0 + border-radius 8px + vertical-align bottom + + @media (min-width 500px) + width 60px + height 60px + + > div + + > .name + display inline-block + margin .4em 0 + color #777 + font-size 16px + font-weight bold + text-align left + text-decoration none + + &:hover + text-decoration underline + + > .username + display block + text-align left + margin 0 + color #ccc + + > .body + padding 8px 0 + + > .renote + margin 8px 0 + + > .mk-note-preview + padding 16px + border dashed 1px #c0dac6 + border-radius 8px + + > .location + margin 4px 0 + font-size 12px + color #ccc + + > .map + width 100% + height 200px + + &:empty + display none + + > .mk-url-preview + margin-top 8px + + > .media + > img + display block + max-width 100% + + > .tags + margin 4px 0 0 0 + + > * + display inline-block + margin 0 8px 0 0 + padding 2px 8px 2px 16px + font-size 90% + color #8d969e + background #edf0f3 + border-radius 4px + + &:before + content "" + display block + position absolute + top 0 + bottom 0 + left 4px + width 8px + height 8px + margin auto 0 + background #fff + border-radius 100% + + > .time + font-size 16px + color #c0c0c0 + + > footer + font-size 1.2em + + > button + margin 0 + padding 8px + background transparent + border none + box-shadow none + font-size 1em + color #ddd + cursor pointer + + &:not(:last-child) + margin-right 28px + + &:hover + color #666 + + > .count + display inline + margin 0 0 0 8px + color #999 + + &.reacted + color $theme-color + + > .replies + > * + border-top 1px solid #eef0f2 + +</style> + +<style lang="stylus" module> +.text + display block + margin 0 + padding 0 + overflow-wrap break-word + font-size 16px + color #717171 + + @media (min-width 500px) + font-size 24px + +</style> diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue new file mode 100644 index 0000000000..b9a6db315d --- /dev/null +++ b/src/client/app/mobile/views/components/note-preview.vue @@ -0,0 +1,100 @@ +<template> +<div class="mk-note-preview"> + <router-link class="avatar-anchor" :to="note.user | userPage"> + <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + </router-link> + <div class="main"> + <header> + <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> + <span class="username">@{{ note.user | acct }}</span> + <router-link class="time" :to="note | notePage"> + <mk-time :time="note.createdAt"/> + </router-link> + </header> + <div class="body"> + <mk-sub-note-content class="text" :note="note"/> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['note'] +}); +</script> + +<style lang="stylus" scoped> +.mk-note-preview + margin 0 + padding 0 + font-size 0.9em + background #fff + + &:after + content "" + display block + clear both + + &:hover + > .main > footer > button + color #888 + + > .avatar-anchor + display block + float left + margin 0 12px 0 0 + + > .avatar + display block + width 48px + height 48px + margin 0 + border-radius 8px + vertical-align bottom + + > .main + float left + width calc(100% - 60px) + + > header + display flex + margin-bottom 4px + white-space nowrap + + > .name + display block + margin 0 .5em 0 0 + padding 0 + overflow hidden + color #607073 + font-size 1em + font-weight 700 + text-align left + text-decoration none + text-overflow ellipsis + + &:hover + text-decoration underline + + > .username + text-align left + margin 0 .5em 0 0 + color #d1d8da + + > .time + margin-left auto + color #b2b8bb + + > .body + + > .text + cursor default + margin 0 + padding 0 + font-size 1.1em + color #717171 + +</style> diff --git a/src/client/app/mobile/views/components/note.sub.vue b/src/client/app/mobile/views/components/note.sub.vue new file mode 100644 index 0000000000..d489f3a053 --- /dev/null +++ b/src/client/app/mobile/views/components/note.sub.vue @@ -0,0 +1,109 @@ +<template> +<div class="sub"> + <router-link class="avatar-anchor" :to="note.user | userPage"> + <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/> + </router-link> + <div class="main"> + <header> + <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> + <span class="username">@{{ note.user | acct }}</span> + <router-link class="created-at" :to="note | notePage"> + <mk-time :time="note.createdAt"/> + </router-link> + </header> + <div class="body"> + <mk-sub-note-content class="text" :note="note"/> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['note'] +}); +</script> + +<style lang="stylus" scoped> +.sub + font-size 0.9em + padding 16px + + &:after + content "" + display block + clear both + + > .avatar-anchor + display block + float left + margin 0 10px 0 0 + + @media (min-width 500px) + margin-right 16px + + > .avatar + display block + width 44px + height 44px + margin 0 + border-radius 8px + vertical-align bottom + + @media (min-width 500px) + width 52px + height 52px + + > .main + float left + width calc(100% - 54px) + + @media (min-width 500px) + width calc(100% - 68px) + + > header + display flex + margin-bottom 2px + white-space nowrap + + > .name + display block + margin 0 0.5em 0 0 + padding 0 + overflow hidden + color #607073 + font-size 1em + font-weight 700 + text-align left + text-decoration none + text-overflow ellipsis + + &:hover + text-decoration underline + + > .username + text-align left + margin 0 + color #d1d8da + + > .created-at + margin-left auto + color #b2b8bb + + > .body + + > .text + cursor default + margin 0 + padding 0 + font-size 1.1em + color #717171 + + pre + max-height 120px + font-size 80% + +</style> + diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue new file mode 100644 index 0000000000..033de4f429 --- /dev/null +++ b/src/client/app/mobile/views/components/note.vue @@ -0,0 +1,523 @@ +<template> +<div class="note" :class="{ renote: isRenote }"> + <div class="reply-to" v-if="p.reply"> + <x-sub :note="p.reply"/> + </div> + <div class="renote" v-if="isRenote"> + <p> + <router-link class="avatar-anchor" :to="note.user | userPage"> + <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + </router-link> + %fa:retweet% + <span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span> + <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> + <span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span> + </p> + <mk-time :time="note.createdAt"/> + </div> + <article> + <router-link class="avatar-anchor" :to="p.user | userPage"> + <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/> + </router-link> + <div class="main"> + <header> + <router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link> + <span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span> + <span class="username">@{{ p.user | acct }}</span> + <div class="info"> + <span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span> + <router-link class="created-at" :to="p | notePage"> + <mk-time :time="p.createdAt"/> + </router-link> + </div> + </header> + <div class="body"> + <p class="channel" v-if="p.channel != null"><a target="_blank">{{ p.channel.title }}</a>:</p> + <div class="text"> + <a class="reply" v-if="p.reply"> + %fa:reply% + </a> + <mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/> + <a class="rp" v-if="p.renote != null">RP:</a> + </div> + <div class="media" v-if="p.media.length > 0"> + <mk-media-list :media-list="p.media"/> + </div> + <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> + <div class="tags" v-if="p.tags && p.tags.length > 0"> + <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link> + </div> + <mk-url-preview v-for="url in urls" :url="url" :key="url"/> + <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a> + <div class="map" v-if="p.geo" ref="map"></div> + <span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span> + <div class="renote" v-if="p.renote"> + <mk-note-preview :note="p.renote"/> + </div> + </div> + <footer> + <mk-reactions-viewer :note="p" ref="reactionsViewer"/> + <button @click="reply"> + %fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p> + </button> + <button @click="renote" title="Renote"> + %fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p> + </button> + <button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton"> + %fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p> + </button> + <button class="menu" @click="menu" ref="menuButton"> + %fa:ellipsis-h% + </button> + </footer> + </div> + </article> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import parse from '../../../../../text/parse'; + +import MkNoteMenu from '../../../common/views/components/note-menu.vue'; +import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; +import XSub from './note.sub.vue'; + +export default Vue.extend({ + components: { + XSub + }, + + props: ['note'], + + data() { + return { + connection: null, + connectionId: null + }; + }, + + computed: { + isRenote(): boolean { + return (this.note.renote && + this.note.text == null && + this.note.mediaIds.length == 0 && + this.note.poll == null); + }, + p(): any { + return this.isRenote ? this.note.renote : this.note; + }, + reactionsCount(): number { + return this.p.reactionCounts + ? Object.keys(this.p.reactionCounts) + .map(key => this.p.reactionCounts[key]) + .reduce((a, b) => a + b) + : 0; + }, + urls(): string[] { + if (this.p.text) { + const ast = parse(this.p.text); + return ast + .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) + .map(t => t.url); + } else { + return null; + } + } + }, + + created() { + if ((this as any).os.isSignedIn) { + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + } + }, + + mounted() { + this.capture(true); + + if ((this as any).os.isSignedIn) { + this.connection.on('_connected_', this.onStreamConnected); + } + + // Draw map + if (this.p.geo) { + const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true; + if (shouldShowMap) { + (this as any).os.getGoogleMaps().then(maps => { + const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]); + const map = new maps.Map(this.$refs.map, { + center: uluru, + zoom: 15 + }); + new maps.Marker({ + position: uluru, + map: map + }); + }); + } + } + }, + + beforeDestroy() { + this.decapture(true); + + if ((this as any).os.isSignedIn) { + this.connection.off('_connected_', this.onStreamConnected); + (this as any).os.stream.dispose(this.connectionId); + } + }, + + methods: { + capture(withHandler = false) { + if ((this as any).os.isSignedIn) { + this.connection.send({ + type: 'capture', + id: this.p.id + }); + if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated); + } + }, + decapture(withHandler = false) { + if ((this as any).os.isSignedIn) { + this.connection.send({ + type: 'decapture', + id: this.p.id + }); + if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated); + } + }, + onStreamConnected() { + this.capture(); + }, + onStreamNoteUpdated(data) { + const note = data.note; + if (note.id == this.note.id) { + this.$emit('update:note', note); + } else if (note.id == this.note.renoteId) { + this.note.renote = note; + } + }, + reply() { + (this as any).apis.post({ + reply: this.p + }); + }, + renote() { + (this as any).apis.post({ + renote: this.p + }); + }, + react() { + (this as any).os.new(MkReactionPicker, { + source: this.$refs.reactButton, + note: this.p, + compact: true + }); + }, + menu() { + (this as any).os.new(MkNoteMenu, { + source: this.$refs.menuButton, + note: this.p, + compact: true + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.note + font-size 12px + border-bottom solid 1px #eaeaea + + &:first-child + border-radius 8px 8px 0 0 + + > .renote + border-radius 8px 8px 0 0 + + &:last-of-type + border-bottom none + + @media (min-width 350px) + font-size 14px + + @media (min-width 500px) + font-size 16px + + > .renote + color #9dbb00 + background linear-gradient(to bottom, #edfde2 0%, #fff 100%) + + > p + margin 0 + padding 8px 16px + line-height 28px + + @media (min-width 500px) + padding 16px + + .avatar-anchor + display inline-block + + .avatar + vertical-align bottom + width 28px + height 28px + margin 0 8px 0 0 + border-radius 6px + + [data-fa] + margin-right 4px + + .name + font-weight bold + + > .mk-time + position absolute + top 8px + right 16px + font-size 0.9em + line-height 28px + + @media (min-width 500px) + top 16px + + & + article + padding-top 8px + + > .reply-to + background rgba(0, 0, 0, 0.0125) + + > .mk-note-preview + background transparent + + > article + padding 14px 16px 9px 16px + + &:after + content "" + display block + clear both + + > .avatar-anchor + display block + float left + margin 0 10px 8px 0 + position -webkit-sticky + position sticky + top 62px + + @media (min-width 500px) + margin-right 16px + + > .avatar + display block + width 48px + height 48px + margin 0 + border-radius 6px + vertical-align bottom + + @media (min-width 500px) + width 58px + height 58px + border-radius 8px + + > .main + float left + width calc(100% - 58px) + + @media (min-width 500px) + width calc(100% - 74px) + + > header + display flex + align-items center + white-space nowrap + + @media (min-width 500px) + margin-bottom 2px + + > .name + display block + margin 0 0.5em 0 0 + padding 0 + overflow hidden + color #627079 + font-size 1em + font-weight bold + text-decoration none + text-overflow ellipsis + + &:hover + text-decoration underline + + > .is-bot + margin 0 0.5em 0 0 + padding 1px 6px + font-size 12px + color #aaa + border solid 1px #ddd + border-radius 3px + + > .username + margin 0 0.5em 0 0 + color #ccc + + > .info + margin-left auto + font-size 0.9em + + > .mobile + margin-right 6px + color #c0c0c0 + + > .created-at + color #c0c0c0 + + > .body + + > .text + display block + margin 0 + padding 0 + overflow-wrap break-word + font-size 1.1em + color #717171 + + >>> .quote + margin 8px + padding 6px 12px + color #aaa + border-left solid 3px #eee + + > .reply + margin-right 8px + color #717171 + + > .rp + margin-left 4px + font-style oblique + color #a0bf46 + + [data-is-me]:after + content "you" + padding 0 4px + margin-left 4px + font-size 80% + color $theme-color-foreground + background $theme-color + border-radius 4px + + .mk-url-preview + margin-top 8px + + > .channel + margin 0 + + > .tags + margin 4px 0 0 0 + + > * + display inline-block + margin 0 8px 0 0 + padding 2px 8px 2px 16px + font-size 90% + color #8d969e + background #edf0f3 + border-radius 4px + + &:before + content "" + display block + position absolute + top 0 + bottom 0 + left 4px + width 8px + height 8px + margin auto 0 + background #fff + border-radius 100% + + > .media + > img + display block + max-width 100% + + > .location + margin 4px 0 + font-size 12px + color #ccc + + > .map + width 100% + height 200px + + &:empty + display none + + > .app + font-size 12px + color #ccc + + > .mk-poll + font-size 80% + + > .renote + margin 8px 0 + + > .mk-note-preview + padding 16px + border dashed 1px #c0dac6 + border-radius 8px + + > footer + > button + margin 0 + padding 8px + background transparent + border none + box-shadow none + font-size 1em + color #ddd + cursor pointer + + &:not(:last-child) + margin-right 28px + + &:hover + color #666 + + > .count + display inline + margin 0 0 0 8px + color #999 + + &.reacted + color $theme-color + + &.menu + @media (max-width 350px) + display none + +</style> + +<style lang="stylus" module> +.text + code + padding 4px 8px + margin 0 0.5em + font-size 80% + color #525252 + background #f8f8f8 + border-radius 2px + + pre > code + padding 16px + margin 0 +</style> diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue new file mode 100644 index 0000000000..573026d53e --- /dev/null +++ b/src/client/app/mobile/views/components/notes.vue @@ -0,0 +1,111 @@ +<template> +<div class="mk-notes"> + <slot name="head"></slot> + <slot></slot> + <template v-for="(note, i) in _notes"> + <mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> + <p class="date" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> + <span>%fa:angle-up%{{ note._datetext }}</span> + <span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span> + </p> + </template> + <footer> + <slot name="tail"></slot> + </footer> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + notes: { + type: Array, + default: () => [] + } + }, + computed: { + _notes(): any[] { + return (this.notes as any).map(note => { + const date = new Date(note.createdAt).getDate(); + const month = new Date(note.createdAt).getMonth() + 1; + note._date = date; + note._datetext = `${month}月 ${date}日`; + return note; + }); + } + }, + methods: { + onNoteUpdated(i, note) { + Vue.set((this as any).notes, i, note); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-notes + background #fff + border-radius 8px + box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + + > .init + padding 64px 0 + text-align center + color #999 + + > [data-fa] + margin-right 4px + + > .empty + margin 0 auto + padding 32px + max-width 400px + text-align center + color #999 + + > [data-fa] + display block + margin-bottom 16px + font-size 3em + color #ccc + + > .date + display block + margin 0 + line-height 32px + text-align center + font-size 0.9em + color #aaa + background #fdfdfd + border-bottom solid 1px #eaeaea + + span + margin 0 16px + + [data-fa] + margin-right 8px + + > footer + text-align center + border-top solid 1px #eaeaea + border-bottom-left-radius 4px + border-bottom-right-radius 4px + + &:empty + display none + + > button + margin 0 + padding 16px + width 100% + color $theme-color + border-radius 0 0 8px 8px + + &:disabled + opacity 0.7 + +</style> diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue new file mode 100644 index 0000000000..d39b2fbf9f --- /dev/null +++ b/src/client/app/mobile/views/components/notification-preview.vue @@ -0,0 +1,128 @@ +<template> +<div class="mk-notification-preview" :class="notification.type"> + <template v-if="notification.type == 'reaction'"> + <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + <div class="text"> + <p><mk-reaction-icon :reaction="notification.reaction"/>{{ notification.user | userName }}</p> + <p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%</p> + </div> + </template> + + <template v-if="notification.type == 'renote'"> + <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + <div class="text"> + <p>%fa:retweet%{{ notification.note.user | userName }}</p> + <p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%</p> + </div> + </template> + + <template v-if="notification.type == 'quote'"> + <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + <div class="text"> + <p>%fa:quote-left%{{ notification.note.user | userName }}</p> + <p class="note-preview">{{ getNoteSummary(notification.note) }}</p> + </div> + </template> + + <template v-if="notification.type == 'follow'"> + <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + <div class="text"> + <p>%fa:user-plus%{{ notification.user | userName }}</p> + </div> + </template> + + <template v-if="notification.type == 'reply'"> + <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + <div class="text"> + <p>%fa:reply%{{ notification.note.user | userName }}</p> + <p class="note-preview">{{ getNoteSummary(notification.note) }}</p> + </div> + </template> + + <template v-if="notification.type == 'mention'"> + <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + <div class="text"> + <p>%fa:at%{{ notification.note.user | userName }}</p> + <p class="note-preview">{{ getNoteSummary(notification.note) }}</p> + </div> + </template> + + <template v-if="notification.type == 'poll_vote'"> + <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + <div class="text"> + <p>%fa:chart-pie%{{ notification.user | userName }}</p> + <p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%</p> + </div> + </template> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import getNoteSummary from '../../../../../renderers/get-note-summary'; + +export default Vue.extend({ + props: ['notification'], + data() { + return { + getNoteSummary + }; + } +}); +</script> + +<style lang="stylus" scoped> +.mk-notification-preview + margin 0 + padding 8px + color #fff + overflow-wrap break-word + + &:after + content "" + display block + clear both + + img + display block + float left + min-width 36px + min-height 36px + max-width 36px + max-height 36px + border-radius 6px + + .text + float right + width calc(100% - 36px) + padding-left 8px + + p + margin 0 + + i, mk-reaction-icon + margin-right 4px + + .note-ref + + [data-fa] + font-size 1em + font-weight normal + font-style normal + display inline-block + margin-right 3px + + &.renote, &.quote + .text p i + color #77B255 + + &.follow + .text p i + color #53c7ce + + &.reply, &.mention + .text p i + color #fff + +</style> + diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue new file mode 100644 index 0000000000..4f7c8968b2 --- /dev/null +++ b/src/client/app/mobile/views/components/notification.vue @@ -0,0 +1,158 @@ +<template> +<div class="mk-notification"> + <div class="notification reaction" v-if="notification.type == 'reaction'"> + <mk-time :time="notification.createdAt"/> + <router-link class="avatar-anchor" :to="notification.user | userPage"> + <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + </router-link> + <div class="text"> + <p> + <mk-reaction-icon :reaction="notification.reaction"/> + <router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link> + </p> + <router-link class="note-ref" :to="notification.note | notePage"> + %fa:quote-left%{{ getNoteSummary(notification.note) }} + %fa:quote-right% + </router-link> + </div> + </div> + + <div class="notification renote" v-if="notification.type == 'renote'"> + <mk-time :time="notification.createdAt"/> + <router-link class="avatar-anchor" :to="notification.user | userPage"> + <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + </router-link> + <div class="text"> + <p> + %fa:retweet% + <router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link> + </p> + <router-link class="note-ref" :to="notification.note | notePage"> + %fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right% + </router-link> + </div> + </div> + + <template v-if="notification.type == 'quote'"> + <mk-note :note="notification.note"/> + </template> + + <div class="notification follow" v-if="notification.type == 'follow'"> + <mk-time :time="notification.createdAt"/> + <router-link class="avatar-anchor" :to="notification.user | userPage"> + <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + </router-link> + <div class="text"> + <p> + %fa:user-plus% + <router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link> + </p> + </div> + </div> + + <template v-if="notification.type == 'reply'"> + <mk-note :note="notification.note"/> + </template> + + <template v-if="notification.type == 'mention'"> + <mk-note :note="notification.note"/> + </template> + + <div class="notification poll_vote" v-if="notification.type == 'poll_vote'"> + <mk-time :time="notification.createdAt"/> + <router-link class="avatar-anchor" :to="notification.user | userPage"> + <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + </router-link> + <div class="text"> + <p> + %fa:chart-pie% + <router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link> + </p> + <router-link class="note-ref" :to="notification.note | notePage"> + %fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right% + </router-link> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import getNoteSummary from '../../../../../renderers/get-note-summary'; + +export default Vue.extend({ + props: ['notification'], + data() { + return { + getNoteSummary + }; + } +}); +</script> + +<style lang="stylus" scoped> +.mk-notification + + > .notification + padding 16px + overflow-wrap break-word + + &:after + content "" + display block + clear both + + > .mk-time + display inline + position absolute + top 16px + right 12px + vertical-align top + color rgba(0, 0, 0, 0.6) + font-size 0.9em + + > .avatar-anchor + display block + float left + + img + min-width 36px + min-height 36px + max-width 36px + max-height 36px + border-radius 6px + + > .text + float right + width calc(100% - 36px) + padding-left 8px + + p + margin 0 + + i, .mk-reaction-icon + margin-right 4px + + > .note-preview + color rgba(0, 0, 0, 0.7) + + > .note-ref + color rgba(0, 0, 0, 0.7) + + [data-fa] + font-size 1em + font-weight normal + font-style normal + display inline-block + margin-right 3px + + &.renote + .text p i + color #77B255 + + &.follow + .text p i + color #53c7ce + +</style> + diff --git a/src/client/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue new file mode 100644 index 0000000000..d68b990dfa --- /dev/null +++ b/src/client/app/mobile/views/components/notifications.vue @@ -0,0 +1,168 @@ +<template> +<div class="mk-notifications"> + <div class="notifications" v-if="notifications.length != 0"> + <template v-for="(notification, i) in _notifications"> + <mk-notification :notification="notification" :key="notification.id"/> + <p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date"> + <span>%fa:angle-up%{{ notification._datetext }}</span> + <span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span> + </p> + </template> + </div> + <button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications"> + <template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template> + {{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:mobile.tags.mk-notifications.more%' }} + </button> + <p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:mobile.tags.mk-notifications.empty%</p> + <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + data() { + return { + fetching: true, + fetchingMoreNotifications: false, + notifications: [], + moreNotifications: false, + connection: null, + connectionId: null + }; + }, + computed: { + _notifications(): any[] { + return (this.notifications as any).map(notification => { + const date = new Date(notification.createdAt).getDate(); + const month = new Date(notification.createdAt).getMonth() + 1; + notification._date = date; + notification._datetext = `${month}月 ${date}日`; + return notification; + }); + } + }, + mounted() { + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + + this.connection.on('notification', this.onNotification); + + const max = 10; + + (this as any).api('i/notifications', { + limit: max + 1 + }).then(notifications => { + if (notifications.length == max + 1) { + this.moreNotifications = true; + notifications.pop(); + } + + this.notifications = notifications; + this.fetching = false; + this.$emit('fetched'); + }); + }, + beforeDestroy() { + this.connection.off('notification', this.onNotification); + (this as any).os.stream.dispose(this.connectionId); + }, + methods: { + fetchMoreNotifications() { + this.fetchingMoreNotifications = true; + + const max = 30; + + (this as any).api('i/notifications', { + limit: max + 1, + untilId: this.notifications[this.notifications.length - 1].id + }).then(notifications => { + if (notifications.length == max + 1) { + this.moreNotifications = true; + notifications.pop(); + } else { + this.moreNotifications = false; + } + this.notifications = this.notifications.concat(notifications); + this.fetchingMoreNotifications = false; + }); + }, + onNotification(notification) { + // TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない + this.connection.send({ + type: 'read_notification', + id: notification.id + }); + + this.notifications.unshift(notification); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-notifications + margin 8px auto + padding 0 + max-width 500px + width calc(100% - 16px) + background #fff + border-radius 8px + box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + + @media (min-width 500px) + margin 16px auto + width calc(100% - 32px) + + > .notifications + + > .mk-notification + margin 0 auto + max-width 500px + border-bottom solid 1px rgba(0, 0, 0, 0.05) + + &:last-child + border-bottom none + + > .date + display block + margin 0 + line-height 32px + text-align center + font-size 0.8em + color #aaa + background #fdfdfd + border-bottom solid 1px rgba(0, 0, 0, 0.05) + + span + margin 0 16px + + i + margin-right 8px + + > .more + display block + width 100% + padding 16px + color #555 + border-top solid 1px rgba(0, 0, 0, 0.05) + + > [data-fa] + margin-right 4px + + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > .fetching + margin 0 + padding 16px + text-align center + color #aaa + + > [data-fa] + margin-right 4px + +</style> diff --git a/src/client/app/mobile/views/components/notify.vue b/src/client/app/mobile/views/components/notify.vue new file mode 100644 index 0000000000..6d4a481dbe --- /dev/null +++ b/src/client/app/mobile/views/components/notify.vue @@ -0,0 +1,49 @@ +<template> +<div class="mk-notify"> + <mk-notification-preview :notification="notification"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as anime from 'animejs'; + +export default Vue.extend({ + props: ['notification'], + mounted() { + this.$nextTick(() => { + anime({ + targets: this.$el, + bottom: '0px', + duration: 500, + easing: 'easeOutQuad' + }); + + setTimeout(() => { + anime({ + targets: this.$el, + bottom: '-64px', + duration: 500, + easing: 'easeOutQuad', + complete: () => this.$destroy() + }); + }, 6000); + }); + } +}); +</script> + +<style lang="stylus" scoped> +.mk-notify + position fixed + z-index 1024 + bottom -64px + left 0 + width 100% + height 64px + pointer-events none + -webkit-backdrop-filter blur(2px) + backdrop-filter blur(2px) + background-color rgba(#000, 0.5) + +</style> diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue new file mode 100644 index 0000000000..70be6db7b2 --- /dev/null +++ b/src/client/app/mobile/views/components/post-form.vue @@ -0,0 +1,275 @@ +<template> +<div class="mk-post-form"> + <header> + <button class="cancel" @click="cancel">%fa:times%</button> + <div> + <span class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</span> + <span class="geo" v-if="geo">%fa:map-marker-alt%</span> + <button class="submit" :disabled="posting" @click="post">{{ reply ? '返信' : '%i18n:mobile.tags.mk-post-form.submit%' }}</button> + </div> + </header> + <div class="form"> + <mk-note-preview v-if="reply" :note="reply"/> + <textarea v-model="text" ref="text" :disabled="posting" :placeholder="reply ? '%i18n:mobile.tags.mk-post-form.reply-placeholder%' : '%i18n:mobile.tags.mk-post-form.note-placeholder%'"></textarea> + <div class="attaches" v-show="files.length != 0"> + <x-draggable class="files" :list="files" :options="{ animation: 150 }"> + <div class="file" v-for="file in files" :key="file.id"> + <div class="img" :style="`background-image: url(${file.url}?thumbnail&size=128)`" @click="detachMedia(file)"></div> + </div> + </x-draggable> + </div> + <mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false"/> + <mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/> + <button class="upload" @click="chooseFile">%fa:upload%</button> + <button class="drive" @click="chooseFileFromDrive">%fa:cloud%</button> + <button class="kao" @click="kao">%fa:R smile%</button> + <button class="poll" @click="poll = true">%fa:chart-pie%</button> + <button class="geo" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button> + <input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as XDraggable from 'vuedraggable'; +import getKao from '../../../common/scripts/get-kao'; + +export default Vue.extend({ + components: { + XDraggable + }, + props: ['reply'], + data() { + return { + posting: false, + text: '', + uploadings: [], + files: [], + poll: false, + geo: null + }; + }, + mounted() { + this.$nextTick(() => { + this.focus(); + }); + }, + methods: { + focus() { + (this.$refs.text as any).focus(); + }, + chooseFile() { + (this.$refs.file as any).click(); + }, + chooseFileFromDrive() { + (this as any).apis.chooseDriveFile({ + multiple: true + }).then(files => { + files.forEach(this.attachMedia); + }); + }, + attachMedia(driveFile) { + this.files.push(driveFile); + this.$emit('change-attached-media', this.files); + }, + detachMedia(file) { + this.files = this.files.filter(x => x.id != file.id); + this.$emit('change-attached-media', this.files); + }, + onChangeFile() { + Array.from((this.$refs.file as any).files).forEach(this.upload); + }, + upload(file) { + (this.$refs.uploader as any).upload(file); + }, + onChangeUploadings(uploads) { + this.$emit('change-uploadings', uploads); + }, + setGeo() { + if (navigator.geolocation == null) { + alert('お使いの端末は位置情報に対応していません'); + return; + } + + navigator.geolocation.getCurrentPosition(pos => { + this.geo = pos.coords; + }, err => { + alert('エラー: ' + err.message); + }, { + enableHighAccuracy: true + }); + }, + removeGeo() { + this.geo = null; + }, + clear() { + this.text = ''; + this.files = []; + this.poll = false; + this.$emit('change-attached-media'); + }, + post() { + this.posting = true; + const viaMobile = (this as any).os.i.clientSettings.disableViaMobile !== true; + (this as any).api('notes/create', { + text: this.text == '' ? undefined : this.text, + mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined, + replyId: this.reply ? this.reply.id : undefined, + poll: this.poll ? (this.$refs.poll as any).get() : undefined, + geo: this.geo ? { + coordinates: [this.geo.longitude, this.geo.latitude], + altitude: this.geo.altitude, + accuracy: this.geo.accuracy, + altitudeAccuracy: this.geo.altitudeAccuracy, + heading: isNaN(this.geo.heading) ? null : this.geo.heading, + speed: this.geo.speed, + } : null, + viaMobile: viaMobile + }).then(data => { + this.$emit('note'); + this.$destroy(); + }).catch(err => { + this.posting = false; + }); + }, + cancel() { + this.$emit('cancel'); + this.$destroy(); + }, + kao() { + this.text += getKao(); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-post-form + max-width 500px + width calc(100% - 16px) + margin 8px auto + background #fff + border-radius 8px + box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + + @media (min-width 500px) + margin 16px auto + width calc(100% - 32px) + + > header + z-index 1 + height 50px + box-shadow 0 1px 0 0 rgba(0, 0, 0, 0.1) + + > .cancel + padding 0 + width 50px + line-height 50px + font-size 24px + color #555 + + > div + position absolute + top 0 + right 0 + color #657786 + + > .text-count + line-height 50px + + > .geo + margin 0 8px + line-height 50px + + > .submit + margin 8px + padding 0 16px + line-height 34px + vertical-align bottom + color $theme-color-foreground + background $theme-color + border-radius 4px + + &:disabled + opacity 0.7 + + > .form + max-width 500px + margin 0 auto + + > .mk-note-preview + padding 16px + + > .attaches + + > .files + display block + margin 0 + padding 4px + list-style none + + &:after + content "" + display block + clear both + + > .file + display block + float left + margin 0 + padding 0 + border solid 4px transparent + + > .img + width 64px + height 64px + background-size cover + background-position center center + + > .mk-uploader + margin 8px 0 0 0 + padding 8px + + > .file + display none + + > textarea + display block + padding 12px + margin 0 + width 100% + max-width 100% + min-width 100% + min-height 80px + font-size 16px + color #333 + border none + border-bottom solid 1px #ddd + border-radius 0 + + &:disabled + opacity 0.5 + + > .upload + > .drive + > .kao + > .poll + > .geo + display inline-block + padding 0 + margin 0 + width 48px + height 48px + font-size 20px + color #657786 + background transparent + outline none + border none + border-radius 0 + box-shadow none + +</style> + diff --git a/src/client/app/mobile/views/components/sub-note-content.vue b/src/client/app/mobile/views/components/sub-note-content.vue new file mode 100644 index 0000000000..22e6ebe3f1 --- /dev/null +++ b/src/client/app/mobile/views/components/sub-note-content.vue @@ -0,0 +1,43 @@ +<template> +<div class="mk-sub-note-content"> + <div class="body"> + <a class="reply" v-if="note.replyId">%fa:reply%</a> + <mk-note-html v-if="note.text" :text="note.text" :i="os.i"/> + <a class="rp" v-if="note.renoteId">RP: ...</a> + </div> + <details v-if="note.media.length > 0"> + <summary>({{ note.media.length }}個のメディア)</summary> + <mk-media-list :media-list="note.media"/> + </details> + <details v-if="note.poll"> + <summary>%i18n:mobile.tags.mk-sub-note-content.poll%</summary> + <mk-poll :note="note"/> + </details> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['note'] +}); +</script> + +<style lang="stylus" scoped> +.mk-sub-note-content + overflow-wrap break-word + + > .body + > .reply + margin-right 6px + color #717171 + + > .rp + margin-left 4px + font-style oblique + color #a0bf46 + + mk-poll + font-size 80% + +</style> diff --git a/src/client/app/mobile/views/components/timeline.vue b/src/client/app/mobile/views/components/timeline.vue new file mode 100644 index 0000000000..4d6abcd167 --- /dev/null +++ b/src/client/app/mobile/views/components/timeline.vue @@ -0,0 +1,109 @@ +<template> +<div class="mk-timeline"> + <mk-friends-maker v-if="alone"/> + <mk-notes :notes="notes"> + <div class="init" v-if="fetching"> + %fa:spinner .pulse%%i18n:common.loading% + </div> + <div class="empty" v-if="!fetching && notes.length == 0"> + %fa:R comments% + %i18n:mobile.tags.mk-home-timeline.empty-timeline% + </div> + <button v-if="!fetching && existMore" @click="more" :disabled="moreFetching" slot="tail"> + <span v-if="!moreFetching">%i18n:mobile.tags.mk-timeline.load-more%</span> + <span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span> + </button> + </mk-notes> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +const limit = 10; + +export default Vue.extend({ + props: { + date: { + type: Date, + required: false + } + }, + data() { + return { + fetching: true, + moreFetching: false, + notes: [], + existMore: false, + connection: null, + connectionId: null + }; + }, + computed: { + alone(): boolean { + return (this as any).os.i.followingCount == 0; + } + }, + mounted() { + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + + this.connection.on('note', this.onNote); + this.connection.on('follow', this.onChangeFollowing); + this.connection.on('unfollow', this.onChangeFollowing); + + this.fetch(); + }, + beforeDestroy() { + this.connection.off('note', this.onNote); + this.connection.off('follow', this.onChangeFollowing); + this.connection.off('unfollow', this.onChangeFollowing); + (this as any).os.stream.dispose(this.connectionId); + }, + methods: { + fetch(cb?) { + this.fetching = true; + (this as any).api('notes/timeline', { + limit: limit + 1, + untilDate: this.date ? (this.date as any).getTime() : undefined + }).then(notes => { + if (notes.length == limit + 1) { + notes.pop(); + this.existMore = true; + } + this.notes = notes; + this.fetching = false; + this.$emit('loaded'); + if (cb) cb(); + }); + }, + more() { + this.moreFetching = true; + (this as any).api('notes/timeline', { + limit: limit + 1, + untilId: this.notes[this.notes.length - 1].id + }).then(notes => { + if (notes.length == limit + 1) { + notes.pop(); + this.existMore = true; + } else { + this.existMore = false; + } + this.notes = this.notes.concat(notes); + this.moreFetching = false; + }); + }, + onNote(note) { + this.notes.unshift(note); + }, + onChangeFollowing() { + this.fetch(); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-friends-maker + margin-bottom 8px +</style> diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue new file mode 100644 index 0000000000..f1b24bf2da --- /dev/null +++ b/src/client/app/mobile/views/components/ui.header.vue @@ -0,0 +1,242 @@ +<template> +<div class="header"> + <mk-special-message/> + <div class="main" ref="main"> + <div class="backdrop"></div> + <p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ os.i | userName }}</b>さん</p> + <div class="content" ref="mainContainer"> + <button class="nav" @click="$parent.isDrawerOpening = true">%fa:bars%</button> + <template v-if="hasUnreadNotifications || hasUnreadMessagingMessages || hasGameInvitations">%fa:circle%</template> + <h1> + <slot>Misskey</slot> + </h1> + <slot name="func"></slot> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as anime from 'animejs'; + +export default Vue.extend({ + props: ['func'], + data() { + return { + hasUnreadNotifications: false, + hasUnreadMessagingMessages: false, + hasGameInvitations: false, + connection: null, + connectionId: null + }; + }, + mounted() { + if ((this as any).os.isSignedIn) { + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + + this.connection.on('read_all_notifications', this.onReadAllNotifications); + this.connection.on('unread_notification', this.onUnreadNotification); + this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages); + this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage); + this.connection.on('othello_invited', this.onOthelloInvited); + this.connection.on('othello_no_invites', this.onOthelloNoInvites); + + // Fetch count of unread notifications + (this as any).api('notifications/get_unread_count').then(res => { + if (res.count > 0) { + this.hasUnreadNotifications = true; + } + }); + + // Fetch count of unread messaging messages + (this as any).api('messaging/unread').then(res => { + if (res.count > 0) { + this.hasUnreadMessagingMessages = true; + } + }); + + const ago = (new Date().getTime() - new Date((this as any).os.i.lastUsedAt).getTime()) / 1000 + const isHisasiburi = ago >= 3600; + (this as any).os.i.lastUsedAt = new Date(); + if (isHisasiburi) { + (this.$refs.welcomeback as any).style.display = 'block'; + (this.$refs.main as any).style.overflow = 'hidden'; + + anime({ + targets: this.$refs.welcomeback, + top: '0', + opacity: 1, + delay: 1000, + duration: 500, + easing: 'easeOutQuad' + }); + + anime({ + targets: this.$refs.mainContainer, + opacity: 0, + delay: 1000, + duration: 500, + easing: 'easeOutQuad' + }); + + setTimeout(() => { + anime({ + targets: this.$refs.welcomeback, + top: '-48px', + opacity: 0, + duration: 500, + complete: () => { + (this.$refs.welcomeback as any).style.display = 'none'; + (this.$refs.main as any).style.overflow = 'initial'; + }, + easing: 'easeInQuad' + }); + + anime({ + targets: this.$refs.mainContainer, + opacity: 1, + duration: 500, + easing: 'easeInQuad' + }); + }, 2500); + } + } + }, + beforeDestroy() { + if ((this as any).os.isSignedIn) { + this.connection.off('read_all_notifications', this.onReadAllNotifications); + this.connection.off('unread_notification', this.onUnreadNotification); + this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages); + this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage); + this.connection.off('othello_invited', this.onOthelloInvited); + this.connection.off('othello_no_invites', this.onOthelloNoInvites); + (this as any).os.stream.dispose(this.connectionId); + } + }, + methods: { + onReadAllNotifications() { + this.hasUnreadNotifications = false; + }, + onUnreadNotification() { + this.hasUnreadNotifications = true; + }, + onReadAllMessagingMessages() { + this.hasUnreadMessagingMessages = false; + }, + onUnreadMessagingMessage() { + this.hasUnreadMessagingMessages = true; + }, + onOthelloInvited() { + this.hasGameInvitations = true; + }, + onOthelloNoInvites() { + this.hasGameInvitations = false; + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.header + $height = 48px + + position fixed + top 0 + z-index 1024 + width 100% + box-shadow 0 1px 0 rgba(#000, 0.075) + + > .main + color rgba(#fff, 0.9) + + > .backdrop + position absolute + top 0 + z-index 1000 + width 100% + height $height + -webkit-backdrop-filter blur(12px) + backdrop-filter blur(12px) + //background-color rgba(#1b2023, 0.75) + background-color #1b2023 + + > p + display none + position absolute + z-index 1002 + top $height + width 100% + line-height $height + margin 0 + text-align center + color #fff + opacity 0 + + > .content + z-index 1001 + + > h1 + display block + margin 0 auto + padding 0 + width 100% + max-width calc(100% - 112px) + text-align center + font-size 1.1em + font-weight normal + line-height $height + white-space nowrap + overflow hidden + text-overflow ellipsis + + [data-fa], [data-icon] + margin-right 4px + + > img + display inline-block + vertical-align bottom + width ($height - 16px) + height ($height - 16px) + margin 8px + border-radius 6px + + > .nav + display block + position absolute + top 0 + left 0 + padding 0 + width $height + font-size 1.4em + line-height $height + border-right solid 1px rgba(#000, 0.1) + + > [data-fa] + transition all 0.2s ease + + > [data-fa].circle + position absolute + top 8px + left 8px + pointer-events none + font-size 10px + color $theme-color + + > button:last-child + display block + position absolute + top 0 + right 0 + padding 0 + width $height + text-align center + font-size 1.4em + color inherit + line-height $height + border-left solid 1px rgba(#000, 0.1) + +</style> diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue new file mode 100644 index 0000000000..f96e285407 --- /dev/null +++ b/src/client/app/mobile/views/components/ui.nav.vue @@ -0,0 +1,242 @@ +<template> +<div class="nav"> + <transition name="back"> + <div class="backdrop" + v-if="isOpen" + @click="$parent.isDrawerOpening = false" + @touchstart="$parent.isDrawerOpening = false" + ></div> + </transition> + <transition name="nav"> + <div class="body" v-if="isOpen"> + <router-link class="me" v-if="os.isSignedIn" :to="`/@${os.i.username}`"> + <img class="avatar" :src="`${os.i.avatarUrl}?thumbnail&size=128`" alt="avatar"/> + <p class="name">{{ os.i | userName }}</p> + </router-link> + <div class="links"> + <ul> + <li><router-link to="/">%fa:home%%i18n:mobile.tags.mk-ui-nav.home%%fa:angle-right%</router-link></li> + <li><router-link to="/i/notifications">%fa:R bell%%i18n:mobile.tags.mk-ui-nav.notifications%<template v-if="hasUnreadNotifications">%fa:circle%</template>%fa:angle-right%</router-link></li> + <li><router-link to="/i/messaging">%fa:R comments%%i18n:mobile.tags.mk-ui-nav.messaging%<template v-if="hasUnreadMessagingMessages">%fa:circle%</template>%fa:angle-right%</router-link></li> + <li><router-link to="/othello">%fa:gamepad%ゲーム<template v-if="hasGameInvitations">%fa:circle%</template>%fa:angle-right%</router-link></li> + </ul> + <ul> + <li><router-link to="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-ui-nav.drive%%fa:angle-right%</router-link></li> + </ul> + <ul> + <li><a @click="search">%fa:search%%i18n:mobile.tags.mk-ui-nav.search%%fa:angle-right%</a></li> + </ul> + <ul> + <li><router-link to="/i/settings">%fa:cog%%i18n:mobile.tags.mk-ui-nav.settings%%fa:angle-right%</router-link></li> + </ul> + </div> + <a :href="aboutUrl"><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a> + </div> + </transition> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { docsUrl, lang } from '../../../config'; + +export default Vue.extend({ + props: ['isOpen'], + data() { + return { + hasUnreadNotifications: false, + hasUnreadMessagingMessages: false, + hasGameInvitations: false, + connection: null, + connectionId: null, + aboutUrl: `${docsUrl}/${lang}/about` + }; + }, + mounted() { + if ((this as any).os.isSignedIn) { + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + + this.connection.on('read_all_notifications', this.onReadAllNotifications); + this.connection.on('unread_notification', this.onUnreadNotification); + this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages); + this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage); + this.connection.on('othello_invited', this.onOthelloInvited); + this.connection.on('othello_no_invites', this.onOthelloNoInvites); + + // Fetch count of unread notifications + (this as any).api('notifications/get_unread_count').then(res => { + if (res.count > 0) { + this.hasUnreadNotifications = true; + } + }); + + // Fetch count of unread messaging messages + (this as any).api('messaging/unread').then(res => { + if (res.count > 0) { + this.hasUnreadMessagingMessages = true; + } + }); + } + }, + beforeDestroy() { + if ((this as any).os.isSignedIn) { + this.connection.off('read_all_notifications', this.onReadAllNotifications); + this.connection.off('unread_notification', this.onUnreadNotification); + this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages); + this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage); + this.connection.off('othello_invited', this.onOthelloInvited); + this.connection.off('othello_no_invites', this.onOthelloNoInvites); + (this as any).os.stream.dispose(this.connectionId); + } + }, + methods: { + search() { + const query = window.prompt('%i18n:mobile.tags.mk-ui-nav.search%'); + if (query == null || query == '') return; + this.$router.push('/search?q=' + encodeURIComponent(query)); + }, + onReadAllNotifications() { + this.hasUnreadNotifications = false; + }, + onUnreadNotification() { + this.hasUnreadNotifications = true; + }, + onReadAllMessagingMessages() { + this.hasUnreadMessagingMessages = false; + }, + onUnreadMessagingMessage() { + this.hasUnreadMessagingMessages = true; + }, + onOthelloInvited() { + this.hasGameInvitations = true; + }, + onOthelloNoInvites() { + this.hasGameInvitations = false; + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.nav + .backdrop + position fixed + top 0 + left 0 + z-index 1025 + width 100% + height 100% + background rgba(0, 0, 0, 0.2) + + .body + position fixed + top 0 + left 0 + z-index 1026 + width 240px + height 100% + overflow auto + -webkit-overflow-scrolling touch + color #777 + background #fff + + .me + display block + margin 0 + padding 16px + + .avatar + display inline + max-width 64px + border-radius 32px + vertical-align middle + + .name + display block + margin 0 16px + position absolute + top 0 + left 80px + padding 0 + width calc(100% - 112px) + color #777 + line-height 96px + overflow hidden + text-overflow ellipsis + white-space nowrap + + ul + display block + margin 16px 0 + padding 0 + list-style none + + &:first-child + margin-top 0 + + li + display block + font-size 1em + line-height 1em + + a + display block + padding 0 20px + line-height 3rem + line-height calc(1rem + 30px) + color #777 + text-decoration none + + > [data-fa]:first-child + margin-right 0.5em + + > [data-fa].circle + margin-left 6px + font-size 10px + color $theme-color + + > [data-fa]:last-child + position absolute + top 0 + right 0 + padding 0 20px + font-size 1.2em + line-height calc(1rem + 30px) + color #ccc + + .about + margin 0 + padding 1em 0 + text-align center + font-size 0.8em + opacity 0.5 + + a + color #777 + +.nav-enter-active, +.nav-leave-active { + opacity: 1; + transform: translateX(0); + transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); +} +.nav-enter, +.nav-leave-active { + opacity: 0; + transform: translateX(-240px); +} + +.back-enter-active, +.back-leave-active { + opacity: 1; + transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); +} +.back-enter, +.back-leave-active { + opacity: 0; +} + +</style> diff --git a/src/client/app/mobile/views/components/ui.vue b/src/client/app/mobile/views/components/ui.vue new file mode 100644 index 0000000000..325ce9d40e --- /dev/null +++ b/src/client/app/mobile/views/components/ui.vue @@ -0,0 +1,75 @@ +<template> +<div class="mk-ui"> + <x-header> + <template slot="func"><slot name="func"></slot></template> + <slot name="header"></slot> + </x-header> + <x-nav :is-open="isDrawerOpening"/> + <div class="content"> + <slot></slot> + </div> + <mk-stream-indicator v-if="os.isSignedIn"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import MkNotify from './notify.vue'; +import XHeader from './ui.header.vue'; +import XNav from './ui.nav.vue'; + +export default Vue.extend({ + components: { + XHeader, + XNav + }, + props: ['title'], + data() { + return { + isDrawerOpening: false, + connection: null, + connectionId: null + }; + }, + mounted() { + if ((this as any).os.isSignedIn) { + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + + this.connection.on('notification', this.onNotification); + } + }, + beforeDestroy() { + if ((this as any).os.isSignedIn) { + this.connection.off('notification', this.onNotification); + (this as any).os.stream.dispose(this.connectionId); + } + }, + methods: { + onNotification(notification) { + // TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない + this.connection.send({ + type: 'read_notification', + id: notification.id + }); + + (this as any).os.new(MkNotify, { + notification + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-ui + display flex + flex 1 + flex-direction column + padding-top 48px + + > .content + display flex + flex 1 + flex-direction column +</style> diff --git a/src/client/app/mobile/views/components/user-card.vue b/src/client/app/mobile/views/components/user-card.vue new file mode 100644 index 0000000000..432560a54a --- /dev/null +++ b/src/client/app/mobile/views/components/user-card.vue @@ -0,0 +1,63 @@ +<template> +<div class="mk-user-card"> + <header :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''"> + <a :href="user | userPage"> + <img :src="`${user.avatarUrl}?thumbnail&size=200`" alt="avatar"/> + </a> + </header> + <a class="name" :href="user | userPage" target="_blank">{{ user | userName }}</a> + <p class="username">@{{ user | acct }}</p> + <mk-follow-button :user="user"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['user'] +}); +</script> + +<style lang="stylus" scoped> +.mk-user-card + display inline-block + width 200px + text-align center + border-radius 8px + background #fff + + > header + display block + height 80px + background-color #ddd + background-size cover + background-position center + border-radius 8px 8px 0 0 + + > a + > img + position absolute + top 20px + left calc(50% - 40px) + width 80px + height 80px + border solid 2px #fff + border-radius 8px + + > .name + display block + margin 24px 0 0 0 + font-size 16px + color #555 + + > .username + margin 0 + font-size 15px + color #ccc + + > .mk-follow-button + display inline-block + margin 8px 0 16px 0 + +</style> diff --git a/src/client/app/mobile/views/components/user-preview.vue b/src/client/app/mobile/views/components/user-preview.vue new file mode 100644 index 0000000000..23a83b5e3a --- /dev/null +++ b/src/client/app/mobile/views/components/user-preview.vue @@ -0,0 +1,104 @@ +<template> +<div class="mk-user-preview"> + <router-link class="avatar-anchor" :to="user | userPage"> + <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + </router-link> + <div class="main"> + <header> + <router-link class="name" :to="user | userPage">{{ user | userName }}</router-link> + <span class="username">@{{ user | acct }}</span> + </header> + <div class="body"> + <div class="description">{{ user.description }}</div> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['user'] +}); +</script> + +<style lang="stylus" scoped> +.mk-user-preview + margin 0 + padding 16px + font-size 12px + + @media (min-width 350px) + font-size 14px + + @media (min-width 500px) + font-size 16px + + &:after + content "" + display block + clear both + + > .avatar-anchor + display block + float left + margin 0 10px 0 0 + + @media (min-width 500px) + margin-right 16px + + > .avatar + display block + width 48px + height 48px + margin 0 + border-radius 6px + vertical-align bottom + + @media (min-width 500px) + width 58px + height 58px + border-radius 8px + + > .main + float left + width calc(100% - 58px) + + @media (min-width 500px) + width calc(100% - 74px) + + > header + @media (min-width 500px) + margin-bottom 2px + + > .name + display inline + margin 0 + padding 0 + color #777 + font-size 1em + font-weight 700 + text-align left + text-decoration none + + &:hover + text-decoration underline + + > .username + text-align left + margin 0 0 0 8px + color #ccc + + > .body + + > .description + cursor default + display block + margin 0 + padding 0 + overflow-wrap break-word + font-size 1.1em + color #717171 + +</style> diff --git a/src/client/app/mobile/views/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue new file mode 100644 index 0000000000..7a04441f76 --- /dev/null +++ b/src/client/app/mobile/views/components/user-timeline.vue @@ -0,0 +1,76 @@ +<template> +<div class="mk-user-timeline"> + <mk-notes :notes="notes"> + <div class="init" v-if="fetching"> + %fa:spinner .pulse%%i18n:common.loading% + </div> + <div class="empty" v-if="!fetching && notes.length == 0"> + %fa:R comments% + {{ withMedia ? '%i18n:mobile.tags.mk-user-timeline.no-notes-with-media%' : '%i18n:mobile.tags.mk-user-timeline.no-notes%' }} + </div> + <button v-if="!fetching && existMore" @click="more" :disabled="moreFetching" slot="tail"> + <span v-if="!moreFetching">%i18n:mobile.tags.mk-user-timeline.load-more%</span> + <span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span> + </button> + </mk-notes> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +const limit = 10; + +export default Vue.extend({ + props: ['user', 'withMedia'], + data() { + return { + fetching: true, + notes: [], + existMore: false, + moreFetching: false + }; + }, + mounted() { + (this as any).api('users/notes', { + userId: this.user.id, + withMedia: this.withMedia, + limit: limit + 1 + }).then(notes => { + if (notes.length == limit + 1) { + notes.pop(); + this.existMore = true; + } + this.notes = notes; + this.fetching = false; + this.$emit('loaded'); + }); + }, + methods: { + more() { + this.moreFetching = true; + (this as any).api('users/notes', { + userId: this.user.id, + withMedia: this.withMedia, + limit: limit + 1, + untilId: this.notes[this.notes.length - 1].id + }).then(notes => { + if (notes.length == limit + 1) { + notes.pop(); + this.existMore = true; + } else { + this.existMore = false; + } + this.notes = this.notes.concat(notes); + this.moreFetching = false; + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-user-timeline + max-width 600px + margin 0 auto +</style> diff --git a/src/client/app/mobile/views/components/users-list.vue b/src/client/app/mobile/views/components/users-list.vue new file mode 100644 index 0000000000..b11e4549d6 --- /dev/null +++ b/src/client/app/mobile/views/components/users-list.vue @@ -0,0 +1,133 @@ +<template> +<div class="mk-users-list"> + <nav> + <span :data-is-active="mode == 'all'" @click="mode = 'all'">%i18n:mobile.tags.mk-users-list.all%<span>{{ count }}</span></span> + <span v-if="os.isSignedIn && youKnowCount" :data-is-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:mobile.tags.mk-users-list.known%<span>{{ youKnowCount }}</span></span> + </nav> + <div class="users" v-if="!fetching && users.length != 0"> + <mk-user-preview v-for="u in users" :user="u" :key="u.id"/> + </div> + <button class="more" v-if="!fetching && next != null" @click="more" :disabled="moreFetching"> + <span v-if="!moreFetching">%i18n:mobile.tags.mk-users-list.load-more%</span> + <span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span> + </button> + <p class="no" v-if="!fetching && users.length == 0"> + <slot></slot> + </p> + <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['fetch', 'count', 'youKnowCount'], + data() { + return { + limit: 30, + mode: 'all', + fetching: true, + moreFetching: false, + users: [], + next: null + }; + }, + watch: { + mode() { + this._fetch(); + } + }, + mounted() { + this._fetch(() => { + this.$emit('loaded'); + }); + }, + methods: { + _fetch(cb?) { + this.fetching = true; + this.fetch(this.mode == 'iknow', this.limit, null, obj => { + this.users = obj.users; + this.next = obj.next; + this.fetching = false; + if (cb) cb(); + }); + }, + more() { + this.moreFetching = true; + this.fetch(this.mode == 'iknow', this.limit, this.next, obj => { + this.moreFetching = false; + this.users = this.users.concat(obj.users); + this.next = obj.next; + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-users-list + + > nav + display flex + justify-content center + margin 0 auto + max-width 600px + border-bottom solid 1px rgba(0, 0, 0, 0.2) + + > span + display block + flex 1 1 + text-align center + line-height 52px + font-size 14px + color #657786 + border-bottom solid 2px transparent + + &[data-is-active] + font-weight bold + color $theme-color + border-color $theme-color + + > span + display inline-block + margin-left 4px + padding 2px 5px + font-size 12px + line-height 1 + color #fff + background rgba(0, 0, 0, 0.3) + border-radius 20px + + > .users + margin 8px auto + max-width 500px + width calc(100% - 16px) + background #fff + border-radius 8px + box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + + @media (min-width 500px) + margin 16px auto + width calc(100% - 32px) + + > * + border-bottom solid 1px rgba(0, 0, 0, 0.05) + + > .no + margin 0 + padding 16px + text-align center + color #aaa + + > .fetching + margin 0 + padding 16px + text-align center + color #aaa + + > [data-fa] + margin-right 4px + +</style> diff --git a/src/client/app/mobile/views/components/widget-container.vue b/src/client/app/mobile/views/components/widget-container.vue new file mode 100644 index 0000000000..7319c90849 --- /dev/null +++ b/src/client/app/mobile/views/components/widget-container.vue @@ -0,0 +1,68 @@ +<template> +<div class="mk-widget-container" :class="{ naked, hideHeader: !showHeader }"> + <header v-if="showHeader"> + <div class="title"><slot name="header"></slot></div> + <slot name="func"></slot> + </header> + <slot></slot> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: { + showHeader: { + type: Boolean, + default: true + }, + naked: { + type: Boolean, + default: false + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-widget-container + background #eee + border-radius 8px + box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + overflow hidden + + &.hideHeader + background #fff + + &.naked + background transparent !important + box-shadow none !important + + > header + > .title + margin 0 + padding 8px 10px + font-size 15px + font-weight normal + color #465258 + background #fff + border-radius 8px 8px 0 0 + + > [data-fa] + margin-right 6px + + &:empty + display none + + > button + position absolute + z-index 2 + top 0 + right 0 + padding 0 + width 42px + height 100% + font-size 15px + color #465258 + +</style> diff --git a/src/client/app/mobile/views/directives/index.ts b/src/client/app/mobile/views/directives/index.ts new file mode 100644 index 0000000000..324e07596d --- /dev/null +++ b/src/client/app/mobile/views/directives/index.ts @@ -0,0 +1,6 @@ +import Vue from 'vue'; + +import userPreview from './user-preview'; + +Vue.directive('userPreview', userPreview); +Vue.directive('user-preview', userPreview); diff --git a/src/client/app/mobile/views/directives/user-preview.ts b/src/client/app/mobile/views/directives/user-preview.ts new file mode 100644 index 0000000000..1a54abc20d --- /dev/null +++ b/src/client/app/mobile/views/directives/user-preview.ts @@ -0,0 +1,2 @@ +// nope +export default {}; diff --git a/src/client/app/mobile/views/pages/drive.vue b/src/client/app/mobile/views/pages/drive.vue new file mode 100644 index 0000000000..200379f222 --- /dev/null +++ b/src/client/app/mobile/views/pages/drive.vue @@ -0,0 +1,107 @@ +<template> +<mk-ui> + <span slot="header"> + <template v-if="folder">%fa:R folder-open%{{ folder.name }}</template> + <template v-if="file"><mk-file-type-icon data-icon :type="file.type"/>{{ file.name }}</template> + <template v-if="!folder && !file">%fa:cloud%%i18n:mobile.tags.mk-drive-page.drive%</template> + </span> + <template slot="func"><button @click="fn">%fa:ellipsis-h%</button></template> + <mk-drive + ref="browser" + :init-folder="initFolder" + :init-file="initFile" + :is-naked="true" + :top="48" + @begin-fetch="Progress.start()" + @fetched-mid="Progress.set(0.5)" + @fetched="Progress.done()" + @move-root="onMoveRoot" + @open-folder="onOpenFolder" + @open-file="onOpenFile" + /> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import Progress from '../../../common/scripts/loading'; + +export default Vue.extend({ + data() { + return { + Progress, + folder: null, + file: null, + initFolder: null, + initFile: null + }; + }, + created() { + this.initFolder = this.$route.params.folder; + this.initFile = this.$route.params.file; + + window.addEventListener('popstate', this.onPopState); + }, + mounted() { + document.title = 'Misskey Drive'; + document.documentElement.style.background = '#fff'; + }, + beforeDestroy() { + window.removeEventListener('popstate', this.onPopState); + }, + methods: { + onPopState() { + if (this.$route.params.folder) { + (this.$refs as any).browser.cd(this.$route.params.folder, true); + } else if (this.$route.params.file) { + (this.$refs as any).browser.cf(this.$route.params.file, true); + } else { + (this.$refs as any).browser.goRoot(true); + } + }, + fn() { + (this.$refs as any).browser.openContextMenu(); + }, + onMoveRoot(silent) { + const title = 'Misskey Drive'; + + if (!silent) { + // Rewrite URL + history.pushState(null, title, '/i/drive'); + } + + document.title = title; + + this.file = null; + this.folder = null; + }, + onOpenFolder(folder, silent) { + const title = folder.name + ' | Misskey Drive'; + + if (!silent) { + // Rewrite URL + history.pushState(null, title, '/i/drive/folder/' + folder.id); + } + + document.title = title; + + this.file = null; + this.folder = folder; + }, + onOpenFile(file, silent) { + const title = file.name + ' | Misskey Drive'; + + if (!silent) { + // Rewrite URL + history.pushState(null, title, '/i/drive/file/' + file.id); + } + + document.title = title; + + this.file = file; + this.folder = null; + } + } +}); +</script> + diff --git a/src/client/app/mobile/views/pages/followers.vue b/src/client/app/mobile/views/pages/followers.vue new file mode 100644 index 0000000000..f4225d556d --- /dev/null +++ b/src/client/app/mobile/views/pages/followers.vue @@ -0,0 +1,71 @@ +<template> +<mk-ui> + <template slot="header" v-if="!fetching"> + <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""> + {{ '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', name) }} + </template> + <mk-users-list + v-if="!fetching" + :fetch="fetchUsers" + :count="user.followersCount" + :you-know-count="user.followersYouKnowCount" + @loaded="onLoaded" + > + %i18n:mobile.tags.mk-user-followers.no-users% + </mk-users-list> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import Progress from '../../../common/scripts/loading'; +import parseAcct from '../../../../../acct/parse'; +import getUserName from '../../../../../renderers/get-user-name'; + +export default Vue.extend({ + data() { + return { + fetching: true, + user: null + }; + }, + computed: { + name() { + return getUserName(this.user); + } + }, + watch: { + $route: 'fetch' + }, + created() { + this.fetch(); + }, + mounted() { + document.documentElement.style.background = '#313a42'; + }, + methods: { + fetch() { + Progress.start(); + this.fetching = true; + + (this as any).api('users/show', parseAcct(this.$route.params.user)).then(user => { + this.user = user; + this.fetching = false; + + document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', this.name) + ' | Misskey'; + }); + }, + onLoaded() { + Progress.done(); + }, + fetchUsers(iknow, limit, cursor, cb) { + (this as any).api('users/followers', { + userId: this.user.id, + iknow: iknow, + limit: limit, + cursor: cursor ? cursor : undefined + }).then(cb); + } + } +}); +</script> diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue new file mode 100644 index 0000000000..d0dcc117c2 --- /dev/null +++ b/src/client/app/mobile/views/pages/following.vue @@ -0,0 +1,70 @@ +<template> +<mk-ui> + <template slot="header" v-if="!fetching"> + <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""> + {{ '%i18n:mobile.tags.mk-user-following-page.following-of%'.replace('{}', name) }} + </template> + <mk-users-list + v-if="!fetching" + :fetch="fetchUsers" + :count="user.followingCount" + :you-know-count="user.followingYouKnowCount" + @loaded="onLoaded" + > + %i18n:mobile.tags.mk-user-following.no-users% + </mk-users-list> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import Progress from '../../../common/scripts/loading'; +import parseAcct from '../../../../../acct/parse'; + +export default Vue.extend({ + data() { + return { + fetching: true, + user: null + }; + }, + computed: { + name(): string { + return Vue.filter('userName')(this.user); + } + }, + watch: { + $route: 'fetch' + }, + created() { + this.fetch(); + }, + mounted() { + document.documentElement.style.background = '#313a42'; + }, + methods: { + fetch() { + Progress.start(); + this.fetching = true; + + (this as any).api('users/show', parseAcct(this.$route.params.user)).then(user => { + this.user = user; + this.fetching = false; + + document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', this.name) + ' | Misskey'; + }); + }, + onLoaded() { + Progress.done(); + }, + fetchUsers(iknow, limit, cursor, cb) { + (this as any).api('users/following', { + userId: this.user.id, + iknow: iknow, + limit: limit, + cursor: cursor ? cursor : undefined + }).then(cb); + } + } +}); +</script> diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue new file mode 100644 index 0000000000..3de2ba1c3e --- /dev/null +++ b/src/client/app/mobile/views/pages/home.vue @@ -0,0 +1,259 @@ +<template> +<mk-ui> + <span slot="header" @click="showTl = !showTl"> + <template v-if="showTl">%fa:home%タイムライン</template> + <template v-else>%fa:home%ウィジェット</template> + <span style="margin-left:8px"> + <template v-if="showTl">%fa:angle-down%</template> + <template v-else>%fa:angle-up%</template> + </span> + </span> + <template slot="func"> + <button @click="fn" v-if="showTl">%fa:pencil-alt%</button> + <button @click="customizing = !customizing" v-else>%fa:cog%</button> + </template> + <main> + <div class="tl"> + <mk-timeline @loaded="onLoaded" v-show="showTl"/> + </div> + <div class="widgets" v-show="!showTl"> + <template v-if="customizing"> + <header> + <select v-model="widgetAdderSelected"> + <option value="profile">プロフィール</option> + <option value="calendar">カレンダー</option> + <option value="activity">アクティビティ</option> + <option value="rss">RSSリーダー</option> + <option value="photo-stream">フォトストリーム</option> + <option value="slideshow">スライドショー</option> + <option value="version">バージョン</option> + <option value="access-log">アクセスログ</option> + <option value="server">サーバー情報</option> + <option value="donation">寄付のお願い</option> + <option value="nav">ナビゲーション</option> + <option value="tips">ヒント</option> + </select> + <button @click="addWidget">追加</button> + <p><a @click="hint">カスタマイズのヒント</a></p> + </header> + <x-draggable + :list="widgets" + :options="{ handle: '.handle', animation: 150 }" + @sort="onWidgetSort" + > + <div v-for="widget in widgets" class="customize-container" :key="widget.id"> + <header> + <span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button> + </header> + <div @click="widgetFunc(widget.id)"> + <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :is-mobile="true"/> + </div> + </div> + </x-draggable> + </template> + <template v-else> + <component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :is-mobile="true" @chosen="warp"/> + </template> + </div> + </main> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as XDraggable from 'vuedraggable'; +import * as uuid from 'uuid'; +import Progress from '../../../common/scripts/loading'; +import getNoteSummary from '../../../../../renderers/get-note-summary'; + +export default Vue.extend({ + components: { + XDraggable + }, + data() { + return { + connection: null, + connectionId: null, + unreadCount: 0, + showTl: true, + widgets: [], + customizing: false, + widgetAdderSelected: null + }; + }, + created() { + if ((this as any).os.i.clientSettings.mobileHome == null) { + Vue.set((this as any).os.i.clientSettings, 'mobileHome', [{ + name: 'calendar', + id: 'a', data: {} + }, { + name: 'activity', + id: 'b', data: {} + }, { + name: 'rss', + id: 'c', data: {} + }, { + name: 'photo-stream', + id: 'd', data: {} + }, { + name: 'donation', + id: 'e', data: {} + }, { + name: 'nav', + id: 'f', data: {} + }, { + name: 'version', + id: 'g', data: {} + }]); + this.widgets = (this as any).os.i.clientSettings.mobileHome; + this.saveHome(); + } else { + this.widgets = (this as any).os.i.clientSettings.mobileHome; + } + + this.$watch('os.i.clientSettings', i => { + this.widgets = (this as any).os.i.clientSettings.mobileHome; + }, { + deep: true + }); + }, + mounted() { + document.title = 'Misskey'; + document.documentElement.style.background = '#313a42'; + + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + + this.connection.on('note', this.onStreamNote); + this.connection.on('mobile_home_updated', this.onHomeUpdated); + document.addEventListener('visibilitychange', this.onVisibilitychange, false); + + Progress.start(); + }, + beforeDestroy() { + this.connection.off('note', this.onStreamNote); + this.connection.off('mobile_home_updated', this.onHomeUpdated); + (this as any).os.stream.dispose(this.connectionId); + document.removeEventListener('visibilitychange', this.onVisibilitychange); + }, + methods: { + fn() { + (this as any).apis.post(); + }, + onLoaded() { + Progress.done(); + }, + onStreamNote(note) { + if (document.hidden && note.userId !== (this as any).os.i.id) { + this.unreadCount++; + document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`; + } + }, + onVisibilitychange() { + if (!document.hidden) { + this.unreadCount = 0; + document.title = 'Misskey'; + } + }, + onHomeUpdated(data) { + if (data.home) { + (this as any).os.i.clientSettings.mobileHome = data.home; + this.widgets = data.home; + } else { + const w = (this as any).os.i.clientSettings.mobileHome.find(w => w.id == data.id); + if (w != null) { + w.data = data.data; + this.$refs[w.id][0].preventSave = true; + this.$refs[w.id][0].props = w.data; + this.widgets = (this as any).os.i.clientSettings.mobileHome; + } + } + }, + hint() { + alert('ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。'); + }, + widgetFunc(id) { + const w = this.$refs[id][0]; + if (w.func) w.func(); + }, + onWidgetSort() { + this.saveHome(); + }, + addWidget() { + const widget = { + name: this.widgetAdderSelected, + id: uuid(), + data: {} + }; + + this.widgets.unshift(widget); + this.saveHome(); + }, + removeWidget(widget) { + this.widgets = this.widgets.filter(w => w.id != widget.id); + this.saveHome(); + }, + saveHome() { + (this as any).os.i.clientSettings.mobileHome = this.widgets; + (this as any).api('i/update_mobile_home', { + home: this.widgets + }); + }, + warp() { + + } + } +}); +</script> + +<style lang="stylus" scoped> +main + + > .tl + > .mk-timeline + max-width 600px + margin 0 auto + padding 8px + + @media (min-width 500px) + padding 16px + + > .widgets + margin 0 auto + max-width 500px + + @media (min-width 500px) + padding 8px + + > header + padding 8px + background #fff + + .widget + margin 8px + + .customize-container + margin 8px + background #fff + + > header + line-height 32px + background #eee + + > .handle + padding 0 8px + + > .remove + position absolute + top 0 + right 0 + padding 0 8px + line-height 32px + + > div + padding 8px + + > * + pointer-events none + +</style> diff --git a/src/client/app/mobile/views/pages/index.vue b/src/client/app/mobile/views/pages/index.vue new file mode 100644 index 0000000000..0ea47d913b --- /dev/null +++ b/src/client/app/mobile/views/pages/index.vue @@ -0,0 +1,16 @@ +<template> +<component :is="os.isSignedIn ? 'home' : 'welcome'"></component> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import Home from './home.vue'; +import Welcome from './welcome.vue'; + +export default Vue.extend({ + components: { + Home, + Welcome + } +}); +</script> diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue new file mode 100644 index 0000000000..3b6fb11db5 --- /dev/null +++ b/src/client/app/mobile/views/pages/messaging-room.vue @@ -0,0 +1,42 @@ +<template> +<mk-ui> + <span slot="header"> + <template v-if="user">%fa:R comments%{{ user | userName }}</template> + <template v-else><mk-ellipsis/></template> + </span> + <mk-messaging-room v-if="!fetching" :user="user" :is-naked="true"/> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import parseAcct from '../../../../../acct/parse'; + +export default Vue.extend({ + data() { + return { + fetching: true, + user: null + }; + }, + watch: { + $route: 'fetch' + }, + created() { + document.documentElement.style.background = '#fff'; + this.fetch(); + }, + methods: { + fetch() { + this.fetching = true; + (this as any).api('users/show', parseAcct(this.$route.params.user)).then(user => { + this.user = user; + this.fetching = false; + + document.title = `%i18n:mobile.tags.mk-messaging-room-page.message%: ${Vue.filter('userName')(this.user)} | Misskey`; + }); + } + } +}); +</script> + diff --git a/src/client/app/mobile/views/pages/messaging.vue b/src/client/app/mobile/views/pages/messaging.vue new file mode 100644 index 0000000000..fa735a2530 --- /dev/null +++ b/src/client/app/mobile/views/pages/messaging.vue @@ -0,0 +1,23 @@ +<template> +<mk-ui> + <span slot="header">%fa:R comments%%i18n:mobile.tags.mk-messaging-page.message%</span> + <mk-messaging @navigate="navigate" :header-top="48"/> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import getAcct from '../../../../../acct/render'; + +export default Vue.extend({ + mounted() { + document.title = 'Misskey %i18n:mobile.tags.mk-messaging-page.message%'; + document.documentElement.style.background = '#fff'; + }, + methods: { + navigate(user) { + (this as any).$router.push(`/i/messaging/${getAcct(user)}`); + } + } +}); +</script> diff --git a/src/client/app/mobile/views/pages/note.vue b/src/client/app/mobile/views/pages/note.vue new file mode 100644 index 0000000000..89b8c776f2 --- /dev/null +++ b/src/client/app/mobile/views/pages/note.vue @@ -0,0 +1,85 @@ +<template> +<mk-ui> + <span slot="header">%fa:R sticky-note%%i18n:mobile.tags.mk-note-page.title%</span> + <main v-if="!fetching"> + <a v-if="note.next" :href="note.next">%fa:angle-up%%i18n:mobile.tags.mk-note-page.next%</a> + <div> + <mk-note-detail :note="note"/> + </div> + <a v-if="note.prev" :href="note.prev">%fa:angle-down%%i18n:mobile.tags.mk-note-page.prev%</a> + </main> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import Progress from '../../../common/scripts/loading'; + +export default Vue.extend({ + data() { + return { + fetching: true, + note: null + }; + }, + watch: { + $route: 'fetch' + }, + created() { + this.fetch(); + }, + mounted() { + document.title = 'Misskey'; + document.documentElement.style.background = '#313a42'; + }, + methods: { + fetch() { + Progress.start(); + this.fetching = true; + + (this as any).api('notes/show', { + noteId: this.$route.params.note + }).then(note => { + this.note = note; + this.fetching = false; + + Progress.done(); + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +main + text-align center + + > div + margin 8px auto + padding 0 + max-width 500px + width calc(100% - 16px) + + @media (min-width 500px) + margin 16px auto + width calc(100% - 32px) + + > a + display inline-block + + &:first-child + margin-top 8px + + @media (min-width 500px) + margin-top 16px + + &:last-child + margin-bottom 8px + + @media (min-width 500px) + margin-bottom 16px + + > [data-fa] + margin-right 4px + +</style> diff --git a/src/client/app/mobile/views/pages/notifications.vue b/src/client/app/mobile/views/pages/notifications.vue new file mode 100644 index 0000000000..6d45e22a9c --- /dev/null +++ b/src/client/app/mobile/views/pages/notifications.vue @@ -0,0 +1,32 @@ +<template> +<mk-ui> + <span slot="header">%fa:R bell%%i18n:mobile.tags.mk-notifications-page.notifications%</span> + <template slot="func"><button @click="fn">%fa:check%</button></template> + <mk-notifications @fetched="onFetched"/> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import Progress from '../../../common/scripts/loading'; + +export default Vue.extend({ + mounted() { + document.title = 'Misskey | %i18n:mobile.tags.mk-notifications-page.notifications%'; + document.documentElement.style.background = '#313a42'; + + Progress.start(); + }, + methods: { + fn() { + const ok = window.confirm('%i18n:mobile.tags.mk-notifications-page.read-all%'); + if (!ok) return; + + (this as any).api('notifications/markAsRead_all'); + }, + onFetched() { + Progress.done(); + } + } +}); +</script> diff --git a/src/client/app/mobile/views/pages/othello.vue b/src/client/app/mobile/views/pages/othello.vue new file mode 100644 index 0000000000..e04e583c20 --- /dev/null +++ b/src/client/app/mobile/views/pages/othello.vue @@ -0,0 +1,50 @@ +<template> +<mk-ui> + <span slot="header">%fa:gamepad%オセロ</span> + <mk-othello v-if="!fetching" :init-game="game" @gamed="onGamed"/> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import Progress from '../../../common/scripts/loading'; + +export default Vue.extend({ + data() { + return { + fetching: false, + game: null + }; + }, + watch: { + $route: 'fetch' + }, + created() { + this.fetch(); + }, + mounted() { + document.title = 'Misskey オセロ'; + document.documentElement.style.background = '#fff'; + }, + methods: { + fetch() { + if (this.$route.params.game == null) return; + + Progress.start(); + this.fetching = true; + + (this as any).api('othello/games/show', { + gameId: this.$route.params.game + }).then(game => { + this.game = game; + this.fetching = false; + + Progress.done(); + }); + }, + onGamed(game) { + history.pushState(null, null, '/othello/' + game.id); + } + } +}); +</script> diff --git a/src/client/app/mobile/views/pages/profile-setting.vue b/src/client/app/mobile/views/pages/profile-setting.vue new file mode 100644 index 0000000000..7f0ff5aad7 --- /dev/null +++ b/src/client/app/mobile/views/pages/profile-setting.vue @@ -0,0 +1,226 @@ +<template> +<mk-ui> + <span slot="header">%fa:user%%i18n:mobile.tags.mk-profile-setting-page.title%</span> + <div :class="$style.content"> + <p>%fa:info-circle%%i18n:mobile.tags.mk-profile-setting.will-be-published%</p> + <div :class="$style.form"> + <div :style="os.i.bannerUrl ? `background-image: url(${os.i.bannerUrl}?thumbnail&size=1024)` : ''" @click="setBanner"> + <img :src="`${os.i.avatarUrl}?thumbnail&size=200`" alt="avatar" @click="setAvatar"/> + </div> + <label> + <p>%i18n:mobile.tags.mk-profile-setting.name%</p> + <input v-model="name" type="text"/> + </label> + <label> + <p>%i18n:mobile.tags.mk-profile-setting.location%</p> + <input v-model="location" type="text"/> + </label> + <label> + <p>%i18n:mobile.tags.mk-profile-setting.description%</p> + <textarea v-model="description"></textarea> + </label> + <label> + <p>%i18n:mobile.tags.mk-profile-setting.birthday%</p> + <input v-model="birthday" type="date"/> + </label> + <label> + <p>%i18n:mobile.tags.mk-profile-setting.avatar%</p> + <button @click="setAvatar" :disabled="avatarSaving">%i18n:mobile.tags.mk-profile-setting.set-avatar%</button> + </label> + <label> + <p>%i18n:mobile.tags.mk-profile-setting.banner%</p> + <button @click="setBanner" :disabled="bannerSaving">%i18n:mobile.tags.mk-profile-setting.set-banner%</button> + </label> + </div> + <button :class="$style.save" @click="save" :disabled="saving">%fa:check%%i18n:mobile.tags.mk-profile-setting.save%</button> + </div> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + data() { + return { + name: null, + location: null, + description: null, + birthday: null, + avatarSaving: false, + bannerSaving: false, + saving: false + }; + }, + created() { + this.name = (this as any).os.i.name || ''; + this.location = (this as any).os.i.profile.location; + this.description = (this as any).os.i.description; + this.birthday = (this as any).os.i.profile.birthday; + }, + mounted() { + document.title = 'Misskey | %i18n:mobile.tags.mk-profile-setting-page.title%'; + document.documentElement.style.background = '#313a42'; + }, + methods: { + setAvatar() { + (this as any).apis.chooseDriveFile({ + multiple: false + }).then(file => { + this.avatarSaving = true; + + (this as any).api('i/update', { + avatarId: file.id + }).then(() => { + this.avatarSaving = false; + alert('%i18n:mobile.tags.mk-profile-setting.avatar-saved%'); + }); + }); + }, + setBanner() { + (this as any).apis.chooseDriveFile({ + multiple: false + }).then(file => { + this.bannerSaving = true; + + (this as any).api('i/update', { + bannerId: file.id + }).then(() => { + this.bannerSaving = false; + alert('%i18n:mobile.tags.mk-profile-setting.banner-saved%'); + }); + }); + }, + save() { + this.saving = true; + + (this as any).api('i/update', { + name: this.name || null, + location: this.location || null, + description: this.description || null, + birthday: this.birthday || null + }).then(() => { + this.saving = false; + alert('%i18n:mobile.tags.mk-profile-setting.saved%'); + }); + } + } +}); +</script> + +<style lang="stylus" module> +@import '~const.styl' + +.content + margin 8px auto + max-width 500px + width calc(100% - 16px) + + @media (min-width 500px) + margin 16px auto + width calc(100% - 32px) + + > p + display block + margin 0 0 8px 0 + padding 12px 16px + font-size 14px + color #79d4e6 + border solid 1px #71afbb + //color #276f86 + //background #f8ffff + //border solid 1px #a9d5de + border-radius 8px + + > [data-fa] + margin-right 6px + +.form + position relative + background #fff + box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + border-radius 8px + + &:before + content "" + display block + position absolute + bottom -20px + left calc(50% - 10px) + border-top solid 10px rgba(0, 0, 0, 0.2) + border-right solid 10px transparent + border-bottom solid 10px transparent + border-left solid 10px transparent + + &:after + content "" + display block + position absolute + bottom -16px + left calc(50% - 8px) + border-top solid 8px #fff + border-right solid 8px transparent + border-bottom solid 8px transparent + border-left solid 8px transparent + + > div + height 128px + background-color #e4e4e4 + background-size cover + background-position center + border-radius 8px 8px 0 0 + + > img + position absolute + top 25px + left calc(50% - 40px) + width 80px + height 80px + border solid 2px #fff + border-radius 8px + + > label + display block + margin 0 + padding 16px + border-bottom solid 1px #eee + + &:last-of-type + border none + + > p:first-child + display block + margin 0 + padding 0 0 4px 0 + font-weight bold + color #2f3c42 + + > input[type="text"] + > textarea + display block + width 100% + padding 12px + font-size 16px + color #192427 + border solid 2px #ddd + border-radius 4px + + > textarea + min-height 80px + +.save + display block + margin 8px 0 0 0 + padding 16px + width 100% + font-size 16px + color $theme-color-foreground + background $theme-color + border-radius 8px + + &:disabled + opacity 0.7 + + > [data-fa] + margin-right 4px + +</style> diff --git a/src/client/app/mobile/views/pages/search.vue b/src/client/app/mobile/views/pages/search.vue new file mode 100644 index 0000000000..a96832beec --- /dev/null +++ b/src/client/app/mobile/views/pages/search.vue @@ -0,0 +1,93 @@ +<template> +<mk-ui> + <span slot="header">%fa:search% {{ q }}</span> + <main v-if="!fetching"> + <mk-notes :class="$style.notes" :notes="notes"> + <span v-if="notes.length == 0">{{ '%i18n:mobile.tags.mk-search-notes.empty%'.replace('{}', q) }}</span> + <button v-if="existMore" @click="more" :disabled="fetching" slot="tail"> + <span v-if="!fetching">%i18n:mobile.tags.mk-timeline.load-more%</span> + <span v-if="fetching">%i18n:common.loading%<mk-ellipsis/></span> + </button> + </mk-notes> + </main> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import Progress from '../../../common/scripts/loading'; +import parse from '../../../common/scripts/parse-search-query'; + +const limit = 20; + +export default Vue.extend({ + data() { + return { + fetching: true, + existMore: false, + notes: [], + offset: 0 + }; + }, + watch: { + $route: 'fetch' + }, + computed: { + q(): string { + return this.$route.query.q; + } + }, + mounted() { + document.title = `%i18n:mobile.tags.mk-search-page.search%: ${this.q} | Misskey`; + document.documentElement.style.background = '#313a42'; + + this.fetch(); + }, + methods: { + fetch() { + this.fetching = true; + Progress.start(); + + (this as any).api('notes/search', Object.assign({ + limit: limit + 1 + }, parse(this.q))).then(notes => { + if (notes.length == limit + 1) { + notes.pop(); + this.existMore = true; + } + this.notes = notes; + this.fetching = false; + Progress.done(); + }); + }, + more() { + this.offset += limit; + (this as any).api('notes/search', Object.assign({ + limit: limit + 1, + offset: this.offset + }, parse(this.q))).then(notes => { + if (notes.length == limit + 1) { + notes.pop(); + } else { + this.existMore = false; + } + this.notes = this.notes.concat(notes); + }); + } + } +}); +</script> + +<style lang="stylus" module> +.notes + margin 8px auto + max-width 500px + width calc(100% - 16px) + background #fff + border-radius 8px + box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + + @media (min-width 500px) + margin 16px auto + width calc(100% - 32px) +</style> diff --git a/src/client/app/mobile/views/pages/selectdrive.vue b/src/client/app/mobile/views/pages/selectdrive.vue new file mode 100644 index 0000000000..3480a0d103 --- /dev/null +++ b/src/client/app/mobile/views/pages/selectdrive.vue @@ -0,0 +1,96 @@ +<template> +<div class="mk-selectdrive"> + <header> + <h1>%i18n:mobile.tags.mk-selectdrive-page.select-file%<span class="count" v-if="files.length > 0">({{ files.length }})</span></h1> + <button class="upload" @click="upload">%fa:upload%</button> + <button v-if="multiple" class="ok" @click="ok">%fa:check%</button> + </header> + <mk-drive ref="browser" select-file :multiple="multiple" is-naked :top="42"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + data() { + return { + files: [] + }; + }, + computed: { + multiple(): boolean { + const q = (new URL(location.toString())).searchParams; + return q.get('multiple') == 'true'; + } + }, + mounted() { + document.title = '%i18n:desktop.tags.mk-selectdrive-page.title%'; + }, + methods: { + onSelected(file) { + this.files = [file]; + this.ok(); + }, + onChangeSelection(files) { + this.files = files; + }, + upload() { + (this.$refs.browser as any).selectLocalFile(); + }, + close() { + window.close(); + }, + ok() { + window.opener.cb(this.multiple ? this.files : this.files[0]); + this.close(); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-selectdrive + width 100% + height 100% + background #fff + + > header + position fixed + top 0 + left 0 + width 100% + z-index 1000 + background #fff + box-shadow 0 1px rgba(0, 0, 0, 0.1) + + > h1 + margin 0 + padding 0 + text-align center + line-height 42px + font-size 1em + font-weight normal + + > .count + margin-left 4px + opacity 0.5 + + > .upload + position absolute + top 0 + left 0 + line-height 42px + width 42px + + > .ok + position absolute + top 0 + right 0 + line-height 42px + width 42px + + > .mk-drive + top 42px + +</style> diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue new file mode 100644 index 0000000000..8d248f5cbf --- /dev/null +++ b/src/client/app/mobile/views/pages/settings.vue @@ -0,0 +1,108 @@ +<template> +<mk-ui> + <span slot="header">%fa:cog%%i18n:mobile.tags.mk-settings-page.settings%</span> + <div :class="$style.content"> + <p v-html="'%i18n:mobile.tags.mk-settings.signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></p> + <ul> + <li><router-link to="./settings/profile">%fa:user%%i18n:mobile.tags.mk-settings-page.profile%%fa:angle-right%</router-link></li> + <li><router-link to="./settings/authorized-apps">%fa:puzzle-piece%%i18n:mobile.tags.mk-settings-page.applications%%fa:angle-right%</router-link></li> + <li><router-link to="./settings/twitter">%fa:B twitter%%i18n:mobile.tags.mk-settings-page.twitter-integration%%fa:angle-right%</router-link></li> + <li><router-link to="./settings/signin-history">%fa:sign-in-alt%%i18n:mobile.tags.mk-settings-page.signin-history%%fa:angle-right%</router-link></li> + </ul> + <ul> + <li><a @click="signout">%fa:power-off%%i18n:mobile.tags.mk-settings-page.signout%</a></li> + </ul> + <p><small>ver {{ version }} ({{ codename }})</small></p> + </div> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { version, codename } from '../../../config'; + +export default Vue.extend({ + data() { + return { + version, + codename + }; + }, + computed: { + name(): string { + return Vue.filter('userName')((this as any).os.i); + } + }, + mounted() { + document.title = 'Misskey | %i18n:mobile.tags.mk-settings-page.settings%'; + document.documentElement.style.background = '#313a42'; + }, + methods: { + signout() { + (this as any).os.signout(); + } + } +}); +</script> + +<style lang="stylus" module> +.content + + > p + display block + margin 24px + text-align center + color #cad2da + + > ul + $radius = 8px + + display block + margin 16px auto + padding 0 + max-width 500px + width calc(100% - 32px) + list-style none + background #fff + border solid 1px rgba(0, 0, 0, 0.2) + border-radius $radius + + > li + display block + border-bottom solid 1px #ddd + + &:hover + background rgba(0, 0, 0, 0.1) + + &:first-child + border-top-left-radius $radius + border-top-right-radius $radius + + &:last-child + border-bottom-left-radius $radius + border-bottom-right-radius $radius + border-bottom none + + > a + $height = 48px + + display block + position relative + padding 0 16px + line-height $height + color #4d635e + + > [data-fa]:nth-of-type(1) + margin-right 4px + + > [data-fa]:nth-of-type(2) + display block + position absolute + top 0 + right 8px + z-index 1 + padding 0 20px + font-size 1.2em + line-height $height + +</style> diff --git a/src/client/app/mobile/views/pages/signup.vue b/src/client/app/mobile/views/pages/signup.vue new file mode 100644 index 0000000000..9dc07a4b86 --- /dev/null +++ b/src/client/app/mobile/views/pages/signup.vue @@ -0,0 +1,57 @@ +<template> +<div class="signup"> + <h1>Misskeyをはじめる</h1> + <p>いつでも、どこからでもMisskeyを利用できます。もちろん、無料です。</p> + <div class="form"> + <p>新規登録</p> + <div> + <mk-signup/> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + mounted() { + document.documentElement.style.background = '#293946'; + } +}); +</script> + +<style lang="stylus" scoped> +.signup + padding 16px + margin 0 auto + max-width 500px + + h1 + margin 0 + padding 8px + font-size 1.5em + font-weight normal + color #c3c6ca + + & + p + margin 0 0 16px 0 + padding 0 8px 0 8px + color #949fa9 + + .form + background #fff + border solid 1px rgba(0, 0, 0, 0.2) + border-radius 8px + overflow hidden + + > p + margin 0 + padding 12px 20px + color #555 + background #f5f5f5 + border-bottom solid 1px #ddd + + > div + padding 16px + +</style> diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue new file mode 100644 index 0000000000..3d9fbda942 --- /dev/null +++ b/src/client/app/mobile/views/pages/user.vue @@ -0,0 +1,245 @@ +<template> +<mk-ui> + <span slot="header" v-if="!fetching">%fa:user% {{ user | userName }}</span> + <main v-if="!fetching"> + <header> + <div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''"></div> + <div class="body"> + <div class="top"> + <a class="avatar"> + <img :src="`${user.avatarUrl}?thumbnail&size=200`" alt="avatar"/> + </a> + <mk-follow-button v-if="os.isSignedIn && os.i.id != user.id" :user="user"/> + </div> + <div class="title"> + <h1>{{ user | userName }}</h1> + <span class="username">@{{ user | acct }}</span> + <span class="followed" v-if="user.isFollowed">%i18n:mobile.tags.mk-user.follows-you%</span> + </div> + <div class="description">{{ user.description }}</div> + <div class="info"> + <p class="location" v-if="user.host === null && user.profile.location"> + %fa:map-marker%{{ user.profile.location }} + </p> + <p class="birthday" v-if="user.host === null && user.profile.birthday"> + %fa:birthday-cake%{{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳) + </p> + </div> + <div class="status"> + <a> + <b>{{ user.notesCount | number }}</b> + <i>%i18n:mobile.tags.mk-user.notes%</i> + </a> + <a :href="user | userPage('following')"> + <b>{{ user.followingCount | number }}</b> + <i>%i18n:mobile.tags.mk-user.following%</i> + </a> + <a :href="user | userPage('followers')"> + <b>{{ user.followersCount | number }}</b> + <i>%i18n:mobile.tags.mk-user.followers%</i> + </a> + </div> + </div> + </header> + <nav> + <div class="nav-container"> + <a :data-is-active=" page == 'home' " @click="page = 'home'">%i18n:mobile.tags.mk-user.overview%</a> + <a :data-is-active=" page == 'notes' " @click="page = 'notes'">%i18n:mobile.tags.mk-user.timeline%</a> + <a :data-is-active=" page == 'media' " @click="page = 'media'">%i18n:mobile.tags.mk-user.media%</a> + </div> + </nav> + <div class="body"> + <x-home v-if="page == 'home'" :user="user"/> + <mk-user-timeline v-if="page == 'notes'" :user="user"/> + <mk-user-timeline v-if="page == 'media'" :user="user" with-media/> + </div> + </main> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as age from 's-age'; +import parseAcct from '../../../../../acct/parse'; +import Progress from '../../../common/scripts/loading'; +import XHome from './user/home.vue'; + +export default Vue.extend({ + components: { + XHome + }, + data() { + return { + fetching: true, + user: null, + page: 'home' + }; + }, + computed: { + age(): number { + return age(this.user.profile.birthday); + } + }, + watch: { + $route: 'fetch' + }, + created() { + this.fetch(); + }, + mounted() { + document.documentElement.style.background = '#313a42'; + }, + methods: { + fetch() { + Progress.start(); + + (this as any).api('users/show', parseAcct(this.$route.params.user)).then(user => { + this.user = user; + this.fetching = false; + + Progress.done(); + document.title = Vue.filter('userName')(this.user) + ' | Misskey'; + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +main + > header + + > .banner + padding-bottom 33.3% + background-color #1b1b1b + background-size cover + background-position center + + > .body + padding 12px + margin 0 auto + max-width 600px + + > .top + &:after + content '' + display block + clear both + + > .avatar + display block + float left + width 25% + height 40px + + > img + display block + position absolute + left -2px + bottom -2px + width 100% + border 3px solid #313a42 + border-radius 6px + + @media (min-width 500px) + left -4px + bottom -4px + border 4px solid #313a42 + border-radius 12px + + > .mk-follow-button + float right + height 40px + + > .title + margin 8px 0 + + > h1 + margin 0 + line-height 22px + font-size 20px + color #fff + + > .username + display inline-block + line-height 20px + font-size 16px + font-weight bold + color #657786 + + > .followed + margin-left 8px + padding 2px 4px + font-size 12px + color #657786 + background #f8f8f8 + border-radius 4px + + > .description + margin 8px 0 + color #fff + + > .info + margin 8px 0 + + > p + display inline + margin 0 16px 0 0 + color #a9b9c1 + + > i + margin-right 4px + + > .status + > a + color #657786 + + &:not(:last-child) + margin-right 16px + + > b + margin-right 4px + font-size 16px + color #fff + + > i + font-size 14px + + > nav + position -webkit-sticky + position sticky + top 48px + box-shadow 0 4px 4px rgba(0, 0, 0, 0.3) + background-color #313a42 + z-index 1 + + > .nav-container + display flex + justify-content center + margin 0 auto + max-width 600px + + > a + display block + flex 1 1 + text-align center + line-height 52px + font-size 14px + text-decoration none + color #657786 + border-bottom solid 2px transparent + + &[data-is-active] + font-weight bold + color $theme-color + border-color $theme-color + + > .body + padding 8px + + @media (min-width 500px) + padding 16px + +</style> diff --git a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue new file mode 100644 index 0000000000..2841c0d63a --- /dev/null +++ b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue @@ -0,0 +1,63 @@ +<template> +<div class="root followers-you-know"> + <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-followers-you-know.loading%<mk-ellipsis/></p> + <div v-if="!fetching && users.length > 0"> + <a v-for="user in users" :key="user.id" :href="user | userPage"> + <img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user | userName"/> + </a> + </div> + <p class="empty" v-if="!fetching && users.length == 0">%i18n:mobile.tags.mk-user-overview-followers-you-know.no-users%</p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['user'], + data() { + return { + fetching: true, + users: [] + }; + }, + mounted() { + (this as any).api('users/followers', { + userId: this.user.id, + iknow: true, + limit: 30 + }).then(res => { + this.fetching = false; + this.users = res.users; + }); + } +}); +</script> + +<style lang="stylus" scoped> +.root.followers-you-know + + > div + padding 4px + + > a + display inline-block + margin 4px + + > img + width 48px + height 48px + vertical-align bottom + border-radius 100% + + > .initializing + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > i + margin-right 4px + +</style> diff --git a/src/client/app/mobile/views/pages/user/home.friends.vue b/src/client/app/mobile/views/pages/user/home.friends.vue new file mode 100644 index 0000000000..469781abb9 --- /dev/null +++ b/src/client/app/mobile/views/pages/user/home.friends.vue @@ -0,0 +1,54 @@ +<template> +<div class="root friends"> + <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-frequently-replied-users.loading%<mk-ellipsis/></p> + <div v-if="!fetching && users.length > 0"> + <mk-user-card v-for="user in users" :key="user.id" :user="user"/> + </div> + <p class="empty" v-if="!fetching && users.length == 0">%i18n:mobile.tags.mk-user-overview-frequently-replied-users.no-users%</p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['user'], + data() { + return { + fetching: true, + users: [] + }; + }, + mounted() { + (this as any).api('users/get_frequently_replied_users', { + userId: this.user.id + }).then(res => { + this.users = res.map(x => x.user); + this.fetching = false; + }); + } +}); +</script> + +<style lang="stylus" scoped> +.root.friends + > div + overflow-x scroll + -webkit-overflow-scrolling touch + white-space nowrap + padding 8px + + > .mk-user-card + &:not(:last-child) + margin-right 8px + + > .fetching + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > i + margin-right 4px + +</style> diff --git a/src/client/app/mobile/views/pages/user/home.notes.vue b/src/client/app/mobile/views/pages/user/home.notes.vue new file mode 100644 index 0000000000..02afed9b88 --- /dev/null +++ b/src/client/app/mobile/views/pages/user/home.notes.vue @@ -0,0 +1,57 @@ +<template> +<div class="root notes"> + <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-notes.loading%<mk-ellipsis/></p> + <div v-if="!fetching && notes.length > 0"> + <mk-note-card v-for="note in notes" :key="note.id" :note="note"/> + </div> + <p class="empty" v-if="!fetching && notes.length == 0">%i18n:mobile.tags.mk-user-overview-notes.no-notes%</p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['user'], + data() { + return { + fetching: true, + notes: [] + }; + }, + mounted() { + (this as any).api('users/notes', { + userId: this.user.id + }).then(notes => { + this.notes = notes; + this.fetching = false; + }); + } +}); +</script> + +<style lang="stylus" scoped> +.root.notes + + > div + overflow-x scroll + -webkit-overflow-scrolling touch + white-space nowrap + padding 8px + + > * + vertical-align top + + &:not(:last-child) + margin-right 8px + + > .fetching + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > i + margin-right 4px + +</style> diff --git a/src/client/app/mobile/views/pages/user/home.photos.vue b/src/client/app/mobile/views/pages/user/home.photos.vue new file mode 100644 index 0000000000..0e0d6926a1 --- /dev/null +++ b/src/client/app/mobile/views/pages/user/home.photos.vue @@ -0,0 +1,79 @@ +<template> +<div class="root photos"> + <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-photos.loading%<mk-ellipsis/></p> + <div class="stream" v-if="!fetching && images.length > 0"> + <a v-for="image in images" + class="img" + :style="`background-image: url(${image.media.url}?thumbnail&size=256)`" + :href="image.note | notePage" + ></a> + </div> + <p class="empty" v-if="!fetching && images.length == 0">%i18n:mobile.tags.mk-user-overview-photos.no-photos%</p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['user'], + data() { + return { + fetching: true, + images: [] + }; + }, + mounted() { + (this as any).api('users/notes', { + userId: this.user.id, + withMedia: true, + limit: 6 + }).then(notes => { + notes.forEach(note => { + note.media.forEach(media => { + if (this.images.length < 9) this.images.push({ + note, + media + }); + }); + }); + this.fetching = false; + }); + } +}); +</script> + +<style lang="stylus" scoped> +.root.photos + + > .stream + display -webkit-flex + display -moz-flex + display -ms-flex + display flex + justify-content center + flex-wrap wrap + padding 8px + + > .img + flex 1 1 33% + width 33% + height 80px + background-position center center + background-size cover + background-clip content-box + border solid 2px transparent + border-radius 4px + + > .initializing + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > i + margin-right 4px + +</style> + diff --git a/src/client/app/mobile/views/pages/user/home.vue b/src/client/app/mobile/views/pages/user/home.vue new file mode 100644 index 0000000000..c0cd9b8da8 --- /dev/null +++ b/src/client/app/mobile/views/pages/user/home.vue @@ -0,0 +1,94 @@ +<template> +<div class="root home"> + <mk-note-detail v-if="user.pinnedNote" :note="user.pinnedNote" :compact="true"/> + <section class="recent-notes"> + <h2>%fa:R comments%%i18n:mobile.tags.mk-user-overview.recent-notes%</h2> + <div> + <x-notes :user="user"/> + </div> + </section> + <section class="images"> + <h2>%fa:image%%i18n:mobile.tags.mk-user-overview.images%</h2> + <div> + <x-photos :user="user"/> + </div> + </section> + <section class="activity"> + <h2>%fa:chart-bar%%i18n:mobile.tags.mk-user-overview.activity%</h2> + <div> + <mk-activity :user="user"/> + </div> + </section> + <section class="frequently-replied-users"> + <h2>%fa:users%%i18n:mobile.tags.mk-user-overview.frequently-replied-users%</h2> + <div> + <x-friends :user="user"/> + </div> + </section> + <section class="followers-you-know" v-if="os.isSignedIn && os.i.id !== user.id"> + <h2>%fa:users%%i18n:mobile.tags.mk-user-overview.followers-you-know%</h2> + <div> + <x-followers-you-know :user="user"/> + </div> + </section> + <p v-if="user.host === null">%i18n:mobile.tags.mk-user-overview.last-used-at%: <b><mk-time :time="user.lastUsedAt"/></b></p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XNotes from './home.notes.vue'; +import XPhotos from './home.photos.vue'; +import XFriends from './home.friends.vue'; +import XFollowersYouKnow from './home.followers-you-know.vue'; + +export default Vue.extend({ + components: { + XNotes, + XPhotos, + XFriends, + XFollowersYouKnow + }, + props: ['user'] +}); +</script> + +<style lang="stylus" scoped> +.root.home + max-width 600px + margin 0 auto + + > .mk-note-detail + margin 0 0 8px 0 + + > section + background #eee + border-radius 8px + box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + + &:not(:last-child) + margin-bottom 8px + + > h2 + margin 0 + padding 8px 10px + font-size 15px + font-weight normal + color #465258 + background #fff + border-radius 8px 8px 0 0 + + > i + margin-right 6px + + > .activity + > div + padding 8px + + > p + display block + margin 16px + text-align center + color #cad2da + +</style> diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue new file mode 100644 index 0000000000..27baf8bee4 --- /dev/null +++ b/src/client/app/mobile/views/pages/welcome.vue @@ -0,0 +1,206 @@ +<template> +<div class="welcome"> + <h1><b>Misskey</b>へようこそ</h1> + <p>Twitter風ミニブログSNS、Misskeyへようこそ。共有したいことを投稿したり、タイムラインでみんなの投稿を読むこともできます。<br><a href="/signup">アカウントを作成する</a></p> + <div class="form"> + <p>%fa:lock% ログイン</p> + <div> + <form @submit.prevent="onSubmit"> + <input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" placeholder="ユーザー名" autofocus required @change="onUsernameChange"/> + <input v-model="password" type="password" placeholder="パスワード" required/> + <input v-if="user && user.twoFactorEnabled" v-model="token" type="number" placeholder="トークン" required/> + <button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</button> + </form> + <div> + <a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a> + </div> + </div> + </div> + <div class="tl"> + <p>%fa:comments R% タイムラインを見てみる</p> + <mk-welcome-timeline/> + </div> + <div class="users"> + <router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/@${user.username}`"> + <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + </router-link> + </div> + <footer> + <small>{{ copyright }}</small> + </footer> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { apiUrl, copyright } from '../../../config'; + +export default Vue.extend({ + data() { + return { + signing: false, + user: null, + username: '', + password: '', + token: '', + apiUrl, + copyright, + users: [] + }; + }, + mounted() { + (this as any).api('users', { + sort: '+follower', + limit: 20 + }).then(users => { + this.users = users; + }); + }, + methods: { + onUsernameChange() { + (this as any).api('users/show', { + username: this.username + }).then(user => { + this.user = user; + }); + }, + onSubmit() { + this.signing = true; + + (this as any).api('signin', { + username: this.username, + password: this.password, + token: this.user && this.user.twoFactorEnabled ? this.token : undefined + }).then(() => { + location.reload(); + }).catch(() => { + alert('something happened'); + this.signing = false; + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.welcome + padding 16px + margin 0 auto + max-width 500px + + h1 + margin 0 + padding 8px + font-size 1.5em + font-weight normal + color #cacac3 + + & + p + margin 0 0 16px 0 + padding 0 8px 0 8px + color #949fa9 + + .form + margin-bottom 16px + background #fff + border solid 1px rgba(0, 0, 0, 0.2) + border-radius 8px + overflow hidden + + > p + margin 0 + padding 12px 20px + color #555 + background #f5f5f5 + border-bottom solid 1px #ddd + + > div + + > form + padding 16px + border-bottom solid 1px #ddd + + input + display block + padding 12px + margin 0 0 16px 0 + width 100% + font-size 1em + color rgba(0, 0, 0, 0.7) + background #fff + outline none + border solid 1px #ddd + border-radius 4px + + button + display block + width 100% + padding 10px + margin 0 + color #333 + font-size 1em + text-align center + text-decoration none + text-shadow 0 1px 0 rgba(255, 255, 255, 0.9) + background-image linear-gradient(#fafafa, #eaeaea) + border 1px solid #ddd + border-bottom-color #cecece + border-radius 4px + + &:active + background-color #767676 + background-image none + border-color #444 + box-shadow 0 1px 3px rgba(0, 0, 0, 0.075), inset 0 0 5px rgba(0, 0, 0, 0.2) + + > div + padding 16px + text-align center + + > .tl + background #fff + border solid 1px rgba(0, 0, 0, 0.2) + border-radius 8px + overflow hidden + + > p + margin 0 + padding 12px 20px + color #555 + background #f5f5f5 + border-bottom solid 1px #ddd + + > .mk-welcome-timeline + max-height 300px + overflow auto + + > .users + margin 12px 0 0 0 + + > * + display inline-block + margin 4px + + > * + display inline-block + width 38px + height 38px + vertical-align top + border-radius 6px + + > footer + text-align center + color #fff + + > small + display block + margin 16px 0 0 0 + opacity 0.7 + +</style> + +<style lang="stylus"> +html +body + background linear-gradient(to bottom, #1e1d65, #bd6659) +</style> diff --git a/src/client/app/mobile/views/widgets/activity.vue b/src/client/app/mobile/views/widgets/activity.vue new file mode 100644 index 0000000000..48dcafb3ed --- /dev/null +++ b/src/client/app/mobile/views/widgets/activity.vue @@ -0,0 +1,32 @@ +<template> +<div class="mkw-activity"> + <mk-widget-container :show-header="!props.compact"> + <template slot="header">%fa:chart-bar%アクティビティ</template> + <div :class="$style.body"> + <mk-activity :user="os.i"/> + </div> + </mk-widget-container> +</div> +</template> + +<script lang="ts"> +import define from '../../../common/define-widget'; + +export default define({ + name: 'activity', + props: () => ({ + compact: false + }) +}).extend({ + methods: { + func() { + this.props.compact = !this.props.compact; + } + } +}); +</script> + +<style lang="stylus" module> +.body + padding 8px +</style> diff --git a/src/client/app/mobile/views/widgets/index.ts b/src/client/app/mobile/views/widgets/index.ts new file mode 100644 index 0000000000..4de912b64c --- /dev/null +++ b/src/client/app/mobile/views/widgets/index.ts @@ -0,0 +1,7 @@ +import Vue from 'vue'; + +import wActivity from './activity.vue'; +import wProfile from './profile.vue'; + +Vue.component('mkw-activity', wActivity); +Vue.component('mkw-profile', wProfile); diff --git a/src/client/app/mobile/views/widgets/profile.vue b/src/client/app/mobile/views/widgets/profile.vue new file mode 100644 index 0000000000..502f886ceb --- /dev/null +++ b/src/client/app/mobile/views/widgets/profile.vue @@ -0,0 +1,63 @@ +<template> +<div class="mkw-profile"> + <mk-widget-container> + <div :class="$style.banner" + :style="os.i.bannerUrl ? `background-image: url(${os.i.bannerUrl}?thumbnail&size=256)` : ''" + ></div> + <img :class="$style.avatar" + :src="`${os.i.avatarUrl}?thumbnail&size=96`" + alt="avatar" + /> + <router-link :class="$style.name" :to="os.i | userPage">{{ os.i | userName }}</router-link> + </mk-widget-container> +</div> +</template> + +<script lang="ts"> +import define from '../../../common/define-widget'; + +export default define({ + name: 'profile' +}); +</script> + +<style lang="stylus" module> +.banner + height 100px + background-color #f5f5f5 + background-size cover + background-position center + cursor pointer + +.banner:before + content "" + display block + width 100% + height 100% + background rgba(0, 0, 0, 0.5) + +.avatar + display block + position absolute + width 58px + height 58px + margin 0 + vertical-align bottom + top ((100px - 58px) / 2) + left ((100px - 58px) / 2) + border none + border-radius 100% + box-shadow 0 0 16px rgba(0, 0, 0, 0.5) + +.name + display block + position absolute + top 0 + left 92px + margin 0 + line-height 100px + color #fff + font-weight bold + text-shadow 0 0 8px rgba(0, 0, 0, 0.5) + +</style> diff --git a/src/web/app/reset.styl b/src/client/app/reset.styl similarity index 69% rename from src/web/app/reset.styl rename to src/client/app/reset.styl index 940a9ed18e..10bd3113a2 100644 --- a/src/web/app/reset.styl +++ b/src/client/app/reset.styl @@ -1,19 +1,7 @@ -* - position relative - box-sizing border-box - background-clip padding-box !important - -html -body - margin 0 - padding 0 - -body - overflow-wrap break-word - input:not([type]) input[type='text'] input[type='password'] +input[type='search'] input[type='email'] textarea button @@ -23,9 +11,11 @@ progress appearance none box-shadow none +textarea + font-family sans-serif + button margin 0 - padding 0 background transparent border none cursor pointer diff --git a/src/client/app/safe.js b/src/client/app/safe.js new file mode 100644 index 0000000000..2fd5361725 --- /dev/null +++ b/src/client/app/safe.js @@ -0,0 +1,31 @@ +/** + * ブラウザの検証 + */ + +// Detect an old browser +if (!('fetch' in window)) { + alert( + 'お使いのブラウザが古いためMisskeyを動作させることができません。' + + 'バージョンを最新のものに更新するか、別のブラウザをお試しください。' + + '\n\n' + + 'Your browser seems outdated. ' + + 'To run Misskey, please update your browser to latest version or try other browsers.'); +} + +// Detect Edge +if (navigator.userAgent.toLowerCase().indexOf('edge') != -1) { + alert( + '現在、お使いのブラウザ(Microsoft Edge)ではMisskeyは正しく動作しません。' + + 'サポートしているブラウザ: Google Chrome, Mozilla Firefox, Apple Safari など' + + '\n\n' + + 'Currently, Misskey cannot run correctly on your browser (Microsoft Edge). ' + + 'Supported browsers: Google Chrome, Mozilla Firefox, Apple Safari, etc'); +} + +// Check whether cookie enabled +if (!navigator.cookieEnabled) { + alert( + 'Misskeyを利用するにはCookieを有効にしてください。' + + '\n\n' + + 'To use Misskey, please enable Cookie.'); +} diff --git a/src/web/app/status/style.styl b/src/client/app/stats/style.styl similarity index 64% rename from src/web/app/status/style.styl rename to src/client/app/stats/style.styl index b48d7aeb9e..5ae230ea56 100644 --- a/src/web/app/status/style.styl +++ b/src/client/app/stats/style.styl @@ -1,4 +1,5 @@ -@import "../base" +@import "../app" +@import "../reset" html color #456267 diff --git a/src/web/app/stats/tags/index.tag b/src/client/app/stats/tags/index.tag similarity index 73% rename from src/web/app/stats/tags/index.tag rename to src/client/app/stats/tags/index.tag index 134fad3c0c..f8944c0832 100644 --- a/src/web/app/stats/tags/index.tag +++ b/src/client/app/stats/tags/index.tag @@ -1,11 +1,11 @@ <mk-index> <h1>Misskey<i>Statistics</i></h1> - <main if={ !initializing }> + <main v-if="!initializing"> <mk-users stats={ stats }/> - <mk-posts stats={ stats }/> + <mk-notes stats={ stats }/> </main> - <footer><a href={ CONFIG.url }>{ CONFIG.host }</a></footer> - <style> + <footer><a href={ _URL_ }>{ _HOST_ }</a></footer> + <style lang="stylus" scoped> :scope display block margin 0 auto @@ -40,13 +40,13 @@ > a color #546567 </style> - <script> + <script lang="typescript"> this.mixin('api'); this.initializing = true; this.on('mount', () => { - this.api('stats').then(stats => { + this.$root.$data.os.api('stats').then(stats => { this.update({ initializing: false, stats @@ -56,21 +56,21 @@ </script> </mk-index> -<mk-posts> - <h2>%i18n:stats.posts-count% <b>{ stats.posts_count }</b></h2> - <mk-posts-chart if={ !initializing } data={ data }/> - <style> +<mk-notes> + <h2>%i18n:stats.notes-count% <b>{ stats.notesCount }</b></h2> + <mk-notes-chart v-if="!initializing" data={ data }/> + <style lang="stylus" scoped> :scope display block </style> - <script> + <script lang="typescript"> this.mixin('api'); this.initializing = true; this.stats = this.opts.stats; this.on('mount', () => { - this.api('aggregation/posts', { + this.$root.$data.os.api('aggregation/notes', { limit: 365 }).then(data => { this.update({ @@ -80,23 +80,23 @@ }); }); </script> -</mk-posts> +</mk-notes> <mk-users> - <h2>%i18n:stats.users-count% <b>{ stats.users_count }</b></h2> - <mk-users-chart if={ !initializing } data={ data }/> - <style> + <h2>%i18n:stats.users-count% <b>{ stats.usersCount }</b></h2> + <mk-users-chart v-if="!initializing" data={ data }/> + <style lang="stylus" scoped> :scope display block </style> - <script> + <script lang="typescript"> this.mixin('api'); this.initializing = true; this.stats = this.opts.stats; this.on('mount', () => { - this.api('aggregation/users', { + this.$root.$data.os.api('aggregation/users', { limit: 365 }).then(data => { this.update({ @@ -108,11 +108,11 @@ </script> </mk-users> -<mk-posts-chart> +<mk-notes-chart> <svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none"> - <title>Black ... Total<br/>Blue ... Posts<br/>Red ... Replies<br/>Green ... Reposts</title> + <title>Black ... Total<br/>Blue ... Notes<br/>Red ... Replies<br/>Green ... Renotes</title> <polyline - riot-points={ pointsPost } + riot-points={ pointsNote } fill="none" stroke-width="1" stroke="#41ddde"/> @@ -122,7 +122,7 @@ stroke-width="1" stroke="#f7796c"/> <polyline - riot-points={ pointsRepost } + riot-points={ pointsRenote } fill="none" stroke-width="1" stroke="#a1de41"/> @@ -133,7 +133,7 @@ stroke="#555" stroke-dasharray="2 2"/> </svg> - <style> + <style lang="stylus" scoped> :scope display block @@ -142,12 +142,12 @@ padding 1px width 100% </style> - <script> + <script lang="typescript"> this.viewBoxX = 365; this.viewBoxY = 80; this.data = this.opts.data.reverse(); - this.data.forEach(d => d.total = d.posts + d.replies + d.reposts); + this.data.forEach(d => d.total = d.notes + d.replies + d.renotes); const peak = Math.max.apply(null, this.data.map(d => d.total)); this.on('mount', () => { @@ -156,14 +156,14 @@ this.render = () => { this.update({ - pointsPost: this.data.map((d, i) => `${i},${(1 - (d.posts / peak)) * this.viewBoxY}`).join(' '), + pointsNote: this.data.map((d, i) => `${i},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' '), pointsReply: this.data.map((d, i) => `${i},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' '), - pointsRepost: this.data.map((d, i) => `${i},${(1 - (d.reposts / peak)) * this.viewBoxY}`).join(' '), + pointsRenote: this.data.map((d, i) => `${i},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' '), pointsTotal: this.data.map((d, i) => `${i},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ') }); }; </script> -</mk-posts-chart> +</mk-notes-chart> <mk-users-chart> <svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none"> @@ -178,7 +178,7 @@ stroke-width="1" stroke="#555"/> </svg> - <style> + <style lang="stylus" scoped> :scope display block @@ -187,7 +187,7 @@ padding 1px width 100% </style> - <script> + <script lang="typescript"> this.viewBoxX = 365; this.viewBoxY = 80; diff --git a/src/web/app/stats/tags/index.js b/src/client/app/stats/tags/index.ts similarity index 100% rename from src/web/app/stats/tags/index.js rename to src/client/app/stats/tags/index.ts diff --git a/src/web/app/stats/style.styl b/src/client/app/status/style.styl similarity index 64% rename from src/web/app/stats/style.styl rename to src/client/app/status/style.styl index b48d7aeb9e..5ae230ea56 100644 --- a/src/web/app/stats/style.styl +++ b/src/client/app/status/style.styl @@ -1,4 +1,5 @@ -@import "../base" +@import "../app" +@import "../reset" html color #456267 diff --git a/src/web/app/status/tags/index.tag b/src/client/app/status/tags/index.tag similarity index 86% rename from src/web/app/status/tags/index.tag rename to src/client/app/status/tags/index.tag index 6fb6041c3c..899467097a 100644 --- a/src/web/app/status/tags/index.tag +++ b/src/client/app/status/tags/index.tag @@ -1,12 +1,12 @@ <mk-index> <h1>Misskey<i>Status</i></h1> - <p><i class="fa fa-info-circle"></i>%i18n:status.all-systems-maybe-operational%</p> + <p>%fa:info-circle%%i18n:status.all-systems-maybe-operational%</p> <main> <mk-cpu-usage connection={ connection }/> <mk-mem-usage connection={ connection }/> </main> - <footer><a href={ CONFIG.url }>{ CONFIG.host }</a></footer> - <style> + <footer><a href={ _URL_ }>{ _HOST_ }</a></footer> + <style lang="stylus" scoped> :scope display block margin 0 auto @@ -19,7 +19,7 @@ font-size 24px font-weight normal - > i + > [data-fa] font-style normal color #f43b16 @@ -31,7 +31,7 @@ //border solid 1px #99ccb2 border-radius 4px - > i + > [data-fa] margin-right 5px > main @@ -50,8 +50,8 @@ > a color #546567 </style> - <script> - import Connection from '../../common/scripts/server-stream'; + <script lang="typescript"> + import Connection from '../../common/scripts/streaming/server-stream'; this.mixin('api'); @@ -59,7 +59,7 @@ this.connection = new Connection(); this.on('mount', () => { - this.api('meta').then(meta => { + this.$root.$data.os.api('meta').then(meta => { this.update({ initializing: false, meta @@ -77,11 +77,11 @@ <mk-cpu-usage> <h2>CPU <b>{ percentage }%</b></h2> <mk-line-chart ref="chart"/> - <style> + <style lang="stylus" scoped> :scope display block </style> - <script> + <script lang="typescript"> this.connection = this.opts.connection; this.on('mount', () => { @@ -93,7 +93,7 @@ }); this.onStats = stats => { - this.refs.chart.addData(1 - stats.cpu_usage); + this.$refs.chart.addData(1 - stats.cpu_usage); const percentage = (stats.cpu_usage * 100).toFixed(0); @@ -107,11 +107,11 @@ <mk-mem-usage> <h2>MEM <b>{ percentage }%</b></h2> <mk-line-chart ref="chart"/> - <style> + <style lang="stylus" scoped> :scope display block </style> - <script> + <script lang="typescript"> this.connection = this.opts.connection; this.on('mount', () => { @@ -124,7 +124,7 @@ this.onStats = stats => { stats.mem.used = stats.mem.total - stats.mem.free; - this.refs.chart.addData(1 - (stats.mem.used / stats.mem.total)); + this.$refs.chart.addData(1 - (stats.mem.used / stats.mem.total)); const percentage = (stats.mem.used / stats.mem.total * 100).toFixed(0); @@ -164,7 +164,7 @@ stroke="#f43b16" stroke-width="0.5"/> </svg> - <style> + <style lang="stylus" scoped> :scope display block padding 16px @@ -176,8 +176,8 @@ padding 1px width 100% </style> - <script> - import uuid from '../../common/scripts/uuid'; + <script lang="typescript"> + import uuid from 'uuid'; this.viewBoxX = 100; this.viewBoxY = 30; diff --git a/src/web/app/status/tags/index.js b/src/client/app/status/tags/index.ts similarity index 100% rename from src/web/app/status/tags/index.js rename to src/client/app/status/tags/index.ts diff --git a/src/client/app/sw.js b/src/client/app/sw.js new file mode 100644 index 0000000000..ac7ea20acf --- /dev/null +++ b/src/client/app/sw.js @@ -0,0 +1,67 @@ +/** + * Service Worker + */ + +import composeNotification from './common/scripts/compose-notification'; + +// キャッシュするリソース +const cachee = [ + '/' +]; + +// インストールされたとき +self.addEventListener('install', ev => { + console.info('installed'); + + ev.waitUntil(Promise.all([ + self.skipWaiting(), // Force activate + caches.open(_VERSION_).then(cache => cache.addAll(cachee)) // Cache + ])); +}); + +// アクティベートされたとき +self.addEventListener('activate', ev => { + // Clean up old caches + ev.waitUntil( + caches.keys().then(keys => Promise.all( + keys + .filter(key => key != _VERSION_) + .map(key => caches.delete(key)) + )) + ); +}); + +// リクエストが発生したとき +self.addEventListener('fetch', ev => { + ev.respondWith( + // キャッシュがあるか確認してあればそれを返す + caches.match(ev.request).then(response => + response || fetch(ev.request) + ) + ); +}); + +// プッシュ通知を受け取ったとき +self.addEventListener('push', ev => { + // クライアント取得 + ev.waitUntil(self.clients.matchAll({ + includeUncontrolled: true + }).then(clients => { + // クライアントがあったらストリームに接続しているということなので通知しない + if (clients.length != 0) return; + + const { type, body } = ev.data.json(); + + const n = composeNotification(type, body); + return self.registration.showNotification(n.title, { + body: n.body, + icon: n.icon, + }); + })); +}); + +self.addEventListener('message', ev => { + if (ev.data == 'clear') { + caches.keys().then(keys => keys.forEach(key => caches.delete(key))); + } +}); diff --git a/src/tsconfig.json b/src/client/app/tsconfig.json similarity index 81% rename from src/tsconfig.json rename to src/client/app/tsconfig.json index ecff047a74..e31b52dab1 100644 --- a/src/tsconfig.json +++ b/src/client/app/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "allowJs": true, "noEmitOnError": false, "noImplicitAny": false, "noImplicitReturns": true, @@ -11,13 +12,12 @@ "target": "es2017", "module": "commonjs", "removeComments": false, - "noLib": false + "noLib": false, + "strict": true, + "strictNullChecks": false }, "compileOnSave": false, "include": [ "./**/*.ts" - ], - "exclude": [ - "./web/app/**/*.ts" ] } diff --git a/src/client/app/v.d.ts b/src/client/app/v.d.ts new file mode 100644 index 0000000000..8f3a240d80 --- /dev/null +++ b/src/client/app/v.d.ts @@ -0,0 +1,4 @@ +declare module "*.vue" { + import Vue from 'vue'; + export default Vue; +} diff --git a/src/client/assets/404.js b/src/client/assets/404.js new file mode 100644 index 0000000000..9e498fe7c2 --- /dev/null +++ b/src/client/assets/404.js @@ -0,0 +1,25 @@ +const yn = window.confirm( + 'サーバー上に存在しないスクリプトがリクエストされました。お使いのMisskeyのバージョンが古いことが原因の可能性があります。Misskeyを更新しますか?\n\nA script that does not exist on the server was requested. It may be caused by an old version of Misskey you’re using. Do you want to delete the cache?'); + +const langYn = window.confirm('また、言語を日本語に設定すると解決する場合があります。日本語に設定しますか?\n\nAlso, setting the language to Japanese may solve the problem. Would you like to set it to Japanese?'); + +if (langYn) { + localStorage.setItem('lang', 'ja'); +} + +if (yn) { + // Clear cache (serive worker) + try { + navigator.serviceWorker.controller.postMessage('clear'); + + navigator.serviceWorker.getRegistrations().then(registrations => { + registrations.forEach(registration => registration.unregister()); + }); + } catch (e) { + console.error(e); + } + + localStorage.removeItem('v'); + + location.reload(true); +} diff --git a/src/client/assets/code-highlight.css b/src/client/assets/code-highlight.css new file mode 100644 index 0000000000..f0807dc9c3 --- /dev/null +++ b/src/client/assets/code-highlight.css @@ -0,0 +1,93 @@ +.hljs { + font-family: Consolas, 'Courier New', Courier, Monaco, monospace; +} + +.hljs, +.hljs-subst { + color: #444; +} + +.hljs-comment { + color: #888888; +} + +.hljs-keyword { + color: #2973b7; +} + +.hljs-number { + color: #ae81ff; +} + +.hljs-string { + color: #e96900; +} + +.hljs-regexp { + color: #e9003f; +} + +.hljs-attribute, +.hljs-selector-tag, +.hljs-meta-keyword, +.hljs-doctag, +.hljs-name { + font-weight: bold; +} + +.hljs-type, +.hljs-selector-id, +.hljs-selector-class, +.hljs-quote, +.hljs-template-tag, +.hljs-deletion { + color: #880000; +} + +.hljs-title, +.hljs-section { + color: #880000; + font-weight: bold; +} + +.hljs-symbol, +.hljs-variable, +.hljs-template-variable, +.hljs-link, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #BC6060; +} + +/* Language color: hue: 90; */ + +.hljs-literal { + color: #78A960; +} + +.hljs-built_in, +.hljs-bullet, +.hljs-code, +.hljs-addition { + color: #397300; +} + +/* Meta color: hue: 200 */ + +.hljs-meta { + color: #1f7199; +} + +.hljs-meta-string { + color: #4d99bf; +} + +/* Misc effects */ + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/src/web/assets/error.jpg b/src/client/assets/error.jpg similarity index 100% rename from src/web/assets/error.jpg rename to src/client/assets/error.jpg diff --git a/src/web/assets/label.svg b/src/client/assets/label.svg similarity index 100% rename from src/web/assets/label.svg rename to src/client/assets/label.svg diff --git a/src/client/assets/manifest.json b/src/client/assets/manifest.json new file mode 100644 index 0000000000..a0f6745b01 --- /dev/null +++ b/src/client/assets/manifest.json @@ -0,0 +1,14 @@ +{ + "short_name": "Misskey", + "name": "Misskey", + "start_url": "/", + "display": "standalone", + "background_color": "#313a42", + "icons": { + "16": "/assets/favicon/16.png", + "32": "/assets/favicon/32.png", + "64": "/assets/favicon/64.png", + "128": "/assets/favicon/128.png", + "256": "/assets/favicon/256.png" + } +} diff --git a/src/client/assets/message.mp3 b/src/client/assets/message.mp3 new file mode 100644 index 0000000000..6427744475 Binary files /dev/null and b/src/client/assets/message.mp3 differ diff --git a/src/client/assets/othello-put-me.mp3 b/src/client/assets/othello-put-me.mp3 new file mode 100644 index 0000000000..4e0e72091c Binary files /dev/null and b/src/client/assets/othello-put-me.mp3 differ diff --git a/src/client/assets/othello-put-you.mp3 b/src/client/assets/othello-put-you.mp3 new file mode 100644 index 0000000000..9244189c2d Binary files /dev/null and b/src/client/assets/othello-put-you.mp3 differ diff --git a/src/client/assets/post.mp3 b/src/client/assets/post.mp3 new file mode 100644 index 0000000000..d3da88a933 Binary files /dev/null and b/src/client/assets/post.mp3 differ diff --git a/src/web/assets/reactions/angry.png b/src/client/assets/reactions/angry.png similarity index 100% rename from src/web/assets/reactions/angry.png rename to src/client/assets/reactions/angry.png diff --git a/src/web/assets/reactions/confused.png b/src/client/assets/reactions/confused.png similarity index 100% rename from src/web/assets/reactions/confused.png rename to src/client/assets/reactions/confused.png diff --git a/src/web/assets/reactions/congrats.png b/src/client/assets/reactions/congrats.png similarity index 100% rename from src/web/assets/reactions/congrats.png rename to src/client/assets/reactions/congrats.png diff --git a/src/web/assets/reactions/hmm.png b/src/client/assets/reactions/hmm.png similarity index 100% rename from src/web/assets/reactions/hmm.png rename to src/client/assets/reactions/hmm.png diff --git a/src/web/assets/reactions/laugh.png b/src/client/assets/reactions/laugh.png similarity index 100% rename from src/web/assets/reactions/laugh.png rename to src/client/assets/reactions/laugh.png diff --git a/src/web/assets/reactions/like.png b/src/client/assets/reactions/like.png similarity index 100% rename from src/web/assets/reactions/like.png rename to src/client/assets/reactions/like.png diff --git a/src/web/assets/reactions/love.png b/src/client/assets/reactions/love.png similarity index 100% rename from src/web/assets/reactions/love.png rename to src/client/assets/reactions/love.png diff --git a/src/web/assets/reactions/pudding.png b/src/client/assets/reactions/pudding.png similarity index 100% rename from src/web/assets/reactions/pudding.png rename to src/client/assets/reactions/pudding.png diff --git a/src/web/assets/reactions/surprise.png b/src/client/assets/reactions/surprise.png similarity index 100% rename from src/web/assets/reactions/surprise.png rename to src/client/assets/reactions/surprise.png diff --git a/src/client/assets/recover.html b/src/client/assets/recover.html new file mode 100644 index 0000000000..b1889c72e6 --- /dev/null +++ b/src/client/assets/recover.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> + +<html> + <head> + <meta charset="utf-8"> + <title>Misskeyのリカバリ</title> + <script> + const yn = window.confirm('キャッシュをクリアしますか?(他のタブでMisskeyを開いている状態だと正常にクリアできないので、他のMisskeyのタブをすべて閉じてから行ってください)\n\nDo you want to clear caches? (Please close all other Misskey tabs before clear cache)'); + if (yn) { + try { + navigator.serviceWorker.controller.postMessage('clear'); + navigator.serviceWorker.getRegistrations().then(registrations => { + registrations.forEach(registration => registration.unregister()); + }); + } catch (e) { + console.error(e); + } + alert('キャッシュをクリアしました。\n\ncache cleared.'); + alert('まもなくページを再度読み込みします。再度読み込みが終わると、再度キャッシュをクリアするか尋ねられるので、「キャンセル」を選択して抜けてください。\n\nWe will reload the page shortly. After that, you are asked whether you want to clear the cache again, so please select "Cancel" and exit.'); + setTimeout(() => { + location.reload(true); + }, 100); + } else { + location.href = '/'; + } + </script> + </head> +</html> diff --git a/src/web/assets/title.svg b/src/client/assets/title.svg similarity index 100% rename from src/web/assets/title.svg rename to src/client/assets/title.svg diff --git a/src/web/assets/unread.svg b/src/client/assets/unread.svg similarity index 100% rename from src/web/assets/unread.svg rename to src/client/assets/unread.svg diff --git a/src/client/assets/version.html b/src/client/assets/version.html new file mode 100644 index 0000000000..d8a98279a6 --- /dev/null +++ b/src/client/assets/version.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> + +<html> + <head> + <meta charset="utf-8"> + <title>Misskeyのリカバリ</title> + <script> + const v = window.prompt('Enter version:'); + if (v) { + localStorage.setItem('v', v); + } + + const lang = window.prompt('Enter language (optional):'); + if (lang && lang.length > 0) { + localStorage.setItem('lang', lang); + } + + setTimeout(() => { + location.href = '/'; + }, 500); + </script> + </head> +</html> diff --git a/src/client/assets/welcome-bg.svg b/src/client/assets/welcome-bg.svg new file mode 100644 index 0000000000..ba8cd8dc0a --- /dev/null +++ b/src/client/assets/welcome-bg.svg @@ -0,0 +1,579 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:osb="http://www.openswatchbook.org/uri/2009/osb" + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="1920" + height="1080" + viewBox="0 0 507.99999 285.75001" + version="1.1" + id="svg8" + inkscape:version="0.92.1 r15371" + sodipodi:docname="welcome-bg.svg"> + <defs + id="defs2"> + <pattern + inkscape:collect="always" + xlink:href="#Checkerboard" + id="pattern7194" + patternTransform="scale(1.3152942)" /> + <linearGradient + id="linearGradient7169" + inkscape:collect="always"> + <stop + id="stop7165" + offset="0" + style="stop-color:#eaeaea;stop-opacity:1" /> + <stop + id="stop7167" + offset="1" + style="stop-color:#000000;stop-opacity:1" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + id="linearGradient7044"> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0" + id="stop7040" /> + <stop + style="stop-color:#ffffff;stop-opacity:1" + offset="1" + id="stop7042" /> + </linearGradient> + <pattern + inkscape:collect="always" + xlink:href="#Checkerboard" + id="pattern7010" + patternTransform="matrix(1.673813,0,0,1.673813,-177.6001,-146.38611)" /> + <pattern + inkscape:stockid="Checkerboard" + id="Checkerboard" + patternTransform="translate(0,0) scale(10,10)" + height="2" + width="2" + patternUnits="userSpaceOnUse" + inkscape:collect="always" + inkscape:isstock="true"> + <rect + id="rect6201" + height="1" + width="1" + y="0" + x="0" + style="fill:black;stroke:none" /> + <rect + id="rect6203" + height="1" + width="1" + y="1" + x="1" + style="fill:black;stroke:none" /> + </pattern> + <linearGradient + id="linearGradient5406" + osb:paint="solid"> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0" + id="stop5404" /> + </linearGradient> + <pattern + patternUnits="userSpaceOnUse" + width="15.999999" + height="16.000025" + patternTransform="matrix(0.26458333,0,0,0.26458333,-16.933332,263.1333)" + id="pattern6465"> + <path + d="m 8.0000542,8.0000126 h 7.9998878 c 3e-5,0 5.7e-5,3.78e-5 5.7e-5,3.78e-5 V 15.99995 c 0,3.7e-5 -2.7e-5,7.5e-5 -5.7e-5,7.5e-5 H 8.0000542 c -3.03e-5,0 -5.67e-5,-3.8e-5 -5.67e-5,-7.5e-5 V 8.0000504 c 0,0 2.64e-5,-3.78e-5 5.67e-5,-3.78e-5 z M 5.6692913e-5,0 H 7.9999408 c 3.02e-5,0 5.67e-5,3.7795275e-5 5.67e-5,7.5590551e-5 V 7.999937 c 0,3.78e-5 -2.65e-5,7.56e-5 -5.67e-5,7.56e-5 H 5.6692913e-5 C 2.2677165e-5,8.0000126 0,7.9999748 0,7.999937 V 7.5590551e-5 C 0,3.7795276e-5 2.2677165e-5,0 5.6692913e-5,0 Z" + style="opacity:1;fill:#db1545;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:15.99999905;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="rect6445-2" + inkscape:connector-curvature="0" /> + </pattern> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient7044" + id="linearGradient6476" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(3.223659,0,0,2.5556636,-579.27357,808.39)" + x1="86.490868" + y1="-216.62756" + x2="176.77992" + y2="-216.62756" /> + <mask + maskUnits="userSpaceOnUse" + id="mask6472"> + <rect + transform="rotate(-90)" + y="-0.91986513" + x="-300.45657" + height="511.36566" + width="291.06116" + id="rect6474" + style="opacity:1;fill:url(#linearGradient6476);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.92238116;stroke-miterlimit:4;stroke-dasharray:none" /> + </mask> + <pattern + patternUnits="userSpaceOnUse" + width="2340.7208" + height="2340.7236" + patternTransform="matrix(0.26458333,0,0,0.26458333,-63.499801,-58.601683)" + id="pattern7142"> + <path + d="m 1170.3684,1170.3628 h 1170.3448 c 0,0 0.01,0 0.01,0 v 1170.3457 c 0,0 0,0.011 -0.01,0.011 H 1170.3684 c 0,0 -0.01,0 -0.01,-0.011 v -1170.344 c 0,0 0,0 0.01,0 z M 0.00869291,1.1338583e-5 H 1170.352 c 0,0 0.01,0.0052913414 0.01,0.01096063142 V 1170.3511 c 0,0 0,0.011 -0.01,0.011 H 0.00869291 C 0.00340157,1170.3625 0,1170.3549 0,1170.3511 V 0.01096063 C 0,0.00566929 0.00312945,0 0.00869291,0 Z" + style="opacity:1;fill:#763971;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2340.72119141;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path7135" + inkscape:connector-curvature="0" /> + </pattern> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient7169" + id="linearGradient7157" + x1="-3.631536" + y1="155.11069" + x2="511.52777" + y2="155.11069" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(2.184742,0,0,6.5696504,-17.948376,-1979.8074)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient7169" + id="linearGradient7200" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.57804632,0,0,1.73822,6.5011419,-523.82404)" + x1="-3.631536" + y1="155.11069" + x2="511.52777" + y2="155.11069" /> + <mask + maskUnits="userSpaceOnUse" + id="mask7196"> + <rect + transform="rotate(90)" + y="-512.56537" + x="4.4019437" + height="516.7157" + width="297.78595" + id="rect7198" + style="opacity:1;fill:url(#linearGradient7200);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.1217103;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + </mask> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#1e1d65" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.84705882" + inkscape:pageshadow="2" + inkscape:zoom="0.79170474" + inkscape:cx="1093.7227" + inkscape:cy="695.27372" + inkscape:document-units="mm" + inkscape:current-layer="layer5" + showgrid="true" + units="px" + inkscape:pagecheckerboard="false" + inkscape:window-width="1920" + inkscape:window-height="1017" + inkscape:window-x="-8" + inkscape:window-y="1072" + inkscape:window-maximized="1" + objecttolerance="1" + guidetolerance="10000" + gridtolerance="10000" + inkscape:snap-bbox="true" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + showguides="false" + inkscape:lockguides="true"> + <inkscape:grid + type="xygrid" + id="grid6443" + spacingx="2.1166667" + spacingy="2.1166667" + empspacing="4" + color="#3f3fff" + opacity="0.1254902" + enabled="false" /> + <sodipodi:guide + position="-69.219003,3.872392" + orientation="1,0" + id="guide6508" + inkscape:locked="true" /> + </sodipodi:namedview> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="レイヤー 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(0,-11.249983)" + style="display:inline" + sodipodi:insensitive="true"> + <rect + style="display:inline;opacity:0.2;fill:url(#pattern7194);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="rect7178" + width="568.07599" + height="367.82269" + x="-37.871731" + y="-52.665051" + mask="url(#mask7196)" /> + </g> + <g + inkscape:groupmode="layer" + id="layer2" + inkscape:label="レイヤー 2" + style="display:inline"> + <rect + transform="translate(0,-11.249983)" + style="opacity:0.5;fill:none;stroke:none;stroke-width:140.99996948" + width="596.8999" + height="596.90082" + x="-63.49987" + y="-58.600021" + id="rect6468" + mask="url(#mask6472)" /> + <path + transform="translate(0,-11.249983)" + sodipodi:type="star" + style="fill:#000000;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none" + id="path6921" + sodipodi:sides="4" + sodipodi:cx="117.63232" + sodipodi:cy="102.13793" + sodipodi:r1="5.7652407" + sodipodi:r2="2.8826203" + sodipodi:arg1="1.4464413" + sodipodi:arg2="2.2318395" + inkscape:flatsided="false" + inkscape:rounded="0" + inkscape:randomized="0" + d="m 118.34741,107.85865 -2.48485,-3.44532 -3.95096,-1.56031 3.44531,-2.48485 1.56032,-3.950959 2.48484,3.445318 3.95097,1.560311 -3.44532,2.48485 z" + inkscape:transform-center-x="1.481982e-006" + inkscape:transform-center-y="-1.1450451e-006" /> + <path + transform="translate(0,-11.249983)" + sodipodi:type="star" + style="fill:#000000;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none" + id="path6923" + sodipodi:sides="4" + sodipodi:cx="317.5" + sodipodi:cy="75.679596" + sodipodi:r1="3.949214" + sodipodi:r2="1.974607" + sodipodi:arg1="1.6614562" + sodipodi:arg2="2.4468544" + inkscape:flatsided="false" + inkscape:rounded="0" + inkscape:randomized="0" + d="m 317.14246,79.612591 -1.1594,-2.668882 -2.41606,-1.621658 2.66889,-1.15939 1.62165,-2.41606 1.1594,2.668882 2.41606,1.621658 -2.66889,1.15939 z" + inkscape:transform-center-x="4.0000001e-006" /> + <path + transform="translate(0,-11.249983)" + sodipodi:type="star" + style="fill:#000000;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none" + id="path6925" + sodipodi:sides="4" + sodipodi:cx="230.97409" + sodipodi:cy="57.802349" + sodipodi:r1="2.2613134" + sodipodi:r2="1.1306567" + sodipodi:arg1="1.2490458" + sodipodi:arg2="2.0344439" + inkscape:flatsided="false" + inkscape:rounded="0" + inkscape:randomized="0" + d="m 231.68918,59.947619 -1.22073,-1.13398 -1.63963,-0.2962 1.13398,-1.220735 0.2962,-1.639625 1.22074,1.13398 1.63962,0.2962 -1.13398,1.220735 z" + inkscape:transform-center-x="2.9099099e-006" /> + <path + transform="translate(0,-11.249983)" + sodipodi:type="star" + style="fill:#000000;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none" + id="path6927" + sodipodi:sides="4" + sodipodi:cx="260.65033" + sodipodi:cy="106.42847" + sodipodi:r1="1.59899" + sodipodi:r2="0.79949504" + sodipodi:arg1="2.0344439" + sodipodi:arg2="2.8198421" + inkscape:flatsided="false" + inkscape:rounded="0" + inkscape:randomized="0" + d="m 259.93524,107.85865 -0.0434,-1.17736 -0.67171,-0.96791 1.17736,-0.0434 0.96791,-0.67171 0.0434,1.17735 0.67171,0.96792 -1.17736,0.0434 z" + inkscape:transform-center-x="3.2837838e-006" + inkscape:transform-center-y="-1.1990991e-006" /> + <path + sodipodi:type="star" + style="fill:#000000;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none" + id="path6925-2" + sodipodi:sides="4" + sodipodi:cx="87.956078" + sodipodi:cy="127.16609" + sodipodi:r1="2.2613134" + sodipodi:r2="1.1306567" + sodipodi:arg1="1.2490458" + sodipodi:arg2="2.0344439" + inkscape:flatsided="false" + inkscape:rounded="0" + inkscape:randomized="0" + d="m 88.671168,129.31136 -1.220735,-1.13398 -1.639626,-0.2962 1.13398,-1.22073 0.296201,-1.63963 1.220735,1.13398 1.639625,0.2962 -1.13398,1.22074 z" + inkscape:transform-center-x="2.4830149e-006" + transform="matrix(0.91666666,0,0,1,7.1509006,-11.249983)" /> + <ellipse + style="opacity:0.68000034;fill:#6e76a3;fill-opacity:1;stroke:none;stroke-width:0.06383465" + id="path5313-3-7" + cx="178.44102" + cy="110.95996" + rx="21.691566" + ry="5.0825601" + transform="rotate(-1.570553,-410.38805,-5.6250559)" /> + <ellipse + style="opacity:0.68000034;fill:#6e76a3;fill-opacity:1;stroke:none;stroke-width:0.08063243" + id="path5313-3-7-5" + cx="200.1326" + cy="116.80371" + rx="27.399597" + ry="6.4200115" + transform="rotate(-1.570553,-410.38805,-5.6250559)" /> + <ellipse + style="opacity:0.68000034;fill:#6e76a3;fill-opacity:1;stroke:none;stroke-width:0.06734787" + id="path5313-3-7-2" + cx="-429.23041" + cy="90.631134" + rx="24.144913" + ry="5.0825605" + transform="matrix(-0.99537478,-0.09606802,-0.09606802,0.99537478,0,-11.249983)" /> + <ellipse + style="opacity:0.68000034;fill:#6e76a3;fill-opacity:1;stroke:none;stroke-width:0.08507013" + id="path5313-3-7-5-9" + cx="-405.08548" + cy="96.474884" + rx="30.498529" + ry="6.4200115" + transform="matrix(-0.99537478,-0.09606802,-0.09606802,0.99537478,0,-11.249983)" /> + <ellipse + style="opacity:0.68000034;fill:#6e76a3;fill-opacity:1;stroke:none;stroke-width:0.05208009" + id="path5313-3-7-2-9" + cx="-46.428764" + cy="163.90004" + rx="18.893074" + ry="3.884198" + transform="matrix(-0.99073724,0.13579293,0.14607844,0.98927301,0,-11.249983)" /> + <ellipse + style="opacity:0.68000034;fill:#6e76a3;fill-opacity:1;stroke:none;stroke-width:0.06578472" + id="path5313-3-7-5-9-1" + cx="-27.535677" + cy="168.36595" + rx="23.864695" + ry="4.9063048" + transform="matrix(-0.99073724,0.13579293,0.14607844,0.98927301,0,-11.249983)" /> + <path + transform="translate(0,-11.249983)" + sodipodi:type="star" + style="fill:#000000;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none" + id="path6923-9" + sodipodi:sides="4" + sodipodi:cx="459.82239" + sodipodi:cy="139.8455" + sodipodi:r1="3.949214" + sodipodi:r2="1.9746071" + sodipodi:arg1="1.6614562" + sodipodi:arg2="2.4468544" + inkscape:flatsided="false" + inkscape:rounded="0" + inkscape:randomized="0" + d="m 459.46484,143.7785 -1.15939,-2.66888 -2.41606,-1.62166 2.66889,-1.15939 1.62165,-2.41606 1.15939,2.66888 2.41606,1.62166 -2.66888,1.15939 z" + inkscape:transform-center-x="4.0000001e-006" /> + <circle + transform="translate(0,-11.249983)" + style="opacity:0.1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0.81509405;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path5229" + cx="192.18326" + cy="74.677902" + r="2.7216933" /> + <path + sodipodi:type="star" + style="fill:#ffffff;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none" + id="path6923-8" + sodipodi:sides="4" + sodipodi:cx="53.989292" + sodipodi:cy="88.908768" + sodipodi:r1="3.949214" + sodipodi:r2="1.9746071" + sodipodi:arg1="1.6614562" + sodipodi:arg2="2.4468544" + inkscape:flatsided="false" + inkscape:rounded="0" + inkscape:randomized="0" + d="m 53.631747,92.841763 -1.15939,-2.668883 -2.41606,-1.621657 2.668883,-1.159391 1.621657,-2.41606 1.15939,2.668883 2.416061,1.621658 -2.668883,1.15939 z" + inkscape:transform-center-x="2.0634674e-006" + transform="matrix(0.61390676,-0.48689202,0.48689202,0.61390676,-23.159158,48.648961)" + inkscape:transform-center-y="1.4320049e-006" /> + <path + sodipodi:type="star" + style="fill:#ffffff;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none" + id="path6923-8-3" + sodipodi:sides="4" + sodipodi:cx="53.989292" + sodipodi:cy="88.908768" + sodipodi:r1="3.949214" + sodipodi:r2="1.9746071" + sodipodi:arg1="1.6614562" + sodipodi:arg2="2.4468544" + inkscape:flatsided="false" + inkscape:rounded="0" + inkscape:randomized="0" + d="m 53.631747,92.841763 -1.15939,-2.668883 -2.41606,-1.621657 2.668883,-1.159391 1.621657,-2.41606 1.15939,2.668883 2.416061,1.621658 -2.668883,1.15939 z" + inkscape:transform-center-x="3.0260172e-006" + transform="matrix(0.58032639,0.43093706,-0.43093706,0.58032639,446.58431,23.35553)" + inkscape:transform-center-y="-1.3594204e-006" /> + <circle + transform="translate(0,-11.249983)" + style="opacity:0.1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.28035584;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path5229-6" + cx="347.17841" + cy="36.709366" + r="0.9361406" /> + <circle + transform="translate(0,-11.249983)" + style="opacity:0.1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.28035584;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path5229-6-5" + cx="116.0927" + cy="42.136036" + r="0.9361406" /> + <circle + transform="translate(0,-11.249983)" + style="opacity:0.15;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.55002564;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path5229-0" + cx="456.28247" + cy="47.488548" + r="1.8365992" /> + </g> + <g + inkscape:groupmode="layer" + id="layer5" + inkscape:label="レイヤー 4" + style="display:none"> + <path + transform="translate(0,-11.249983)" + style="display:inline;fill:#ffff7c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none" + d="m 377.25876,69.781182 a 18.234796,18.234796 0 0 1 8.1747,15.19442 18.234796,18.234796 0 0 1 -18.23455,18.235058 18.234796,18.234796 0 0 1 -10.14098,-3.08921 20.380066,20.380066 0 0 0 17.64905,10.2402 20.380066,20.380066 0 0 0 20.38015,-20.380152 20.380066,20.380066 0 0 0 -17.82837,-20.200316 z" + id="path6914" + inkscape:connector-curvature="0" /> + </g> + <g + inkscape:groupmode="layer" + id="layer4" + inkscape:label="レイヤー 3" + style="display:none"> + <circle + style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.36438358;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path5306" + cx="168.31279" + cy="2.1908164" + r="36.253109" /> + <path + style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.39123487px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 201.1259,19.428383 2.66976,2.617062 1.21734,-1.978474 -0.34264,5.194221 -4.15215,2.110811 1.0283,-1.928856 -2.76172,-2.210044 z" + id="path5168" + inkscape:connector-curvature="0" /> + <path + style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.89719725px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 196.25421,26.631949 6.0286,8.817373 -3.70059,3.384671 -1.84127,-4.638447 -2.48924,2.916491 -2.23471,-6.507119 z" + id="path5174" + inkscape:connector-curvature="0" /> + <path + style="display:inline;opacity:0.1;fill:#000500;fill-opacity:1;stroke:none;stroke-width:0.05121958px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 187.00695,34.050482 1.26268,2.214392 1.44195,-0.54357 1.31981,0.86123 0.21375,1.739039 -1.36828,1.61618 -1.80409,0.265403 -1.1589,-1.059687 -0.23516,-1.721875 1.11047,-0.916698 -0.43413,-0.680502 -0.4102,0.997264 0.74387,1.070883 -0.49255,1.027197 -1.26776,0.228606 -0.5501,-0.871237 0.15467,-0.82956 0.93559,-0.424446 0.58058,-1.450625 -0.75664,-1.131455 z" + id="path6985" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccccccccccccccccc" /> + <path + style="display:inline;opacity:0.1;fill:#000016;fill-opacity:1;stroke:none;stroke-width:0.04695854px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 203.23593,14.367789 4.43345,3.766934 0.87976,-0.995725 0.46812,0.475437 -0.80488,0.995031 0.83731,0.705238 0.86731,-0.962102 0.50998,0.516259 -0.87206,0.921255 0.99505,0.941692 -0.44277,0.42746 -0.91483,-0.900095 -0.8367,0.879711 -0.43031,-0.474867 0.78065,-0.831436 -0.86665,-0.779727 -0.81136,0.912638 -0.55866,-0.483362 0.8179,-0.927279 -4.48211,-3.638676 z" + id="path6891-8" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccccccccccccccccc" /> + <path + style="display:inline;opacity:0.05;fill:#000016;fill-opacity:1;stroke:none;stroke-width:0.58045781px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 204.43932,-5.3971152 6.34563,7.5781721 -3.73895,4.9604312 0.33681,4.6546149 -5.20345,5.793617 c 2.83273,-8.049795 3.31033,-11.8140092 3.09986,-18.9271334 z" + id="path5208" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccc" /> + <path + style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.11183073px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205.60259,0.56695919 1.24493,0.127049 0.0916,-0.59592195 0.28719,0.07174803 -0.065,0.56786179 0.62071,0.0788993 -0.0423,0.36840374 -0.62423,-0.048236 -0.0804,0.8381885 0.52004,0.075191 -0.0192,0.3709729 -0.5764,-0.058257 -0.10087,0.8125312 0.54747,0.039404 -0.04,0.4153104 -0.5593,-0.071919 -0.0636,0.6224815 -0.3736,0.00386 0.0816,-0.6437327 -1.20305,-0.1533942 0.0499,-0.3674909 1.2006,0.1064631 0.11092,-0.7647515 -1.19622,-0.1448386 0.027,-0.3701253 1.23042,0.1176518 0.12327,-0.8721654 -1.26199,-0.1134749 z" + id="path7229" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccccccccccccccccccccccccc" /> + <path + style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.16325578px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 204.68821,9.1424652 1.78173,-0.049987 -1.44996,0.7563273 1.12166,0.7127945 -1.34099,0.0029 0.93885,1.309289 -1.59949,-0.942185 z" + id="path7212-4-6" + inkscape:connector-curvature="0" /> + <path + style="display:inline;opacity:0.05;fill:#000016;fill-opacity:1;stroke:none;stroke-width:0.71902335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 180.87434,36.932251 -8.12162,8.095249 -6.61262,-3.934427 -5.68596,1.043018 -7.6496,-6.371879 c 10.33078,4.527622 19.43137,4.062311 28.0698,1.168039 z" + id="path5208-6" + inkscape:connector-curvature="0" + sodipodi:nodetypes="cccccc" /> + <path + style="display:inline;opacity:0.1;fill:#000016;fill-opacity:1;stroke:none;stroke-width:0.04569969px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 156.79314,37.138611 -0.83209,5.600235 1.27513,0.214749 -0.15211,0.631281 -1.23602,-0.153244 -0.15211,1.0545 1.24093,0.221743 -0.16427,0.686859 -1.20964,-0.246683 -0.26626,1.306416 -0.58089,-0.145968 0.27316,-1.218758 -1.15712,-0.238846 0.17092,-0.599741 1.08842,0.21735 0.19853,-1.117028 -1.17126,-0.200972 0.11204,-0.710141 1.18676,0.198837 0.70106,-5.574493 z" + id="path6891-8-9" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccccccccccccccccc" /> + <path + style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.84177661px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 143.96364,29.933272 -4.59686,9.216397 3.65156,2.834687 1.22043,-4.692866 2.51661,2.524357 1.39851,-6.542721 z" + id="path5174-1" + inkscape:connector-curvature="0" /> + <path + style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.56489706px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 142.60658,28.70585 -2.96842,6.930652 -3.79379,-3.925042 4.56394,-5.124749 z" + id="path5285" + inkscape:connector-curvature="0" /> + <path + style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.35393918px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 137.9306,23.319484 -3.42616,1.224261 1.2143,1.906916 -4.40128,-2.508612 -0.0822,-4.53226 1.25123,1.720316 3.10894,-1.477793 z" + id="path5168-0" + inkscape:connector-curvature="0" /> + <path + style="display:inline;opacity:0.1;fill:#000500;fill-opacity:1;stroke:none;stroke-width:0.0498465px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 132.55595,11.444656 -2.31852,0.882408 0.30663,1.468015 -1.02588,1.140069 -1.70428,-0.05499 -1.34908,-1.557886 0.015,-1.774566 1.1926,-0.955614 1.69096,0.03182 0.7151,1.205156 0.71942,-0.315492 -0.89748,-0.543864 -1.14121,0.554849 -0.91394,-0.627513 -0.0299,-1.2533405 0.92017,-0.3984462 0.77453,0.2730438 0.26797,0.9632459 1.30792,0.775623 1.20137,-0.558052 z" + id="path6985-7" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccccccccccccccccc" /> + <path + style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15882961px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 131.32384,2.4817954 -1.6313,-0.4305236 1.16551,1.0474206 -1.19547,0.453907 1.23564,0.290212 -1.16202,1.0740836 1.68796,-0.5749329 z" + id="path7212-4-6-8" + inkscape:connector-curvature="0" /> + <path + style="display:inline;opacity:0.05;fill:#000016;fill-opacity:1;stroke:none;stroke-width:0.55575538px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 137.04207,-21.420699 -7.13207,5.035868 1.31743,5.70794 -2.10914,4.1341529 2.26645,6.93249012 c 0.67636,-8.23493742 2.69888,-15.39599902 5.65733,-21.81045102 z" + id="path5208-4" + inkscape:connector-curvature="0" + sodipodi:nodetypes="cccccc" /> + </g> +</svg> diff --git a/src/client/assets/welcome-fg.svg b/src/client/assets/welcome-fg.svg new file mode 100644 index 0000000000..5c795c3027 --- /dev/null +++ b/src/client/assets/welcome-fg.svg @@ -0,0 +1,380 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:osb="http://www.openswatchbook.org/uri/2009/osb" + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="1920" + height="1080" + viewBox="0 0 507.99999 285.75001" + version="1.1" + id="svg8" + inkscape:version="0.92.1 r15371" + sodipodi:docname="welcome-fg.svg"> + <defs + id="defs2"> + <linearGradient + inkscape:collect="always" + id="linearGradient7044"> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0" + id="stop7040" /> + <stop + style="stop-color:#ffffff;stop-opacity:1" + offset="1" + id="stop7042" /> + </linearGradient> + <pattern + inkscape:collect="always" + xlink:href="#Checkerboard" + id="pattern7010" + patternTransform="matrix(1.673813,0,0,1.673813,-177.6001,-146.38611)" /> + <pattern + inkscape:stockid="Checkerboard" + id="Checkerboard" + patternTransform="translate(0,0) scale(10,10)" + height="2" + width="2" + patternUnits="userSpaceOnUse" + inkscape:collect="always"> + <rect + id="rect6201" + height="1" + width="1" + y="0" + x="0" + style="fill:black;stroke:none" /> + <rect + id="rect6203" + height="1" + width="1" + y="1" + x="1" + style="fill:black;stroke:none" /> + </pattern> + <linearGradient + id="linearGradient5406" + osb:paint="solid"> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0" + id="stop5404" /> + </linearGradient> + <pattern + patternUnits="userSpaceOnUse" + width="15.999999" + height="16.000025" + patternTransform="matrix(0.26458333,0,0,0.26458333,-16.933332,263.1333)" + id="pattern6465"> + <path + d="m 8.0000542,8.0000126 h 7.9998878 c 3e-5,0 5.7e-5,3.78e-5 5.7e-5,3.78e-5 V 15.99995 c 0,3.7e-5 -2.7e-5,7.5e-5 -5.7e-5,7.5e-5 H 8.0000542 c -3.03e-5,0 -5.67e-5,-3.8e-5 -5.67e-5,-7.5e-5 V 8.0000504 c 0,0 2.64e-5,-3.78e-5 5.67e-5,-3.78e-5 z M 5.6692913e-5,0 H 7.9999408 c 3.02e-5,0 5.67e-5,3.7795275e-5 5.67e-5,7.5590551e-5 V 7.999937 c 0,3.78e-5 -2.65e-5,7.56e-5 -5.67e-5,7.56e-5 H 5.6692913e-5 C 2.2677165e-5,8.0000126 0,7.9999748 0,7.999937 V 7.5590551e-5 C 0,3.7795276e-5 2.2677165e-5,0 5.6692913e-5,0 Z" + style="opacity:1;fill:#db1545;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:15.99999905;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="rect6445-2" + inkscape:connector-curvature="0" /> + </pattern> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient7044" + id="linearGradient6476" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(3.223659,0,0,2.5556636,-579.27357,808.39)" + x1="86.490868" + y1="-216.62756" + x2="176.77992" + y2="-216.62756" /> + <mask + maskUnits="userSpaceOnUse" + id="mask6472"> + <rect + transform="rotate(-90)" + y="-0.91986513" + x="-300.45657" + height="511.36566" + width="291.06116" + id="rect6474" + style="opacity:1;fill:url(#linearGradient6476);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.92238116;stroke-miterlimit:4;stroke-dasharray:none" /> + </mask> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#1e1d65" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.84705882" + inkscape:pageshadow="2" + inkscape:zoom="0.6363961" + inkscape:cx="720.54406" + inkscape:cy="371.58659" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="true" + units="px" + inkscape:pagecheckerboard="true" + inkscape:window-width="1920" + inkscape:window-height="1057" + inkscape:window-x="1912" + inkscape:window-y="1143" + inkscape:window-maximized="1" + objecttolerance="1" + guidetolerance="10000" + gridtolerance="10000" + inkscape:snap-bbox="true" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + showguides="false"> + <inkscape:grid + type="xygrid" + id="grid6443" + spacingx="2.1166667" + spacingy="2.1166667" + empspacing="4" + color="#3f3fff" + opacity="0.1254902" + enabled="false" /> + <sodipodi:guide + position="-69.219003,3.872392" + orientation="1,0" + id="guide6508" + inkscape:locked="false" /> + </sodipodi:namedview> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:groupmode="layer" + id="layer2" + inkscape:label="Back" + style="display:inline"> + <path + style="fill:#253276;fill-opacity:1;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 500.58203,825.29688 -54.2207,18.9121 18.91406,56.74219 -45.39258,10.08594 -11.34765,-39.08789 -46.6543,12.60937 13.87109,34.04493 -55.48047,15.13086 -12.60937,-44.13086 -47.91406,13.86914 13.86914,44.13086 -32.78321,11.3496 17.65235,35.30469 278.66211,-63.04492 z m -11.0957,26.45312 0.44726,11.5918 -12.03711,2.67382 -3.5664,-9.80664 z m 4.90429,24.51953 0.89258,9.80859 -9.36328,2.67383 -4.45703,-9.36133 z m -201.5,32.09766 v 11.14453 l -8.4707,1.7832 -4.9043,-8.91601 z" + id="path4522" + inkscape:connector-curvature="0" + transform="scale(0.26458333)" /> + <path + transform="translate(0,-11.249983)" + style="fill:#253276;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 381.65643,238.28361 -47.37344,16.34717 116.09827,29.02457 -14.01186,-23.68672 -31.02626,-0.33362 z" + id="path4520" + inkscape:connector-curvature="0" /> + </g> + <g + inkscape:label="Ground" + inkscape:groupmode="layer" + id="layer1" + transform="translate(0,-11.249983)" + style="display:inline"> + <circle + style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:1.99730551" + id="path5392" + cx="253.06117" + cy="887.61829" + r="642.68146" /> + </g> + <g + inkscape:groupmode="layer" + id="layer3" + inkscape:label="Front"> + <path + style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:1.00157475;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="m 565.38867,666.80078 -115.20508,24.36914 70.24414,231.09766 121.20118,-18.97656 8.61523,-148.01368 -76.28906,21.625 z m -30.15234,38.82813 3.09765,47.0625 -11.44531,2.49414 -9.14062,-46.10743 z m -26.41211,5.20898 10.30664,46.03906 -9.47852,2.06641 -17.14257,-44.88672 z m 41.45508,65.93945 2.80078,44.04493 -12.50391,3.40234 L 532.1543,781.75 Z m -25.15039,6.90039 9.4414,42.18165 -9.54297,2.59765 -13.99804,-40.91015 z m 85.48242,50.83789 1,42.35938 -22.15235,4.89648 -4.53906,-41.66406 z m -54.21485,10.16797 4.54102,41.66211 -7.67188,1.89649 -8.07421,-40.73047 z m -16.66992,4.20899 9.05469,40.45703 -8.88477,2.19727 -12.02734,-39.66016 z" + id="path5398" + transform="scale(0.26458333)" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,-11.249983)" + style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 329.51477,199.15082 -32.04286,18.26817 12.8142,1.28619 -6.02656,28.18505 32.94792,3.49531 0.51681,-27.76301 11.91226,1.00737 z m -14.10711,25.93826 6.27123,0.90288 -1.15019,5.4805 -6.00929,-0.898 z m 13.58524,2.09643 0.42171,5.50053 -6.35262,-0.44337 1.22618,-5.67857 z m -15.04127,5.73678 6.21844,0.90138 -1.87301,4.94347 -5.07899,-0.81761 z m 8.80707,1.53673 6.3403,1.10313 0.43128,4.98637 -7.83808,-1.19409 z" + id="path6874" + inkscape:connector-curvature="0" + sodipodi:nodetypes="cccccccccccccccccccccccccccc" /> + <path + transform="translate(0,-11.249983)" + style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 366.28967,254.78298 7.49431,-30.40441 -7.41388,-2.66046 1.18763,-3.36104 7.21205,2.27141 1.38362,-5.73044 -7.20912,-2.66047 1.28561,-3.65794 7.01313,2.7643 2.17341,-7.01022 3.35519,1.48161 -2.1734,6.51147 6.70747,2.66046 -1.28564,3.16213 -6.31255,-2.46154 -1.68638,6.02735 6.80837,2.46447 -0.9887,3.84808 -6.90052,-2.47031 -6.71038,30.41026 z" + id="path6891" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccccccccccccccccc" /> + <path + transform="translate(0,-11.249983)" + style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 74.047433,217.56203 -1.20251,0.65577 2.314585,6.84299 -4.564578,1.31517 13.625009,41.10395 21.186821,-5.50251 -7.183542,-43.56323 -22.044649,6.35259 z m 16.734379,10.06088 1.478463,10.23607 -8.339026,1.96939 -3.82509,-9.42992 z m 3.780131,14.55519 0.781863,9.82627 -7.001121,1.81797 -3.593063,-9.29297 z" + id="path6944" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,-11.249983)" + style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.24600939px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 43.603475,280.06036 -10.564819,-28.58824 -6.574764,2.28618 -0.916385,-3.37337 6.23111,-2.47535 -2.011396,-5.37101 -6.431418,2.16468 -1.002197,-3.66725 6.348194,-1.96596 -2.123972,-6.85578 3.11982,-0.81419 1.86458,6.45975 6.080155,-1.86705 0.744318,3.27357 -5.700174,1.79072 1.953823,5.78639 6.048884,-2.08256 1.308957,3.64208 -6.116434,2.13257 11.116753,28.12778 z" + id="path6891-8" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccccccccccccccccc" /> + <path + transform="translate(0,-11.249983)" + style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 411.98753,264.70523 3.91734,-12.57157 -7.13355,-3.53259 -1.396,-8.02014 5.81668,-6.93436 10.92618,-0.52461 7.35863,5.88054 0.0806,8.11138 -5.67524,6.95564 -7.37536,-0.96565 -1.04168,4.03744 5.21293,-1.96321 1.42492,-6.58308 5.61592,-1.7579 5.33002,3.98422 -1.35343,5.14755 -3.67857,2.33882 -4.89966,-2.03926 -7.52592,2.91667 -1.60892,6.84465 z" + id="path6985" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccccccccccccccccc" /> + <path + transform="translate(0,-11.249983)" + style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.27861062px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 438.77767,272.41521 -0.009,-2.99656 1.24656,2.44908 1.28337,-1.87551 -0.0534,2.25473 2.30831,-1.55949 -1.70125,2.67579 z" + id="path7212" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,-11.249983)" + style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.29395995px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 387.1467,259.13862 -0.3913,-3.17093 1.60741,2.46066 1.09423,-2.12083 0.23196,2.39229 2.19942,-1.8946 -1.42637,3.01207 z" + id="path7212-4" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,-11.249983)" + style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 457.96894,278.42384 1.02302,-2.77836 -1.31183,-0.56021 0.33336,-0.616 1.26318,0.48291 0.54568,-1.37607 0.81934,0.31324 -0.47741,1.4022 1.87364,0.67714 0.47795,-1.14765 0.83893,0.26207 -0.47245,1.28672 1.80283,0.70884 0.41215,-1.23149 0.92825,0.33529 -0.49337,1.23952 1.38917,0.51162 -0.21081,0.85845 -1.42731,-0.56527 -1.05878,2.6669 -0.81279,-0.33034 0.94975,-2.68892 -1.68742,-0.7038 -1.03512,2.65627 -0.83236,-0.27915 0.99293,-2.75061 -1.92628,-0.79522 -1.00194,2.82543 z" + id="path7229" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccccccccccccccccccccccccc" /> + <path + transform="translate(0,-11.249983)" + id="path7233" + style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.3185696px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 73.482785,265.42476 4.944364,-1.72314 -0.207904,-0.52164 -2.012479,0.86151 -0.0213,-0.63037 -0.837931,0.3339 0.324488,0.46118 -2.371778,0.68852 z m 0.497305,0.21764 4.223597,-1.35549 0.556753,4.37406 -2.879727,0.92419 z" + inkscape:connector-curvature="0" + sodipodi:nodetypes="cccccccccccccc" /> + <path + transform="translate(0,-11.249983)" + style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 156.55184,206.61884 0.47605,-0.20403 1.0201,8.90891 -0.47605,0.20402 z" + id="path7236" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,-11.249983)" + style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 160.97229,209.47512 0.20402,4.96451 0.47605,-0.068 0.068,-5.03251 z" + id="path7238" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,-11.249983)" + style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.34364724px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 23.838748,287.33572 -2.186787,-3.04882 3.027872,1.63785 -0.07842,-2.79635 1.585239,2.33549 1.177306,-3.18042 0.241718,3.90016 z" + id="path7212-4-6" + inkscape:connector-curvature="0" /> + <circle + transform="translate(0,-11.249983)" + style="opacity:0.5;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path4535" + cx="120.03474" + cy="193.66763" + r="2.5126758" /> + <circle + transform="translate(0,-11.249983)" + style="opacity:0.5;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path4535-2" + cx="97.333473" + cy="218.84901" + r="2.5126758" /> + <circle + transform="translate(0,-11.249983)" + style="opacity:0.5;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path4535-24" + cx="70.128021" + cy="226.19046" + r="2.5126758" /> + <circle + transform="translate(0,-11.249983)" + style="opacity:0.25;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.41842699;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path4535-25" + cx="118.05532" + cy="234.83446" + r="1.6838019" /> + <circle + transform="translate(0,-11.249983)" + style="opacity:0.5;fill:#ffbe16;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.3186785;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path4535-9" + cx="110.59546" + cy="252.2408" + r="1.5653913" /> + <circle + transform="translate(0,-11.249983)" + style="opacity:0.5;fill:#ffbe16;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.3186785;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path4535-9-7" + cx="122.43651" + cy="242.53113" + r="1.5653913" /> + <circle + transform="translate(0,-11.249983)" + style="opacity:0.5;fill:#ffbe16;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.3186785;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path4535-9-2" + cx="64.415337" + cy="265.26596" + r="1.5653913" /> + <circle + transform="translate(0,-11.249983)" + style="opacity:0.1;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.44323444;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path4535-24-4" + cx="69.61615" + cy="226.18503" + r="7.648705" /> + <circle + transform="translate(0,-11.249983)" + style="opacity:0.1;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.44323444;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path4535-24-4-4" + cx="97.333473" + cy="218.84901" + r="7.648705" /> + <circle + transform="translate(0,-11.249983)" + style="opacity:0.1;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.44323444;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path4535-24-4-2" + cx="119.52941" + cy="193.50121" + r="7.648705" /> + <circle + transform="translate(0,-11.249983)" + style="opacity:0.02999998;fill:#ffbe16;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.13750315;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path4535-9-2-6" + cx="64.415337" + cy="265.26596" + r="4.9115925" /> + <circle + transform="translate(0,-11.249983)" + style="opacity:0.02999998;fill:#ffbe16;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.13750315;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path4535-9-2-6-7" + cx="110.59546" + cy="252.2408" + r="4.9115925" /> + <circle + transform="translate(0,-11.249983)" + style="opacity:0.02999998;fill:#ffbe16;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.13750315;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path4535-9-2-6-3" + cx="122.43651" + cy="242.53113" + r="4.9115925" /> + <circle + transform="translate(0,-11.249983)" + style="opacity:0.05;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.44323444;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path4535-24-4-4-8" + cx="117.52492" + cy="234.88242" + r="7.648705" /> + </g> +</svg> diff --git a/src/client/const.styl b/src/client/const.styl new file mode 100644 index 0000000000..b6560701d9 --- /dev/null +++ b/src/client/const.styl @@ -0,0 +1,4 @@ +json('../const.json') + +$theme-color = themeColor +$theme-color-foreground = themeColorForeground diff --git a/src/client/docs/about.en.pug b/src/client/docs/about.en.pug new file mode 100644 index 0000000000..893d9dd6a1 --- /dev/null +++ b/src/client/docs/about.en.pug @@ -0,0 +1,3 @@ +h1 About Misskey + +p Misskey is a mini blog SNS. diff --git a/src/client/docs/about.ja.pug b/src/client/docs/about.ja.pug new file mode 100644 index 0000000000..fec933b0c6 --- /dev/null +++ b/src/client/docs/about.ja.pug @@ -0,0 +1,3 @@ +h1 Misskeyについて + +p MisskeyはミニブログSNSです。 diff --git a/src/client/docs/api.ja.pug b/src/client/docs/api.ja.pug new file mode 100644 index 0000000000..665cfdc4b8 --- /dev/null +++ b/src/client/docs/api.ja.pug @@ -0,0 +1,103 @@ +h1 Misskey API + +p MisskeyはWeb APIを公開しており、様々な操作をプログラム上から行うことができます。 +p APIを自分のアカウントから利用する場合(自分のアカウントのみ操作したい場合)と、アプリケーションから利用する場合(不特定のアカウントを操作したい場合)とで利用手順が異なりますので、それぞれのケースについて説明します。 + +section + h2 自分の所有するアカウントからAPIにアクセスする場合 + p 「設定 > API」で、APIにアクセスするのに必要なAPIキーを取得してください。 + p APIにアクセスする際には、リクエストにAPIキーを「i」というパラメータ名で含めます。 + div.ui.info.warn: p %fa:exclamation-triangle%アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。 + p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。 + +section + h2 アプリケーションからAPIにアクセスする場合 + p + | 直接ユーザーのAPIキーをアプリケーションが扱うのは危険なので、 + | アプリケーションからAPIを利用する際には、アプリケーションとアプリケーションを利用するユーザーが結び付けられた専用のトークン(アクセストークン)をMisskeyに発行してもらい、 + | そのトークンをリクエストのパラメータに含める必要があります。 + div.ui.info: p %fa:info-circle%アクセストークンは、ユーザーが自分のアカウントにあなたのアプリケーションがアクセスすることを許可した場合のみ発行されます + + p それでは、アクセストークンを取得するまでの流れを説明します。 + + section + h3 1.アプリケーションを登録する + p まず、あなたのアプリケーションやWebサービス(以後、あなたのアプリと呼びます)をMisskeyに登録します。 + p + a(href=common.config.dev_url, target="_blank") デベロッパーセンター + | にアクセスし、「アプリ > アプリ作成」に進みます。 + | フォームに必要事項を記入し、アプリを作成してください。フォームの記入欄の説明は以下の通りです: + + table + thead + tr + th 名前 + th 説明 + tbody + tr + td アプリケーション名 + td あなたのアプリの名称。 + tr + td アプリの概要 + td あなたのアプリの簡単な説明や紹介。 + tr + td コールバックURL + td ユーザーが後述する認証フォームで認証を終えた際にリダイレクトするURLを設定できます。あなたのアプリがWebサービスである場合に有用です。 + tr + td 権限 + td あなたのアプリが要求する権限。ここで要求した機能だけがAPIからアクセスできます。 + + p 登録が済むとあなたのアプリのシークレットキーが入手できます。このシークレットキーは後で使用します。 + div.ui.info.warn: p %fa:exclamation-triangle%アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。 + + section + h3 2.ユーザーに認証させる + p あなたのアプリを使ってもらうには、ユーザーにアカウントへのアクセスの許可をもらう必要があります。 + p + | 認証セッションを開始するには、#{common.config.api_url}/auth/session/generate へパラメータに appSecret としてシークレットキーを含めたリクエストを送信します。 + | リクエスト形式はJSONで、メソッドはPOSTです。 + | レスポンスとして認証セッションのトークンや認証フォームのURLが取得できるので、認証フォームのURLをブラウザで表示し、ユーザーにフォームを提示してください。 + + p + | あなたのアプリがコールバックURLを設定している場合、 + | ユーザーがあなたのアプリの連携を許可すると設定しているコールバックURLに token という名前でセッションのトークンが含まれたクエリを付けてリダイレクトします。 + + p + | あなたのアプリがコールバックURLを設定していない場合、ユーザーがあなたのアプリの連携を許可したことを(何らかの方法で(たとえばボタンを押させるなど))確認出来るようにしてください。 + + section + h3 3.ユーザーのアクセストークンを取得する + p ユーザーが連携を許可したら、#{common.config.api_url}/auth/session/userkey へ次のパラメータを含むリクエストを送信します: + table + thead + tr + th 名前 + th 型 + th 説明 + tbody + tr + td appSecret + td string + td あなたのアプリのシークレットキー + tr + td token + td string + td セッションのトークン + p 上手くいけば、認証したユーザーのアクセストークンがレスポンスとして取得できます。おめでとうございます! + + p アクセストークンが取得できたら、「ユーザーのアクセストークン+あなたのアプリのシークレットキーをsha256したもの」を「i」というパラメータでリクエストに含めると、APIにアクセスすることができます。 + + p 「i」パラメータの生成方法を擬似コードで表すと次のようになります: + pre: code + | const i = sha256(accessToken + secretKey); + + p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。 + +section + h2 Misskey APIの利用 + p APIはすべてリクエストのパラメータ・レスポンスともにJSON形式です。また、すべてのエンドポイントはPOSTメソッドのみ受け付けます。 + p APIリファレンスもご確認ください。 + + section + h3 レートリミット + p Misskey APIにはレートリミットがあり、短時間のうちに多数のリクエストを送信すると、一定時間APIを利用することができなくなることがあります。 diff --git a/src/client/docs/api/endpoints/notes/create.yaml b/src/client/docs/api/endpoints/notes/create.yaml new file mode 100644 index 0000000000..04ada2ecd5 --- /dev/null +++ b/src/client/docs/api/endpoints/notes/create.yaml @@ -0,0 +1,59 @@ +endpoint: "notes/create" + +desc: + ja: "投稿します。" + en: "Compose new note." + +params: + - name: "text" + type: "string" + optional: true + desc: + ja: "投稿の本文" + en: "The text of your note" + - name: "cw" + type: "string" + optional: true + desc: + ja: "コンテンツの警告。このパラメータを指定すると設定したテキストで投稿のコンテンツを隠す事が出来ます。" + en: "Content Warning" + - name: "mediaIds" + type: "id(DriveFile)[]" + optional: true + desc: + ja: "添付するメディア(1~4つ)" + en: "Media you want to attach (1~4)" + - name: "replyId" + type: "id(Note)" + optional: true + desc: + ja: "返信する投稿" + en: "The note you want to reply" + - name: "renoteId" + type: "id(Note)" + optional: true + desc: + ja: "引用する投稿" + en: "The note you want to quote" + - name: "poll" + type: "object" + optional: true + desc: + ja: "投票" + en: "The poll" + defName: "poll" + def: + - name: "choices" + type: "string[]" + optional: false + desc: + ja: "投票の選択肢" + en: "Choices of a poll" + +res: + - name: "createdNote" + type: "entity(Note)" + optional: false + desc: + ja: "作成した投稿" + en: "A note that created" diff --git a/src/client/docs/api/endpoints/notes/timeline.yaml b/src/client/docs/api/endpoints/notes/timeline.yaml new file mode 100644 index 0000000000..71c346f355 --- /dev/null +++ b/src/client/docs/api/endpoints/notes/timeline.yaml @@ -0,0 +1,32 @@ +endpoint: "notes/timeline" + +desc: + ja: "タイムラインを取得します。" + en: "Get your timeline." + +params: + - name: "limit" + type: "number" + optional: true + desc: + ja: "取得する最大の数" + - name: "sinceId" + type: "id(Note)" + optional: true + desc: + ja: "指定すると、この投稿を基点としてより新しい投稿を取得します" + - name: "untilId" + type: "id(Note)" + optional: true + desc: + ja: "指定すると、この投稿を基点としてより古い投稿を取得します" + - name: "sinceDate" + type: "number" + optional: true + desc: + ja: "指定した時間を基点としてより新しい投稿を取得します。数値は、1970 年 1 月 1 日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。" + - name: "untilDate" + type: "number" + optional: true + desc: + ja: "指定した時間を基点としてより古い投稿を取得します。数値は、1970 年 1 月 1 日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。" diff --git a/src/client/docs/api/endpoints/style.styl b/src/client/docs/api/endpoints/style.styl new file mode 100644 index 0000000000..2af9fe9a77 --- /dev/null +++ b/src/client/docs/api/endpoints/style.styl @@ -0,0 +1,21 @@ +@import "../style" + +#url + padding 8px 12px 8px 8px + font-family Consolas, 'Courier New', Courier, Monaco, monospace + color #fff + background #222e40 + border-radius 4px + + > .method + display inline-block + margin 0 8px 0 0 + padding 0 6px + color #f4fcff + background #17afc7 + border-radius 4px + user-select none + pointer-events none + + > .host + opacity 0.7 diff --git a/src/client/docs/api/endpoints/view.pug b/src/client/docs/api/endpoints/view.pug new file mode 100644 index 0000000000..d271a5517a --- /dev/null +++ b/src/client/docs/api/endpoints/view.pug @@ -0,0 +1,32 @@ +extends ../../layout.pug +include ../mixins + +block meta + link(rel="stylesheet" href="/assets/api/endpoints/style.css") + +block main + h1= endpoint + + p#url + span.method POST + span.host + = url.host + | / + span.path= url.path + + p#desc= desc[lang] || desc['ja'] + + section + h2 %i18n:docs.api.endpoints.params% + +propTable(params) + + if paramDefs + each paramDef in paramDefs + section(id= paramDef.name) + h3= paramDef.name + +propTable(paramDef.params) + + if res + section + h2 %i18n:docs.api.endpoints.res% + +propTable(res) diff --git a/src/client/docs/api/entities/drive-file.yaml b/src/client/docs/api/entities/drive-file.yaml new file mode 100644 index 0000000000..02ab0d608e --- /dev/null +++ b/src/client/docs/api/entities/drive-file.yaml @@ -0,0 +1,73 @@ +name: "DriveFile" + +desc: + ja: "ドライブのファイル。" + en: "A file of Drive." + +props: + - name: "id" + type: "id" + optional: false + desc: + ja: "ファイルID" + en: "The ID of this file" + - name: "createdAt" + type: "date" + optional: false + desc: + ja: "アップロード日時" + en: "The upload date of this file" + - name: "userId" + type: "id(User)" + optional: false + desc: + ja: "所有者ID" + en: "The ID of the owner of this file" + - name: "user" + type: "entity(User)" + optional: true + desc: + ja: "所有者" + en: "The owner of this file" + - name: "name" + type: "string" + optional: false + desc: + ja: "ファイル名" + en: "The name of this file" + - name: "md5" + type: "string" + optional: false + desc: + ja: "ファイルのMD5ハッシュ値" + en: "The md5 hash value of this file" + - name: "type" + type: "string" + optional: false + desc: + ja: "ファイルの種類" + en: "The type of this file" + - name: "datasize" + type: "number" + optional: false + desc: + ja: "ファイルサイズ(bytes)" + en: "The size of this file (bytes)" + - name: "url" + type: "string" + optional: false + desc: + ja: "ファイルのURL" + en: "The URL of this file" + - name: "folderId" + type: "id(DriveFolder)" + optional: true + desc: + ja: "フォルダID" + en: "The ID of the folder of this file" + - name: "folder" + type: "entity(DriveFolder)" + optional: true + desc: + ja: "フォルダ" + en: "The folder of this file" diff --git a/src/client/docs/api/entities/note.yaml b/src/client/docs/api/entities/note.yaml new file mode 100644 index 0000000000..718d331d13 --- /dev/null +++ b/src/client/docs/api/entities/note.yaml @@ -0,0 +1,174 @@ +name: "Note" + +desc: + ja: "投稿。" + en: "A note." + +props: + - name: "id" + type: "id" + optional: false + desc: + ja: "投稿ID" + en: "The ID of this note" + - name: "createdAt" + type: "date" + optional: false + desc: + ja: "投稿日時" + en: "The posted date of this note" + - name: "viaMobile" + type: "boolean" + optional: true + desc: + ja: "モバイル端末から投稿したか否か(自己申告であることに留意)" + en: "Whether this note sent via a mobile device" + - name: "text" + type: "string" + optional: true + desc: + ja: "投稿の本文 (ローカルの場合Markdown風のフォーマット)" + en: "The text of this note (in Markdown like format if local)" + - name: "textHtml" + type: "string" + optional: true + desc: + ja: "投稿の本文 (HTML) (投稿時は無視)" + en: "The text of this note (in HTML. Ignored when posting.)" + - name: "mediaIds" + type: "id(DriveFile)[]" + optional: true + desc: + ja: "添付されているメディアのID (なければレスポンスでは空配列)" + en: "The IDs of the attached media (empty array for response if no media is attached)" + - name: "media" + type: "entity(DriveFile)[]" + optional: true + desc: + ja: "添付されているメディア" + en: "The attached media" + - name: "userId" + type: "id(User)" + optional: false + desc: + ja: "投稿者ID" + en: "The ID of author of this note" + - name: "user" + type: "entity(User)" + optional: true + desc: + ja: "投稿者" + en: "The author of this note" + - name: "myReaction" + type: "string" + optional: true + desc: + ja: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>" + en: "The your <a href='/docs/api/reactions'>reaction</a> of this note" + - name: "reactionCounts" + type: "object" + optional: false + desc: + ja: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト" + - name: "replyId" + type: "id(Note)" + optional: true + desc: + ja: "返信した投稿のID" + en: "The ID of the replyed note" + - name: "reply" + type: "entity(Note)" + optional: true + desc: + ja: "返信した投稿" + en: "The replyed note" + - name: "renoteId" + type: "id(Note)" + optional: true + desc: + ja: "引用した投稿のID" + en: "The ID of the quoted note" + - name: "renote" + type: "entity(Note)" + optional: true + desc: + ja: "引用した投稿" + en: "The quoted note" + - name: "poll" + type: "object" + optional: true + desc: + ja: "投票" + en: "The poll" + defName: "poll" + def: + - name: "choices" + type: "object[]" + optional: false + desc: + ja: "投票の選択肢" + en: "The choices of this poll" + defName: "choice" + def: + - name: "id" + type: "number" + optional: false + desc: + ja: "選択肢ID" + en: "The ID of this choice" + - name: "isVoted" + type: "boolean" + optional: true + desc: + ja: "自分がこの選択肢に投票したかどうか" + en: "Whether you voted to this choice" + - name: "text" + type: "string" + optional: false + desc: + ja: "選択肢本文" + en: "The text of this choice" + - name: "votes" + type: "number" + optional: false + desc: + ja: "この選択肢に投票された数" + en: "The number voted for this choice" + - name: "geo" + type: "object" + optional: true + desc: + ja: "位置情報" + en: "Geo location" + defName: "geo" + def: + - name: "coordinates" + type: "number[]" + optional: false + desc: + ja: "座標。最初に経度:-180〜180で表す。最後に緯度:-90〜90で表す。" + - name: "altitude" + type: "number" + optional: false + desc: + ja: "高度。メートル単位で表す。" + - name: "accuracy" + type: "number" + optional: false + desc: + ja: "緯度、経度の精度。メートル単位で表す。" + - name: "altitudeAccuracy" + type: "number" + optional: false + desc: + ja: "高度の精度。メートル単位で表す。" + - name: "heading" + type: "number" + optional: false + desc: + ja: "方角。0〜360の角度で表す。0が北、90が東、180が南、270が西。" + - name: "speed" + type: "number" + optional: false + desc: + ja: "速度。メートル / 秒数で表す。" diff --git a/src/client/docs/api/entities/post.yaml b/src/client/docs/api/entities/post.yaml new file mode 100644 index 0000000000..718d331d13 --- /dev/null +++ b/src/client/docs/api/entities/post.yaml @@ -0,0 +1,174 @@ +name: "Note" + +desc: + ja: "投稿。" + en: "A note." + +props: + - name: "id" + type: "id" + optional: false + desc: + ja: "投稿ID" + en: "The ID of this note" + - name: "createdAt" + type: "date" + optional: false + desc: + ja: "投稿日時" + en: "The posted date of this note" + - name: "viaMobile" + type: "boolean" + optional: true + desc: + ja: "モバイル端末から投稿したか否か(自己申告であることに留意)" + en: "Whether this note sent via a mobile device" + - name: "text" + type: "string" + optional: true + desc: + ja: "投稿の本文 (ローカルの場合Markdown風のフォーマット)" + en: "The text of this note (in Markdown like format if local)" + - name: "textHtml" + type: "string" + optional: true + desc: + ja: "投稿の本文 (HTML) (投稿時は無視)" + en: "The text of this note (in HTML. Ignored when posting.)" + - name: "mediaIds" + type: "id(DriveFile)[]" + optional: true + desc: + ja: "添付されているメディアのID (なければレスポンスでは空配列)" + en: "The IDs of the attached media (empty array for response if no media is attached)" + - name: "media" + type: "entity(DriveFile)[]" + optional: true + desc: + ja: "添付されているメディア" + en: "The attached media" + - name: "userId" + type: "id(User)" + optional: false + desc: + ja: "投稿者ID" + en: "The ID of author of this note" + - name: "user" + type: "entity(User)" + optional: true + desc: + ja: "投稿者" + en: "The author of this note" + - name: "myReaction" + type: "string" + optional: true + desc: + ja: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>" + en: "The your <a href='/docs/api/reactions'>reaction</a> of this note" + - name: "reactionCounts" + type: "object" + optional: false + desc: + ja: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト" + - name: "replyId" + type: "id(Note)" + optional: true + desc: + ja: "返信した投稿のID" + en: "The ID of the replyed note" + - name: "reply" + type: "entity(Note)" + optional: true + desc: + ja: "返信した投稿" + en: "The replyed note" + - name: "renoteId" + type: "id(Note)" + optional: true + desc: + ja: "引用した投稿のID" + en: "The ID of the quoted note" + - name: "renote" + type: "entity(Note)" + optional: true + desc: + ja: "引用した投稿" + en: "The quoted note" + - name: "poll" + type: "object" + optional: true + desc: + ja: "投票" + en: "The poll" + defName: "poll" + def: + - name: "choices" + type: "object[]" + optional: false + desc: + ja: "投票の選択肢" + en: "The choices of this poll" + defName: "choice" + def: + - name: "id" + type: "number" + optional: false + desc: + ja: "選択肢ID" + en: "The ID of this choice" + - name: "isVoted" + type: "boolean" + optional: true + desc: + ja: "自分がこの選択肢に投票したかどうか" + en: "Whether you voted to this choice" + - name: "text" + type: "string" + optional: false + desc: + ja: "選択肢本文" + en: "The text of this choice" + - name: "votes" + type: "number" + optional: false + desc: + ja: "この選択肢に投票された数" + en: "The number voted for this choice" + - name: "geo" + type: "object" + optional: true + desc: + ja: "位置情報" + en: "Geo location" + defName: "geo" + def: + - name: "coordinates" + type: "number[]" + optional: false + desc: + ja: "座標。最初に経度:-180〜180で表す。最後に緯度:-90〜90で表す。" + - name: "altitude" + type: "number" + optional: false + desc: + ja: "高度。メートル単位で表す。" + - name: "accuracy" + type: "number" + optional: false + desc: + ja: "緯度、経度の精度。メートル単位で表す。" + - name: "altitudeAccuracy" + type: "number" + optional: false + desc: + ja: "高度の精度。メートル単位で表す。" + - name: "heading" + type: "number" + optional: false + desc: + ja: "方角。0〜360の角度で表す。0が北、90が東、180が南、270が西。" + - name: "speed" + type: "number" + optional: false + desc: + ja: "速度。メートル / 秒数で表す。" diff --git a/src/client/docs/api/entities/style.styl b/src/client/docs/api/entities/style.styl new file mode 100644 index 0000000000..bddf0f53ab --- /dev/null +++ b/src/client/docs/api/entities/style.styl @@ -0,0 +1 @@ +@import "../style" diff --git a/src/client/docs/api/entities/user.yaml b/src/client/docs/api/entities/user.yaml new file mode 100644 index 0000000000..cccf42f221 --- /dev/null +++ b/src/client/docs/api/entities/user.yaml @@ -0,0 +1,173 @@ +name: "User" + +desc: + ja: "ユーザー。" + en: "A user." + +props: + - name: "id" + type: "id" + optional: false + desc: + ja: "ユーザーID" + en: "The ID of this user" + - name: "createdAt" + type: "date" + optional: false + desc: + ja: "アカウント作成日時" + en: "The registered date of this user" + - name: "username" + type: "string" + optional: false + desc: + ja: "ユーザー名" + en: "The username of this user" + - name: "description" + type: "string" + optional: false + desc: + ja: "アカウントの説明(自己紹介)" + en: "The description of this user" + - name: "avatarId" + type: "id(DriveFile)" + optional: true + desc: + ja: "アバターのID" + en: "The ID of the avatar of this user" + - name: "avatarUrl" + type: "string" + optional: false + desc: + ja: "アバターのURL" + en: "The URL of the avatar of this user" + - name: "bannerId" + type: "id(DriveFile)" + optional: true + desc: + ja: "バナーのID" + en: "The ID of the banner of this user" + - name: "bannerUrl" + type: "string" + optional: false + desc: + ja: "バナーのURL" + en: "The URL of the banner of this user" + - name: "followersCount" + type: "number" + optional: false + desc: + ja: "フォロワーの数" + en: "The number of the followers for this user" + - name: "followingCount" + type: "number" + optional: false + desc: + ja: "フォローしているユーザーの数" + en: "The number of the following users for this user" + - name: "isFollowing" + type: "boolean" + optional: true + desc: + ja: "自分がこのユーザーをフォローしているか" + - name: "isFollowed" + type: "boolean" + optional: true + desc: + ja: "自分がこのユーザーにフォローされているか" + - name: "isMuted" + type: "boolean" + optional: true + desc: + ja: "自分がこのユーザーをミュートしているか" + en: "Whether you muted this user" + - name: "notesCount" + type: "number" + optional: false + desc: + ja: "投稿の数" + en: "The number of the notes of this user" + - name: "pinnedNote" + type: "entity(Note)" + optional: true + desc: + ja: "ピン留めされた投稿" + en: "The pinned note of this user" + - name: "pinnedNoteId" + type: "id(Note)" + optional: true + desc: + ja: "ピン留めされた投稿のID" + en: "The ID of the pinned note of this user" + - name: "driveCapacity" + type: "number" + optional: false + desc: + ja: "ドライブの容量(bytes)" + en: "The capacity of drive of this user (bytes)" + - name: "host" + type: "string | null" + optional: false + desc: + ja: "ホスト (例: example.com:3000)" + en: "Host (e.g. example.com:3000)" + - name: "account" + type: "object" + optional: false + desc: + ja: "このサーバーにおけるアカウント" + en: "The account of this user on this server" + defName: "account" + def: + - name: "lastUsedAt" + type: "date" + optional: false + desc: + ja: "最終利用日時" + en: "The last used date of this user" + - name: "isBot" + type: "boolean" + optional: true + desc: + ja: "botか否か(自己申告であることに留意)" + en: "Whether is bot or not" + - name: "twitter" + type: "object" + optional: true + desc: + ja: "連携されているTwitterアカウント情報" + en: "The info of the connected twitter account of this user" + defName: "twitter" + def: + - name: "userId" + type: "string" + optional: false + desc: + ja: "ユーザーID" + en: "The user ID" + - name: "screenName" + type: "string" + optional: false + desc: + ja: "ユーザー名" + en: "The screen name of this user" + - name: "profile" + type: "object" + optional: false + desc: + ja: "プロフィール" + en: "The profile of this user" + defName: "profile" + def: + - name: "location" + type: "string" + optional: true + desc: + ja: "場所" + en: "The location of this user" + - name: "birthday" + type: "string" + optional: true + desc: + ja: "誕生日 (YYYY-MM-DD)" + en: "The birthday of this user (YYYY-MM-DD)" diff --git a/src/client/docs/api/entities/view.pug b/src/client/docs/api/entities/view.pug new file mode 100644 index 0000000000..2156463dc7 --- /dev/null +++ b/src/client/docs/api/entities/view.pug @@ -0,0 +1,20 @@ +extends ../../layout.pug +include ../mixins + +block meta + link(rel="stylesheet" href="/assets/api/entities/style.css") + +block main + h1= name + + p#desc= desc[lang] || desc['ja'] + + section + h2 %i18n:docs.api.entities.properties% + +propTable(props) + + if propDefs + each propDef in propDefs + section(id= propDef.name) + h3= propDef.name + +propTable(propDef.params) diff --git a/src/client/docs/api/gulpfile.ts b/src/client/docs/api/gulpfile.ts new file mode 100644 index 0000000000..9980ede231 --- /dev/null +++ b/src/client/docs/api/gulpfile.ts @@ -0,0 +1,188 @@ +/** + * Gulp tasks + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as glob from 'glob'; +import * as gulp from 'gulp'; +import * as pug from 'pug'; +import * as yaml from 'js-yaml'; +import * as mkdirp from 'mkdirp'; + +import locales from '../../../../locales'; +import I18nReplacer from '../../../build/i18n'; +import fa from '../../../build/fa'; +import config from './../../../config'; + +import generateVars from '../vars'; + +const langs = Object.keys(locales); + +const kebab = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase(); + +const parseParam = param => { + const id = param.type.match(/^id\((.+?)\)|^id/); + const entity = param.type.match(/^entity\((.+?)\)/); + const isObject = /^object/.test(param.type); + const isDate = /^date/.test(param.type); + const isArray = /\[\]$/.test(param.type); + if (id) { + param.kind = 'id'; + param.type = 'string'; + param.entity = id[1]; + if (isArray) { + param.type += '[]'; + } + } + if (entity) { + param.kind = 'entity'; + param.type = 'object'; + param.entity = entity[1]; + if (isArray) { + param.type += '[]'; + } + } + if (isObject) { + param.kind = 'object'; + } + if (isDate) { + param.kind = 'date'; + param.type = 'string'; + if (isArray) { + param.type += '[]'; + } + } + + return param; +}; + +const sortParams = params => { + params.sort((a, b) => { + if (a.name < b.name) + return -1; + if (a.name > b.name) + return 1; + return 0; + }); + return params; +}; + +const extractDefs = params => { + let defs = []; + + params.forEach(param => { + if (param.def) { + defs.push({ + name: param.defName, + params: sortParams(param.def.map(p => parseParam(p))) + }); + + const childDefs = extractDefs(param.def); + + defs = defs.concat(childDefs); + } + }); + + return sortParams(defs); +}; + +gulp.task('doc:api', [ + 'doc:api:endpoints', + 'doc:api:entities' +]); + +gulp.task('doc:api:endpoints', async () => { + const commonVars = await generateVars(); + glob('./src/client/docs/api/endpoints/**/*.yaml', (globErr, files) => { + if (globErr) { + console.error(globErr); + return; + } + //console.log(files); + files.forEach(file => { + const ep: any = yaml.safeLoad(fs.readFileSync(file, 'utf-8')); + const vars = { + endpoint: ep.endpoint, + url: { + host: config.api_url, + path: ep.endpoint + }, + desc: ep.desc, + params: sortParams(ep.params.map(p => parseParam(p))), + paramDefs: extractDefs(ep.params), + res: ep.res ? sortParams(ep.res.map(p => parseParam(p))) : null, + resDefs: ep.res ? extractDefs(ep.res) : null, + }; + langs.forEach(lang => { + pug.renderFile('./src/client/docs/api/endpoints/view.pug', Object.assign({}, vars, { + lang, + title: ep.endpoint, + src: `https://github.com/syuilo/misskey/tree/master/src/client/docs/api/endpoints/${ep.endpoint}.yaml`, + kebab, + common: commonVars + }), (renderErr, html) => { + if (renderErr) { + console.error(renderErr); + return; + } + const i18n = new I18nReplacer(lang); + html = html.replace(i18n.pattern, i18n.replacement); + html = fa(html); + const htmlPath = `./built/client/docs/${lang}/api/endpoints/${ep.endpoint}.html`; + mkdirp(path.dirname(htmlPath), (mkdirErr) => { + if (mkdirErr) { + console.error(mkdirErr); + return; + } + fs.writeFileSync(htmlPath, html, 'utf-8'); + }); + }); + }); + }); + }); +}); + +gulp.task('doc:api:entities', async () => { + const commonVars = await generateVars(); + glob('./src/client/docs/api/entities/**/*.yaml', (globErr, files) => { + if (globErr) { + console.error(globErr); + return; + } + files.forEach(file => { + const entity = yaml.safeLoad(fs.readFileSync(file, 'utf-8')) as any; + const vars = { + name: entity.name, + desc: entity.desc, + props: sortParams(entity.props.map(p => parseParam(p))), + propDefs: extractDefs(entity.props), + }; + langs.forEach(lang => { + pug.renderFile('./src/client/docs/api/entities/view.pug', Object.assign({}, vars, { + lang, + title: entity.name, + src: `https://github.com/syuilo/misskey/tree/master/src/client/docs/api/entities/${kebab(entity.name)}.yaml`, + kebab, + common: commonVars + }), (renderErr, html) => { + if (renderErr) { + console.error(renderErr); + return; + } + const i18n = new I18nReplacer(lang); + html = html.replace(i18n.pattern, i18n.replacement); + html = fa(html); + const htmlPath = `./built/client/docs/${lang}/api/entities/${kebab(entity.name)}.html`; + mkdirp(path.dirname(htmlPath), (mkdirErr) => { + if (mkdirErr) { + console.error(mkdirErr); + return; + } + fs.writeFileSync(htmlPath, html, 'utf-8'); + }); + }); + }); + }); + }); +}); diff --git a/src/client/docs/api/mixins.pug b/src/client/docs/api/mixins.pug new file mode 100644 index 0000000000..686bf6a2b6 --- /dev/null +++ b/src/client/docs/api/mixins.pug @@ -0,0 +1,37 @@ +mixin propTable(props) + table.props + thead: tr + th %i18n:docs.api.props.name% + th %i18n:docs.api.props.type% + th %i18n:docs.api.props.optional% + th %i18n:docs.api.props.description% + tbody + each prop in props + tr + td.name= prop.name + td.type + i= prop.type + if prop.kind == 'id' + if prop.entity + | ( + a(href=`/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity + | ID) + else + | (ID) + else if prop.kind == 'entity' + | ( + a(href=`/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity + | ) + else if prop.kind == 'object' + if prop.def + | ( + a(href=`#${prop.defName}`)= prop.defName + | ) + else if prop.kind == 'date' + | (Date) + td.optional + if prop.optional + | %i18n:docs.api.props.yes% + else + | %i18n:docs.api.props.no% + td.desc!= prop.desc[lang] || prop.desc['ja'] diff --git a/src/client/docs/api/style.styl b/src/client/docs/api/style.styl new file mode 100644 index 0000000000..3675a4da6f --- /dev/null +++ b/src/client/docs/api/style.styl @@ -0,0 +1,11 @@ +@import "../style" + +table.props + .name + font-weight bold + + .name + .type + .optional + font-family Consolas, 'Courier New', Courier, Monaco, monospace + diff --git a/src/client/docs/gulpfile.ts b/src/client/docs/gulpfile.ts new file mode 100644 index 0000000000..56bf6188c8 --- /dev/null +++ b/src/client/docs/gulpfile.ts @@ -0,0 +1,77 @@ +/** + * Gulp tasks + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as glob from 'glob'; +import * as gulp from 'gulp'; +import * as pug from 'pug'; +import * as mkdirp from 'mkdirp'; +import stylus = require('gulp-stylus'); +import cssnano = require('gulp-cssnano'); + +import I18nReplacer from '../../build/i18n'; +import fa from '../../build/fa'; +import generateVars from './vars'; + +require('./api/gulpfile.ts'); + +gulp.task('doc', [ + 'doc:docs', + 'doc:api', + 'doc:styles' +]); + +gulp.task('doc:docs', async () => { + const commonVars = await generateVars(); + + glob('./src/client/docs/**/*.*.pug', (globErr, files) => { + if (globErr) { + console.error(globErr); + return; + } + files.forEach(file => { + const [, name, lang] = file.match(/docs\/(.+?)\.(.+?)\.pug$/); + const vars = { + common: commonVars, + lang: lang, + title: fs.readFileSync(file, 'utf-8').match(/^h1 (.+?)\r?\n/)[1], + src: `https://github.com/syuilo/misskey/tree/master/src/client/docs/${name}.${lang}.pug`, + }; + pug.renderFile(file, vars, (renderErr, content) => { + if (renderErr) { + console.error(renderErr); + return; + } + + pug.renderFile('./src/client/docs/layout.pug', Object.assign({}, vars, { + content + }), (renderErr2, html) => { + if (renderErr2) { + console.error(renderErr2); + return; + } + const i18n = new I18nReplacer(lang); + html = html.replace(i18n.pattern, i18n.replacement); + html = fa(html); + const htmlPath = `./built/client/docs/${lang}/${name}.html`; + mkdirp(path.dirname(htmlPath), (mkdirErr) => { + if (mkdirErr) { + console.error(mkdirErr); + return; + } + fs.writeFileSync(htmlPath, html, 'utf-8'); + }); + }); + }); + }); + }); +}); + +gulp.task('doc:styles', () => + gulp.src('./src/client/docs/**/*.styl') + .pipe(stylus()) + .pipe((cssnano as any)()) + .pipe(gulp.dest('./built/client/docs/assets/')) +); diff --git a/src/client/docs/index.en.pug b/src/client/docs/index.en.pug new file mode 100644 index 0000000000..1fcc870d3d --- /dev/null +++ b/src/client/docs/index.en.pug @@ -0,0 +1,3 @@ +h1 Misskey Docs + +p Welcome to docs of Misskey. diff --git a/src/client/docs/index.ja.pug b/src/client/docs/index.ja.pug new file mode 100644 index 0000000000..4a0bf7fa1d --- /dev/null +++ b/src/client/docs/index.ja.pug @@ -0,0 +1,3 @@ +h1 Misskey ドキュメント + +p Misskeyのドキュメントへようこそ diff --git a/src/client/docs/layout.pug b/src/client/docs/layout.pug new file mode 100644 index 0000000000..29d2a3ff69 --- /dev/null +++ b/src/client/docs/layout.pug @@ -0,0 +1,41 @@ +doctype html + +html(lang= lang) + head + meta(charset="UTF-8") + meta(name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no") + title + | #{title} | Misskey Docs + link(rel="stylesheet" href="/assets/style.css") + block meta + + //- FontAwesome style + style #{common.facss} + + body + nav + ul + each doc in common.docs + li: a(href=`/docs/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja'] + section + h2 API + ul + li Entities + ul + each entity in common.entities + li: a(href=`/docs/${lang}/api/entities/${common.kebab(entity)}`)= entity + li Endpoints + ul + each endpoint in common.endpoints + li: a(href=`/docs/${lang}/api/endpoints/${common.kebab(endpoint)}`)= endpoint + main + article + block main + if content + | !{content} + + footer + p + | %i18n:docs.edit-this-page-on-github% + a(href=src target="_blank") %i18n:docs.edit-this-page-on-github-link% + small= common.copyright diff --git a/src/client/docs/license.en.pug b/src/client/docs/license.en.pug new file mode 100644 index 0000000000..45d8b76473 --- /dev/null +++ b/src/client/docs/license.en.pug @@ -0,0 +1,17 @@ +h1 License + +div!= common.license + +details + summary Libraries + + section + h2 Libraries + + each dependency, name in common.dependencies + details + summary= name + + section + h3= name + pre= dependency.licenseText diff --git a/src/client/docs/license.ja.pug b/src/client/docs/license.ja.pug new file mode 100644 index 0000000000..6eb9ac308e --- /dev/null +++ b/src/client/docs/license.ja.pug @@ -0,0 +1,17 @@ +h1 ライセンス + +div!= common.license + +details + summary サードパーティ + + section + h2 サードパーティ + + each dependency, name in common.dependencies + details + summary= name + + section + h3= name + pre= dependency.licenseText diff --git a/src/client/docs/mute.ja.pug b/src/client/docs/mute.ja.pug new file mode 100644 index 0000000000..807f7b67a7 --- /dev/null +++ b/src/client/docs/mute.ja.pug @@ -0,0 +1,13 @@ +h1 ミュート + +p ユーザーページから、そのユーザーをミュートすることができます。 + +p ユーザーをミュートすると、そのユーザーに関する次のコンテンツがMisskeyに表示されなくなります: +ul + li タイムラインや投稿の検索結果内の、そのユーザーの投稿(およびそれらの投稿に対する返信やRenote) + li そのユーザーからの通知 + li メッセージ履歴一覧内の、そのユーザーとのメッセージ履歴 + +p ミュートを行ったことは相手に通知されず、ミュートされていることを知ることもできません。 + +p 設定>ミュート から、自分がミュートしているユーザー一覧を確認することができます。 diff --git a/src/client/docs/search.ja.pug b/src/client/docs/search.ja.pug new file mode 100644 index 0000000000..fc62d16cae --- /dev/null +++ b/src/client/docs/search.ja.pug @@ -0,0 +1,120 @@ +h1 検索 + +p 投稿を検索することができます。 +p + | キーワードを半角スペースで区切ると、and検索になります。 + | 例えば、「git コミット」と検索すると、「gitで編集したファイルの特定の行だけコミットする方法がわからない」などがマッチします。 + +section + h2 キーワードの除外 + p キーワードの前に「-」(ハイフン)をプリフィクスすると、そのキーワードを含まない投稿に限定します。 + p 例えば、「gitというキーワードを含むが、コミットというキーワードは含まない投稿」を検索したい場合、クエリは以下のようになります: + code git -コミット + +section + h2 完全一致 + p テキストを「"""」で囲むと、そのテキストと完全に一致する投稿を検索します。 + p 例えば、「"""にゃーん"""」と検索すると、「にゃーん」という投稿のみがヒットし、「にゃーん…」という投稿はヒットしません。 + +section + h2 タグ + p キーワードの前に「#」(シャープ)をプリフィクスすると、そのキーワードと一致するタグを持つ投稿に限定します。 + +section + h2 オプション + p + | オプションを使用して、より高度な検索を行えます。 + | オプションを指定するには、「オプション名:値」という形式でクエリに含めます。 + p 利用可能なオプション一覧です: + + table + thead + tr + th 名前 + th 説明 + tbody + tr + td user + td + | 指定されたユーザー名のユーザーの投稿に限定します。 + | 「,」(カンマ)で区切って、複数ユーザーを指定することもできます。 + br + | 例えば、 + code user:himawari,sakurako + | と検索すると「@himawariまたは@sakurakoの投稿」だけに限定します。 + | (つまりユーザーのホワイトリストです) + tr + td exclude_user + td + | 指定されたユーザー名のユーザーの投稿を除外します。 + | 「,」(カンマ)で区切って、複数ユーザーを指定することもできます。 + br + | 例えば、 + code exclude_user:akari,chinatsu + | と検索すると「@akariまたは@chinatsu以外の投稿」に限定します。 + | (つまりユーザーのブラックリストです) + tr + td follow + td + | true ... フォローしているユーザーに限定。 + br + | false ... フォローしていないユーザーに限定。 + br + | null ... 特に限定しない(デフォルト) + tr + td mute + td + | mute_all ... ミュートしているユーザーの投稿とその投稿に対する返信やRenoteを除外する(デフォルト) + br + | mute_related ... ミュートしているユーザーの投稿に対する返信やRenoteだけ除外する + br + | mute_direct ... ミュートしているユーザーの投稿だけ除外する + br + | disabled ... ミュートしているユーザーの投稿とその投稿に対する返信やRenoteも含める + br + | direct_only ... ミュートしているユーザーの投稿だけに限定 + br + | related_only ... ミュートしているユーザーの投稿に対する返信やRenoteだけに限定 + br + | all_only ... ミュートしているユーザーの投稿とその投稿に対する返信やRenoteに限定 + tr + td reply + td + | true ... 返信に限定。 + br + | false ... 返信でない投稿に限定。 + br + | null ... 特に限定しない(デフォルト) + tr + td renote + td + | true ... Renoteに限定。 + br + | false ... Renoteでない投稿に限定。 + br + | null ... 特に限定しない(デフォルト) + tr + td media + td + | true ... メディアが添付されている投稿に限定。 + br + | false ... メディアが添付されていない投稿に限定。 + br + | null ... 特に限定しない(デフォルト) + tr + td poll + td + | true ... 投票が添付されている投稿に限定。 + br + | false ... 投票が添付されていない投稿に限定。 + br + | null ... 特に限定しない(デフォルト) + tr + td until + td 上限の日時。(YYYY-MM-DD) + tr + td since + td 下限の日時。(YYYY-MM-DD) + + p 例えば、「@syuiloの2017年11月1日から2017年12月31日までの『Misskey』というテキストを含む返信ではない投稿」を検索したい場合、クエリは以下のようになります: + code user:syuilo since:2017-11-01 until:2017-12-31 reply:false Misskey diff --git a/src/client/docs/style.styl b/src/client/docs/style.styl new file mode 100644 index 0000000000..bc165f8728 --- /dev/null +++ b/src/client/docs/style.styl @@ -0,0 +1,120 @@ +@import "../style" +@import "./ui" + +body + margin 0 + color #34495e + word-break break-word + +main + margin 0 0 0 256px + padding 64px + width 100% + max-width 768px + + section + margin 32px 0 + + h1 + margin 0 0 24px 0 + padding 16px 0 + font-size 1.5em + border-bottom solid 2px #eee + + h2 + margin 0 0 24px 0 + padding 0 0 16px 0 + font-size 1.4em + border-bottom solid 1px #eee + + h3 + margin 0 + padding 0 + font-size 1.25em + + h4 + margin 0 + + p + margin 1em 0 + line-height 1.6em + + footer + margin 32px 0 0 0 + border-top solid 2px #eee + + > small + margin 16px 0 0 0 + color #aaa + +nav + display block + position fixed + z-index 10000 + top 0 + left 0 + width 256px + height 100% + overflow auto + padding 32px + background #fff + border-right solid 2px #eee + +@media (max-width 1025px) + main + margin 0 + max-width 100% + + nav + position relative + width 100% + max-height 128px + background #f9f9f9 + border-right none + +@media (max-width 768px) + main + padding 32px + +@media (max-width 512px) + main + padding 16px + +table + display block + width 100% + max-width 100% + overflow auto + border-spacing 0 + border-collapse collapse + + thead + font-weight bold + border-bottom solid 2px #eee + + tr + th + text-align left + + tbody + tr + &:nth-child(odd) + background #fbfbfb + + th, td + padding 8px 16px + min-width 128px + +code + display inline-block + padding 8px 10px + font-family Consolas, 'Courier New', Courier, Monaco, monospace + color #295c92 + background #f2f2f2 + border-radius 4px + +pre + overflow auto + + > code + display block diff --git a/src/client/docs/tou.ja.pug b/src/client/docs/tou.ja.pug new file mode 100644 index 0000000000..7663258f82 --- /dev/null +++ b/src/client/docs/tou.ja.pug @@ -0,0 +1,3 @@ +h1 利用規約 + +p 公序良俗に反する行為はおやめください。 diff --git a/src/client/docs/ui.styl b/src/client/docs/ui.styl new file mode 100644 index 0000000000..8d5515712f --- /dev/null +++ b/src/client/docs/ui.styl @@ -0,0 +1,19 @@ +.ui.info + display block + margin 1em 0 + padding 0 1em + font-size 90% + color rgba(#000, 0.87) + background #f8f8f9 + border-radius 4px + overflow hidden + + > p + opacity 0.8 + + > [data-fa]:first-child + margin-right 0.25em + + &.warn + color #573a08 + background #FFFAF3 diff --git a/src/client/docs/vars.ts b/src/client/docs/vars.ts new file mode 100644 index 0000000000..32b961aaa9 --- /dev/null +++ b/src/client/docs/vars.ts @@ -0,0 +1,64 @@ +import * as fs from 'fs'; +import * as util from 'util'; +import * as glob from 'glob'; +import * as yaml from 'js-yaml'; +import * as licenseChecker from 'license-checker'; +import * as tmp from 'tmp'; + +import { fa } from '../../build/fa'; +import config from '../../config'; +import { licenseHtml } from '../../build/license'; +const constants = require('../../const.json'); + +export default async function(): Promise<{ [key: string]: any }> { + const vars = {} as { [key: string]: any }; + + const endpoints = glob.sync('./src/client/docs/api/endpoints/**/*.yaml'); + vars['endpoints'] = endpoints.map(ep => { + const _ep = yaml.safeLoad(fs.readFileSync(ep, 'utf-8')) as any; + return _ep.endpoint; + }); + + const entities = glob.sync('./src/client/docs/api/entities/**/*.yaml'); + vars['entities'] = entities.map(x => { + const _x = yaml.safeLoad(fs.readFileSync(x, 'utf-8')) as any; + return _x.name; + }); + + const docs = glob.sync('./src/client/docs/**/*.*.pug'); + vars['docs'] = {}; + docs.forEach(x => { + const [, name, lang] = x.match(/docs\/(.+?)\.(.+?)\.pug$/); + if (vars['docs'][name] == null) { + vars['docs'][name] = { + name, + title: {} + }; + } + vars['docs'][name]['title'][lang] = fs.readFileSync(x, 'utf-8').match(/^h1 (.+?)\r?\n/)[1]; + }); + + vars['kebab'] = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase(); + + vars['config'] = config; + + vars['copyright'] = constants.copyright; + + vars['facss'] = fa.dom.css(); + + vars['license'] = licenseHtml; + + const tmpObj = tmp.fileSync(); + fs.writeFileSync(tmpObj.name, JSON.stringify({ + licenseText: '' + }), 'utf-8'); + const dependencies = await util.promisify(licenseChecker.init).bind(licenseChecker)({ + start: __dirname + '/../../../', + customPath: tmpObj.name + }); + tmpObj.removeCallback(); + + vars['dependencies'] = dependencies; + + return vars; +} diff --git a/src/client/element.scss b/src/client/element.scss new file mode 100644 index 0000000000..917198e024 --- /dev/null +++ b/src/client/element.scss @@ -0,0 +1,12 @@ +/* Element variable definitons */ +/* SEE: http://element.eleme.io/#/en-US/component/custom-theme */ + +@import '../const.json'; + +/* theme color */ +$--color-primary: $themeColor; + +/* icon font path, required */ +$--font-path: '~element-ui/lib/theme-chalk/fonts'; + +@import "~element-ui/packages/theme-chalk/src/index"; diff --git a/src/client/style.styl b/src/client/style.styl new file mode 100644 index 0000000000..6d1e53e5a6 --- /dev/null +++ b/src/client/style.styl @@ -0,0 +1,37 @@ +@charset 'utf-8' + +@import "./const" + +/* + ::selection + background $theme-color + color #fff +*/ + +* + position relative + box-sizing border-box + background-clip padding-box !important + tap-highlight-color transparent + -webkit-tap-highlight-color transparent + +html, body + margin 0 + padding 0 + scroll-behavior smooth + text-size-adjust 100% + font-family sans-serif + +a + text-decoration none + color $theme-color + cursor pointer + tap-highlight-color rgba($theme-color, 0.7) !important + -webkit-tap-highlight-color rgba($theme-color, 0.7) !important + + &:hover + text-decoration underline + + * + cursor pointer + diff --git a/src/conf.ts b/src/conf.ts deleted file mode 100644 index b04a4c8594..0000000000 --- a/src/conf.ts +++ /dev/null @@ -1,3 +0,0 @@ -import load from './config'; - -export default load(); diff --git a/src/config.ts b/src/config.ts deleted file mode 100644 index 8f4ada5af9..0000000000 --- a/src/config.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Config loader - */ - -import * as fs from 'fs'; -import * as URL from 'url'; -import * as yaml from 'js-yaml'; -import isUrl = require('is-url'); - -/** - * Path of configuration directory - */ -const dir = `${__dirname}/../.config`; - -/** - * Path of configuration file - */ -export const path = process.env.NODE_ENV == 'test' - ? `${dir}/test.yml` - : `${dir}/default.yml`; - -/** - * ユーザーが設定する必要のある情報 - */ -type Source = { - maintainer: string; - url: string; - secondary_url: string; - port: number; - https: { - enable: boolean; - key: string; - cert: string; - ca: string; - }; - mongodb: { - host: string; - port: number; - db: string; - user: string; - pass: string; - }; - redis: { - host: string; - port: number; - pass: string; - }; - elasticsearch: { - enable: boolean; - host: string; - port: number; - pass: string; - }; - recaptcha: { - siteKey: string; - secretKey: string; - }; - accesslog?: string; - accesses?: { - enable: boolean; - port: number; - }; - twitter?: { - consumer_key: string; - consumer_secret: string; - }; - github_bot?: { - hook_secret: string; - username: string; - }; -}; - -/** - * Misskeyが自動的に(ユーザーが設定した情報から推論して)設定する情報 - */ -type Mixin = { - host: string; - scheme: string; - secondary_host: string; - secondary_scheme: string; - api_url: string; - auth_url: string; - about_url: string; - stats_url: string; - status_url: string; - dev_url: string; - drive_url: string; -}; - -export type Config = Source & Mixin; - -export default function load() { - const config = yaml.safeLoad(fs.readFileSync(path, 'utf-8')) as Source; - - const mixin = {} as Mixin; - - // Validate URLs - if (!isUrl(config.url)) urlError(config.url); - if (!isUrl(config.secondary_url)) urlError(config.secondary_url); - - const url = URL.parse(config.url); - const head = url.host.split('.')[0]; - - if (head != 'misskey') { - console.error(`プライマリドメインは、必ず「misskey」ドメインで始まっていなければなりません(現在の設定では「${head}」で始まっています)。例えば「https://misskey.xyz」「http://misskey.my.app.example.com」などが正しいプライマリURLです。`); - process.exit(); - } - - config.url = normalizeUrl(config.url); - config.secondary_url = normalizeUrl(config.secondary_url); - - mixin.host = config.url.substr(config.url.indexOf('://') + 3); - mixin.scheme = config.url.substr(0, config.url.indexOf('://')); - mixin.secondary_host = config.secondary_url.substr(config.secondary_url.indexOf('://') + 3); - mixin.secondary_scheme = config.secondary_url.substr(0, config.secondary_url.indexOf('://')); - mixin.api_url = `${mixin.scheme}://api.${mixin.host}`; - mixin.auth_url = `${mixin.scheme}://auth.${mixin.host}`; - mixin.dev_url = `${mixin.scheme}://dev.${mixin.host}`; - mixin.about_url = `${mixin.scheme}://about.${mixin.host}`; - mixin.stats_url = `${mixin.scheme}://stats.${mixin.host}`; - mixin.status_url = `${mixin.scheme}://status.${mixin.host}`; - mixin.drive_url = `${mixin.secondary_scheme}://file.${mixin.secondary_host}`; - - return Object.assign(config, mixin); -} - -function normalizeUrl(url: string) { - return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url; -} - -function urlError(url: string) { - console.error(`「${url}」は、正しいURLではありません。先頭に http:// または https:// をつけ忘れてないかなど確認してください。`); - process.exit(); -} diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000000..7bfdca4612 --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,3 @@ +import load from './load'; + +export default load(); diff --git a/src/config/load.ts b/src/config/load.ts new file mode 100644 index 0000000000..9f4e2151f3 --- /dev/null +++ b/src/config/load.ts @@ -0,0 +1,57 @@ +/** + * Config loader + */ + +import * as fs from 'fs'; +import { URL } from 'url'; +import * as yaml from 'js-yaml'; +import { Source, Mixin } from './types'; +import isUrl = require('is-url'); + +/** + * Path of configuration directory + */ +const dir = `${__dirname}/../../.config`; + +/** + * Path of configuration file + */ +const path = process.env.NODE_ENV == 'test' + ? `${dir}/test.yml` + : `${dir}/default.yml`; + +export default function load() { + const config = yaml.safeLoad(fs.readFileSync(path, 'utf-8')) as Source; + + const mixin = {} as Mixin; + + // Validate URLs + if (!isUrl(config.url)) urlError(config.url); + + const url = new URL(config.url); + config.url = normalizeUrl(config.url); + + mixin.host = url.host; + mixin.hostname = url.hostname; + mixin.scheme = url.protocol.replace(/:$/, ''); + mixin.ws_scheme = mixin.scheme.replace('http', 'ws'); + mixin.ws_url = `${mixin.ws_scheme}://${mixin.host}`; + mixin.api_url = `${mixin.scheme}://${mixin.host}/api`; + mixin.auth_url = `${mixin.scheme}://${mixin.host}/auth`; + mixin.dev_url = `${mixin.scheme}://${mixin.host}/dev`; + mixin.docs_url = `${mixin.scheme}://${mixin.host}/docs`; + mixin.stats_url = `${mixin.scheme}://${mixin.host}/stats`; + mixin.status_url = `${mixin.scheme}://${mixin.host}/status`; + mixin.drive_url = `${mixin.scheme}://${mixin.host}/files`; + + return Object.assign(config, mixin); +} + +function normalizeUrl(url: string) { + return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url; +} + +function urlError(url: string) { + console.error(`「${url}」は、正しいURLではありません。先頭に http:// または https:// をつけ忘れてないかなど確認してください。`); + process.exit(); +} diff --git a/src/config/types.ts b/src/config/types.ts new file mode 100644 index 0000000000..f802e70d1e --- /dev/null +++ b/src/config/types.ts @@ -0,0 +1,97 @@ +/** + * ユーザーが設定する必要のある情報 + */ +export type Source = { + /** + * メンテナ情報 + */ + maintainer: { + /** + * メンテナの名前 + */ + name: string; + /** + * メンテナの連絡先(URLかmailto形式のURL) + */ + url: string; + }; + url: string; + port: number; + https?: { [x: string]: string }; + mongodb: { + host: string; + port: number; + db: string; + user: string; + pass: string; + }; + redis: { + host: string; + port: number; + pass: string; + }; + elasticsearch: { + enable: boolean; + host: string; + port: number; + pass: string; + }; + recaptcha: { + site_key: string; + secret_key: string; + }; + accesslog?: string; + accesses?: { + enable: boolean; + port: number; + }; + twitter?: { + consumer_key: string; + consumer_secret: string; + }; + github_bot?: { + hook_secret: string; + username: string; + }; + othello_ai?: { + id: string; + i: string; + }; + line_bot?: { + channel_secret: string; + channel_access_token: string; + }; + analysis?: { + mecab_command?: string; + }; + + /** + * Service Worker + */ + sw?: { + public_key: string; + private_key: string; + }; + + google_maps_api_key: string; +}; + +/** + * Misskeyが自動的に(ユーザーが設定した情報から推論して)設定する情報 + */ +export type Mixin = { + host: string; + hostname: string; + scheme: string; + ws_scheme: string; + api_url: string; + ws_url: string; + auth_url: string; + docs_url: string; + stats_url: string; + status_url: string; + dev_url: string; + drive_url: string; +}; + +export type Config = Source & Mixin; diff --git a/src/const.json b/src/const.json index 1032ed538f..65dc734fab 100644 --- a/src/const.json +++ b/src/const.json @@ -1,5 +1,5 @@ { - "themeColor": "#87bb35", - "themeColorForeground": "#fff", - "idea": ["#f13049", "#f43636"] + "copyright": "Copyright (c) 2014-2018 syuilo", + "themeColor": "#5cbb2d", + "themeColorForeground": "#fff" } diff --git a/src/crypto_key.cc b/src/crypto_key.cc new file mode 100644 index 0000000000..c8e4d8f7f0 --- /dev/null +++ b/src/crypto_key.cc @@ -0,0 +1,111 @@ +#include <nan.h> +#include <openssl/bio.h> +#include <openssl/buffer.h> +#include <openssl/crypto.h> +#include <openssl/pem.h> +#include <openssl/rsa.h> +#include <openssl/x509.h> + +NAN_METHOD(extractPublic) +{ + const auto sourceString = info[0]->ToString(); + if (!sourceString->IsOneByte()) { + Nan::ThrowError("Malformed character found"); + return; + } + + size_t sourceLength = sourceString->Length(); + const auto sourceBuf = new char[sourceLength]; + + Nan::DecodeWrite(sourceBuf, sourceLength, sourceString); + + const auto source = BIO_new_mem_buf(sourceBuf, sourceLength); + if (source == nullptr) { + Nan::ThrowError("Memory allocation failed"); + delete sourceBuf; + return; + } + + const auto rsa = PEM_read_bio_RSAPrivateKey(source, nullptr, nullptr, nullptr); + + BIO_free(source); + delete sourceBuf; + + if (rsa == nullptr) { + Nan::ThrowError("Decode failed"); + return; + } + + const auto destination = BIO_new(BIO_s_mem()); + if (destination == nullptr) { + Nan::ThrowError("Memory allocation failed"); + return; + } + + const auto result = PEM_write_bio_RSAPublicKey(destination, rsa); + + RSA_free(rsa); + + if (result != 1) { + Nan::ThrowError("Public key extraction failed"); + BIO_free(destination); + return; + } + + char *pem; + const auto pemLength = BIO_get_mem_data(destination, &pem); + + info.GetReturnValue().Set(Nan::Encode(pem, pemLength)); + BIO_free(destination); +} + +NAN_METHOD(generate) +{ + const auto exponent = BN_new(); + const auto mem = BIO_new(BIO_s_mem()); + const auto rsa = RSA_new(); + char *data; + long result; + + if (exponent == nullptr || mem == nullptr || rsa == nullptr) { + Nan::ThrowError("Memory allocation failed"); + goto done; + } + + result = BN_set_word(exponent, 65537); + if (result != 1) { + Nan::ThrowError("Exponent setting failed"); + goto done; + } + + result = RSA_generate_key_ex(rsa, 2048, exponent, nullptr); + if (result != 1) { + Nan::ThrowError("Key generation failed"); + goto done; + } + + result = PEM_write_bio_RSAPrivateKey(mem, rsa, NULL, NULL, 0, NULL, NULL); + if (result != 1) { + Nan::ThrowError("Key export failed"); + goto done; + } + + result = BIO_get_mem_data(mem, &data); + info.GetReturnValue().Set(Nan::Encode(data, result)); + +done: + RSA_free(rsa); + BIO_free(mem); + BN_free(exponent); +} + +NAN_MODULE_INIT(InitAll) +{ + Nan::Set(target, Nan::New<v8::String>("extractPublic").ToLocalChecked(), + Nan::GetFunction(Nan::New<v8::FunctionTemplate>(extractPublic)).ToLocalChecked()); + + Nan::Set(target, Nan::New<v8::String>("generate").ToLocalChecked(), + Nan::GetFunction(Nan::New<v8::FunctionTemplate>(generate)).ToLocalChecked()); +} + +NODE_MODULE(crypto_key, InitAll); diff --git a/src/crypto_key.d.ts b/src/crypto_key.d.ts new file mode 100644 index 0000000000..48efef2980 --- /dev/null +++ b/src/crypto_key.d.ts @@ -0,0 +1,2 @@ +export function extractPublic(keypair: String): String; +export function generate(): String; diff --git a/src/db/elasticsearch.ts b/src/db/elasticsearch.ts index 75054a31c2..957b7ad97d 100644 --- a/src/db/elasticsearch.ts +++ b/src/db/elasticsearch.ts @@ -1,5 +1,5 @@ import * as elasticsearch from 'elasticsearch'; -import config from '../conf'; +import config from '../config'; // Init ElasticSearch connection const client = new elasticsearch.Client({ diff --git a/src/db/mongodb.ts b/src/db/mongodb.ts index 6ee7f4534f..05bb72bfde 100644 --- a/src/db/mongodb.ts +++ b/src/db/mongodb.ts @@ -1,11 +1,41 @@ -import * as mongo from 'monk'; +import config from '../config'; -import config from '../conf'; +const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null; +const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null; -const uri = config.mongodb.user && config.mongodb.pass - ? `mongodb://${config.mongodb.user}:${config.mongodb.pass}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}` +const uri = u && p + ? `mongodb://${u}:${p}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}` : `mongodb://${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`; +/** + * monk + */ +import mongo from 'monk'; + const db = mongo(uri); export default db; + +/** + * MongoDB native module (officialy) + */ +import * as mongodb from 'mongodb'; + +let mdb: mongodb.Db; + +const nativeDbConn = async (): Promise<mongodb.Db> => { + if (mdb) return mdb; + + const db = await ((): Promise<mongodb.Db> => new Promise((resolve, reject) => { + (mongodb as any).MongoClient.connect(uri, (e, client) => { + if (e) return reject(e); + resolve(client.db(config.mongodb.db)); + }); + }))(); + + mdb = db; + + return db; +}; + +export { nativeDbConn }; diff --git a/src/db/redis.ts b/src/db/redis.ts index 2e0867de61..f8d66ebda0 100644 --- a/src/db/redis.ts +++ b/src/db/redis.ts @@ -1,5 +1,5 @@ import * as redis from 'redis'; -import config from '../conf'; +import config from '../config'; export default redis.createClient( config.redis.port, diff --git a/src/docs/api/entities/post.pug b/src/docs/api/entities/post.pug deleted file mode 100644 index e505d3fcb6..0000000000 --- a/src/docs/api/entities/post.pug +++ /dev/null @@ -1,149 +0,0 @@ -extend ../../BASE - -block title - | Entity: Post - -block content - h1 Post - p 投稿を表します。 - - section - h2 Properties - table.entity - thead: tr - td Name - td Type - td Description - tbody - tr.nullable.optional - td app - td: a(href='./app', target='_blank') App - td 投稿したアプリ - tr.nullable - td app_id - td ID - td 投稿したアプリのID - tr - td created_at - td Date - td 投稿日時 - tr - td id - td ID - td 投稿ID - tr.optional - td is_liked - td Boolean - td いいね したかどうか - tr - td likes_count - td Number - td いいね数 - tr.nullable.optional - td media_ids - td ID[] - td 添付されたメディアのIDの配列 - tr.nullable.optional - td media - td: a(href='./drive-file', target='_blank') DriveFile[] - td 添付されたメディアの配列 - tr - td replies_count - td Number - td 返信数 - tr.optional - td reply_to - td: a(href='./post', target='_blank') Post - td 返信先の投稿 - tr.nullable - td reply_to_id - td ID - td 返信先の投稿のID - tr.optional - td repost - td: a(href='./post', target='_blank') Post - td Repostした投稿 - tr - td repost_count - td Number - td Repostされた数 - tr.nullable - td repost_id - td ID - td Repostした投稿のID - tr.nullable - td text - td String - td 本文 - tr.optional - td user - td: a(href='./user', target='_blank') User - td 投稿者 - tr - td user_id - td ID - td 投稿者のID - - section - h2 Example - pre: code. - { - "created_at": "2016-12-10T00:28:50.114Z", - "media_ids": null, - "reply_to_id": "584a16b15860fc52320137e3", - "repost_id": null, - "text": "小日向美穂だぞ!", - "user_id": "5848bf7764e572683f4402f8", - "app_id": null, - "likes_count": 1, - "replies_count": 1, - "id": "584b4c42d8e5186f8f755d0c", - "user": { - "birthday": null, - "created_at": "2016-12-08T02:03:35.332Z", - "bio": "女が嫌いです、女性は好きです", - "followers_count": 11, - "following_count": 11, - "links": null, - "location": "", - "name": "女が嫌い", - "posts_count": 26, - "likes_count": 2, - "liked_count": 20, - "username": "onnnagakirai", - "id": "5848bf7764e572683f4402f8", - "avatar_url": "https://file.himasaku.net/5848c0ec64e572683f4402fc", - "banner_url": "https://file.himasaku.net/5848c12864e572683f4402fd", - "is_following": true, - "is_followed": true - }, - "reply_to": { - "created_at": "2016-12-09T02:28:01.563Z", - "media_ids": null, - "reply_to_id": "5849d35e547e4249be329884", - "repost_id": null, - "text": "アイコン小日向美穂?", - "user_id": "57d01a501fdf2d07be417afe", - "app_id": null, - "replies_count": 1, - "id": "584a16b15860fc52320137e3", - "user": { - "birthday": null, - "created_at": "2016-09-07T13:46:56.605Z", - "bio": "どうすれば君だけのために生きていけるの", - "followers_count": 51, - "following_count": 97, - "links": null, - "location": "川崎", - "name": "きな子", - "posts_count": 4813, - "username": "syuilo", - "likes_count": 3141, - "liked_count": 750, - "id": "57d01a501fdf2d07be417afe", - "avatar_url": "https://file.himasaku.net/583ddc6e64df272771f74c1a", - "banner_url": "https://file.himasaku.net/584bfc82d8e5186f8f755ec5" - } - }, - "is_liked": true - } diff --git a/src/docs/api/entities/user.pug b/src/docs/api/entities/user.pug deleted file mode 100644 index a37886bb19..0000000000 --- a/src/docs/api/entities/user.pug +++ /dev/null @@ -1,122 +0,0 @@ -extend ../../BASE - -block title - | Entity: User - -block content - h1 User - p ユーザーを表します。 - - section - h2 Properties - table.entity - thead: tr - td Name - td Type - td Description - tbody - tr.nullable.optional - td avatar_id - td ID - td アバターに設定しているドライブのファイルのID - tr.nullable - td avatar_url - td String - td アバターURL - tr.nullable.optional - td banner_id - td ID - td バナーに設定しているドライブのファイルのID - tr.nullable - td banner_url - td String - td バナーURL - tr.nullable - td bio - td String - td プロフィール - tr.nullable - td birthday - td String - td 誕生日(YYYY-MM-DD) - tr - td created_at - td Date - td アカウント作成日時 - tr.optional - td drive_capacity - td Number - td ドライブの最大容量(byte単位) - tr - td followers_count - td Number - td フォロワー数 - tr - td following_count - td Number - td フォロー数 - tr - td id - td ID - td ユーザーID - tr.optional - td is_bot - td Boolean - td botかどうか - tr.optional - td is_followed - td Boolean - td フォローされているか - tr.optional - td is_following - td Boolean - td フォローしているか - tr - td liked_count - td Number - td 投稿にいいねされた数 - tr - td likes_count - td Number - td 投稿にいいねした数 - tr.nullable - td location - td String - td 住処 - tr - td name - td String - td ニックネーム - tr - td posts_count - td Number - td 投稿数 - tr - td username - td String - td ユーザー名 - - section - h2 Example - pre: code. - { - "avatar_id": "583ddc6e64df272771f74c1a", - "avatar_url": "https://file.himasaku.net/583ddc6e64df272771f74c1a", - "banner_id": "584bfc82d8e5186f8f755ec5", - "banner_url": "https://file.himasaku.net/584bfc82d8e5186f8f755ec5", - "bio": "どうすれば君だけのために生きていけるの", - "birthday": "1997-12-06", - "created_at": "2016-09-07T13:46:56.605Z", - "drive_capacity": 1073741824, - "email": null, - "followers_count": 51, - "following_count": 97, - "id": "57d01a501fdf2d07be417afe", - "liked_count": 750, - "likes_count": 3130, - "links": null, - "location": "川崎", - "name": "きな子", - "posts_count": 4811, - "username": "syuilo" - } diff --git a/src/docs/api/getting-started.md b/src/docs/api/getting-started.md deleted file mode 100644 index e13659914e..0000000000 --- a/src/docs/api/getting-started.md +++ /dev/null @@ -1,73 +0,0 @@ -Getting Started -================================================================ -MisskeyはREST APIやStreaming APIを提供しており、プログラムからMisskeyの全ての機能を利用することができます。 -それらのAPIを利用するには、まずAPIを利用したいアカウントのアクセストークンを取得する必要があります: - -自分のアクセストークンを取得したい場合 ----------------------------------------------------------------- -自分自身のアクセストークンは、設定 > API で確認できます。 -<p class="tip"> - アカウントを乗っ取られてしまう可能性があるため、トークンは第三者に教えないでください(アプリなどにも入力しないでください)。<br> - 万が一トークンが漏れたりその可能性がある場合は トークンを再生成できます。(副作用として、ログインしているすべてのデバイスでログアウトが発生します) -</p> - -他人のアクセストークンを取得する ----------------------------------------------------------------- -不特定多数のユーザーからAPIを利用したい場合、アプリケーションを作成します。 -アプリケーションを作成すると、ユーザーが連携を許可した時に、そのユーザーのアクセストークンを取得することができます。 - -アプリケーションを作成してアクセストークンを取得するまでの流れを説明します。 - -### アプリケーションを作成する -まずはあなたのアプリケーションを作成しましょう。 - | <a href=#{dev_url} target="_blank">デベロッパーセンター</a>にアクセスし、アプリ > アプリ作成 に進みます。 - br - | 次に、フォームに必要事項を記入します: - dl - dt アプリケーション名 - dd あなたのアプリケーションの名前。 - dt Named ID - dd アプリを識別する/a-z-/で構成されたID。 - dt アプリの概要 - dd アプリの簡単な説明を入力してください。 - dt コールバックURL - dd あなたのアプリケーションがWebアプリケーションである場合、ユーザーが後述するフォームで認証を終えた際にリダイレクトするURLを設定できます。 - dt 権限 - dd アプリケーションが要求する権限。ここで要求した機能だけがAPIからアクセスできます。 - p.tip - | 権限はアプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーはすべて無効になります。 - p - | アプリケーションを作成すると、作ったアプリの管理ページに進みます。 - br - | アプリのシークレットキー(App Secret)が表示されていますので、メモしておいてください。 - p.tip - | アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。 - - section - h3 ユーザーに認証させる - p あなたのアプリを使ってもらうには、ユーザーにアカウントへアクセスすることを許可してもらい、Misskeyにそのユーザーのアクセストークンを発行してもらう必要があります。 - p 認証セッションを開始するには、<code>#{api_url}/auth/session/generate</code>へパラメータに<code>app_secret</code>としてApp Secretを含めたリクエストを送信します。 - p - | そうすると、レスポンスとして認証セッションのトークンや認証フォームのURLが取得できます。 - br - | この認証フォームのURLをブラウザで表示し、ユーザーにフォームを表示してください。 - section - h4 あなたのアプリがコールバックURLを設定している場合 - p ユーザーがアプリの連携を許可すると設定しているコールバックURLに<code>token</code>という名前でセッションのトークンが含まれたクエリを付けてリダイレクトします。 - section - h4 あなたのアプリがコールバックURLを設定していない場合 - p ユーザーがアプリの連携を許可したことを(何らかの方法で(たとえばボタンを押させるなど))確認出来るようにしてください。 - p - | 次に、<code>#{api_url}/auth/session/userkey</code>へ<code>app_secret</code>としてApp Secretを、<code>token</code>としてセッションのトークンをパラメータとして付与したリクエストを送信してください。 - br - | 上手くいけば、認証したユーザーのアクセストークンがレスポンスとして取得できます。おめでとうございます! - p - | 以降アクセストークンは、<strong>ユーザーのアクセストークン+アプリのシークレットキーをsha256したもの</strong>として扱います。 - - p アクセストークンを取得できたら、あとは簡単です。REST APIなら、リクエストにアクセストークンを<code>i</code>としてパラメータに含めるだけです。 - - section - h2 リクエスト形式 - p <code>application/json</code>を受け付けます。 - p.tip - | 現在<code>application/x-www-form-urlencoded</code>も受け付けていますが、将来的にこのサポートはされなくなる予定です。 diff --git a/src/docs/api/library.md b/src/docs/api/library.md deleted file mode 100644 index 71ddbe345d..0000000000 --- a/src/docs/api/library.md +++ /dev/null @@ -1,8 +0,0 @@ -ライブラリ -================================================================ - -Misskey APIを便利に利用するためのライブラリ一覧です。 - -.NET ----------------------------------------------------------------- -* **[Misq (公式)](https://github.com/syuilo/Misq)** diff --git a/src/docs/index.md b/src/docs/index.md deleted file mode 100644 index 0846cf27e8..0000000000 --- a/src/docs/index.md +++ /dev/null @@ -1,4 +0,0 @@ -Misskeyについて -================================================================ - -誰か書いて diff --git a/src/docs/link-to-twitter.md b/src/docs/link-to-twitter.md deleted file mode 100644 index 77fb744576..0000000000 --- a/src/docs/link-to-twitter.md +++ /dev/null @@ -1,9 +0,0 @@ -Twitterと連携する -================================================================ - -設定 -> Twitter から、お使いのMisskeyアカウントとお使いのTwitterアカウントを関連付けることができます。 -アカウントの関連付けを行うと、プロフィールにTwitterアカウントへのリンクが表示されたりなどします。 - -MisskeyがあなたのTwitterアカウントでツイートしたり誰かをフォローしたりといったことは、 -一切行いませんのでご安心ください。(Misskeyはそのような権限を取得しないので、行おうと思っても行えません) -Twitterのアプリケーション認証フォームでこの権限の詳細を確認することができます。また、いつでも連携を取り消すことができます。 diff --git a/src/docs/tou.md b/src/docs/tou.md deleted file mode 100644 index fbf87867b4..0000000000 --- a/src/docs/tou.md +++ /dev/null @@ -1,4 +0,0 @@ -利用規約 -================================================================ - -公序良俗に反する行為はおやめください。 diff --git a/src/file/server.ts b/src/file/server.ts deleted file mode 100644 index ee67cf7860..0000000000 --- a/src/file/server.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * File Server - */ - -import * as fs from 'fs'; -import * as express from 'express'; -import * as bodyParser from 'body-parser'; -import * as cors from 'cors'; -import * as mongodb from 'mongodb'; -import * as gm from 'gm'; - -import File from '../api/models/drive-file'; - -/** - * Init app - */ -const app = express(); - -app.disable('x-powered-by'); -app.locals.cache = true; -app.use(bodyParser.urlencoded({ extended: true })); -app.use(cors()); - -/** - * Statics - */ -app.use('/assets', express.static(`${__dirname}/assets`, { - maxAge: 1000 * 60 * 60 * 24 * 365 // 一年 -})); - -app.get('/', (req, res) => { - res.send('yee haw'); -}); - -app.get('/default-avatar.jpg', (req, res) => { - const file = fs.readFileSync(`${__dirname}/assets/avatar.jpg`); - send(file, 'image/jpeg', req, res); -}); - -app.get('/app-default.jpg', (req, res) => { - const file = fs.readFileSync(`${__dirname}/assets/dummy.png`); - send(file, 'image/png', req, res); -}); - -async function raw(data: Buffer, type: string, download: boolean, res: express.Response): Promise<any> { - res.header('Content-Type', type); - - if (download) { - res.header('Content-Disposition', 'attachment'); - } - - res.send(data); -} - -async function thumbnail(data: Buffer, type: string, resize: number, res: express.Response): Promise<any> { - if (!/^image\/.*$/.test(type)) { - data = fs.readFileSync(`${__dirname}/assets/dummy.png`); - } - - let g = gm(data); - - if (resize) { - g = g.resize(resize, resize); - } - - g - .compress('jpeg') - .quality(80) - .toBuffer('jpeg', (err, img) => { - if (err !== undefined && err !== null) { - console.error(err); - res.sendStatus(500); - return; - } - - res.header('Content-Type', 'image/jpeg'); - res.send(img); - }); -} - -function send(data: Buffer, type: string, req: express.Request, res: express.Response): void { - if (req.query.thumbnail !== undefined) { - thumbnail(data, type, req.query.size, res); - } else { - raw(data, type, req.query.download !== undefined, res); - } -} - -/** - * Routing - */ - -app.get('/:id', async (req, res) => { - // Validate id - if (!mongodb.ObjectID.isValid(req.params.id)) { - res.status(400).send('incorrect id'); - return; - } - - const file = await File.findOne({ _id: new mongodb.ObjectID(req.params.id) }); - - if (file == null) { - res.status(404).sendFile(`${__dirname} / assets / dummy.png`); - return; - } else if (file.data == null) { - res.sendStatus(400); - return; - } - - send(file.data.buffer, file.type, req, res); -}); - -app.get('/:id/:name', async (req, res) => { - // Validate id - if (!mongodb.ObjectID.isValid(req.params.id)) { - res.status(400).send('incorrect id'); - return; - } - - const file = await File.findOne({ _id: new mongodb.ObjectID(req.params.id) }); - - if (file == null) { - res.status(404).sendFile(`${__dirname}/assets/dummy.png`); - return; - } else if (file.data == null) { - res.sendStatus(400); - return; - } - - send(file.data.buffer, file.type, req, res); -}); - -module.exports = app; diff --git a/src/himasaku/assets/himasaku.png b/src/himasaku/assets/himasaku.png deleted file mode 100644 index 25cd91e954..0000000000 Binary files a/src/himasaku/assets/himasaku.png and /dev/null differ diff --git a/src/himasaku/assets/index.html b/src/himasaku/assets/index.html deleted file mode 100644 index f9e45d7a74..0000000000 --- a/src/himasaku/assets/index.html +++ /dev/null @@ -1,35 +0,0 @@ -<!DOCTYPE html> -<html> - <head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no"> - <meta name="description" content="ひまさく"> - <meta name="keywords" content="ひまさく, さくひま, 向日葵, 櫻子"> - <title>ひまさく</title> - <style> - html { - height: 100%; - font-size: 0; - } - - body { - margin: 0; - height: 100%; - overflow: hidden; - } - - img { - display: block; - position: absolute; - max-width: 100%; - margin: auto; - top: 0; right: 0; bottom: 0; left: 0; - pointer-events: none; - user-select: none; - } - </style> - </head> - <body> - <img src="/himasaku.png" alt="ひまさく"> - </body> -</html> diff --git a/src/himasaku/server.ts b/src/himasaku/server.ts deleted file mode 100644 index fb129513de..0000000000 --- a/src/himasaku/server.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Himasaku Server - */ - -import * as express from 'express'; - -/** - * Init app - */ -const app = express(); - -app.disable('x-powered-by'); -app.locals.cache = true; - -app.get('/himasaku.png', (req, res) => { - res.sendFile(`${__dirname}/assets/himasaku.png`); -}); - -app.get('*', (req, res) => { - res.sendFile(`${__dirname}/assets/index.html`); -}); - -module.exports = app; diff --git a/src/index.ts b/src/index.ts index aa53c91239..68b289793b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,11 +4,10 @@ Error.stackTraceLimit = Infinity; -import * as fs from 'fs'; import * as os from 'os'; import * as cluster from 'cluster'; import * as debug from 'debug'; -import * as chalk from 'chalk'; +import chalk from 'chalk'; // import portUsed = require('tcp-port-used'); import isRoot = require('is-root'); import { master } from 'accesses'; @@ -21,14 +20,23 @@ import MachineInfo from './utils/machineInfo'; import DependencyInfo from './utils/dependencyInfo'; import stats from './utils/stats'; -import { Config, path as configPath } from './config'; -import loadConfig from './config'; +import loadConfig from './config/load'; +import { Config } from './config/types'; + +import parseOpt from './parse-opt'; const clusterLog = debug('misskey:cluster'); const ev = new Xev(); process.title = 'Misskey'; +if (process.env.NODE_ENV != 'production') { + process.env.DEBUG = 'misskey:*'; +} + +// https://github.com/Automattic/kue/issues/822 +require('events').EventEmitter.prototype._maxListeners = 512; + // Start app main(); @@ -36,20 +44,22 @@ main(); * Init process */ function main() { + const opt = parseOpt(process.argv, 2); + if (cluster.isMaster) { - masterMain(); + masterMain(opt); ev.mount(); stats(); } else { - workerMain(); + workerMain(opt); } } /** * Init master process */ -async function masterMain() { +async function masterMain(opt) { let config: Config; try { @@ -69,19 +79,35 @@ async function masterMain() { } spawnWorkers(() => { - Logger.info(chalk.bold.green( - `Now listening on port ${chalk.underline(config.port.toString())}`)); + if (!opt['only-processor']) { + Logger.info(chalk.bold.green( + `Now listening on port ${chalk.underline(config.port.toString())}`)); - Logger.info(chalk.bold.green(config.url)); + Logger.info(chalk.bold.green(config.url)); + } + + if (!opt['only-server']) { + Logger.info(chalk.bold.green('Now processing jobs')); + } }); } /** * Init worker process */ -function workerMain() { - // start server - require('./server'); +async function workerMain(opt) { + if (!opt['only-processor']) { + // start server + await require('./server').default(); + } + + if (!opt['only-server']) { + // start processor + require('./queue').default(); + } + + // Send a 'ready' message to parent process + process.send('ready'); } /** @@ -89,7 +115,6 @@ function workerMain() { */ async function init(): Promise<Config> { Logger.info('Welcome to Misskey!'); - Logger.info(chalk.bold('Misskey <aoi>')); Logger.info('Initializing...'); EnvironmentInfo.show(); @@ -97,11 +122,17 @@ async function init(): Promise<Config> { new DependencyInfo().showAll(); const configLogger = new Logger('Config'); - if (!fs.existsSync(configPath)) { - throw 'Configuration not found - Please run "npm run config" command.'; - } + let config; - const config = loadConfig(); + try { + config = loadConfig(); + } catch (exception) { + if (exception.code === 'ENOENT') { + throw 'Configuration not found - Please run "npm run config" command.'; + } + + throw exception; + } configLogger.info('Successfully loaded'); configLogger.info(`maintainer: ${config.maintainer}`); diff --git a/src/models/access-token.ts b/src/models/access-token.ts new file mode 100644 index 0000000000..4451ca140d --- /dev/null +++ b/src/models/access-token.ts @@ -0,0 +1,16 @@ +import * as mongo from 'mongodb'; +import db from '../db/mongodb'; + +const AccessToken = db.get<IAccessTokens>('accessTokens'); +AccessToken.createIndex('token'); +AccessToken.createIndex('hash'); +export default AccessToken; + +export type IAccessTokens = { + _id: mongo.ObjectID; + createdAt: Date; + appId: mongo.ObjectID; + userId: mongo.ObjectID; + token: string; + hash: string; +}; diff --git a/src/api/serializers/app.ts b/src/models/app.ts similarity index 60% rename from src/api/serializers/app.ts rename to src/models/app.ts index 9d1c46dca4..45c95d92d8 100644 --- a/src/api/serializers/app.ts +++ b/src/models/app.ts @@ -1,21 +1,41 @@ -/** - * Module dependencies - */ import * as mongo from 'mongodb'; import deepcopy = require('deepcopy'); -import App from '../models/app'; -import AccessToken from '../models/access-token'; -import config from '../../conf'; +import AccessToken from './access-token'; +import db from '../db/mongodb'; +import config from '../config'; + +const App = db.get<IApp>('apps'); +App.createIndex('nameId'); +App.createIndex('nameIdLower'); +App.createIndex('secret'); +export default App; + +export type IApp = { + _id: mongo.ObjectID; + createdAt: Date; + userId: mongo.ObjectID; + secret: string; + name: string; + nameId: string; + nameIdLower: string; + description: string; + permission: string[]; + callbackUrl: string; +}; + +export function isValidNameId(nameId: string): boolean { + return typeof nameId == 'string' && /^[a-zA-Z0-9_]{1,30}$/.test(nameId); +} /** - * Serialize an app + * Pack an app for API response * * @param {any} app * @param {any} me? * @param {any} options? * @return {Promise<any>} */ -export default ( +export const pack = ( app: any, me?: any, options?: { @@ -56,27 +76,27 @@ export default ( _app.id = _app._id; delete _app._id; - delete _app.name_id_lower; + delete _app.nameIdLower; // Visible by only owner if (!opts.includeSecret) { delete _app.secret; } - _app.icon_url = _app.icon != null + _app.iconUrl = _app.icon != null ? `${config.drive_url}/${_app.icon}` : `${config.drive_url}/app-default.jpg`; if (me) { // 既に連携しているか const exist = await AccessToken.count({ - app_id: _app.id, - user_id: me, + appId: _app.id, + userId: me, }, { limit: 1 }); - _app.is_authorized = exist === 1; + _app.isAuthorized = exist === 1; } resolve(_app); diff --git a/src/api/serializers/auth-session.ts b/src/models/auth-session.ts similarity index 57% rename from src/api/serializers/auth-session.ts rename to src/models/auth-session.ts index a9acf1243a..6fe3468a7b 100644 --- a/src/api/serializers/auth-session.ts +++ b/src/models/auth-session.ts @@ -1,25 +1,33 @@ -/** - * Module dependencies - */ import * as mongo from 'mongodb'; import deepcopy = require('deepcopy'); -import serializeApp from './app'; +import db from '../db/mongodb'; +import { pack as packApp } from './app'; + +const AuthSession = db.get<IAuthSession>('authSessions'); +export default AuthSession; + +export interface IAuthSession { + _id: mongo.ObjectID; + createdAt: Date; + appId: mongo.ObjectID; + userId: mongo.ObjectID; + token: string; +} /** - * Serialize an auth session + * Pack an auth session for API response * * @param {any} session * @param {any} me? * @return {Promise<any>} */ -export default ( +export const pack = ( session: any, me?: any ) => new Promise<any>(async (resolve, reject) => { let _session: any; // TODO: Populate session if it ID - _session = deepcopy(session); // Me @@ -34,7 +42,7 @@ export default ( delete _session._id; // Populate app - _session.app = await serializeApp(_session.app_id, me); + _session.app = await packApp(_session.appId, me); resolve(_session); }); diff --git a/src/models/channel-watching.ts b/src/models/channel-watching.ts new file mode 100644 index 0000000000..44ca06883f --- /dev/null +++ b/src/models/channel-watching.ts @@ -0,0 +1,13 @@ +import * as mongo from 'mongodb'; +import db from '../db/mongodb'; + +const ChannelWatching = db.get<IChannelWatching>('channelWatching'); +export default ChannelWatching; + +export interface IChannelWatching { + _id: mongo.ObjectID; + createdAt: Date; + deletedAt: Date; + channelId: mongo.ObjectID; + userId: mongo.ObjectID; +} diff --git a/src/models/channel.ts b/src/models/channel.ts new file mode 100644 index 0000000000..67386ac072 --- /dev/null +++ b/src/models/channel.ts @@ -0,0 +1,75 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import { IUser } from './user'; +import Watching from './channel-watching'; +import db from '../db/mongodb'; + +const Channel = db.get<IChannel>('channels'); +export default Channel; + +export type IChannel = { + _id: mongo.ObjectID; + createdAt: Date; + title: string; + userId: mongo.ObjectID; + index: number; + watchingCount: number; +}; + +/** + * Pack a channel for API response + * + * @param channel target + * @param me? serializee + * @return response + */ +export const pack = ( + channel: string | mongo.ObjectID | IChannel, + me?: string | mongo.ObjectID | IUser +) => new Promise<any>(async (resolve, reject) => { + + let _channel: any; + + // Populate the channel if 'channel' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(channel)) { + _channel = await Channel.findOne({ + _id: channel + }); + } else if (typeof channel === 'string') { + _channel = await Channel.findOne({ + _id: new mongo.ObjectID(channel) + }); + } else { + _channel = deepcopy(channel); + } + + // Rename _id to id + _channel.id = _channel._id; + delete _channel._id; + + // Remove needless properties + delete _channel.userId; + + // Me + const meId: mongo.ObjectID = me + ? mongo.ObjectID.prototype.isPrototypeOf(me) + ? me as mongo.ObjectID + : typeof me === 'string' + ? new mongo.ObjectID(me) + : (me as IUser)._id + : null; + + if (me) { + //#region Watchしているかどうか + const watch = await Watching.findOne({ + userId: meId, + channelId: _channel.id, + deletedAt: { $exists: false } + }); + + _channel.isWatching = watch !== null; + //#endregion + } + + resolve(_channel); +}); diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts new file mode 100644 index 0000000000..c86570f0f7 --- /dev/null +++ b/src/models/drive-file.ts @@ -0,0 +1,119 @@ +import * as mongodb from 'mongodb'; +import deepcopy = require('deepcopy'); +import { pack as packFolder } from './drive-folder'; +import config from '../config'; +import monkDb, { nativeDbConn } from '../db/mongodb'; + +const DriveFile = monkDb.get<IDriveFile>('driveFiles.files'); + +DriveFile.createIndex('metadata.uri', { sparse: true, unique: true }); + +export default DriveFile; + +const getGridFSBucket = async (): Promise<mongodb.GridFSBucket> => { + const db = await nativeDbConn(); + const bucket = new mongodb.GridFSBucket(db, { + bucketName: 'driveFiles' + }); + return bucket; +}; + +export { getGridFSBucket }; + +export type IMetadata = { + properties: any; + userId: mongodb.ObjectID; + folderId: mongodb.ObjectID; + comment: string; + uri: string; +}; + +export type IDriveFile = { + _id: mongodb.ObjectID; + uploadDate: Date; + md5: string; + filename: string; + contentType: string; + metadata: IMetadata; +}; + +export function validateFileName(name: string): boolean { + return ( + (name.trim().length > 0) && + (name.length <= 200) && + (name.indexOf('\\') === -1) && + (name.indexOf('/') === -1) && + (name.indexOf('..') === -1) + ); +} + +/** + * Pack a drive file for API response + * + * @param {any} file + * @param {any} options? + * @return {Promise<any>} + */ +export const pack = ( + file: any, + options?: { + detail: boolean + } +) => new Promise<any>(async (resolve, reject) => { + const opts = Object.assign({ + detail: false + }, options); + + let _file: any; + + // Populate the file if 'file' is ID + if (mongodb.ObjectID.prototype.isPrototypeOf(file)) { + _file = await DriveFile.findOne({ + _id: file + }); + } else if (typeof file === 'string') { + _file = await DriveFile.findOne({ + _id: new mongodb.ObjectID(file) + }); + } else { + _file = deepcopy(file); + } + + if (!_file) return reject('invalid file arg.'); + + // rendered target + let _target: any = {}; + + _target.id = _file._id; + _target.createdAt = _file.uploadDate; + _target.name = _file.filename; + _target.type = _file.contentType; + _target.datasize = _file.length; + _target.md5 = _file.md5; + + _target = Object.assign(_target, _file.metadata); + + _target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`; + + if (_target.properties == null) _target.properties = {}; + + if (opts.detail) { + if (_target.folderId) { + // Populate folder + _target.folder = await packFolder(_target.folderId, { + detail: true + }); + } + + /* + if (_target.tags) { + // Populate tags + _target.tags = await _target.tags.map(async (tag: any) => + await serializeDriveTag(tag) + ); + } + */ + } + + resolve(_target); +}); diff --git a/src/api/serializers/drive-folder.ts b/src/models/drive-folder.ts similarity index 56% rename from src/api/serializers/drive-folder.ts rename to src/models/drive-folder.ts index a428464108..45cc9c9649 100644 --- a/src/api/serializers/drive-folder.ts +++ b/src/models/drive-folder.ts @@ -1,19 +1,34 @@ -/** - * Module dependencies - */ import * as mongo from 'mongodb'; -import DriveFolder from '../models/drive-folder'; -import DriveFile from '../models/drive-file'; import deepcopy = require('deepcopy'); +import db from '../db/mongodb'; +import DriveFile from './drive-file'; + +const DriveFolder = db.get<IDriveFolder>('driveFolders'); +export default DriveFolder; + +export type IDriveFolder = { + _id: mongo.ObjectID; + createdAt: Date; + name: string; + userId: mongo.ObjectID; + parentId: mongo.ObjectID; +}; + +export function isValidFolderName(name: string): boolean { + return ( + (name.trim().length > 0) && + (name.length <= 200) + ); +} /** - * Serialize a drive folder + * Pack a drive folder for API response * * @param {any} folder * @param {any} options? * @return {Promise<any>} */ -const self = ( +export const pack = ( folder: any, options?: { detail: boolean @@ -40,25 +55,23 @@ const self = ( if (opts.detail) { const childFoldersCount = await DriveFolder.count({ - parent_id: _folder.id + parentId: _folder.id }); const childFilesCount = await DriveFile.count({ - folder_id: _folder.id + 'metadata.folderId': _folder.id }); - _folder.folders_count = childFoldersCount; - _folder.files_count = childFilesCount; + _folder.foldersCount = childFoldersCount; + _folder.filesCount = childFilesCount; } - if (opts.detail && _folder.parent_id) { + if (opts.detail && _folder.parentId) { // Populate parent folder - _folder.parent = await self(_folder.parent_id, { + _folder.parent = await pack(_folder.parentId, { detail: true }); } resolve(_folder); }); - -export default self; diff --git a/src/models/favorite.ts b/src/models/favorite.ts new file mode 100644 index 0000000000..73f8881926 --- /dev/null +++ b/src/models/favorite.ts @@ -0,0 +1,12 @@ +import * as mongo from 'mongodb'; +import db from '../db/mongodb'; + +const Favorites = db.get<IFavorite>('favorites'); +export default Favorites; + +export type IFavorite = { + _id: mongo.ObjectID; + createdAt: Date; + userId: mongo.ObjectID; + noteId: mongo.ObjectID; +}; diff --git a/src/models/followed-log.ts b/src/models/followed-log.ts new file mode 100644 index 0000000000..9e3ca17822 --- /dev/null +++ b/src/models/followed-log.ts @@ -0,0 +1,12 @@ +import { ObjectID } from 'mongodb'; +import db from '../db/mongodb'; + +const FollowedLog = db.get<IFollowedLog>('followedLogs'); +export default FollowedLog; + +export type IFollowedLog = { + _id: ObjectID; + createdAt: Date; + userId: ObjectID; + count: number; +}; diff --git a/src/models/following-log.ts b/src/models/following-log.ts new file mode 100644 index 0000000000..045ff7bf02 --- /dev/null +++ b/src/models/following-log.ts @@ -0,0 +1,12 @@ +import { ObjectID } from 'mongodb'; +import db from '../db/mongodb'; + +const FollowingLog = db.get<IFollowingLog>('followingLogs'); +export default FollowingLog; + +export type IFollowingLog = { + _id: ObjectID; + createdAt: Date; + userId: ObjectID; + count: number; +}; diff --git a/src/models/following.ts b/src/models/following.ts new file mode 100644 index 0000000000..b4090d8c7e --- /dev/null +++ b/src/models/following.ts @@ -0,0 +1,13 @@ +import * as mongo from 'mongodb'; +import db from '../db/mongodb'; + +const Following = db.get<IFollowing>('following'); +Following.createIndex(['followerId', 'followeeId'], { unique: true }); +export default Following; + +export type IFollowing = { + _id: mongo.ObjectID; + createdAt: Date; + followeeId: mongo.ObjectID; + followerId: mongo.ObjectID; +}; diff --git a/src/models/messaging-history.ts b/src/models/messaging-history.ts new file mode 100644 index 0000000000..6864e22d2f --- /dev/null +++ b/src/models/messaging-history.ts @@ -0,0 +1,13 @@ +import * as mongo from 'mongodb'; +import db from '../db/mongodb'; + +const MessagingHistory = db.get<IMessagingHistory>('messagingHistories'); +export default MessagingHistory; + +export type IMessagingHistory = { + _id: mongo.ObjectID; + updatedAt: Date; + userId: mongo.ObjectID; + partnerId: mongo.ObjectID; + messageId: mongo.ObjectID; +}; diff --git a/src/models/messaging-message.ts b/src/models/messaging-message.ts new file mode 100644 index 0000000000..974ee54ab8 --- /dev/null +++ b/src/models/messaging-message.ts @@ -0,0 +1,77 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import { pack as packUser } from './user'; +import { pack as packFile } from './drive-file'; +import db from '../db/mongodb'; + +const MessagingMessage = db.get<IMessagingMessage>('messagingMessages'); +export default MessagingMessage; + +export interface IMessagingMessage { + _id: mongo.ObjectID; + createdAt: Date; + text: string; + textHtml: string; + userId: mongo.ObjectID; + recipientId: mongo.ObjectID; + isRead: boolean; + fileId: mongo.ObjectID; +} + +export function isValidText(text: string): boolean { + return text.length <= 1000 && text.trim() != ''; +} + +/** + * Pack a messaging message for API response + * + * @param {any} message + * @param {any} me? + * @param {any} options? + * @return {Promise<any>} + */ +export const pack = ( + message: any, + me?: any, + options?: { + populateRecipient: boolean + } +) => new Promise<any>(async (resolve, reject) => { + const opts = options || { + populateRecipient: true + }; + + let _message: any; + + // Populate the message if 'message' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(message)) { + _message = await MessagingMessage.findOne({ + _id: message + }); + } else if (typeof message === 'string') { + _message = await MessagingMessage.findOne({ + _id: new mongo.ObjectID(message) + }); + } else { + _message = deepcopy(message); + } + + // Rename _id to id + _message.id = _message._id; + delete _message._id; + + // Populate user + _message.user = await packUser(_message.userId, me); + + if (_message.fileId) { + // Populate file + _message.file = await packFile(_message.fileId); + } + + if (opts.populateRecipient) { + // Populate recipient + _message.recipient = await packUser(_message.recipientId, me); + } + + resolve(_message); +}); diff --git a/src/models/meta.ts b/src/models/meta.ts new file mode 100644 index 0000000000..710bb23382 --- /dev/null +++ b/src/models/meta.ts @@ -0,0 +1,8 @@ +import db from '../db/mongodb'; + +const Meta = db.get<IMeta>('meta'); +export default Meta; + +export type IMeta = { + broadcasts: any[]; +}; diff --git a/src/models/mute.ts b/src/models/mute.ts new file mode 100644 index 0000000000..8793615967 --- /dev/null +++ b/src/models/mute.ts @@ -0,0 +1,13 @@ +import * as mongo from 'mongodb'; +import db from '../db/mongodb'; + +const Mute = db.get<IMute>('mute'); +export default Mute; + +export interface IMute { + _id: mongo.ObjectID; + createdAt: Date; + deletedAt: Date; + muterId: mongo.ObjectID; + muteeId: mongo.ObjectID; +} diff --git a/src/api/serializers/post-reaction.ts b/src/models/note-reaction.ts similarity index 57% rename from src/api/serializers/post-reaction.ts rename to src/models/note-reaction.ts index b8807a741c..d499442de9 100644 --- a/src/api/serializers/post-reaction.ts +++ b/src/models/note-reaction.ts @@ -1,19 +1,29 @@ -/** - * Module dependencies - */ import * as mongo from 'mongodb'; import deepcopy = require('deepcopy'); -import Reaction from '../models/post-reaction'; -import serializeUser from './user'; +import db from '../db/mongodb'; +import Reaction from './note-reaction'; +import { pack as packUser } from './user'; + +const NoteReaction = db.get<INoteReaction>('noteReactions'); +NoteReaction.createIndex(['userId', 'noteId'], { unique: true }); +export default NoteReaction; + +export interface INoteReaction { + _id: mongo.ObjectID; + createdAt: Date; + noteId: mongo.ObjectID; + userId: mongo.ObjectID; + reaction: string; +} /** - * Serialize a reaction + * Pack a reaction for API response * * @param {any} reaction * @param {any} me? * @return {Promise<any>} */ -export default ( +export const pack = ( reaction: any, me?: any ) => new Promise<any>(async (resolve, reject) => { @@ -37,7 +47,7 @@ export default ( delete _reaction._id; // Populate user - _reaction.user = await serializeUser(_reaction.user_id, me); + _reaction.user = await packUser(_reaction.userId, me); resolve(_reaction); }); diff --git a/src/models/note-watching.ts b/src/models/note-watching.ts new file mode 100644 index 0000000000..b5ef3b61b7 --- /dev/null +++ b/src/models/note-watching.ts @@ -0,0 +1,13 @@ +import * as mongo from 'mongodb'; +import db from '../db/mongodb'; + +const NoteWatching = db.get<INoteWatching>('noteWatching'); +NoteWatching.createIndex(['userId', 'noteId'], { unique: true }); +export default NoteWatching; + +export interface INoteWatching { + _id: mongo.ObjectID; + createdAt: Date; + userId: mongo.ObjectID; + noteId: mongo.ObjectID; +} diff --git a/src/models/note.ts b/src/models/note.ts new file mode 100644 index 0000000000..a11da196cd --- /dev/null +++ b/src/models/note.ts @@ -0,0 +1,273 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import rap from '@prezzemolo/rap'; +import db from '../db/mongodb'; +import { IUser, pack as packUser } from './user'; +import { pack as packApp } from './app'; +import { pack as packChannel } from './channel'; +import Vote from './poll-vote'; +import Reaction from './note-reaction'; +import { pack as packFile } from './drive-file'; + +const Note = db.get<INote>('notes'); + +Note.createIndex('uri', { sparse: true, unique: true }); + +export default Note; + +export function isValidText(text: string): boolean { + return text.length <= 1000 && text.trim() != ''; +} + +export function isValidCw(text: string): boolean { + return text.length <= 100 && text.trim() != ''; +} + +export type INote = { + _id: mongo.ObjectID; + channelId: mongo.ObjectID; + createdAt: Date; + deletedAt: Date; + mediaIds: mongo.ObjectID[]; + replyId: mongo.ObjectID; + renoteId: mongo.ObjectID; + poll: any; // todo + text: string; + tags: string[]; + textHtml: string; + cw: string; + userId: mongo.ObjectID; + appId: mongo.ObjectID; + viaMobile: boolean; + renoteCount: number; + repliesCount: number; + reactionCounts: any; + mentions: mongo.ObjectID[]; + visibility: 'public' | 'unlisted' | 'private' | 'direct'; + geo: { + coordinates: number[]; + altitude: number; + accuracy: number; + altitudeAccuracy: number; + heading: number; + speed: number; + }; + uri: string; + + _reply?: { + userId: mongo.ObjectID; + }; + _renote?: { + userId: mongo.ObjectID; + }; + _user: { + host: string; + hostLower: string; + account: { + inbox?: string; + }; + }; +}; + +// TODO +export async function physicalDelete(note: string | mongo.ObjectID | INote) { + let n: INote; + + // Populate + if (mongo.ObjectID.prototype.isPrototypeOf(note)) { + n = await Note.findOne({ + _id: note + }); + } else if (typeof note === 'string') { + n = await Note.findOne({ + _id: new mongo.ObjectID(note) + }); + } else { + n = note as INote; + } + + if (n == null) return; + + // この投稿の返信をすべて削除 + const replies = await Note.find({ + replyId: n._id + }); + await Promise.all(replies.map(r => physicalDelete(r))); + + // この投稿のWatchをすべて削除 + + // この投稿のReactionをすべて削除 + + // この投稿に対するFavoriteをすべて削除 +} + +/** + * Pack a note for API response + * + * @param note target + * @param me? serializee + * @param options? serialize options + * @return response + */ +export const pack = async ( + note: string | mongo.ObjectID | INote, + me?: string | mongo.ObjectID | IUser, + options?: { + detail: boolean + } +) => { + const opts = options || { + detail: true, + }; + + // Me + const meId: mongo.ObjectID = me + ? mongo.ObjectID.prototype.isPrototypeOf(me) + ? me as mongo.ObjectID + : typeof me === 'string' + ? new mongo.ObjectID(me) + : (me as IUser)._id + : null; + + let _note: any; + + // Populate the note if 'note' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(note)) { + _note = await Note.findOne({ + _id: note + }); + } else if (typeof note === 'string') { + _note = await Note.findOne({ + _id: new mongo.ObjectID(note) + }); + } else { + _note = deepcopy(note); + } + + if (!_note) throw `invalid note arg ${note}`; + + const id = _note._id; + + // Rename _id to id + _note.id = _note._id; + delete _note._id; + + delete _note.mentions; + if (_note.geo) delete _note.geo.type; + + // Populate user + _note.user = packUser(_note.userId, meId); + + // Populate app + if (_note.appId) { + _note.app = packApp(_note.appId); + } + + // Populate channel + if (_note.channelId) { + _note.channel = packChannel(_note.channelId); + } + + // Populate media + if (_note.mediaIds) { + _note.media = Promise.all(_note.mediaIds.map(fileId => + packFile(fileId) + )); + } + + // When requested a detailed note data + if (opts.detail) { + // Get previous note info + _note.prev = (async () => { + const prev = await Note.findOne({ + userId: _note.userId, + _id: { + $lt: id + } + }, { + fields: { + _id: true + }, + sort: { + _id: -1 + } + }); + return prev ? prev._id : null; + })(); + + // Get next note info + _note.next = (async () => { + const next = await Note.findOne({ + userId: _note.userId, + _id: { + $gt: id + } + }, { + fields: { + _id: true + }, + sort: { + _id: 1 + } + }); + return next ? next._id : null; + })(); + + if (_note.replyId) { + // Populate reply to note + _note.reply = pack(_note.replyId, meId, { + detail: false + }); + } + + if (_note.renoteId) { + // Populate renote + _note.renote = pack(_note.renoteId, meId, { + detail: _note.text == null + }); + } + + // Poll + if (meId && _note.poll) { + _note.poll = (async (poll) => { + const vote = await Vote + .findOne({ + userId: meId, + noteId: id + }); + + if (vote != null) { + const myChoice = poll.choices + .filter(c => c.id == vote.choice)[0]; + + myChoice.isVoted = true; + } + + return poll; + })(_note.poll); + } + + // Fetch my reaction + if (meId) { + _note.myReaction = (async () => { + const reaction = await Reaction + .findOne({ + userId: meId, + noteId: id, + deletedAt: { $exists: false } + }); + + if (reaction) { + return reaction.reaction; + } + + return null; + })(); + } + } + + // resolve promises in _note object + _note = await rap(_note); + + return _note; +}; diff --git a/src/models/notification.ts b/src/models/notification.ts new file mode 100644 index 0000000000..d5ca7135b7 --- /dev/null +++ b/src/models/notification.ts @@ -0,0 +1,104 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import db from '../db/mongodb'; +import { IUser, pack as packUser } from './user'; +import { pack as packNote } from './note'; + +const Notification = db.get<INotification>('notifications'); +export default Notification; + +export interface INotification { + _id: mongo.ObjectID; + createdAt: Date; + + /** + * 通知の受信者 + */ + notifiee?: IUser; + + /** + * 通知の受信者 + */ + notifieeId: mongo.ObjectID; + + /** + * イニシエータ(initiator)、Origin。通知を行う原因となったユーザー + */ + notifier?: IUser; + + /** + * イニシエータ(initiator)、Origin。通知を行う原因となったユーザー + */ + notifierId: mongo.ObjectID; + + /** + * 通知の種類。 + * follow - フォローされた + * mention - 投稿で自分が言及された + * reply - (自分または自分がWatchしている)投稿が返信された + * renote - (自分または自分がWatchしている)投稿がRenoteされた + * quote - (自分または自分がWatchしている)投稿が引用Renoteされた + * reaction - (自分または自分がWatchしている)投稿にリアクションされた + * poll_vote - (自分または自分がWatchしている)投稿の投票に投票された + */ + type: 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'poll_vote'; + + /** + * 通知が読まれたかどうか + */ + isRead: Boolean; +} + +/** + * Pack a notification for API response + */ +export const pack = (notification: any) => new Promise<any>(async (resolve, reject) => { + let _notification: any; + + // Populate the notification if 'notification' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(notification)) { + _notification = await Notification.findOne({ + _id: notification + }); + } else if (typeof notification === 'string') { + _notification = await Notification.findOne({ + _id: new mongo.ObjectID(notification) + }); + } else { + _notification = deepcopy(notification); + } + + // Rename _id to id + _notification.id = _notification._id; + delete _notification._id; + + // Rename notifierId to userId + _notification.userId = _notification.notifierId; + delete _notification.notifierId; + + const me = _notification.notifieeId; + delete _notification.notifieeId; + + // Populate notifier + _notification.user = await packUser(_notification.userId, me); + + switch (_notification.type) { + case 'follow': + // nope + break; + case 'mention': + case 'reply': + case 'renote': + case 'quote': + case 'reaction': + case 'poll_vote': + // Populate note + _notification.note = await packNote(_notification.noteId, me); + break; + default: + console.error(`Unknown type: ${_notification.type}`); + break; + } + + resolve(_notification); +}); diff --git a/src/models/othello-game.ts b/src/models/othello-game.ts new file mode 100644 index 0000000000..297aee3028 --- /dev/null +++ b/src/models/othello-game.ts @@ -0,0 +1,109 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import db from '../db/mongodb'; +import { IUser, pack as packUser } from './user'; + +const OthelloGame = db.get<IOthelloGame>('othelloGames'); +export default OthelloGame; + +export interface IOthelloGame { + _id: mongo.ObjectID; + createdAt: Date; + startedAt: Date; + user1Id: mongo.ObjectID; + user2Id: mongo.ObjectID; + user1Accepted: boolean; + user2Accepted: boolean; + + /** + * どちらのプレイヤーが先行(黒)か + * 1 ... user1 + * 2 ... user2 + */ + black: number; + + isStarted: boolean; + isEnded: boolean; + winnerId: mongo.ObjectID; + logs: Array<{ + at: Date; + color: boolean; + pos: number; + }>; + settings: { + map: string[]; + bw: string | number; + isLlotheo: boolean; + canPutEverywhere: boolean; + loopedBoard: boolean; + }; + form1: any; + form2: any; + + // ログのposを文字列としてすべて連結したもののCRC32値 + crc32: string; +} + +/** + * Pack an othello game for API response + */ +export const pack = ( + game: any, + me?: string | mongo.ObjectID | IUser, + options?: { + detail?: boolean + } +) => new Promise<any>(async (resolve, reject) => { + const opts = Object.assign({ + detail: true + }, options); + + let _game: any; + + // Populate the game if 'game' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(game)) { + _game = await OthelloGame.findOne({ + _id: game + }); + } else if (typeof game === 'string') { + _game = await OthelloGame.findOne({ + _id: new mongo.ObjectID(game) + }); + } else { + _game = deepcopy(game); + } + + // Me + const meId: mongo.ObjectID = me + ? mongo.ObjectID.prototype.isPrototypeOf(me) + ? me as mongo.ObjectID + : typeof me === 'string' + ? new mongo.ObjectID(me) + : (me as IUser)._id + : null; + + // Rename _id to id + _game.id = _game._id; + delete _game._id; + + if (opts.detail === false) { + delete _game.logs; + delete _game.settings.map; + } else { + // 互換性のため + if (_game.settings.map.hasOwnProperty('size')) { + _game.settings.map = _game.settings.map.data.match(new RegExp(`.{1,${_game.settings.map.size}}`, 'g')); + } + } + + // Populate user + _game.user1 = await packUser(_game.user1Id, meId); + _game.user2 = await packUser(_game.user2Id, meId); + if (_game.winnerId) { + _game.winner = await packUser(_game.winnerId, meId); + } else { + _game.winner = null; + } + + resolve(_game); +}); diff --git a/src/models/othello-matching.ts b/src/models/othello-matching.ts new file mode 100644 index 0000000000..8082c258c8 --- /dev/null +++ b/src/models/othello-matching.ts @@ -0,0 +1,44 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import db from '../db/mongodb'; +import { IUser, pack as packUser } from './user'; + +const Matching = db.get<IMatching>('othelloMatchings'); +export default Matching; + +export interface IMatching { + _id: mongo.ObjectID; + createdAt: Date; + parentId: mongo.ObjectID; + childId: mongo.ObjectID; +} + +/** + * Pack an othello matching for API response + */ +export const pack = ( + matching: any, + me?: string | mongo.ObjectID | IUser +) => new Promise<any>(async (resolve, reject) => { + + // Me + const meId: mongo.ObjectID = me + ? mongo.ObjectID.prototype.isPrototypeOf(me) + ? me as mongo.ObjectID + : typeof me === 'string' + ? new mongo.ObjectID(me) + : (me as IUser)._id + : null; + + const _matching = deepcopy(matching); + + // Rename _id to id + _matching.id = _matching._id; + delete _matching._id; + + // Populate user + _matching.parent = await packUser(_matching.parentId, meId); + _matching.child = await packUser(_matching.childId, meId); + + resolve(_matching); +}); diff --git a/src/models/poll-vote.ts b/src/models/poll-vote.ts new file mode 100644 index 0000000000..4d33b100e7 --- /dev/null +++ b/src/models/poll-vote.ts @@ -0,0 +1,13 @@ +import * as mongo from 'mongodb'; +import db from '../db/mongodb'; + +const PollVote = db.get<IPollVote>('pollVotes'); +export default PollVote; + +export interface IPollVote { + _id: mongo.ObjectID; + createdAt: Date; + userId: mongo.ObjectID; + noteId: mongo.ObjectID; + choice: number; +} diff --git a/src/models/signin.ts b/src/models/signin.ts new file mode 100644 index 0000000000..7f56e1a283 --- /dev/null +++ b/src/models/signin.ts @@ -0,0 +1,34 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import db from '../db/mongodb'; + +const Signin = db.get<ISignin>('signin'); +export default Signin; + +export interface ISignin { + _id: mongo.ObjectID; + createdAt: Date; + userId: mongo.ObjectID; + ip: string; + headers: any; + success: boolean; +} + +/** + * Pack a signin record for API response + * + * @param {any} record + * @return {Promise<any>} + */ +export const pack = ( + record: any +) => new Promise<any>(async (resolve, reject) => { + + const _record = deepcopy(record); + + // Rename _id to id + _record.id = _record._id; + delete _record._id; + + resolve(_record); +}); diff --git a/src/models/sw-subscription.ts b/src/models/sw-subscription.ts new file mode 100644 index 0000000000..743d0d2dd9 --- /dev/null +++ b/src/models/sw-subscription.ts @@ -0,0 +1,13 @@ +import * as mongo from 'mongodb'; +import db from '../db/mongodb'; + +const SwSubscription = db.get<ISwSubscription>('swSubscriptions'); +export default SwSubscription; + +export interface ISwSubscription { + _id: mongo.ObjectID; + userId: mongo.ObjectID; + endpoint: string; + auth: string; + publickey: string; +} diff --git a/src/models/user.ts b/src/models/user.ts new file mode 100644 index 0000000000..a2800a3808 --- /dev/null +++ b/src/models/user.ts @@ -0,0 +1,336 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import rap from '@prezzemolo/rap'; +import db from '../db/mongodb'; +import Note, { INote, pack as packNote, physicalDelete as physicalDeleteNote } from './note'; +import Following from './following'; +import Mute from './mute'; +import getFriends from '../server/api/common/get-friends'; +import config from '../config'; + +const User = db.get<IUser>('users'); + +User.createIndex('username'); +User.createIndex('token'); +User.createIndex('uri', { sparse: true, unique: true }); + +export default User; + +type IUserBase = { + _id: mongo.ObjectID; + createdAt: Date; + deletedAt: Date; + followersCount: number; + followingCount: number; + name?: string; + notesCount: number; + driveCapacity: number; + username: string; + usernameLower: string; + avatarId: mongo.ObjectID; + bannerId: mongo.ObjectID; + data: any; + description: string; + latestNote: INote; + pinnedNoteId: mongo.ObjectID; + isSuspended: boolean; + keywords: string[]; + host: string; + hostLower: string; +}; + +export interface ILocalUser extends IUserBase { + host: null; + keypair: string; + email: string; + links: string[]; + password: string; + token: string; + twitter: { + accessToken: string; + accessTokenSecret: string; + userId: string; + screenName: string; + }; + line: { + userId: string; + }; + profile: { + location: string; + birthday: string; // 'YYYY-MM-DD' + tags: string[]; + }; + lastUsedAt: Date; + isBot: boolean; + isPro: boolean; + twoFactorSecret: string; + twoFactorEnabled: boolean; + twoFactorTempSecret: string; + clientSettings: any; + settings: any; +} + +export interface IRemoteUser extends IUserBase { + inbox: string; + uri: string; + publicKey: { + id: string; + publicKeyPem: string; + }; +} + +export type IUser = ILocalUser | IRemoteUser; + +export const isLocalUser = (user: any): user is ILocalUser => + user.host === null; + +export const isRemoteUser = (user: any): user is IRemoteUser => + !isLocalUser(user); + +//#region Validators +export function validateUsername(username: string): boolean { + return typeof username == 'string' && /^[a-zA-Z0-9_]{1,20}$/.test(username); +} + +export function validatePassword(password: string): boolean { + return typeof password == 'string' && password != ''; +} + +export function isValidName(name?: string): boolean { + return name === null || (typeof name == 'string' && name.length < 30 && name.trim() != ''); +} + +export function isValidDescription(description: string): boolean { + return typeof description == 'string' && description.length < 500 && description.trim() != ''; +} + +export function isValidLocation(location: string): boolean { + return typeof location == 'string' && location.length < 50 && location.trim() != ''; +} + +export function isValidBirthday(birthday: string): boolean { + return typeof birthday == 'string' && /^([0-9]{4})\-([0-9]{2})-([0-9]{2})$/.test(birthday); +} +//#endregion + +export function init(user): IUser { + user._id = new mongo.ObjectID(user._id); + user.avatarId = new mongo.ObjectID(user.avatarId); + user.bannerId = new mongo.ObjectID(user.bannerId); + user.pinnedNoteId = new mongo.ObjectID(user.pinnedNoteId); + return user; +} + +// TODO +export async function physicalDelete(user: string | mongo.ObjectID | IUser) { + let u: IUser; + + // Populate + if (mongo.ObjectID.prototype.isPrototypeOf(user)) { + u = await User.findOne({ + _id: user + }); + } else if (typeof user === 'string') { + u = await User.findOne({ + _id: new mongo.ObjectID(user) + }); + } else { + u = user as IUser; + } + + if (u == null) return; + + // このユーザーが行った投稿をすべて削除 + const notes = await Note.find({ userId: u._id }); + await Promise.all(notes.map(n => physicalDeleteNote(n))); + + // このユーザーのお気に入りをすべて削除 + + // このユーザーが行ったメッセージをすべて削除 + + // このユーザーのドライブのファイルをすべて削除 + + // このユーザーに関するfollowingをすべて削除 + + // このユーザーを削除 +} + +/** + * Pack a user for API response + * + * @param user target + * @param me? serializee + * @param options? serialize options + * @return Packed user + */ +export const pack = ( + user: string | mongo.ObjectID | IUser, + me?: string | mongo.ObjectID | IUser, + options?: { + detail?: boolean, + includeSecrets?: boolean + } +) => new Promise<any>(async (resolve, reject) => { + + const opts = Object.assign({ + detail: false, + includeSecrets: false + }, options); + + let _user: any; + + const fields = opts.detail ? { + } : { + settings: false, + clientSettings: false, + profile: false, + keywords: false, + domains: false + }; + + // Populate the user if 'user' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(user)) { + _user = await User.findOne({ + _id: user + }, { fields }); + } else if (typeof user === 'string') { + _user = await User.findOne({ + _id: new mongo.ObjectID(user) + }, { fields }); + } else { + _user = deepcopy(user); + } + + // TODO: ここでエラーにするのではなくダミーのユーザーデータを返す + // SEE: https://github.com/syuilo/misskey/issues/1432 + if (!_user) return reject('invalid user arg.'); + + // Me + const meId: mongo.ObjectID = me + ? mongo.ObjectID.prototype.isPrototypeOf(me) + ? me as mongo.ObjectID + : typeof me === 'string' + ? new mongo.ObjectID(me) + : (me as IUser)._id + : null; + + // Rename _id to id + _user.id = _user._id; + delete _user._id; + + // Remove needless properties + delete _user.latestNote; + + if (_user.host == null) { + // Remove private properties + delete _user.keypair; + delete _user.password; + delete _user.token; + delete _user.twoFactorTempSecret; + delete _user.twoFactorSecret; + delete _user.usernameLower; + if (_user.twitter) { + delete _user.twitter.accessToken; + delete _user.twitter.accessTokenSecret; + } + delete _user.line; + + // Visible via only the official client + if (!opts.includeSecrets) { + delete _user.email; + delete _user.settings; + delete _user.clientSettings; + } + + if (!opts.detail) { + delete _user.twoFactorEnabled; + } + } + + _user.avatarUrl = _user.avatarId != null + ? `${config.drive_url}/${_user.avatarId}` + : `${config.drive_url}/default-avatar.jpg`; + + _user.bannerUrl = _user.bannerId != null + ? `${config.drive_url}/${_user.bannerId}` + : null; + + if (!meId || !meId.equals(_user.id) || !opts.detail) { + delete _user.avatarId; + delete _user.bannerId; + + delete _user.driveCapacity; + } + + if (meId && !meId.equals(_user.id)) { + // Whether the user is following + _user.isFollowing = (async () => { + const follow = await Following.findOne({ + followerId: meId, + followeeId: _user.id + }); + return follow !== null; + })(); + + // Whether the user is followed + _user.isFollowed = (async () => { + const follow2 = await Following.findOne({ + followerId: _user.id, + followeeId: meId + }); + return follow2 !== null; + })(); + + // Whether the user is muted + _user.isMuted = (async () => { + const mute = await Mute.findOne({ + muterId: meId, + muteeId: _user.id, + deletedAt: { $exists: false } + }); + return mute !== null; + })(); + } + + if (opts.detail) { + if (_user.pinnedNoteId) { + // Populate pinned note + _user.pinnedNote = packNote(_user.pinnedNoteId, meId, { + detail: true + }); + } + + if (meId && !meId.equals(_user.id)) { + const myFollowingIds = await getFriends(meId); + + // Get following you know count + _user.followingYouKnowCount = Following.count({ + followeeId: { $in: myFollowingIds }, + followerId: _user.id + }); + + // Get followers you know count + _user.followersYouKnowCount = Following.count({ + followeeId: _user.id, + followerId: { $in: myFollowingIds } + }); + } + } + + // resolve promises in _user object + _user = await rap(_user); + + resolve(_user); +}); + +/* +function img(url) { + return { + thumbnail: { + large: `${url}`, + medium: '', + small: '' + } + }; +} +*/ diff --git a/src/othello/ai/back.ts b/src/othello/ai/back.ts new file mode 100644 index 0000000000..e4d0cfdd33 --- /dev/null +++ b/src/othello/ai/back.ts @@ -0,0 +1,377 @@ +/** + * -AI- + * Botのバックエンド(思考を担当) + * + * 対話と思考を同じプロセスで行うと、思考時間が長引いたときにストリームから + * 切断されてしまうので、別々のプロセスで行うようにします + */ + +import * as request from 'request-promise-native'; +import Othello, { Color } from '../core'; +import conf from '../../config'; +import getUserName from '../../renderers/get-user-name'; + +let game; +let form; + +/** + * BotアカウントのユーザーID + */ +const id = conf.othello_ai.id; + +/** + * BotアカウントのAPIキー + */ +const i = conf.othello_ai.i; + +let note; + +process.on('message', async msg => { + // 親プロセスからデータをもらう + if (msg.type == '_init_') { + game = msg.game; + form = msg.form; + } + + // フォームが更新されたとき + if (msg.type == 'update-form') { + form.find(i => i.id == msg.body.id).value = msg.body.value; + } + + // ゲームが始まったとき + if (msg.type == 'started') { + onGameStarted(msg.body); + + //#region TLに投稿する + const game = msg.body; + const url = `${conf.url}/othello/${game.id}`; + const user = game.user1Id == id ? game.user2 : game.user1; + const isSettai = form[0].value === 0; + const text = isSettai + ? `?[${getUserName(user)}](${conf.url}/@${user.username})さんの接待を始めました!` + : `対局を?[${getUserName(user)}](${conf.url}/@${user.username})さんと始めました! (強さ${form[0].value})`; + + const res = await request.post(`${conf.api_url}/notes/create`, { + json: { i, + text: `${text}\n→[観戦する](${url})` + } + }); + + note = res.createdNote; + //#endregion + } + + // ゲームが終了したとき + if (msg.type == 'ended') { + // ストリームから切断 + process.send({ + type: 'close' + }); + + //#region TLに投稿する + const user = game.user1Id == id ? game.user2 : game.user1; + const isSettai = form[0].value === 0; + const text = isSettai + ? msg.body.winnerId === null + ? `?[${getUserName(user)}](${conf.url}/@${user.username})さんに接待で引き分けました...` + : msg.body.winnerId == id + ? `?[${getUserName(user)}](${conf.url}/@${user.username})さんに接待で勝ってしまいました...` + : `?[${getUserName(user)}](${conf.url}/@${user.username})さんに接待で負けてあげました♪` + : msg.body.winnerId === null + ? `?[${getUserName(user)}](${conf.url}/@${user.username})さんと引き分けました~` + : msg.body.winnerId == id + ? `?[${getUserName(user)}](${conf.url}/@${user.username})さんに勝ちました♪` + : `?[${getUserName(user)}](${conf.url}/@${user.username})さんに負けました...`; + + await request.post(`${conf.api_url}/notes/create`, { + json: { i, + renoteId: note.id, + text: text + } + }); + //#endregion + + process.exit(); + } + + // 打たれたとき + if (msg.type == 'set') { + onSet(msg.body); + } +}); + +let o: Othello; +let botColor: Color; + +// 各マスの強さ +let cellWeights; + +/** + * ゲーム開始時 + * @param g ゲーム情報 + */ +function onGameStarted(g) { + game = g; + + // オセロエンジン初期化 + o = new Othello(game.settings.map, { + isLlotheo: game.settings.isLlotheo, + canPutEverywhere: game.settings.canPutEverywhere, + loopedBoard: game.settings.loopedBoard + }); + + // 各マスの価値を計算しておく + cellWeights = o.map.map((pix, i) => { + if (pix == 'null') return 0; + const [x, y] = o.transformPosToXy(i); + let count = 0; + const get = (x, y) => { + if (x < 0 || y < 0 || x >= o.mapWidth || y >= o.mapHeight) return 'null'; + return o.mapDataGet(o.transformXyToPos(x, y)); + }; + + if (get(x , y - 1) == 'null') count++; + if (get(x + 1, y - 1) == 'null') count++; + if (get(x + 1, y ) == 'null') count++; + if (get(x + 1, y + 1) == 'null') count++; + if (get(x , y + 1) == 'null') count++; + if (get(x - 1, y + 1) == 'null') count++; + if (get(x - 1, y ) == 'null') count++; + if (get(x - 1, y - 1) == 'null') count++; + //return Math.pow(count, 3); + return count >= 4 ? 1 : 0; + }); + + botColor = game.user1Id == id && game.black == 1 || game.user2Id == id && game.black == 2; + + if (botColor) { + think(); + } +} + +function onSet(x) { + o.put(x.color, x.pos); + + if (x.next === botColor) { + think(); + } +} + +const db = {}; + +function think() { + console.log('Thinking...'); + console.time('think'); + + const isSettai = form[0].value === 0; + + // 接待モードのときは、全力(5手先読みくらい)で負けるようにする + const maxDepth = isSettai ? 5 : form[0].value; + + /** + * Botにとってある局面がどれだけ有利か取得する + */ + function staticEval() { + let score = o.canPutSomewhere(botColor).length; + + cellWeights.forEach((weight, i) => { + // 係数 + const coefficient = 30; + weight = weight * coefficient; + + const stone = o.board[i]; + if (stone === botColor) { + // TODO: 価値のあるマスに設置されている自分の石に縦か横に接するマスは価値があると判断する + score += weight; + } else if (stone !== null) { + score -= weight; + } + }); + + // ロセオならスコアを反転 + if (game.settings.isLlotheo) score = -score; + + // 接待ならスコアを反転 + if (isSettai) score = -score; + + return score; + } + + /** + * αβ法での探索 + */ + const dive = (pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => { + // 試し打ち + o.put(o.turn, pos); + + const key = o.board.toString(); + let cache = db[key]; + if (cache) { + if (alpha >= cache.upper) { + o.undo(); + return cache.upper; + } + if (beta <= cache.lower) { + o.undo(); + return cache.lower; + } + alpha = Math.max(alpha, cache.lower); + beta = Math.min(beta, cache.upper); + } else { + cache = { + upper: Infinity, + lower: -Infinity + }; + } + + const isBotTurn = o.turn === botColor; + + // 勝った + if (o.turn === null) { + const winner = o.winner; + + // 勝つことによる基本スコア + const base = 10000; + + let score; + + if (game.settings.isLlotheo) { + // 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する + score = o.winner ? base - (o.blackCount * 100) : base - (o.whiteCount * 100); + } else { + // 勝ちは勝ちでも、より相手の石を少なくした方が美しい勝ちだと判定する + score = o.winner ? base + (o.blackCount * 100) : base + (o.whiteCount * 100); + } + + // 巻き戻し + o.undo(); + + // 接待なら自分が負けた方が高スコア + return isSettai + ? winner !== botColor ? score : -score + : winner === botColor ? score : -score; + } + + if (depth === maxDepth) { + // 静的に評価 + const score = staticEval(); + + // 巻き戻し + o.undo(); + + return score; + } else { + const cans = o.canPutSomewhere(o.turn); + + let value = isBotTurn ? -Infinity : Infinity; + let a = alpha; + let b = beta; + + // 次のターンのプレイヤーにとって最も良い手を取得 + for (const p of cans) { + if (isBotTurn) { + const score = dive(p, a, beta, depth + 1); + value = Math.max(value, score); + a = Math.max(a, value); + if (value >= beta) break; + } else { + const score = dive(p, alpha, b, depth + 1); + value = Math.min(value, score); + b = Math.min(b, value); + if (value <= alpha) break; + } + } + + // 巻き戻し + o.undo(); + + if (value <= alpha) { + cache.upper = value; + } else if (value >= beta) { + cache.lower = value; + } else { + cache.upper = value; + cache.lower = value; + } + + db[key] = cache; + + return value; + } + }; + + /** + * αβ法での探索(キャッシュ無し)(デバッグ用) + */ + const dive2 = (pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => { + // 試し打ち + o.put(o.turn, pos); + + const isBotTurn = o.turn === botColor; + + // 勝った + if (o.turn === null) { + const winner = o.winner; + + // 勝つことによる基本スコア + const base = 10000; + + let score; + + if (game.settings.isLlotheo) { + // 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する + score = o.winner ? base - (o.blackCount * 100) : base - (o.whiteCount * 100); + } else { + // 勝ちは勝ちでも、より相手の石を少なくした方が美しい勝ちだと判定する + score = o.winner ? base + (o.blackCount * 100) : base + (o.whiteCount * 100); + } + + // 巻き戻し + o.undo(); + + // 接待なら自分が負けた方が高スコア + return isSettai + ? winner !== botColor ? score : -score + : winner === botColor ? score : -score; + } + + if (depth === maxDepth) { + // 静的に評価 + const score = staticEval(); + + // 巻き戻し + o.undo(); + + return score; + } else { + const cans = o.canPutSomewhere(o.turn); + + // 次のターンのプレイヤーにとって最も良い手を取得 + for (const p of cans) { + if (isBotTurn) { + alpha = Math.max(alpha, dive2(p, alpha, beta, depth + 1)); + } else { + beta = Math.min(beta, dive2(p, alpha, beta, depth + 1)); + } + if (alpha >= beta) break; + } + + // 巻き戻し + o.undo(); + + return isBotTurn ? alpha : beta; + } + }; + + const cans = o.canPutSomewhere(botColor); + const scores = cans.map(p => dive(p)); + const pos = cans[scores.indexOf(Math.max(...scores))]; + + console.log('Thinked:', pos); + console.timeEnd('think'); + + process.send({ + type: 'put', + pos + }); +} diff --git a/src/othello/ai/front.ts b/src/othello/ai/front.ts new file mode 100644 index 0000000000..ff74b7216e --- /dev/null +++ b/src/othello/ai/front.ts @@ -0,0 +1,233 @@ +/** + * -AI- + * Botのフロントエンド(ストリームとの対話を担当) + * + * 対話と思考を同じプロセスで行うと、思考時間が長引いたときにストリームから + * 切断されてしまうので、別々のプロセスで行うようにします + */ + +import * as childProcess from 'child_process'; +const WebSocket = require('ws'); +import * as ReconnectingWebSocket from 'reconnecting-websocket'; +import * as request from 'request-promise-native'; +import conf from '../../config'; + +// 設定 //////////////////////////////////////////////////////// + +/** + * BotアカウントのAPIキー + */ +const i = conf.othello_ai.i; + +/** + * BotアカウントのユーザーID + */ +const id = conf.othello_ai.id; + +//////////////////////////////////////////////////////////////// + +/** + * ホームストリーム + */ +const homeStream = new ReconnectingWebSocket(`${conf.ws_url}/?i=${i}`, undefined, { + constructor: WebSocket +}); + +homeStream.on('open', () => { + console.log('home stream opened'); +}); + +homeStream.on('close', () => { + console.log('home stream closed'); +}); + +homeStream.on('message', message => { + const msg = JSON.parse(message.toString()); + + // タイムライン上でなんか言われたまたは返信されたとき + if (msg.type == 'mention' || msg.type == 'reply') { + const note = msg.body; + + if (note.userId == id) return; + + // リアクションする + request.post(`${conf.api_url}/notes/reactions/create`, { + json: { i, + noteId: note.id, + reaction: 'love' + } + }); + + if (note.text) { + if (note.text.indexOf('オセロ') > -1) { + request.post(`${conf.api_url}/notes/create`, { + json: { i, + replyId: note.id, + text: '良いですよ~' + } + }); + + invite(note.userId); + } + } + } + + // メッセージでなんか言われたとき + if (msg.type == 'messaging_message') { + const message = msg.body; + if (message.text) { + if (message.text.indexOf('オセロ') > -1) { + request.post(`${conf.api_url}/messaging/messages/create`, { + json: { i, + userId: message.userId, + text: '良いですよ~' + } + }); + + invite(message.userId); + } + } + } +}); + +// ユーザーを対局に誘う +function invite(userId) { + request.post(`${conf.api_url}/othello/match`, { + json: { i, + userId: userId + } + }); +} + +/** + * オセロストリーム + */ +const othelloStream = new ReconnectingWebSocket(`${conf.ws_url}/othello?i=${i}`, undefined, { + constructor: WebSocket +}); + +othelloStream.on('open', () => { + console.log('othello stream opened'); +}); + +othelloStream.on('close', () => { + console.log('othello stream closed'); +}); + +othelloStream.on('message', message => { + const msg = JSON.parse(message.toString()); + + // 招待されたとき + if (msg.type == 'invited') { + onInviteMe(msg.body.parent); + } + + // マッチしたとき + if (msg.type == 'matched') { + gameStart(msg.body); + } +}); + +/** + * ゲーム開始 + * @param game ゲーム情報 + */ +function gameStart(game) { + // ゲームストリームに接続 + const gw = new ReconnectingWebSocket(`${conf.ws_url}/othello-game?i=${i}&game=${game.id}`, undefined, { + constructor: WebSocket + }); + + gw.on('open', () => { + console.log('othello game stream opened'); + + // フォーム + const form = [{ + id: 'strength', + type: 'radio', + label: '強さ', + value: 2, + items: [{ + label: '接待', + value: 0 + }, { + label: '弱', + value: 1 + }, { + label: '中', + value: 2 + }, { + label: '強', + value: 3 + }, { + label: '最強', + value: 5 + }] + }]; + + //#region バックエンドプロセス開始 + const ai = childProcess.fork(__dirname + '/back.js'); + + // バックエンドプロセスに情報を渡す + ai.send({ + type: '_init_', + game, + form + }); + + ai.on('message', msg => { + if (msg.type == 'put') { + gw.send(JSON.stringify({ + type: 'set', + pos: msg.pos + })); + } else if (msg.type == 'close') { + gw.close(); + } + }); + + // ゲームストリームから情報が流れてきたらそのままバックエンドプロセスに伝える + gw.on('message', message => { + const msg = JSON.parse(message.toString()); + ai.send(msg); + }); + //#endregion + + // フォーム初期化 + setTimeout(() => { + gw.send(JSON.stringify({ + type: 'init-form', + body: form + })); + }, 1000); + + // どんな設定内容の対局でも受け入れる + setTimeout(() => { + gw.send(JSON.stringify({ + type: 'accept' + })); + }, 2000); + }); + + gw.on('close', () => { + console.log('othello game stream closed'); + }); +} + +/** + * オセロの対局に招待されたとき + * @param inviter 誘ってきたユーザー + */ +async function onInviteMe(inviter) { + console.log(`Someone invited me: @${inviter.username}`); + + // 承認 + const game = await request.post(`${conf.api_url}/othello/match`, { + json: { + i, + userId: inviter.id + } + }); + + gameStart(game); +} diff --git a/src/othello/ai/index.ts b/src/othello/ai/index.ts new file mode 100644 index 0000000000..5cd1db82da --- /dev/null +++ b/src/othello/ai/index.ts @@ -0,0 +1 @@ +require('./front'); diff --git a/src/othello/core.ts b/src/othello/core.ts new file mode 100644 index 0000000000..217066d375 --- /dev/null +++ b/src/othello/core.ts @@ -0,0 +1,340 @@ +/** + * true ... 黒 + * false ... 白 + */ +export type Color = boolean; +const BLACK = true; +const WHITE = false; + +export type MapPixel = 'null' | 'empty'; + +export type Options = { + isLlotheo: boolean; + canPutEverywhere: boolean; + loopedBoard: boolean; +}; + +export type Undo = { + /** + * 色 + */ + color: Color, + + /** + * どこに打ったか + */ + pos: number; + + /** + * 反転した石の位置の配列 + */ + effects: number[]; + + /** + * ターン + */ + turn: Color; +}; + +/** + * オセロエンジン + */ +export default class Othello { + public map: MapPixel[]; + public mapWidth: number; + public mapHeight: number; + public board: Color[]; + public turn: Color = BLACK; + public opts: Options; + + public prevPos = -1; + public prevColor: Color = null; + + private logs: Undo[] = []; + + /** + * ゲームを初期化します + */ + constructor(map: string[], opts: Options) { + //#region binds + this.put = this.put.bind(this); + //#endregion + + //#region Options + this.opts = opts; + if (this.opts.isLlotheo == null) this.opts.isLlotheo = false; + if (this.opts.canPutEverywhere == null) this.opts.canPutEverywhere = false; + if (this.opts.loopedBoard == null) this.opts.loopedBoard = false; + //#endregion + + //#region Parse map data + this.mapWidth = map[0].length; + this.mapHeight = map.length; + const mapData = map.join(''); + + this.board = mapData.split('').map(d => { + if (d == '-') return null; + if (d == 'b') return BLACK; + if (d == 'w') return WHITE; + return undefined; + }); + + this.map = mapData.split('').map(d => { + if (d == '-' || d == 'b' || d == 'w') return 'empty'; + return 'null'; + }); + //#endregion + + // ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある + if (this.canPutSomewhere(BLACK).length == 0) { + if (this.canPutSomewhere(WHITE).length == 0) { + this.turn = null; + } else { + this.turn = WHITE; + } + } + } + + /** + * 黒石の数 + */ + public get blackCount() { + return this.board.filter(x => x === BLACK).length; + } + + /** + * 白石の数 + */ + public get whiteCount() { + return this.board.filter(x => x === WHITE).length; + } + + /** + * 黒石の比率 + */ + public get blackP() { + if (this.blackCount == 0 && this.whiteCount == 0) return 0; + return this.blackCount / (this.blackCount + this.whiteCount); + } + + /** + * 白石の比率 + */ + public get whiteP() { + if (this.blackCount == 0 && this.whiteCount == 0) return 0; + return this.whiteCount / (this.blackCount + this.whiteCount); + } + + public transformPosToXy(pos: number): number[] { + const x = pos % this.mapWidth; + const y = Math.floor(pos / this.mapWidth); + return [x, y]; + } + + public transformXyToPos(x: number, y: number): number { + return x + (y * this.mapWidth); + } + + /** + * 指定のマスに石を打ちます + * @param color 石の色 + * @param pos 位置 + */ + public put(color: Color, pos: number) { + this.prevPos = pos; + this.prevColor = color; + + this.board[pos] = color; + + // 反転させられる石を取得 + const effects = this.effects(color, pos); + + // 反転させる + for (const pos of effects) { + this.board[pos] = color; + } + + const turn = this.turn; + + this.logs.push({ + color, + pos, + effects, + turn + }); + + this.calcTurn(); + } + + private calcTurn() { + // ターン計算 + if (this.canPutSomewhere(!this.prevColor).length > 0) { + this.turn = !this.prevColor; + } else if (this.canPutSomewhere(this.prevColor).length > 0) { + this.turn = this.prevColor; + } else { + this.turn = null; + } + } + + public undo() { + const undo = this.logs.pop(); + this.prevColor = undo.color; + this.prevPos = undo.pos; + this.board[undo.pos] = null; + for (const pos of undo.effects) { + const color = this.board[pos]; + this.board[pos] = !color; + } + this.turn = undo.turn; + } + + /** + * 指定した位置のマップデータのマスを取得します + * @param pos 位置 + */ + public mapDataGet(pos: number): MapPixel { + const [x, y] = this.transformPosToXy(pos); + if (x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight) return 'null'; + return this.map[pos]; + } + + /** + * 打つことができる場所を取得します + */ + public canPutSomewhere(color: Color): number[] { + const result = []; + + this.board.forEach((x, i) => { + if (this.canPut(color, i)) result.push(i); + }); + + return result; + } + + /** + * 指定のマスに石を打つことができるかどうかを取得します + * @param color 自分の色 + * @param pos 位置 + */ + public canPut(color: Color, pos: number): boolean { + // 既に石が置いてある場所には打てない + if (this.board[pos] !== null) return false; + + if (this.opts.canPutEverywhere) { + // 挟んでなくても置けるモード + return this.mapDataGet(pos) == 'empty'; + } else { + // 相手の石を1つでも反転させられるか + return this.effects(color, pos).length !== 0; + } + } + + /** + * 指定のマスに石を置いた時の、反転させられる石を取得します + * @param color 自分の色 + * @param pos 位置 + */ + public effects(color: Color, pos: number): number[] { + const enemyColor = !color; + + // ひっくり返せる石(の位置)リスト + let stones = []; + + const initPos = pos; + + // 走査 + const iterate = (fn: (i: number) => number[]) => { + let i = 1; + const found = []; + + while (true) { + let [x, y] = fn(i); + + // 座標が指し示す位置がボード外に出たとき + if (this.opts.loopedBoard) { + if (x < 0 ) x = this.mapWidth - ((-x) % this.mapWidth); + if (y < 0 ) y = this.mapHeight - ((-y) % this.mapHeight); + if (x >= this.mapWidth ) x = x % this.mapWidth; + if (y >= this.mapHeight) y = y % this.mapHeight; + + // for debug + //if (x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight) { + // console.log(x, y); + //} + + // 一周して自分に帰ってきたら + if (this.transformXyToPos(x, y) == initPos) { + // ↓のコメントアウトを外すと、「現時点で自分の石が隣接していないが、 + // そこに置いたとするとループして最終的に挟んだことになる」というケースを有効化します。(Test4のマップで違いが分かります) + // このケースを有効にした方が良いのか無効にした方が良いのか判断がつかなかったためとりあえず無効としておきます + // (あと無効な方がゲームとしておもしろそうだった) + stones = stones.concat(found); + break; + } + } else { + if (x == -1 || y == -1 || x == this.mapWidth || y == this.mapHeight) break; + } + + const pos = this.transformXyToPos(x, y); + + //#region 「配置不能」マスに当たった場合走査終了 + const pixel = this.mapDataGet(pos); + if (pixel == 'null') break; + //#endregion + + // 石取得 + const stone = this.board[pos]; + + // 石が置かれていないマスなら走査終了 + if (stone === null) break; + + // 相手の石なら「ひっくり返せるかもリスト」に入れておく + if (stone === enemyColor) found.push(pos); + + // 自分の石なら「ひっくり返せるかもリスト」を「ひっくり返せるリスト」に入れ、走査終了 + if (stone === color) { + stones = stones.concat(found); + break; + } + + i++; + } + }; + + const [x, y] = this.transformPosToXy(pos); + + iterate(i => [x , y - i]); // 上 + iterate(i => [x + i, y - i]); // 右上 + iterate(i => [x + i, y ]); // 右 + iterate(i => [x + i, y + i]); // 右下 + iterate(i => [x , y + i]); // 下 + iterate(i => [x - i, y + i]); // 左下 + iterate(i => [x - i, y ]); // 左 + iterate(i => [x - i, y - i]); // 左上 + + return stones; + } + + /** + * ゲームが終了したか否か + */ + public get isEnded(): boolean { + return this.turn === null; + } + + /** + * ゲームの勝者 (null = 引き分け) + */ + public get winner(): Color { + if (!this.isEnded) return undefined; + + if (this.blackCount == this.whiteCount) return null; + + if (this.opts.isLlotheo) { + return this.blackCount > this.whiteCount ? WHITE : BLACK; + } else { + return this.blackCount > this.whiteCount ? BLACK : WHITE; + } + } +} diff --git a/src/othello/maps.ts b/src/othello/maps.ts new file mode 100644 index 0000000000..68e5a446f1 --- /dev/null +++ b/src/othello/maps.ts @@ -0,0 +1,911 @@ +/** + * 組み込みマップ定義 + * + * データ値: + * (スペース) ... マス無し + * - ... マス + * b ... 初期配置される黒石 + * w ... 初期配置される白石 + */ + +export type Map = { + name?: string; + category?: string; + author?: string; + data: string[]; +}; + +export const fourfour: Map = { + name: '4x4', + category: '4x4', + data: [ + '----', + '-wb-', + '-bw-', + '----' + ] +}; + +export const sixsix: Map = { + name: '6x6', + category: '6x6', + data: [ + '------', + '------', + '--wb--', + '--bw--', + '------', + '------' + ] +}; + +export const roundedSixsix: Map = { + name: '6x6 rounded', + category: '6x6', + author: 'syuilo', + data: [ + ' ---- ', + '------', + '--wb--', + '--bw--', + '------', + ' ---- ' + ] +}; + +export const roundedSixsix2: Map = { + name: '6x6 rounded 2', + category: '6x6', + author: 'syuilo', + data: [ + ' -- ', + ' ---- ', + '--wb--', + '--bw--', + ' ---- ', + ' -- ' + ] +}; + +export const eighteight: Map = { + name: '8x8', + category: '8x8', + data: [ + '--------', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + '--------' + ] +}; + +export const eighteightH1: Map = { + name: '8x8 handicap 1', + category: '8x8', + data: [ + 'b-------', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + '--------' + ] +}; + +export const eighteightH2: Map = { + name: '8x8 handicap 2', + category: '8x8', + data: [ + 'b-------', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + '-------b' + ] +}; + +export const eighteightH3: Map = { + name: '8x8 handicap 3', + category: '8x8', + data: [ + 'b------b', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + '-------b' + ] +}; + +export const eighteightH4: Map = { + name: '8x8 handicap 4', + category: '8x8', + data: [ + 'b------b', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + 'b------b' + ] +}; + +export const eighteightH12: Map = { + name: '8x8 handicap 12', + category: '8x8', + data: [ + 'bb----bb', + 'b------b', + '--------', + '---wb---', + '---bw---', + '--------', + 'b------b', + 'bb----bb' + ] +}; + +export const eighteightH16: Map = { + name: '8x8 handicap 16', + category: '8x8', + data: [ + 'bbb---bb', + 'b------b', + '-------b', + '---wb---', + '---bw---', + 'b-------', + 'b------b', + 'bb---bbb' + ] +}; + +export const eighteightH20: Map = { + name: '8x8 handicap 20', + category: '8x8', + data: [ + 'bbb--bbb', + 'b------b', + 'b------b', + '---wb---', + '---bw---', + 'b------b', + 'b------b', + 'bbb---bb' + ] +}; + +export const eighteightH28: Map = { + name: '8x8 handicap 28', + category: '8x8', + data: [ + 'bbbbbbbb', + 'b------b', + 'b------b', + 'b--wb--b', + 'b--bw--b', + 'b------b', + 'b------b', + 'bbbbbbbb' + ] +}; + +export const roundedEighteight: Map = { + name: '8x8 rounded', + category: '8x8', + author: 'syuilo', + data: [ + ' ------ ', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + ' ------ ' + ] +}; + +export const roundedEighteight2: Map = { + name: '8x8 rounded 2', + category: '8x8', + author: 'syuilo', + data: [ + ' ---- ', + ' ------ ', + '--------', + '---wb---', + '---bw---', + '--------', + ' ------ ', + ' ---- ' + ] +}; + +export const roundedEighteight3: Map = { + name: '8x8 rounded 3', + category: '8x8', + author: 'syuilo', + data: [ + ' -- ', + ' ---- ', + ' ------ ', + '---wb---', + '---bw---', + ' ------ ', + ' ---- ', + ' -- ' + ] +}; + +export const eighteightWithNotch: Map = { + name: '8x8 with notch', + category: '8x8', + author: 'syuilo', + data: [ + '--- ---', + '--------', + '--------', + ' --wb-- ', + ' --bw-- ', + '--------', + '--------', + '--- ---' + ] +}; + +export const eighteightWithSomeHoles: Map = { + name: '8x8 with some holes', + category: '8x8', + author: 'syuilo', + data: [ + '--- ----', + '----- --', + '-- -----', + '---wb---', + '---bw- -', + ' -------', + '--- ----', + '--------' + ] +}; + +export const circle: Map = { + name: 'Circle', + category: '8x8', + author: 'syuilo', + data: [ + ' -- ', + ' ------ ', + ' ------ ', + '---wb---', + '---bw---', + ' ------ ', + ' ------ ', + ' -- ' + ] +}; + +export const smile: Map = { + name: 'Smile', + category: '8x8', + author: 'syuilo', + data: [ + ' ------ ', + '--------', + '-- -- --', + '---wb---', + '-- bw --', + '--- ---', + '--------', + ' ------ ' + ] +}; + +export const window: Map = { + name: 'Window', + category: '8x8', + author: 'syuilo', + data: [ + '--------', + '- -- -', + '- -- -', + '---wb---', + '---bw---', + '- -- -', + '- -- -', + '--------' + ] +}; + +export const reserved: Map = { + name: 'Reserved', + category: '8x8', + author: 'Aya', + data: [ + 'w------b', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + 'b------w' + ] +}; + +export const x: Map = { + name: 'X', + category: '8x8', + author: 'Aya', + data: [ + 'w------b', + '-w----b-', + '--w--b--', + '---wb---', + '---bw---', + '--b--w--', + '-b----w-', + 'b------w' + ] +}; + +export const parallel: Map = { + name: 'Parallel', + category: '8x8', + author: 'Aya', + data: [ + '--------', + '--------', + '--------', + '---bb---', + '---ww---', + '--------', + '--------', + '--------' + ] +}; + +export const lackOfBlack: Map = { + name: 'Lack of Black', + category: '8x8', + data: [ + '--------', + '--------', + '--------', + '---w----', + '---bw---', + '--------', + '--------', + '--------' + ] +}; + +export const squareParty: Map = { + name: 'Square Party', + category: '8x8', + author: 'syuilo', + data: [ + '--------', + '-wwwbbb-', + '-w-wb-b-', + '-wwwbbb-', + '-bbbwww-', + '-b-bw-w-', + '-bbbwww-', + '--------' + ] +}; + +export const minesweeper: Map = { + name: 'Minesweeper', + category: '8x8', + author: 'syuilo', + data: [ + 'b-b--w-w', + '-w-wb-b-', + 'w-b--w-b', + '-b-wb-w-', + '-w-bw-b-', + 'b-w--b-w', + '-b-bw-w-', + 'w-w--b-b' + ] +}; + +export const tenthtenth: Map = { + name: '10x10', + category: '10x10', + data: [ + '----------', + '----------', + '----------', + '----------', + '----wb----', + '----bw----', + '----------', + '----------', + '----------', + '----------' + ] +}; + +export const hole: Map = { + name: 'The Hole', + category: '10x10', + author: 'syuilo', + data: [ + '----------', + '----------', + '--wb--wb--', + '--bw--bw--', + '---- ----', + '---- ----', + '--wb--wb--', + '--bw--bw--', + '----------', + '----------' + ] +}; + +export const grid: Map = { + name: 'Grid', + category: '10x10', + author: 'syuilo', + data: [ + '----------', + '- - -- - -', + '----------', + '- - -- - -', + '----wb----', + '----bw----', + '- - -- - -', + '----------', + '- - -- - -', + '----------' + ] +}; + +export const cross: Map = { + name: 'Cross', + category: '10x10', + author: 'Aya', + data: [ + ' ---- ', + ' ---- ', + ' ---- ', + '----------', + '----wb----', + '----bw----', + '----------', + ' ---- ', + ' ---- ', + ' ---- ' + ] +}; + +export const charX: Map = { + name: 'Char X', + category: '10x10', + author: 'syuilo', + data: [ + '--- ---', + '---- ----', + '----------', + ' -------- ', + ' --wb-- ', + ' --bw-- ', + ' -------- ', + '----------', + '---- ----', + '--- ---' + ] +}; + +export const charY: Map = { + name: 'Char Y', + category: '10x10', + author: 'syuilo', + data: [ + '--- ---', + '---- ----', + '----------', + ' -------- ', + ' --wb-- ', + ' --bw-- ', + ' ------ ', + ' ------ ', + ' ------ ', + ' ------ ' + ] +}; + +export const walls: Map = { + name: 'Walls', + category: '10x10', + author: 'Aya', + data: [ + ' bbbbbbbb ', + 'w--------w', + 'w--------w', + 'w--------w', + 'w---wb---w', + 'w---bw---w', + 'w--------w', + 'w--------w', + 'w--------w', + ' bbbbbbbb ' + ] +}; + +export const cpu: Map = { + name: 'CPU', + category: '10x10', + author: 'syuilo', + data: [ + ' b b b b ', + 'w--------w', + ' -------- ', + 'w--------w', + ' ---wb--- ', + ' ---bw--- ', + 'w--------w', + ' -------- ', + 'w--------w', + ' b b b b ' + ] +}; + +export const checker: Map = { + name: 'Checker', + category: '10x10', + author: 'Aya', + data: [ + '----------', + '----------', + '----------', + '---wbwb---', + '---bwbw---', + '---wbwb---', + '---bwbw---', + '----------', + '----------', + '----------' + ] +}; + +export const japaneseCurry: Map = { + name: 'Japanese curry', + category: '10x10', + author: 'syuilo', + data: [ + 'w-b-b-b-b-', + '-w-b-b-b-b', + 'w-w-b-b-b-', + '-w-w-b-b-b', + 'w-w-wwb-b-', + '-w-wbb-b-b', + 'w-w-w-b-b-', + '-w-w-w-b-b', + 'w-w-w-w-b-', + '-w-w-w-w-b' + ] +}; + +export const mosaic: Map = { + name: 'Mosaic', + category: '10x10', + author: 'syuilo', + data: [ + '- - - - - ', + ' - - - - -', + '- - - - - ', + ' - w w - -', + '- - b b - ', + ' - w w - -', + '- - b b - ', + ' - - - - -', + '- - - - - ', + ' - - - - -', + ] +}; + +export const arena: Map = { + name: 'Arena', + category: '10x10', + author: 'syuilo', + data: [ + '- - -- - -', + ' - - - - ', + '- ------ -', + ' -------- ', + '- --wb-- -', + '- --bw-- -', + ' -------- ', + '- ------ -', + ' - - - - ', + '- - -- - -' + ] +}; + +export const reactor: Map = { + name: 'Reactor', + category: '10x10', + author: 'syuilo', + data: [ + '-w------b-', + 'b- - - -w', + '- --wb-- -', + '---b w---', + '- b wb w -', + '- w bw b -', + '---w b---', + '- --bw-- -', + 'w- - - -b', + '-b------w-' + ] +}; + +export const sixeight: Map = { + name: '6x8', + category: 'Special', + data: [ + '------', + '------', + '------', + '--wb--', + '--bw--', + '------', + '------', + '------' + ] +}; + +export const spark: Map = { + name: 'Spark', + category: 'Special', + author: 'syuilo', + data: [ + ' - - ', + '----------', + ' -------- ', + ' -------- ', + ' ---wb--- ', + ' ---bw--- ', + ' -------- ', + ' -------- ', + '----------', + ' - - ' + ] +}; + +export const islands: Map = { + name: 'Islands', + category: 'Special', + author: 'syuilo', + data: [ + '-------- ', + '---wb--- ', + '---bw--- ', + '-------- ', + ' - - ', + ' - - ', + ' --------', + ' --------', + ' --------', + ' --------' + ] +}; + +export const galaxy: Map = { + name: 'Galaxy', + category: 'Special', + author: 'syuilo', + data: [ + ' ------ ', + ' --www--- ', + ' ------w--- ', + '---bbb--w---', + '--b---b-w-b-', + '-b--wwb-w-b-', + '-b-w-bww--b-', + '-b-w-b---b--', + '---w--bbb---', + ' ---w------ ', + ' ---www-- ', + ' ------ ' + ] +}; + +export const triangle: Map = { + name: 'Triangle', + category: 'Special', + author: 'syuilo', + data: [ + ' -- ', + ' -- ', + ' ---- ', + ' ---- ', + ' --wb-- ', + ' --bw-- ', + ' -------- ', + ' -------- ', + '----------', + '----------' + ] +}; + +export const iphonex: Map = { + name: 'iPhone X', + category: 'Special', + author: 'syuilo', + data: [ + ' -- -- ', + '--------', + '--------', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + '--------', + '--------', + ' ------ ' + ] +}; + +export const dealWithIt: Map = { + name: 'Deal with it!', + category: 'Special', + author: 'syuilo', + data: [ + '------------', + '--w-b-------', + ' --b-w------', + ' --w-b---- ', + ' ------- ' + ] +}; + +export const experiment: Map = { + name: 'Let\'s experiment', + category: 'Special', + author: 'syuilo', + data: [ + ' ------------ ', + '------wb------', + '------bw------', + '--------------', + ' - - ', + '------ ------', + 'bbbbbb wwwwww', + 'bbbbbb wwwwww', + 'bbbbbb wwwwww', + 'bbbbbb wwwwww', + 'wwwwww bbbbbb' + ] +}; + +export const bigBoard: Map = { + name: 'Big board', + category: 'Special', + data: [ + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '-------wb-------', + '-------bw-------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------' + ] +}; + +export const twoBoard: Map = { + name: 'Two board', + category: 'Special', + author: 'Aya', + data: [ + '-------- --------', + '-------- --------', + '-------- --------', + '---wb--- ---wb---', + '---bw--- ---bw---', + '-------- --------', + '-------- --------', + '-------- --------' + ] +}; + +export const test1: Map = { + name: 'Test1', + category: 'Test', + data: [ + '--------', + '---wb---', + '---bw---', + '--------' + ] +}; + +export const test2: Map = { + name: 'Test2', + category: 'Test', + data: [ + '------', + '------', + '-b--w-', + '-w--b-', + '-w--b-' + ] +}; + +export const test3: Map = { + name: 'Test3', + category: 'Test', + data: [ + '-w-', + '--w', + 'w--', + '-w-', + '--w', + 'w--', + '-w-', + '--w', + 'w--', + '-w-', + '---', + 'b--', + ] +}; + +export const test4: Map = { + name: 'Test4', + category: 'Test', + data: [ + '-w--b-', + '-w--b-', + '------', + '-w--b-', + '-w--b-' + ] +}; + +// https://misskey.xyz/othello/5aaabf7fe126e10b5216ea09 64 +export const test5: Map = { + name: 'Test5', + category: 'Test', + data: [ + '--wwwwww--', + '--wwwbwwww', + '-bwwbwbwww', + '-bwwwbwbww', + '-bwwbwbwbw', + '-bwbwbwb-w', + 'bwbwwbbb-w', + 'w-wbbbbb--', + '--w-b-w---', + '----------' + ] +}; diff --git a/src/parse-opt.ts b/src/parse-opt.ts new file mode 100644 index 0000000000..031b1a6fe2 --- /dev/null +++ b/src/parse-opt.ts @@ -0,0 +1,17 @@ +import * as nopt from 'nopt'; + +export default (vector, index) => { + const parsed = nopt({ + 'only-processor': Boolean, + 'only-server': Boolean + }, { + p: ['--only-processor'], + s: ['--only-server'] + }, vector, index); + + if (parsed['only-processor'] && parsed['only-server']) { + throw 'only-processor option and only-server option cannot be set at the same time'; + } + + return parsed; +}; diff --git a/src/publishers/notify.ts b/src/publishers/notify.ts new file mode 100644 index 0000000000..2b89515d42 --- /dev/null +++ b/src/publishers/notify.ts @@ -0,0 +1,50 @@ +import * as mongo from 'mongodb'; +import Notification from '../models/notification'; +import Mute from '../models/mute'; +import { pack } from '../models/notification'; +import stream from './stream'; + +export default ( + notifiee: mongo.ObjectID, + notifier: mongo.ObjectID, + type: string, + content?: any +) => new Promise<any>(async (resolve, reject) => { + if (notifiee.equals(notifier)) { + return resolve(); + } + + // Create notification + const notification = await Notification.insert(Object.assign({ + createdAt: new Date(), + notifieeId: notifiee, + notifierId: notifier, + type: type, + isRead: false + }, content)); + + resolve(notification); + + // Publish notification event + stream(notifiee, 'notification', + await pack(notification)); + + // 3秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する + setTimeout(async () => { + const fresh = await Notification.findOne({ _id: notification._id }, { isRead: true }); + if (!fresh.isRead) { + //#region ただしミュートしているユーザーからの通知なら無視 + const mute = await Mute.find({ + muterId: notifiee, + deletedAt: { $exists: false } + }); + const mutedUserIds = mute.map(m => m.muteeId.toString()); + if (mutedUserIds.indexOf(notifier.toString()) != -1) { + return; + } + //#endregion + + stream(notifiee, 'unread_notification', await pack(notification)); + } + }, 3000); +}); diff --git a/src/publishers/push-sw.ts b/src/publishers/push-sw.ts new file mode 100644 index 0000000000..aab91df62f --- /dev/null +++ b/src/publishers/push-sw.ts @@ -0,0 +1,52 @@ +const push = require('web-push'); +import * as mongo from 'mongodb'; +import Subscription from '../models/sw-subscription'; +import config from '../config'; + +if (config.sw) { + // アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録 + push.setVapidDetails( + config.maintainer.url, + config.sw.public_key, + config.sw.private_key); +} + +export default async function(userId: mongo.ObjectID | string, type, body?) { + if (!config.sw) return; + + if (typeof userId === 'string') { + userId = new mongo.ObjectID(userId); + } + + // Fetch + const subscriptions = await Subscription.find({ + userId: userId + }); + + subscriptions.forEach(subscription => { + const pushSubscription = { + endpoint: subscription.endpoint, + keys: { + auth: subscription.auth, + p256dh: subscription.publickey + } + }; + + push.sendNotification(pushSubscription, JSON.stringify({ + type, body + })).catch(err => { + //console.log(err.statusCode); + //console.log(err.headers); + //console.log(err.body); + + if (err.statusCode == 410) { + Subscription.remove({ + userId: userId, + endpoint: subscription.endpoint, + auth: subscription.auth, + publickey: subscription.publickey + }); + } + }); + }); +} diff --git a/src/publishers/stream.ts b/src/publishers/stream.ts new file mode 100644 index 0000000000..a6d2c2277c --- /dev/null +++ b/src/publishers/stream.ts @@ -0,0 +1,73 @@ +import * as mongo from 'mongodb'; +import * as redis from 'redis'; +import config from '../config'; + +type ID = string | mongo.ObjectID; + +class MisskeyEvent { + private redisClient: redis.RedisClient; + + constructor() { + // Connect to Redis + this.redisClient = redis.createClient( + config.redis.port, config.redis.host); + } + + public publishUserStream(userId: ID, type: string, value?: any): void { + this.publish(`user-stream:${userId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishDriveStream(userId: ID, type: string, value?: any): void { + this.publish(`drive-stream:${userId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishNoteStream(noteId: ID, type: string, value?: any): void { + this.publish(`note-stream:${noteId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishMessagingStream(userId: ID, otherpartyId: ID, type: string, value?: any): void { + this.publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishMessagingIndexStream(userId: ID, type: string, value?: any): void { + this.publish(`messaging-index-stream:${userId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishOthelloStream(userId: ID, type: string, value?: any): void { + this.publish(`othello-stream:${userId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishOthelloGameStream(gameId: ID, type: string, value?: any): void { + this.publish(`othello-game-stream:${gameId}`, type, typeof value === 'undefined' ? null : value); + } + + public publishChannelStream(channelId: ID, type: string, value?: any): void { + this.publish(`channel-stream:${channelId}`, type, typeof value === 'undefined' ? null : value); + } + + private publish(channel: string, type: string, value?: any): void { + const message = value == null ? + { type: type } : + { type: type, body: value }; + + this.redisClient.publish(`misskey:${channel}`, JSON.stringify(message)); + } +} + +const ev = new MisskeyEvent(); + +export default ev.publishUserStream.bind(ev); + +export const publishDriveStream = ev.publishDriveStream.bind(ev); + +export const publishNoteStream = ev.publishNoteStream.bind(ev); + +export const publishMessagingStream = ev.publishMessagingStream.bind(ev); + +export const publishMessagingIndexStream = ev.publishMessagingIndexStream.bind(ev); + +export const publishOthelloStream = ev.publishOthelloStream.bind(ev); + +export const publishOthelloGameStream = ev.publishOthelloGameStream.bind(ev); + +export const publishChannelStream = ev.publishChannelStream.bind(ev); diff --git a/src/queue/index.ts b/src/queue/index.ts new file mode 100644 index 0000000000..4aa1dc032d --- /dev/null +++ b/src/queue/index.ts @@ -0,0 +1,40 @@ +import { createQueue } from 'kue'; + +import config from '../config'; +import http from './processors/http'; + +const queue = createQueue({ + redis: { + port: config.redis.port, + host: config.redis.host, + auth: config.redis.pass + } +}); + +export function createHttp(data) { + return queue + .create('http', data) + .attempts(16) + .backoff({ delay: 16384, type: 'exponential' }); +} + +export function deliver(user, content, to) { + return createHttp({ + type: 'deliver', + user, + content, + to + }); +} + +export default function() { + /* + 256 is the default concurrency limit of Mozilla Firefox and Google + Chromium. + a8af215e691f3a2205a3758d2d96e9d328e100ff - chromium/src.git - Git at Google + https://chromium.googlesource.com/chromium/src.git/+/a8af215e691f3a2205a3758d2d96e9d328e100ff + Network.http.max-connections - MozillaZine Knowledge Base + http://kb.mozillazine.org/Network.http.max-connections + */ + queue.process('http', 256, http); +} diff --git a/src/queue/processors/http/deliver.ts b/src/queue/processors/http/deliver.ts new file mode 100644 index 0000000000..cf843fad07 --- /dev/null +++ b/src/queue/processors/http/deliver.ts @@ -0,0 +1,19 @@ +import * as kue from 'kue'; + +import request from '../../../remote/activitypub/request'; + +export default async (job: kue.Job, done): Promise<void> => { + try { + await request(job.data.user, job.data.to, job.data.content); + done(); + } catch (res) { + if (res.statusCode >= 400 && res.statusCode < 500) { + // HTTPステータスコード4xxはクライアントエラーであり、それはつまり + // 何回再送しても成功することはないということなのでエラーにはしないでおく + done(); + } else { + console.warn(`deliver failed: ${res.statusMessage}`); + done(new Error(res.statusMessage)); + } + } +}; diff --git a/src/queue/processors/http/index.ts b/src/queue/processors/http/index.ts new file mode 100644 index 0000000000..6f8d1dbc2b --- /dev/null +++ b/src/queue/processors/http/index.ts @@ -0,0 +1,18 @@ +import deliver from './deliver'; +import processInbox from './process-inbox'; + +const handlers = { + deliver, + processInbox, +}; + +export default (job, done) => { + const handler = handlers[job.data.type]; + + if (handler) { + handler(job, done); + } else { + console.error(`Unknown job: ${job.data.type}`); + done(); + } +}; diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts new file mode 100644 index 0000000000..ce5b7d5a89 --- /dev/null +++ b/src/queue/processors/http/process-inbox.ts @@ -0,0 +1,66 @@ +import * as kue from 'kue'; +import * as debug from 'debug'; + +import { verifySignature } from 'http-signature'; +import parseAcct from '../../../acct/parse'; +import User, { IRemoteUser } from '../../../models/user'; +import perform from '../../../remote/activitypub/perform'; +import { resolvePerson } from '../../../remote/activitypub/objects/person'; + +const log = debug('misskey:queue:inbox'); + +// ユーザーのinboxにアクティビティが届いた時の処理 +export default async (job: kue.Job, done): Promise<void> => { + const signature = job.data.signature; + const activity = job.data.activity; + + //#region Log + const info = Object.assign({}, activity); + delete info['@context']; + delete info['signature']; + log(info); + //#endregion + + const keyIdLower = signature.keyId.toLowerCase(); + let user; + + if (keyIdLower.startsWith('acct:')) { + const { username, host } = parseAcct(keyIdLower.slice('acct:'.length)); + if (host === null) { + console.warn(`request was made by local user: @${username}`); + done(); + return; + } + + user = await User.findOne({ usernameLower: username, hostLower: host }) as IRemoteUser; + } else { + user = await User.findOne({ + host: { $ne: null }, + 'publicKey.id': signature.keyId + }) as IRemoteUser; + + // アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する + if (user === null) { + user = await resolvePerson(signature.keyId); + } + } + + if (user === null) { + done(new Error('failed to resolve user')); + return; + } + + if (!verifySignature(signature, user.publicKey.publicKeyPem)) { + console.warn('signature verification failed'); + done(); + return; + } + + // アクティビティを処理 + try { + await perform(user, activity); + done(); + } catch (e) { + done(e); + } +}; diff --git a/src/remote/activitypub/kernel/announce/index.ts b/src/remote/activitypub/kernel/announce/index.ts new file mode 100644 index 0000000000..a2cf2d5762 --- /dev/null +++ b/src/remote/activitypub/kernel/announce/index.ts @@ -0,0 +1,35 @@ +import * as debug from 'debug'; + +import Resolver from '../../resolver'; +import { IRemoteUser } from '../../../../models/user'; +import announceNote from './note'; +import { IAnnounce } from '../../type'; + +const log = debug('misskey:activitypub'); + +export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> => { + const uri = activity.id || activity; + + log(`Announce: ${uri}`); + + const resolver = new Resolver(); + + let object; + + try { + object = await resolver.resolve(activity.object); + } catch (e) { + log(`Resolution failed: ${e}`); + throw e; + } + + switch (object.type) { + case 'Note': + announceNote(resolver, actor, activity, object); + break; + + default: + console.warn(`Unknown announce type: ${object.type}`); + break; + } +}; diff --git a/src/remote/activitypub/kernel/announce/note.ts b/src/remote/activitypub/kernel/announce/note.ts new file mode 100644 index 0000000000..68fb23c97f --- /dev/null +++ b/src/remote/activitypub/kernel/announce/note.ts @@ -0,0 +1,45 @@ +import * as debug from 'debug'; + +import Resolver from '../../resolver'; +import post from '../../../../services/note/create'; +import { IRemoteUser } from '../../../../models/user'; +import { IAnnounce, INote } from '../../type'; +import { fetchNote, resolveNote } from '../../objects/note'; + +const log = debug('misskey:activitypub'); + +/** + * アナウンスアクティビティを捌きます + */ +export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise<void> { + const uri = activity.id || activity; + + if (typeof uri !== 'string') { + throw new Error('invalid announce'); + } + + // 既に同じURIを持つものが登録されていないかチェック + const exist = await fetchNote(uri); + if (exist) { + return; + } + + const renote = await resolveNote(note); + + log(`Creating the (Re)Note: ${uri}`); + + //#region Visibility + let visibility = 'public'; + if (!activity.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'unlisted'; + if (activity.cc.length == 0) visibility = 'private'; + // TODO + if (visibility != 'public') throw new Error('unspported visibility'); + //#endergion + + await post(actor, { + createdAt: new Date(activity.published), + renote, + visibility, + uri + }); +} diff --git a/src/remote/activitypub/kernel/create/image.ts b/src/remote/activitypub/kernel/create/image.ts new file mode 100644 index 0000000000..ea36545f0c --- /dev/null +++ b/src/remote/activitypub/kernel/create/image.ts @@ -0,0 +1,6 @@ +import { IRemoteUser } from '../../../../models/user'; +import { createImage } from '../../objects/image'; + +export default async function(actor: IRemoteUser, image): Promise<void> { + await createImage(image.url, actor); +} diff --git a/src/remote/activitypub/kernel/create/index.ts b/src/remote/activitypub/kernel/create/index.ts new file mode 100644 index 0000000000..e11bcac811 --- /dev/null +++ b/src/remote/activitypub/kernel/create/index.ts @@ -0,0 +1,40 @@ +import * as debug from 'debug'; + +import Resolver from '../../resolver'; +import { IRemoteUser } from '../../../../models/user'; +import createNote from './note'; +import createImage from './image'; +import { ICreate } from '../../type'; + +const log = debug('misskey:activitypub'); + +export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => { + const uri = activity.id || activity; + + log(`Create: ${uri}`); + + const resolver = new Resolver(); + + let object; + + try { + object = await resolver.resolve(activity.object); + } catch (e) { + log(`Resolution failed: ${e}`); + throw e; + } + + switch (object.type) { + case 'Image': + createImage(actor, object); + break; + + case 'Note': + createNote(resolver, actor, object); + break; + + default: + console.warn(`Unknown type: ${object.type}`); + break; + } +}; diff --git a/src/remote/activitypub/kernel/create/note.ts b/src/remote/activitypub/kernel/create/note.ts new file mode 100644 index 0000000000..530cf6483f --- /dev/null +++ b/src/remote/activitypub/kernel/create/note.ts @@ -0,0 +1,13 @@ +import Resolver from '../../resolver'; +import { IRemoteUser } from '../../../../models/user'; +import { createNote, fetchNote } from '../../objects/note'; + +/** + * 投稿作成アクティビティを捌きます + */ +export default async function(resolver: Resolver, actor: IRemoteUser, note, silent = false): Promise<void> { + const exist = await fetchNote(note); + if (exist == null) { + await createNote(note); + } +} diff --git a/src/remote/activitypub/kernel/delete/index.ts b/src/remote/activitypub/kernel/delete/index.ts new file mode 100644 index 0000000000..10b47dc4ca --- /dev/null +++ b/src/remote/activitypub/kernel/delete/index.ts @@ -0,0 +1,36 @@ +import Resolver from '../../resolver'; +import deleteNote from './note'; +import Note from '../../../../models/note'; +import { IRemoteUser } from '../../../../models/user'; + +/** + * 削除アクティビティを捌きます + */ +export default async (actor: IRemoteUser, activity): Promise<void> => { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + const resolver = new Resolver(); + + const object = await resolver.resolve(activity.object); + + const uri = (object as any).id; + + switch (object.type) { + case 'Note': + deleteNote(actor, uri); + break; + + case 'Tombstone': + const note = await Note.findOne({ uri }); + if (note != null) { + deleteNote(actor, uri); + } + break; + + default: + console.warn(`Unknown type: ${object.type}`); + break; + } +}; diff --git a/src/remote/activitypub/kernel/delete/note.ts b/src/remote/activitypub/kernel/delete/note.ts new file mode 100644 index 0000000000..64c342d39b --- /dev/null +++ b/src/remote/activitypub/kernel/delete/note.ts @@ -0,0 +1,30 @@ +import * as debug from 'debug'; + +import Note from '../../../../models/note'; +import { IRemoteUser } from '../../../../models/user'; + +const log = debug('misskey:activitypub'); + +export default async function(actor: IRemoteUser, uri: string): Promise<void> { + log(`Deleting the Note: ${uri}`); + + const note = await Note.findOne({ uri }); + + if (note == null) { + throw new Error('note not found'); + } + + if (!note.userId.equals(actor._id)) { + throw new Error('投稿を削除しようとしているユーザーは投稿の作成者ではありません'); + } + + Note.update({ _id: note._id }, { + $set: { + deletedAt: new Date(), + text: null, + textHtml: null, + mediaIds: [], + poll: null + } + }); +} diff --git a/src/remote/activitypub/kernel/follow.ts b/src/remote/activitypub/kernel/follow.ts new file mode 100644 index 0000000000..6a8b5a1bec --- /dev/null +++ b/src/remote/activitypub/kernel/follow.ts @@ -0,0 +1,24 @@ +import User, { IRemoteUser } from '../../../models/user'; +import config from '../../../config'; +import follow from '../../../services/following/create'; +import { IFollow } from '../type'; + +export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { + const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + + if (!id.startsWith(config.url + '/')) { + return null; + } + + const followee = await User.findOne({ _id: id.split('/').pop() }); + + if (followee === null) { + throw new Error('followee not found'); + } + + if (followee.host != null) { + throw new Error('フォローしようとしているユーザーはローカルユーザーではありません'); + } + + await follow(actor, followee, activity); +}; diff --git a/src/remote/activitypub/kernel/index.ts b/src/remote/activitypub/kernel/index.ts new file mode 100644 index 0000000000..15ea9494ae --- /dev/null +++ b/src/remote/activitypub/kernel/index.ts @@ -0,0 +1,51 @@ +import { Object } from '../type'; +import { IRemoteUser } from '../../../models/user'; +import create from './create'; +import performDeleteActivity from './delete'; +import follow from './follow'; +import undo from './undo'; +import like from './like'; +import announce from './announce'; + +const self = async (actor: IRemoteUser, activity: Object): Promise<void> => { + switch (activity.type) { + case 'Create': + await create(actor, activity); + break; + + case 'Delete': + await performDeleteActivity(actor, activity); + break; + + case 'Follow': + await follow(actor, activity); + break; + + case 'Accept': + // noop + break; + + case 'Announce': + await announce(actor, activity); + break; + + case 'Like': + await like(actor, activity); + break; + + case 'Undo': + await undo(actor, activity); + break; + + case 'Collection': + case 'OrderedCollection': + // TODO + break; + + default: + console.warn(`unknown activity type: ${(activity as any).type}`); + return null; + } +}; + +export default self; diff --git a/src/remote/activitypub/kernel/like.ts b/src/remote/activitypub/kernel/like.ts new file mode 100644 index 0000000000..4941608588 --- /dev/null +++ b/src/remote/activitypub/kernel/like.ts @@ -0,0 +1,20 @@ +import Note from '../../../models/note'; +import { IRemoteUser } from '../../../models/user'; +import { ILike } from '../type'; +import create from '../../../services/note/reaction/create'; + +export default async (actor: IRemoteUser, activity: ILike) => { + const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + + // Transform: + // https://misskey.ex/notes/xxxx to + // xxxx + const noteId = id.split('/').pop(); + + const note = await Note.findOne({ _id: noteId }); + if (note === null) { + throw new Error(); + } + + await create(actor, note, 'pudding'); +}; diff --git a/src/remote/activitypub/kernel/undo/follow.ts b/src/remote/activitypub/kernel/undo/follow.ts new file mode 100644 index 0000000000..a85cb0305d --- /dev/null +++ b/src/remote/activitypub/kernel/undo/follow.ts @@ -0,0 +1,24 @@ +import User, { IRemoteUser } from '../../../../models/user'; +import config from '../../../../config'; +import unfollow from '../../../../services/following/delete'; +import { IFollow } from '../../type'; + +export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { + const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + + if (!id.startsWith(config.url + '/')) { + return null; + } + + const followee = await User.findOne({ _id: id.split('/').pop() }); + + if (followee === null) { + throw new Error('followee not found'); + } + + if (followee.host != null) { + throw new Error('フォロー解除しようとしているユーザーはローカルユーザーではありません'); + } + + await unfollow(actor, followee, activity); +}; diff --git a/src/remote/activitypub/kernel/undo/index.ts b/src/remote/activitypub/kernel/undo/index.ts new file mode 100644 index 0000000000..71f547aeb9 --- /dev/null +++ b/src/remote/activitypub/kernel/undo/index.ts @@ -0,0 +1,37 @@ +import * as debug from 'debug'; + +import { IRemoteUser } from '../../../../models/user'; +import { IUndo } from '../../type'; +import unfollow from './follow'; +import Resolver from '../../resolver'; + +const log = debug('misskey:activitypub'); + +export default async (actor: IRemoteUser, activity: IUndo): Promise<void> => { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + const uri = activity.id || activity; + + log(`Undo: ${uri}`); + + const resolver = new Resolver(); + + let object; + + try { + object = await resolver.resolve(activity.object); + } catch (e) { + log(`Resolution failed: ${e}`); + throw e; + } + + switch (object.type) { + case 'Follow': + unfollow(actor, object); + break; + } + + return null; +}; diff --git a/src/remote/activitypub/objects/image.ts b/src/remote/activitypub/objects/image.ts new file mode 100644 index 0000000000..d7bc5aff2f --- /dev/null +++ b/src/remote/activitypub/objects/image.ts @@ -0,0 +1,36 @@ +import * as debug from 'debug'; + +import uploadFromUrl from '../../../services/drive/upload-from-url'; +import { IRemoteUser } from '../../../models/user'; +import { IDriveFile } from '../../../models/drive-file'; +import Resolver from '../resolver'; + +const log = debug('misskey:activitypub'); + +/** + * Imageを作成します。 + */ +export async function createImage(actor: IRemoteUser, value): Promise<IDriveFile> { + const image = await new Resolver().resolve(value); + + if (image.url == null) { + throw new Error('invalid image: url not privided'); + } + + log(`Creating the Image: ${image.url}`); + + return await uploadFromUrl(image.url, actor); +} + +/** + * Imageを解決します。 + * + * Misskeyに対象のImageが登録されていればそれを返し、そうでなければ + * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 + */ +export async function resolveImage(actor: IRemoteUser, value: any): Promise<IDriveFile> { + // TODO + + // リモートサーバーからフェッチしてきて登録 + return await createImage(actor, value); +} diff --git a/src/remote/activitypub/objects/note.ts b/src/remote/activitypub/objects/note.ts new file mode 100644 index 0000000000..3edcb8c63f --- /dev/null +++ b/src/remote/activitypub/objects/note.ts @@ -0,0 +1,110 @@ +import { JSDOM } from 'jsdom'; +import * as debug from 'debug'; + +import config from '../../../config'; +import Resolver from '../resolver'; +import Note, { INote } from '../../../models/note'; +import post from '../../../services/note/create'; +import { INote as INoteActivityStreamsObject, IObject } from '../type'; +import { resolvePerson } from './person'; +import { resolveImage } from './image'; +import { IRemoteUser } from '../../../models/user'; + +const log = debug('misskey:activitypub'); + +/** + * Noteをフェッチします。 + * + * Misskeyに対象のNoteが登録されていればそれを返します。 + */ +export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise<INote> { + const uri = typeof value == 'string' ? value : value.id; + + // URIがこのサーバーを指しているならデータベースからフェッチ + if (uri.startsWith(config.url + '/')) { + return await Note.findOne({ _id: uri.split('/').pop() }); + } + + //#region このサーバーに既に登録されていたらそれを返す + const exist = await Note.findOne({ uri }); + + if (exist) { + return exist; + } + //#endregion + + return null; +} + +/** + * Noteを作成します。 + */ +export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<INote> { + if (resolver == null) resolver = new Resolver(); + + const object = await resolver.resolve(value) as any; + + if (object == null || object.type !== 'Note') { + throw new Error('invalid note'); + } + + const note: INoteActivityStreamsObject = object; + + log(`Creating the Note: ${note.id}`); + + // 投稿者をフェッチ + const actor = await resolvePerson(note.attributedTo) as IRemoteUser; + + //#region Visibility + let visibility = 'public'; + if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'unlisted'; + if (note.cc.length == 0) visibility = 'private'; + // TODO + if (visibility != 'public') throw new Error('unspported visibility'); + //#endergion + + // 添付メディア + // TODO: attachmentは必ずしもImageではない + // TODO: attachmentは必ずしも配列ではない + const media = note.attachment + ? await Promise.all(note.attachment.map(x => resolveImage(actor, x))) + : []; + + // リプライ + const reply = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver) : null; + + const { window } = new JSDOM(note.content); + + return await post(actor, { + createdAt: new Date(note.published), + media, + reply, + renote: undefined, + text: window.document.body.textContent, + viaMobile: false, + geo: undefined, + visibility, + uri: note.id + }, silent); +} + +/** + * Noteを解決します。 + * + * Misskeyに対象のNoteが登録されていればそれを返し、そうでなければ + * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 + */ +export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<INote> { + const uri = typeof value == 'string' ? value : value.id; + + //#region このサーバーに既に登録されていたらそれを返す + const exist = await fetchNote(uri); + + if (exist) { + return exist; + } + //#endregion + + // リモートサーバーからフェッチしてきて登録 + return await createNote(value, resolver); +} diff --git a/src/remote/activitypub/objects/person.ts b/src/remote/activitypub/objects/person.ts new file mode 100644 index 0000000000..f7ec064cdb --- /dev/null +++ b/src/remote/activitypub/objects/person.ts @@ -0,0 +1,142 @@ +import { JSDOM } from 'jsdom'; +import { toUnicode } from 'punycode'; +import * as debug from 'debug'; + +import config from '../../../config'; +import User, { validateUsername, isValidName, IUser, IRemoteUser } from '../../../models/user'; +import webFinger from '../../webfinger'; +import Resolver from '../resolver'; +import { resolveImage } from './image'; +import { isCollectionOrOrderedCollection, IObject, IPerson } from '../type'; + +const log = debug('misskey:activitypub'); + +/** + * Personをフェッチします。 + * + * Misskeyに対象のPersonが登録されていればそれを返します。 + */ +export async function fetchPerson(value: string | IObject, resolver?: Resolver): Promise<IUser> { + const uri = typeof value == 'string' ? value : value.id; + + // URIがこのサーバーを指しているならデータベースからフェッチ + if (uri.startsWith(config.url + '/')) { + return await User.findOne({ _id: uri.split('/').pop() }); + } + + //#region このサーバーに既に登録されていたらそれを返す + const exist = await User.findOne({ uri }); + + if (exist) { + return exist; + } + //#endregion + + return null; +} + +/** + * Personを作成します。 + */ +export async function createPerson(value: any, resolver?: Resolver): Promise<IUser> { + if (resolver == null) resolver = new Resolver(); + + const object = await resolver.resolve(value) as any; + + if ( + object == null || + object.type !== 'Person' || + typeof object.preferredUsername !== 'string' || + !validateUsername(object.preferredUsername) || + !isValidName(object.name == '' ? null : object.name) + ) { + log(`invalid person: ${JSON.stringify(object, null, 2)}`); + throw new Error('invalid person'); + } + + const person: IPerson = object; + + log(`Creating the Person: ${person.id}`); + + const [followersCount = 0, followingCount = 0, notesCount = 0, finger] = await Promise.all([ + resolver.resolve(person.followers).then( + resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, + () => undefined + ), + resolver.resolve(person.following).then( + resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, + () => undefined + ), + resolver.resolve(person.outbox).then( + resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, + () => undefined + ), + webFinger(person.id) + ]); + + const host = toUnicode(finger.subject.replace(/^.*?@/, '')); + const hostLower = host.replace(/[A-Z]+/, matched => matched.toLowerCase()); + const summaryDOM = JSDOM.fragment(person.summary); + + // Create user + const user = await User.insert({ + avatarId: null, + bannerId: null, + createdAt: Date.parse(person.published) || null, + description: summaryDOM.textContent, + followersCount, + followingCount, + notesCount, + name: person.name, + driveCapacity: 1024 * 1024 * 8, // 8MiB + username: person.preferredUsername, + usernameLower: person.preferredUsername.toLowerCase(), + host, + hostLower, + publicKey: { + id: person.publicKey.id, + publicKeyPem: person.publicKey.publicKeyPem + }, + inbox: person.inbox, + uri: person.id + }) as IRemoteUser; + + //#region アイコンとヘッダー画像をフェッチ + const [avatarId, bannerId] = (await Promise.all([ + person.icon, + person.image + ].map(img => + img == null + ? Promise.resolve(null) + : resolveImage(user, img) + ))).map(file => file != null ? file._id : null); + + User.update({ _id: user._id }, { $set: { avatarId, bannerId } }); + + user.avatarId = avatarId; + user.bannerId = bannerId; + //#endregion + + return user; +} + +/** + * Personを解決します。 + * + * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ + * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 + */ +export async function resolvePerson(value: string | IObject, verifier?: string): Promise<IUser> { + const uri = typeof value == 'string' ? value : value.id; + + //#region このサーバーに既に登録されていたらそれを返す + const exist = await fetchPerson(uri); + + if (exist) { + return exist; + } + //#endregion + + // リモートサーバーからフェッチしてきて登録 + return await createPerson(value); +} diff --git a/src/remote/activitypub/perform.ts b/src/remote/activitypub/perform.ts new file mode 100644 index 0000000000..2e4f53adf5 --- /dev/null +++ b/src/remote/activitypub/perform.ts @@ -0,0 +1,7 @@ +import { Object } from './type'; +import { IRemoteUser } from '../../models/user'; +import kernel from './kernel'; + +export default async (actor: IRemoteUser, activity: Object): Promise<void> => { + await kernel(actor, activity); +}; diff --git a/src/remote/activitypub/renderer/accept.ts b/src/remote/activitypub/renderer/accept.ts new file mode 100644 index 0000000000..00c76883a9 --- /dev/null +++ b/src/remote/activitypub/renderer/accept.ts @@ -0,0 +1,4 @@ +export default object => ({ + type: 'Accept', + object +}); diff --git a/src/remote/activitypub/renderer/announce.ts b/src/remote/activitypub/renderer/announce.ts new file mode 100644 index 0000000000..8e4b3d26a6 --- /dev/null +++ b/src/remote/activitypub/renderer/announce.ts @@ -0,0 +1,4 @@ +export default object => ({ + type: 'Announce', + object +}); diff --git a/src/remote/activitypub/renderer/context.ts b/src/remote/activitypub/renderer/context.ts new file mode 100644 index 0000000000..b56f727ae7 --- /dev/null +++ b/src/remote/activitypub/renderer/context.ts @@ -0,0 +1,5 @@ +export default [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + { Hashtag: 'as:Hashtag' } +]; diff --git a/src/remote/activitypub/renderer/create.ts b/src/remote/activitypub/renderer/create.ts new file mode 100644 index 0000000000..de411e1951 --- /dev/null +++ b/src/remote/activitypub/renderer/create.ts @@ -0,0 +1,4 @@ +export default object => ({ + type: 'Create', + object +}); diff --git a/src/remote/activitypub/renderer/document.ts b/src/remote/activitypub/renderer/document.ts new file mode 100644 index 0000000000..91a9f7df38 --- /dev/null +++ b/src/remote/activitypub/renderer/document.ts @@ -0,0 +1,7 @@ +import config from '../../../config'; + +export default ({ _id, contentType }) => ({ + type: 'Document', + mediaType: contentType, + url: `${config.drive_url}/${_id}` +}); diff --git a/src/remote/activitypub/renderer/follow.ts b/src/remote/activitypub/renderer/follow.ts new file mode 100644 index 0000000000..bf8eeff06b --- /dev/null +++ b/src/remote/activitypub/renderer/follow.ts @@ -0,0 +1,8 @@ +import config from '../../../config'; +import { IRemoteUser, ILocalUser } from '../../../models/user'; + +export default (follower: ILocalUser, followee: IRemoteUser) => ({ + type: 'Follow', + actor: `${config.url}/users/${follower._id}`, + object: followee.uri +}); diff --git a/src/remote/activitypub/renderer/hashtag.ts b/src/remote/activitypub/renderer/hashtag.ts new file mode 100644 index 0000000000..cf0b07b48a --- /dev/null +++ b/src/remote/activitypub/renderer/hashtag.ts @@ -0,0 +1,7 @@ +import config from '../../../config'; + +export default tag => ({ + type: 'Hashtag', + href: `${config.url}/search?q=#${encodeURIComponent(tag)}`, + name: '#' + tag +}); diff --git a/src/remote/activitypub/renderer/image.ts b/src/remote/activitypub/renderer/image.ts new file mode 100644 index 0000000000..d671a57e7c --- /dev/null +++ b/src/remote/activitypub/renderer/image.ts @@ -0,0 +1,6 @@ +import config from '../../../config'; + +export default ({ _id }) => ({ + type: 'Image', + url: `${config.drive_url}/${_id}` +}); diff --git a/src/remote/activitypub/renderer/key.ts b/src/remote/activitypub/renderer/key.ts new file mode 100644 index 0000000000..0d5e52557c --- /dev/null +++ b/src/remote/activitypub/renderer/key.ts @@ -0,0 +1,10 @@ +import config from '../../../config'; +import { extractPublic } from '../../../crypto_key'; +import { ILocalUser } from '../../../models/user'; + +export default (user: ILocalUser) => ({ + id: `${config.url}/users/${user._id}/publickey`, + type: 'Key', + owner: `${config.url}/users/${user._id}`, + publicKeyPem: extractPublic(user.keypair) +}); diff --git a/src/remote/activitypub/renderer/like.ts b/src/remote/activitypub/renderer/like.ts new file mode 100644 index 0000000000..061a10ba84 --- /dev/null +++ b/src/remote/activitypub/renderer/like.ts @@ -0,0 +1,8 @@ +import config from '../../../config'; +import { ILocalUser } from '../../../models/user'; + +export default (user: ILocalUser, note) => ({ + type: 'Like', + actor: `${config.url}/users/${user._id}`, + object: note.uri ? note.uri : `${config.url}/notes/${note._id}` +}); diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts new file mode 100644 index 0000000000..c364b13249 --- /dev/null +++ b/src/remote/activitypub/renderer/note.ts @@ -0,0 +1,59 @@ +import renderDocument from './document'; +import renderHashtag from './hashtag'; +import config from '../../../config'; +import DriveFile from '../../../models/drive-file'; +import Note, { INote } from '../../../models/note'; +import User from '../../../models/user'; + +export default async function renderNote(note: INote, dive = true) { + const promisedFiles = note.mediaIds + ? DriveFile.find({ _id: { $in: note.mediaIds } }) + : Promise.resolve([]); + + let inReplyTo; + + if (note.replyId) { + const inReplyToNote = await Note.findOne({ + _id: note.replyId, + }); + + if (inReplyToNote !== null) { + const inReplyToUser = await User.findOne({ + _id: inReplyToNote.userId, + }); + + if (inReplyToUser !== null) { + if (inReplyToNote.uri) { + inReplyTo = inReplyToNote.uri; + } else { + if (dive) { + inReplyTo = await renderNote(inReplyToNote, false); + } else { + inReplyTo = `${config.url}/notes/${inReplyToNote._id}`; + } + } + } + } + } else { + inReplyTo = null; + } + + const user = await User.findOne({ + _id: note.userId + }); + + const attributedTo = `${config.url}/users/${user._id}`; + + return { + id: `${config.url}/notes/${note._id}`, + type: 'Note', + attributedTo, + content: note.textHtml, + published: note.createdAt.toISOString(), + to: 'https://www.w3.org/ns/activitystreams#Public', + cc: `${attributedTo}/followers`, + inReplyTo, + attachment: (await promisedFiles).map(renderDocument), + tag: (note.tags || []).map(renderHashtag) + }; +} diff --git a/src/remote/activitypub/renderer/ordered-collection.ts b/src/remote/activitypub/renderer/ordered-collection.ts new file mode 100644 index 0000000000..2ca0f77354 --- /dev/null +++ b/src/remote/activitypub/renderer/ordered-collection.ts @@ -0,0 +1,6 @@ +export default (id, totalItems, orderedItems) => ({ + id, + type: 'OrderedCollection', + totalItems, + orderedItems +}); diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts new file mode 100644 index 0000000000..f1c8056a75 --- /dev/null +++ b/src/remote/activitypub/renderer/person.ts @@ -0,0 +1,21 @@ +import renderImage from './image'; +import renderKey from './key'; +import config from '../../../config'; + +export default user => { + const id = `${config.url}/users/${user._id}`; + + return { + type: 'Person', + id, + inbox: `${id}/inbox`, + outbox: `${id}/outbox`, + url: `${config.url}/@${user.username}`, + preferredUsername: user.username, + name: user.name, + summary: user.description, + icon: user.avatarId && renderImage({ _id: user.avatarId }), + image: user.bannerId && renderImage({ _id: user.bannerId }), + publicKey: renderKey(user) + }; +}; diff --git a/src/remote/activitypub/renderer/undo.ts b/src/remote/activitypub/renderer/undo.ts new file mode 100644 index 0000000000..f38e224b60 --- /dev/null +++ b/src/remote/activitypub/renderer/undo.ts @@ -0,0 +1,4 @@ +export default object => ({ + type: 'Undo', + object +}); diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts new file mode 100644 index 0000000000..85f43eb91d --- /dev/null +++ b/src/remote/activitypub/request.ts @@ -0,0 +1,44 @@ +import { request } from 'https'; +import { sign } from 'http-signature'; +import { URL } from 'url'; +import * as debug from 'debug'; + +import config from '../../config'; +import { ILocalUser } from '../../models/user'; + +const log = debug('misskey:activitypub:deliver'); + +export default (user: ILocalUser, url: string, object) => new Promise((resolve, reject) => { + log(`--> ${url}`); + + const { protocol, hostname, port, pathname, search } = new URL(url); + + const req = request({ + protocol, + hostname, + port, + method: 'POST', + path: pathname + search, + }, res => { + res.on('end', () => { + log(`${url} --> ${res.statusCode}`); + + if (res.statusCode >= 200 && res.statusCode < 300) { + resolve(); + } else { + reject(res); + } + }); + + res.on('data', () => {}); + res.on('error', reject); + }); + + sign(req, { + authorizationHeaderName: 'Signature', + key: user.keypair, + keyId: `acct:${user.username}@${config.host}` + }); + + req.end(JSON.stringify(object)); +}); diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts new file mode 100644 index 0000000000..f405ff10c3 --- /dev/null +++ b/src/remote/activitypub/resolver.ts @@ -0,0 +1,70 @@ +import * as request from 'request-promise-native'; +import * as debug from 'debug'; +import { IObject } from './type'; +//import config from '../../config'; + +const log = debug('misskey:activitypub:resolver'); + +export default class Resolver { + private history: Set<string>; + + constructor() { + this.history = new Set(); + } + + public async resolveCollection(value) { + const collection = typeof value === 'string' + ? await this.resolve(value) + : value; + + switch (collection.type) { + case 'Collection': + collection.objects = collection.object.items; + break; + + case 'OrderedCollection': + collection.objects = collection.object.orderedItems; + break; + + default: + throw new Error(`unknown collection type: ${collection.type}`); + } + + return collection; + } + + public async resolve(value): Promise<IObject> { + if (value == null) { + throw new Error('resolvee is null (or undefined)'); + } + + if (typeof value !== 'string') { + return value; + } + + if (this.history.has(value)) { + throw new Error('cannot resolve already resolved one'); + } + + this.history.add(value); + + const object = await request({ + url: value, + headers: { + Accept: 'application/activity+json, application/ld+json' + }, + json: true + }); + + if (object === null || ( + Array.isArray(object['@context']) ? + !object['@context'].includes('https://www.w3.org/ns/activitystreams') : + object['@context'] !== 'https://www.w3.org/ns/activitystreams' + )) { + log(`invalid response: ${value}`); + throw new Error('invalid response'); + } + + return object; + } +} diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts new file mode 100644 index 0000000000..08e5493dd4 --- /dev/null +++ b/src/remote/activitypub/type.ts @@ -0,0 +1,100 @@ +export type obj = { [x: string]: any }; + +export interface IObject { + '@context': string | obj | obj[]; + type: string; + id?: string; + summary?: string; + published?: string; + cc?: string[]; + to?: string[]; + attributedTo: string; + attachment?: any[]; + inReplyTo?: any; + content: string; + icon?: any; + image?: any; + url?: string; +} + +export interface IActivity extends IObject { + //type: 'Activity'; + actor: IObject | string; + object: IObject | string; + target?: IObject | string; +} + +export interface ICollection extends IObject { + type: 'Collection'; + totalItems: number; + items: IObject | string | IObject[] | string[]; +} + +export interface IOrderedCollection extends IObject { + type: 'OrderedCollection'; + totalItems: number; + orderedItems: IObject | string | IObject[] | string[]; +} + +export interface INote extends IObject { + type: 'Note'; +} + +export interface IPerson extends IObject { + type: 'Person'; + name: string; + preferredUsername: string; + inbox: string; + publicKey: any; + followers: any; + following: any; + outbox: any; +} + +export const isCollection = (object: IObject): object is ICollection => + object.type === 'Collection'; + +export const isOrderedCollection = (object: IObject): object is IOrderedCollection => + object.type === 'OrderedCollection'; + +export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection => + isCollection(object) || isOrderedCollection(object); + +export interface ICreate extends IActivity { + type: 'Create'; +} + +export interface IDelete extends IActivity { + type: 'Delete'; +} + +export interface IUndo extends IActivity { + type: 'Undo'; +} + +export interface IFollow extends IActivity { + type: 'Follow'; +} + +export interface IAccept extends IActivity { + type: 'Accept'; +} + +export interface ILike extends IActivity { + type: 'Like'; +} + +export interface IAnnounce extends IActivity { + type: 'Announce'; +} + +export type Object = + ICollection | + IOrderedCollection | + ICreate | + IDelete | + IUndo | + IFollow | + IAccept | + ILike | + IAnnounce; diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts new file mode 100644 index 0000000000..346e134c9f --- /dev/null +++ b/src/remote/resolve-user.ts @@ -0,0 +1,31 @@ +import { toUnicode, toASCII } from 'punycode'; +import User from '../models/user'; +import webFinger from './webfinger'; +import config from '../config'; +import { createPerson } from './activitypub/objects/person'; + +export default async (username, host, option) => { + const usernameLower = username.toLowerCase(); + const hostLowerAscii = toASCII(host).toLowerCase(); + const hostLower = toUnicode(hostLowerAscii); + + if (config.host == hostLower) { + return await User.findOne({ usernameLower }); + } + + let user = await User.findOne({ usernameLower, hostLower }, option); + + if (user === null) { + const acctLower = `${usernameLower}@${hostLowerAscii}`; + + const finger = await webFinger(acctLower); + const self = finger.links.find(link => link.rel && link.rel.toLowerCase() === 'self'); + if (!self) { + throw new Error('self link not found'); + } + + user = await createPerson(self.href); + } + + return user; +}; diff --git a/src/remote/webfinger.ts b/src/remote/webfinger.ts new file mode 100644 index 0000000000..4f1ff231c0 --- /dev/null +++ b/src/remote/webfinger.ts @@ -0,0 +1,23 @@ +const WebFinger = require('webfinger.js'); + +const webFinger = new WebFinger({ }); + +type ILink = { + href: string; + rel: string; +}; + +type IWebFinger = { + links: ILink[]; + subject: string; +}; + +export default async function resolve(query): Promise<IWebFinger> { + return await new Promise((res, rej) => webFinger.lookup(query, (error, result) => { + if (error) { + return rej(error); + } + + res(result.object); + })) as IWebFinger; +} diff --git a/src/renderers/get-note-summary.ts b/src/renderers/get-note-summary.ts new file mode 100644 index 0000000000..fc7482ca16 --- /dev/null +++ b/src/renderers/get-note-summary.ts @@ -0,0 +1,45 @@ +/** + * 投稿を表す文字列を取得します。 + * @param {*} note 投稿 + */ +const summarize = (note: any): string => { + let summary = ''; + + // チャンネル + summary += note.channel ? `${note.channel.title}:` : ''; + + // 本文 + summary += note.text ? note.text : ''; + + // メディアが添付されているとき + if (note.media.length != 0) { + summary += ` (${note.media.length}つのメディア)`; + } + + // 投票が添付されているとき + if (note.poll) { + summary += ' (投票)'; + } + + // 返信のとき + if (note.replyId) { + if (note.reply) { + summary += ` RE: ${summarize(note.reply)}`; + } else { + summary += ' RE: ...'; + } + } + + // Renoteのとき + if (note.renoteId) { + if (note.renote) { + summary += ` RP: ${summarize(note.renote)}`; + } else { + summary += ' RP: ...'; + } + } + + return summary.trim(); +}; + +export default summarize; diff --git a/src/renderers/get-notification-summary.ts b/src/renderers/get-notification-summary.ts new file mode 100644 index 0000000000..f9c5a55879 --- /dev/null +++ b/src/renderers/get-notification-summary.ts @@ -0,0 +1,28 @@ +import getUserName from '../renderers/get-user-name'; +import getNoteSummary from './get-note-summary'; +import getReactionEmoji from './get-reaction-emoji'; + +/** + * 通知を表す文字列を取得します。 + * @param notification 通知 + */ +export default function(notification: any): string { + switch (notification.type) { + case 'follow': + return `${getUserName(notification.user)}にフォローされました`; + case 'mention': + return `言及されました:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}」`; + case 'reply': + return `返信されました:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}」`; + case 'renote': + return `Renoteされました:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}」`; + case 'quote': + return `引用されました:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}」`; + case 'reaction': + return `リアクションされました:\n${getUserName(notification.user)} <${getReactionEmoji(notification.reaction)}>「${getNoteSummary(notification.note)}」`; + case 'poll_vote': + return `投票されました:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}」`; + default: + return `<不明な通知タイプ: ${notification.type}>`; + } +} diff --git a/src/renderers/get-reaction-emoji.ts b/src/renderers/get-reaction-emoji.ts new file mode 100644 index 0000000000..c661205379 --- /dev/null +++ b/src/renderers/get-reaction-emoji.ts @@ -0,0 +1,14 @@ +export default function(reaction: string): string { + switch (reaction) { + case 'like': return '👍'; + case 'love': return '❤️'; + case 'laugh': return '😆'; + case 'hmm': return '🤔'; + case 'surprise': return '😮'; + case 'congrats': return '🎉'; + case 'angry': return '💢'; + case 'confused': return '😥'; + case 'pudding': return '🍮'; + default: return ''; + } +} diff --git a/src/renderers/get-user-name.ts b/src/renderers/get-user-name.ts new file mode 100644 index 0000000000..acd5e6626d --- /dev/null +++ b/src/renderers/get-user-name.ts @@ -0,0 +1,5 @@ +import { IUser } from '../models/user'; + +export default function(user: IUser): string { + return user.name || '名無し'; +} diff --git a/src/renderers/get-user-summary.ts b/src/renderers/get-user-summary.ts new file mode 100644 index 0000000000..1bd9a7fb47 --- /dev/null +++ b/src/renderers/get-user-summary.ts @@ -0,0 +1,18 @@ +import { IUser, isLocalUser } from '../models/user'; +import getAcct from '../acct/render'; +import getUserName from './get-user-name'; + +/** + * ユーザーを表す文字列を取得します。 + * @param user ユーザー + */ +export default function(user: IUser): string { + let string = `${getUserName(user)} (@${getAcct(user)})\n` + + `${user.notesCount}投稿、${user.followingCount}フォロー、${user.followersCount}フォロワー\n`; + + if (isLocalUser(user)) { + string += `場所: ${user.profile.location}、誕生日: ${user.profile.birthday}\n`; + } + + return string + `「${user.description}」`; +} diff --git a/src/server.ts b/src/server.ts deleted file mode 100644 index 240800c1e2..0000000000 --- a/src/server.ts +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Core Server - */ - -import * as fs from 'fs'; -import * as http from 'http'; -import * as https from 'https'; -import * as cluster from 'cluster'; -import * as express from 'express'; -import * as morgan from 'morgan'; -import Accesses from 'accesses'; -import vhost = require('vhost'); - -import config from './conf'; - -/** - * Init app - */ -const app = express(); -app.disable('x-powered-by'); -app.set('trust proxy', 'loopback'); - -// Log -if (config.accesses && config.accesses.enable) { - const accesses = new Accesses({ - appName: 'Misskey', - port: config.accesses.port - }); - - app.use(accesses.express); -} - -app.use(morgan(process.env.NODE_ENV == 'production' ? 'combined' : 'dev', { - // create a write stream (in append mode) - stream: config.accesslog ? fs.createWriteStream(config.accesslog) : null -})); - -// Drop request that without 'Host' header -app.use((req, res, next) => { - if (!req.headers['host']) { - res.sendStatus(400); - } else { - next(); - } -}); - -/** - * Register modules - */ -app.use(vhost(`api.${config.host}`, require('./api/server'))); -app.use(vhost(config.secondary_host, require('./himasaku/server'))); -app.use(vhost(`file.${config.secondary_host}`, require('./file/server'))); -app.use(require('./web/server')); - -/** - * Create server - */ -const server = config.https.enable ? - https.createServer({ - key: fs.readFileSync(config.https.key), - cert: fs.readFileSync(config.https.cert), - ca: fs.readFileSync(config.https.ca) - }, app) : - http.createServer(app); - -/** - * Steaming - */ -require('./api/streaming')(server); - -/** - * Server listen - */ -server.listen(config.port, () => { - if (cluster.isWorker) { - // Send a 'ready' message to parent process - process.send('ready'); - } -}); - -/** - * Export app for testing - */ -module.exports = app; diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts new file mode 100644 index 0000000000..643d2945bd --- /dev/null +++ b/src/server/activitypub/inbox.ts @@ -0,0 +1,32 @@ +import * as bodyParser from 'body-parser'; +import * as express from 'express'; +import { parseRequest } from 'http-signature'; +import { createHttp } from '../../queue'; + +const app = express.Router(); + +app.post('/users/:user/inbox', bodyParser.json({ + type() { + return true; + } +}), async (req, res) => { + let signature; + + req.headers.authorization = 'Signature ' + req.headers.signature; + + try { + signature = parseRequest(req); + } catch (exception) { + return res.sendStatus(401); + } + + createHttp({ + type: 'processInbox', + activity: req.body, + signature, + }).save(); + + return res.status(202).end(); +}); + +export default app; diff --git a/src/server/activitypub/index.ts b/src/server/activitypub/index.ts new file mode 100644 index 0000000000..042579db9d --- /dev/null +++ b/src/server/activitypub/index.ts @@ -0,0 +1,18 @@ +import * as express from 'express'; + +import user from './user'; +import inbox from './inbox'; +import outbox from './outbox'; +import publicKey from './publickey'; +import note from './note'; + +const app = express(); +app.disable('x-powered-by'); + +app.use(user); +app.use(inbox); +app.use(outbox); +app.use(publicKey); +app.use(note); + +export default app; diff --git a/src/server/activitypub/note.ts b/src/server/activitypub/note.ts new file mode 100644 index 0000000000..1c2e695b80 --- /dev/null +++ b/src/server/activitypub/note.ts @@ -0,0 +1,28 @@ +import * as express from 'express'; +import context from '../../remote/activitypub/renderer/context'; +import render from '../../remote/activitypub/renderer/note'; +import Note from '../../models/note'; + +const app = express.Router(); + +app.get('/notes/:note', async (req, res, next) => { + const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']); + if (!(['application/activity+json', 'application/ld+json'] as any[]).includes(accepted)) { + return next(); + } + + const note = await Note.findOne({ + _id: req.params.note + }); + + if (note === null) { + return res.sendStatus(404); + } + + const rendered = await render(note); + rendered['@context'] = context; + + res.json(rendered); +}); + +export default app; diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts new file mode 100644 index 0000000000..1c97c17a2e --- /dev/null +++ b/src/server/activitypub/outbox.ts @@ -0,0 +1,28 @@ +import * as express from 'express'; +import context from '../../remote/activitypub/renderer/context'; +import renderNote from '../../remote/activitypub/renderer/note'; +import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection'; +import config from '../../config'; +import Note from '../../models/note'; +import User from '../../models/user'; + +const app = express.Router(); + +app.get('/users/:user/outbox', async (req, res) => { + const userId = req.params.user; + + const user = await User.findOne({ _id: userId }); + + const notes = await Note.find({ userId: user._id }, { + limit: 20, + sort: { _id: -1 } + }); + + const renderedNotes = await Promise.all(notes.map(note => renderNote(note))); + const rendered = renderOrderedCollection(`${config.url}/users/${userId}/inbox`, user.notesCount, renderedNotes); + rendered['@context'] = context; + + res.json(rendered); +}); + +export default app; diff --git a/src/server/activitypub/publickey.ts b/src/server/activitypub/publickey.ts new file mode 100644 index 0000000000..e874b82729 --- /dev/null +++ b/src/server/activitypub/publickey.ts @@ -0,0 +1,23 @@ +import * as express from 'express'; +import context from '../../remote/activitypub/renderer/context'; +import render from '../../remote/activitypub/renderer/key'; +import User, { isLocalUser } from '../../models/user'; + +const app = express.Router(); + +app.get('/users/:user/publickey', async (req, res) => { + const userId = req.params.user; + + const user = await User.findOne({ _id: userId }); + + if (isLocalUser(user)) { + const rendered = render(user); + rendered['@context'] = context; + + res.json(rendered); + } else { + res.sendStatus(400); + } +}); + +export default app; diff --git a/src/server/activitypub/user.ts b/src/server/activitypub/user.ts new file mode 100644 index 0000000000..9e98e92b6a --- /dev/null +++ b/src/server/activitypub/user.ts @@ -0,0 +1,19 @@ +import * as express from 'express'; +import context from '../../remote/activitypub/renderer/context'; +import render from '../../remote/activitypub/renderer/person'; +import User from '../../models/user'; + +const app = express.Router(); + +app.get('/users/:user', async (req, res) => { + const userId = req.params.user; + + const user = await User.findOne({ _id: userId }); + + const rendered = render(user); + rendered['@context'] = context; + + res.json(rendered); +}); + +export default app; diff --git a/src/server/api/api-handler.ts b/src/server/api/api-handler.ts new file mode 100644 index 0000000000..409069b6a0 --- /dev/null +++ b/src/server/api/api-handler.ts @@ -0,0 +1,34 @@ +import * as express from 'express'; + +import { Endpoint } from './endpoints'; +import authenticate from './authenticate'; +import call from './call'; +import { IUser } from '../../models/user'; +import { IApp } from '../../models/app'; + +export default async (endpoint: Endpoint, req: express.Request, res: express.Response) => { + const reply = (x?: any, y?: any) => { + if (x === undefined) { + res.sendStatus(204); + } else if (typeof x === 'number') { + res.status(x).send({ + error: x === 500 ? 'INTERNAL_ERROR' : y + }); + } else { + res.send(x); + } + }; + + let user: IUser; + let app: IApp; + + // Authentication + try { + [user, app] = await authenticate(req.body['i']); + } catch (e) { + return reply(403, 'AUTHENTICATION_FAILED'); + } + + // API invoking + call(endpoint, user, app, req.body, req).then(reply).catch(e => reply(400, e)); +}; diff --git a/src/server/api/authenticate.ts b/src/server/api/authenticate.ts new file mode 100644 index 0000000000..836fb7cfe8 --- /dev/null +++ b/src/server/api/authenticate.ts @@ -0,0 +1,39 @@ +import App, { IApp } from '../../models/app'; +import { default as User, IUser } from '../../models/user'; +import AccessToken from '../../models/access-token'; +import isNativeToken from './common/is-native-token'; + +export default (token: string) => new Promise<[IUser, IApp]>(async (resolve, reject) => { + if (token == null) { + resolve([null, null]); + return; + } + + if (isNativeToken(token)) { + // Fetch user + const user: IUser = await User + .findOne({ token }); + + if (user === null) { + return reject('user not found'); + } + + resolve([user, null]); + } else { + const accessToken = await AccessToken.findOne({ + hash: token.toLowerCase() + }); + + if (accessToken === null) { + return reject('invalid signature'); + } + + const app = await App + .findOne({ _id: accessToken.appId }); + + const user = await User + .findOne({ _id: accessToken.userId }); + + resolve([user, app]); + } +}); diff --git a/src/server/api/bot/core.ts b/src/server/api/bot/core.ts new file mode 100644 index 0000000000..d41af48057 --- /dev/null +++ b/src/server/api/bot/core.ts @@ -0,0 +1,439 @@ +import * as EventEmitter from 'events'; +import * as bcrypt from 'bcryptjs'; + +import User, { IUser, init as initUser, ILocalUser } from '../../../models/user'; + +import getNoteSummary from '../../../renderers/get-note-summary'; +import getUserName from '../../../renderers/get-user-name'; +import getUserSummary from '../../../renderers/get-user-summary'; +import parseAcct from '../../../acct/parse'; +import getNotificationSummary from '../../../renderers/get-notification-summary'; + +const hmm = [ + '?', + 'ふぅ~む...?', + 'ちょっと何言ってるかわからないです', + '「ヘルプ」と言うと利用可能な操作が確認できますよ' +]; + +/** + * Botの頭脳 + */ +export default class BotCore extends EventEmitter { + public user: IUser = null; + + private context: Context = null; + + constructor(user?: IUser) { + super(); + + this.user = user; + } + + public clearContext() { + this.setContext(null); + } + + public setContext(context: Context) { + this.context = context; + this.emit('updated'); + + if (context) { + context.on('updated', () => { + this.emit('updated'); + }); + } + } + + public export() { + return { + user: this.user, + context: this.context ? this.context.export() : null + }; + } + + protected _import(data) { + this.user = data.user ? initUser(data.user) : null; + this.setContext(data.context ? Context.import(this, data.context) : null); + } + + public static import(data) { + const bot = new BotCore(); + bot._import(data); + return bot; + } + + public async q(query: string): Promise<string> { + if (this.context != null) { + return await this.context.q(query); + } + + if (/^@[a-zA-Z0-9_]+$/.test(query)) { + return await this.showUserCommand(query); + } + + switch (query) { + case 'ping': + return 'PONG'; + + case 'help': + case 'ヘルプ': + return '利用可能なコマンド一覧です:\n' + + 'help: これです\n' + + 'me: アカウント情報を見ます\n' + + 'login, signin: サインインします\n' + + 'logout, signout: サインアウトします\n' + + 'note: 投稿します\n' + + 'tl: タイムラインを見ます\n' + + 'no: 通知を見ます\n' + + '@<ユーザー名>: ユーザーを表示します\n' + + '\n' + + 'タイムラインや通知を見た後、「次」というとさらに遡ることができます。'; + + case 'me': + return this.user ? `${getUserName(this.user)}としてサインインしています。\n\n${getUserSummary(this.user)}` : 'サインインしていません'; + + case 'login': + case 'signin': + case 'ログイン': + case 'サインイン': + if (this.user != null) return '既にサインインしていますよ!'; + this.setContext(new SigninContext(this)); + return await this.context.greet(); + + case 'logout': + case 'signout': + case 'ログアウト': + case 'サインアウト': + if (this.user == null) return '今はサインインしてないですよ!'; + this.signout(); + return 'ご利用ありがとうございました <3'; + + case 'note': + case '投稿': + if (this.user == null) return 'まずサインインしてください。'; + this.setContext(new NoteContext(this)); + return await this.context.greet(); + + case 'tl': + case 'タイムライン': + if (this.user == null) return 'まずサインインしてください。'; + this.setContext(new TlContext(this)); + return await this.context.greet(); + + case 'no': + case 'notifications': + case '通知': + if (this.user == null) return 'まずサインインしてください。'; + this.setContext(new NotificationsContext(this)); + return await this.context.greet(); + + case 'guessing-game': + case '数当てゲーム': + this.setContext(new GuessingGameContext(this)); + return await this.context.greet(); + + default: + return hmm[Math.floor(Math.random() * hmm.length)]; + } + } + + public signin(user: IUser) { + this.user = user; + this.emit('signin', user); + this.emit('updated'); + } + + public signout() { + const user = this.user; + this.user = null; + this.emit('signout', user); + this.emit('updated'); + } + + public async refreshUser() { + this.user = await User.findOne({ + _id: this.user._id + }, { + fields: { + data: false + } + }); + + this.emit('updated'); + } + + public async showUserCommand(q: string): Promise<string> { + try { + const user = await require('../endpoints/users/show')(parseAcct(q.substr(1)), this.user); + + const text = getUserSummary(user); + + return text; + } catch (e) { + return `問題が発生したようです...: ${e}`; + } + } +} + +abstract class Context extends EventEmitter { + protected bot: BotCore; + + public abstract async greet(): Promise<string>; + public abstract async q(query: string): Promise<string>; + public abstract export(): any; + + constructor(bot: BotCore) { + super(); + this.bot = bot; + } + + public static import(bot: BotCore, data: any) { + if (data.type == 'guessing-game') return GuessingGameContext.import(bot, data.content); + if (data.type == 'note') return NoteContext.import(bot, data.content); + if (data.type == 'tl') return TlContext.import(bot, data.content); + if (data.type == 'notifications') return NotificationsContext.import(bot, data.content); + if (data.type == 'signin') return SigninContext.import(bot, data.content); + return null; + } +} + +class SigninContext extends Context { + private temporaryUser: ILocalUser = null; + + public async greet(): Promise<string> { + return 'まずユーザー名を教えてください:'; + } + + public async q(query: string): Promise<string> { + if (this.temporaryUser == null) { + // Fetch user + const user = await User.findOne({ + usernameLower: query.toLowerCase(), + host: null + }, { + fields: { + data: false + } + }) as ILocalUser; + + if (user === null) { + return `${query}というユーザーは存在しませんでした... もう一度教えてください:`; + } else { + this.temporaryUser = user; + this.emit('updated'); + return `パスワードを教えてください:`; + } + } else { + // Compare password + const same = await bcrypt.compare(query, this.temporaryUser.password); + + if (same) { + this.bot.signin(this.temporaryUser); + this.bot.clearContext(); + return `${getUserName(this.temporaryUser)}さん、おかえりなさい!`; + } else { + return `パスワードが違います... もう一度教えてください:`; + } + } + } + + public export() { + return { + type: 'signin', + content: { + temporaryUser: this.temporaryUser + } + }; + } + + public static import(bot: BotCore, data: any) { + const context = new SigninContext(bot); + context.temporaryUser = data.temporaryUser; + return context; + } +} + +class NoteContext extends Context { + public async greet(): Promise<string> { + return '内容:'; + } + + public async q(query: string): Promise<string> { + await require('../endpoints/notes/create')({ + text: query + }, this.bot.user); + this.bot.clearContext(); + return '投稿しましたよ!'; + } + + public export() { + return { + type: 'note' + }; + } + + public static import(bot: BotCore, data: any) { + const context = new NoteContext(bot); + return context; + } +} + +class TlContext extends Context { + private next: string = null; + + public async greet(): Promise<string> { + return await this.getTl(); + } + + public async q(query: string): Promise<string> { + if (query == '次') { + return await this.getTl(); + } else { + this.bot.clearContext(); + return await this.bot.q(query); + } + } + + private async getTl() { + const tl = await require('../endpoints/notes/timeline')({ + limit: 5, + untilId: this.next ? this.next : undefined + }, this.bot.user); + + if (tl.length > 0) { + this.next = tl[tl.length - 1].id; + this.emit('updated'); + + const text = tl + .map(note => `${getUserName(note.user)}\n「${getNoteSummary(note)}」`) + .join('\n-----\n'); + + return text; + } else { + return 'タイムラインに表示するものがありません...'; + } + } + + public export() { + return { + type: 'tl', + content: { + next: this.next, + } + }; + } + + public static import(bot: BotCore, data: any) { + const context = new TlContext(bot); + context.next = data.next; + return context; + } +} + +class NotificationsContext extends Context { + private next: string = null; + + public async greet(): Promise<string> { + return await this.getNotifications(); + } + + public async q(query: string): Promise<string> { + if (query == '次') { + return await this.getNotifications(); + } else { + this.bot.clearContext(); + return await this.bot.q(query); + } + } + + private async getNotifications() { + const notifications = await require('../endpoints/i/notifications')({ + limit: 5, + untilId: this.next ? this.next : undefined + }, this.bot.user); + + if (notifications.length > 0) { + this.next = notifications[notifications.length - 1].id; + this.emit('updated'); + + const text = notifications + .map(notification => getNotificationSummary(notification)) + .join('\n-----\n'); + + return text; + } else { + return '通知はありません'; + } + } + + public export() { + return { + type: 'notifications', + content: { + next: this.next, + } + }; + } + + public static import(bot: BotCore, data: any) { + const context = new NotificationsContext(bot); + context.next = data.next; + return context; + } +} + +class GuessingGameContext extends Context { + private secret: number; + private history: number[] = []; + + public async greet(): Promise<string> { + this.secret = Math.floor(Math.random() * 100); + this.emit('updated'); + return '0~100の秘密の数を当ててみてください:'; + } + + public async q(query: string): Promise<string> { + if (query == 'やめる') { + this.bot.clearContext(); + return 'やめました。'; + } + + const guess = parseInt(query, 10); + + if (isNaN(guess)) { + return '整数で推測してください。「やめる」と言うとゲームをやめます。'; + } + + const firsttime = this.history.indexOf(guess) === -1; + + this.history.push(guess); + this.emit('updated'); + + if (this.secret < guess) { + return firsttime ? `${guess}よりも小さいですね` : `もう一度言いますが${guess}より小さいですよ`; + } else if (this.secret > guess) { + return firsttime ? `${guess}よりも大きいですね` : `もう一度言いますが${guess}より大きいですよ`; + } else { + this.bot.clearContext(); + return `正解です🎉 (${this.history.length}回目で当てました)`; + } + } + + public export() { + return { + type: 'guessing-game', + content: { + secret: this.secret, + history: this.history + } + }; + } + + public static import(bot: BotCore, data: any) { + const context = new GuessingGameContext(bot); + context.secret = data.secret; + context.history = data.history; + return context; + } +} diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts new file mode 100644 index 0000000000..be3bfe33d3 --- /dev/null +++ b/src/server/api/bot/interfaces/line.ts @@ -0,0 +1,239 @@ +import * as EventEmitter from 'events'; +import * as express from 'express'; +import * as request from 'request'; +import * as crypto from 'crypto'; +import User from '../../../../models/user'; +import config from '../../../../config'; +import BotCore from '../core'; +import _redis from '../../../../db/redis'; +import prominence = require('prominence'); +import getAcct from '../../../../acct/render'; +import parseAcct from '../../../../acct/parse'; +import getNoteSummary from '../../../../renderers/get-note-summary'; +import getUserName from '../../../../renderers/get-user-name'; + +const redis = prominence(_redis); + +// SEE: https://developers.line.me/media/messaging-api/messages/sticker_list.pdf +const stickers = [ + '297', + '298', + '299', + '300', + '301', + '302', + '303', + '304', + '305', + '306', + '307' +]; + +class LineBot extends BotCore { + private replyToken: string; + + private reply(messages: any[]) { + request.post({ + url: 'https://api.line.me/v2/bot/message/reply', + headers: { + 'Authorization': `Bearer ${config.line_bot.channel_access_token}` + }, + json: { + replyToken: this.replyToken, + messages: messages + } + }, (err, res, body) => { + if (err) { + console.error(err); + return; + } + }); + } + + public async react(ev: any): Promise<void> { + this.replyToken = ev.replyToken; + + switch (ev.type) { + // メッセージ + case 'message': + switch (ev.message.type) { + // テキスト + case 'text': + const res = await this.q(ev.message.text); + if (res == null) return; + // 返信 + this.reply([{ + type: 'text', + text: res + }]); + break; + + // スタンプ + case 'sticker': + // スタンプで返信 + this.reply([{ + type: 'sticker', + packageId: '4', + stickerId: stickers[Math.floor(Math.random() * stickers.length)] + }]); + break; + } + break; + + // noteback + case 'noteback': + const data = ev.noteback.data; + const cmd = data.split('|')[0]; + const arg = data.split('|')[1]; + switch (cmd) { + case 'showtl': + this.showUserTimelineNoteback(arg); + break; + } + break; + } + } + + public static import(data) { + const bot = new LineBot(); + bot._import(data); + return bot; + } + + public async showUserCommand(q: string) { + const user = await require('../../endpoints/users/show')(parseAcct(q.substr(1)), this.user); + + const acct = getAcct(user); + const actions = []; + + actions.push({ + type: 'noteback', + label: 'タイムラインを見る', + data: `showtl|${user.id}` + }); + + if (user.twitter) { + actions.push({ + type: 'uri', + label: 'Twitterアカウントを見る', + uri: `https://twitter.com/${user.twitter.screenName}` + }); + } + + actions.push({ + type: 'uri', + label: 'Webで見る', + uri: `${config.url}/@${acct}` + }); + + this.reply([{ + type: 'template', + altText: await super.showUserCommand(q), + template: { + type: 'buttons', + thumbnailImageUrl: `${user.avatarUrl}?thumbnail&size=1024`, + title: `${getUserName(user)} (@${acct})`, + text: user.description || '(no description)', + actions: actions + } + }]); + + return null; + } + + public async showUserTimelineNoteback(userId: string) { + const tl = await require('../../endpoints/users/notes')({ + userId: userId, + limit: 5 + }, this.user); + + const text = `${getUserName(tl[0].user)}さんのタイムラインはこちらです:\n\n` + tl + .map(note => getNoteSummary(note)) + .join('\n-----\n'); + + this.reply([{ + type: 'text', + text: text + }]); + } +} + +module.exports = async (app: express.Application) => { + if (config.line_bot == null) return; + + const handler = new EventEmitter(); + + handler.on('event', async (ev) => { + + const sourceId = ev.source.userId; + const sessionId = `line-bot-sessions:${sourceId}`; + + const session = await redis.get(sessionId); + let bot: LineBot; + + if (session == null) { + const user = await User.findOne({ + host: null, + 'line': { + userId: sourceId + } + }); + + bot = new LineBot(user); + + bot.on('signin', user => { + User.update(user._id, { + $set: { + 'line': { + userId: sourceId + } + } + }); + }); + + bot.on('signout', user => { + User.update(user._id, { + $set: { + 'line': { + userId: null + } + } + }); + }); + + redis.set(sessionId, JSON.stringify(bot.export())); + } else { + bot = LineBot.import(JSON.parse(session)); + } + + bot.on('updated', () => { + redis.set(sessionId, JSON.stringify(bot.export())); + }); + + if (session != null) bot.refreshUser(); + + bot.react(ev); + }); + + app.post('/hooks/line', (req, res, next) => { + // req.headers['x-line-signature'] は常に string ですが、型定義の都合上 + // string | string[] になっているので string を明示しています + const sig1 = req.headers['x-line-signature'] as string; + + const hash = crypto.createHmac('SHA256', config.line_bot.channel_secret) + .update((req as any).rawBody); + + const sig2 = hash.digest('base64'); + + // シグネチャ比較 + if (sig1 === sig2) { + req.body.events.forEach(ev => { + handler.emit('event', ev); + }); + + res.sendStatus(200); + } else { + res.sendStatus(400); + } + }); +}; diff --git a/src/server/api/call.ts b/src/server/api/call.ts new file mode 100644 index 0000000000..1bfe94bb74 --- /dev/null +++ b/src/server/api/call.ts @@ -0,0 +1,55 @@ +import * as express from 'express'; + +import endpoints, { Endpoint } from './endpoints'; +import limitter from './limitter'; +import { IUser } from '../../models/user'; +import { IApp } from '../../models/app'; + +export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, req?: express.Request) => new Promise(async (ok, rej) => { + const isSecure = user != null && app == null; + + //console.log(endpoint, user, app, data); + + const ep = typeof endpoint == 'string' ? endpoints.find(e => e.name == endpoint) : endpoint; + + if (ep.secure && !isSecure) { + return rej('ACCESS_DENIED'); + } + + if (ep.withCredential && user == null) { + return rej('SIGNIN_REQUIRED'); + } + + if (app && ep.kind) { + if (!app.permission.some(p => p === ep.kind)) { + return rej('PERMISSION_DENIED'); + } + } + + if (ep.withCredential && ep.limit) { + try { + await limitter(ep, user); // Rate limit + } catch (e) { + // drop request if limit exceeded + return rej('RATE_LIMIT_EXCEEDED'); + } + } + + let exec = require(`${__dirname}/endpoints/${ep.name}`); + + if (ep.withFile && req) { + exec = exec.bind(null, req.file); + } + + let res; + + // API invoking + try { + res = await exec(data, user, app); + } catch (e) { + rej(e); + return; + } + + ok(res); +}); diff --git a/src/server/api/common/generate-native-user-token.ts b/src/server/api/common/generate-native-user-token.ts new file mode 100644 index 0000000000..2082b89a5a --- /dev/null +++ b/src/server/api/common/generate-native-user-token.ts @@ -0,0 +1,3 @@ +import rndstr from 'rndstr'; + +export default () => `!${rndstr('a-zA-Z0-9', 32)}`; diff --git a/src/api/common/get-friends.ts b/src/server/api/common/get-friends.ts similarity index 71% rename from src/api/common/get-friends.ts rename to src/server/api/common/get-friends.ts index db6313816d..c1cc3957d8 100644 --- a/src/api/common/get-friends.ts +++ b/src/server/api/common/get-friends.ts @@ -1,22 +1,20 @@ import * as mongodb from 'mongodb'; -import Following from '../models/following'; +import Following from '../../../models/following'; export default async (me: mongodb.ObjectID, includeMe: boolean = true) => { // Fetch relation to other users who the I follows // SELECT followee const myfollowing = await Following .find({ - follower_id: me, - // 削除されたドキュメントは除く - deleted_at: { $exists: false } + followerId: me }, { fields: { - followee_id: true + followeeId: true } }); // ID list of other users who the I follows - const myfollowingIds = myfollowing.map(follow => follow.followee_id); + const myfollowingIds = myfollowing.map(follow => follow.followeeId); if (includeMe) { myfollowingIds.push(me); diff --git a/src/server/api/common/get-host-lower.ts b/src/server/api/common/get-host-lower.ts new file mode 100644 index 0000000000..fc4b30439e --- /dev/null +++ b/src/server/api/common/get-host-lower.ts @@ -0,0 +1,5 @@ +import { toUnicode } from 'punycode'; + +export default host => { + return toUnicode(host).replace(/[A-Z]+/, match => match.toLowerCase()); +}; diff --git a/src/api/common/is-native-token.ts b/src/server/api/common/is-native-token.ts similarity index 100% rename from src/api/common/is-native-token.ts rename to src/server/api/common/is-native-token.ts diff --git a/src/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts similarity index 73% rename from src/api/common/read-messaging-message.ts rename to src/server/api/common/read-messaging-message.ts index 3257ec8b07..c52f9363b5 100644 --- a/src/api/common/read-messaging-message.ts +++ b/src/server/api/common/read-messaging-message.ts @@ -1,8 +1,9 @@ import * as mongo from 'mongodb'; -import Message from '../models/messaging-message'; -import { IMessagingMessage as IMessage } from '../models/messaging-message'; -import publishUserStream from '../event'; -import { publishMessagingStream } from '../event'; +import Message from '../../../models/messaging-message'; +import { IMessagingMessage as IMessage } from '../../../models/messaging-message'; +import publishUserStream from '../../../publishers/stream'; +import { publishMessagingStream } from '../../../publishers/stream'; +import { publishMessagingIndexStream } from '../../../publishers/stream'; /** * Mark as read message(s) @@ -36,12 +37,12 @@ export default ( // Update documents await Message.update({ _id: { $in: ids }, - user_id: otherpartyId, - recipient_id: userId, - is_read: false + userId: otherpartyId, + recipientId: userId, + isRead: false }, { $set: { - is_read: true + isRead: true } }, { multi: true @@ -49,12 +50,13 @@ export default ( // Publish event publishMessagingStream(otherpartyId, userId, 'read', ids.map(id => id.toString())); + publishMessagingIndexStream(userId, 'read', ids.map(id => id.toString())); // Calc count of my unread messages const count = await Message .count({ - recipient_id: userId, - is_read: false + recipientId: userId, + isRead: false }); if (count == 0) { diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts new file mode 100644 index 0000000000..9bd41519fb --- /dev/null +++ b/src/server/api/common/read-notification.ts @@ -0,0 +1,52 @@ +import * as mongo from 'mongodb'; +import { default as Notification, INotification } from '../../../models/notification'; +import publishUserStream from '../../../publishers/stream'; + +/** + * Mark as read notification(s) + */ +export default ( + user: string | mongo.ObjectID, + message: string | string[] | INotification | INotification[] | mongo.ObjectID | mongo.ObjectID[] +) => new Promise<any>(async (resolve, reject) => { + + const userId = mongo.ObjectID.prototype.isPrototypeOf(user) + ? user + : new mongo.ObjectID(user); + + const ids: mongo.ObjectID[] = Array.isArray(message) + ? mongo.ObjectID.prototype.isPrototypeOf(message[0]) + ? (message as mongo.ObjectID[]) + : typeof message[0] === 'string' + ? (message as string[]).map(m => new mongo.ObjectID(m)) + : (message as INotification[]).map(m => m._id) + : mongo.ObjectID.prototype.isPrototypeOf(message) + ? [(message as mongo.ObjectID)] + : typeof message === 'string' + ? [new mongo.ObjectID(message)] + : [(message as INotification)._id]; + + // Update documents + await Notification.update({ + _id: { $in: ids }, + isRead: false + }, { + $set: { + isRead: true + } + }, { + multi: true + }); + + // Calc count of my unread notifications + const count = await Notification + .count({ + notifieeId: userId, + isRead: false + }); + + if (count == 0) { + // 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行 + publishUserStream(userId, 'read_all_notifications'); + } +}); diff --git a/src/server/api/common/signin.ts b/src/server/api/common/signin.ts new file mode 100644 index 0000000000..8bb327694d --- /dev/null +++ b/src/server/api/common/signin.ts @@ -0,0 +1,19 @@ +import config from '../../../config'; + +export default function(res, user, redirect: boolean) { + const expires = 1000 * 60 * 60 * 24 * 365; // One Year + res.cookie('i', user.token, { + path: '/', + domain: `.${config.hostname}`, + secure: config.url.substr(0, 5) === 'https', + httpOnly: false, + expires: new Date(Date.now() + expires), + maxAge: expires + }); + + if (redirect) { + res.redirect(config.url); + } else { + res.sendStatus(204); + } +} diff --git a/src/api/endpoints.ts b/src/server/api/endpoints.ts similarity index 73% rename from src/api/endpoints.ts rename to src/server/api/endpoints.ts index 5bbc480a8e..67f3217faf 100644 --- a/src/api/endpoints.ts +++ b/src/server/api/endpoints.ts @@ -113,7 +113,7 @@ const endpoints: Endpoint[] = [ secure: true }, { - name: 'aggregation/posts', + name: 'aggregation/notes', }, { name: 'aggregation/users', @@ -122,7 +122,7 @@ const endpoints: Endpoint[] = [ name: 'aggregation/users/activity', }, { - name: 'aggregation/users/post', + name: 'aggregation/users/note', }, { name: 'aggregation/users/followers' @@ -134,22 +134,42 @@ const endpoints: Endpoint[] = [ name: 'aggregation/users/reaction' }, { - name: 'aggregation/posts/repost' + name: 'aggregation/notes/renote' }, { - name: 'aggregation/posts/reply' + name: 'aggregation/notes/reply' }, { - name: 'aggregation/posts/reaction' + name: 'aggregation/notes/reaction' }, { - name: 'aggregation/posts/reactions' + name: 'aggregation/notes/reactions' + }, + + { + name: 'sw/register', + withCredential: true }, { name: 'i', withCredential: true }, + { + name: 'i/2fa/register', + withCredential: true, + secure: true + }, + { + name: 'i/2fa/unregister', + withCredential: true, + secure: true + }, + { + name: 'i/2fa/done', + withCredential: true, + secure: true + }, { name: 'i/update', withCredential: true, @@ -159,6 +179,35 @@ const endpoints: Endpoint[] = [ }, kind: 'account-write' }, + { + name: 'i/update_home', + withCredential: true, + secure: true + }, + { + name: 'i/update_mobile_home', + withCredential: true, + secure: true + }, + { + name: 'i/change_password', + withCredential: true, + secure: true + }, + { + name: 'i/regenerate_token', + withCredential: true, + secure: true + }, + { + name: 'i/update_client_setting', + withCredential: true, + secure: true + }, + { + name: 'i/pin', + kind: 'account-write' + }, { name: 'i/appdata/get', withCredential: true @@ -183,6 +232,52 @@ const endpoints: Endpoint[] = [ withCredential: true, kind: 'notification-read' }, + + { + name: 'othello/match', + withCredential: true + }, + + { + name: 'othello/match/cancel', + withCredential: true + }, + + { + name: 'othello/invitations', + withCredential: true + }, + + { + name: 'othello/games', + withCredential: true + }, + + { + name: 'othello/games/show' + }, + + { + name: 'mute/create', + withCredential: true, + kind: 'account/write' + }, + { + name: 'mute/delete', + withCredential: true, + kind: 'account/write' + }, + { + name: 'mute/list', + withCredential: true, + kind: 'account/read' + }, + + { + name: 'notifications/get_unread_count', + withCredential: true, + kind: 'notification-read' + }, { name: 'notifications/delete', withCredential: true, @@ -193,11 +288,6 @@ const endpoints: Endpoint[] = [ withCredential: true, kind: 'notification-write' }, - { - name: 'notifications/mark_as_read', - withCredential: true, - kind: 'notification-write' - }, { name: 'notifications/mark_as_read_all', withCredential: true, @@ -301,7 +391,7 @@ const endpoints: Endpoint[] = [ name: 'users/search_by_username' }, { - name: 'users/posts' + name: 'users/notes' }, { name: 'users/following' @@ -314,6 +404,9 @@ const endpoints: Endpoint[] = [ withCredential: true, kind: 'account-read' }, + { + name: 'users/get_frequently_replied_users' + }, { name: 'following/create', @@ -335,35 +428,35 @@ const endpoints: Endpoint[] = [ }, { - name: 'posts' + name: 'notes' }, { - name: 'posts/show' + name: 'notes/show' }, { - name: 'posts/replies' + name: 'notes/replies' }, { - name: 'posts/context' + name: 'notes/context' }, { - name: 'posts/create', + name: 'notes/create', withCredential: true, limit: { duration: ms('1hour'), max: 120, minInterval: ms('1second') }, - kind: 'post-write' + kind: 'note-write' }, { - name: 'posts/reposts' + name: 'notes/renotes' }, { - name: 'posts/search' + name: 'notes/search' }, { - name: 'posts/timeline', + name: 'notes/timeline', withCredential: true, limit: { duration: ms('10minutes'), @@ -371,7 +464,7 @@ const endpoints: Endpoint[] = [ } }, { - name: 'posts/mentions', + name: 'notes/mentions', withCredential: true, limit: { duration: ms('10minutes'), @@ -379,15 +472,19 @@ const endpoints: Endpoint[] = [ } }, { - name: 'posts/trend', + name: 'notes/trend', withCredential: true }, { - name: 'posts/reactions', + name: 'notes/categorize', withCredential: true }, { - name: 'posts/reactions/create', + name: 'notes/reactions', + withCredential: true + }, + { + name: 'notes/reactions/create', withCredential: true, limit: { duration: ms('1hour'), @@ -396,7 +493,7 @@ const endpoints: Endpoint[] = [ kind: 'reaction-write' }, { - name: 'posts/reactions/delete', + name: 'notes/reactions/delete', withCredential: true, limit: { duration: ms('1hour'), @@ -405,7 +502,7 @@ const endpoints: Endpoint[] = [ kind: 'reaction-write' }, { - name: 'posts/favorites/create', + name: 'notes/favorites/create', withCredential: true, limit: { duration: ms('1hour'), @@ -414,7 +511,7 @@ const endpoints: Endpoint[] = [ kind: 'favorite-write' }, { - name: 'posts/favorites/delete', + name: 'notes/favorites/delete', withCredential: true, limit: { duration: ms('1hour'), @@ -423,7 +520,7 @@ const endpoints: Endpoint[] = [ kind: 'favorite-write' }, { - name: 'posts/polls/vote', + name: 'notes/polls/vote', withCredential: true, limit: { duration: ms('1hour'), @@ -432,7 +529,7 @@ const endpoints: Endpoint[] = [ kind: 'vote-write' }, { - name: 'posts/polls/recommendation', + name: 'notes/polls/recommendation', withCredential: true }, @@ -455,8 +552,33 @@ const endpoints: Endpoint[] = [ name: 'messaging/messages/create', withCredential: true, kind: 'messaging-write' - } - + }, + { + name: 'channels/create', + withCredential: true, + limit: { + duration: ms('1hour'), + max: 3, + minInterval: ms('10seconds') + } + }, + { + name: 'channels/show' + }, + { + name: 'channels/notes' + }, + { + name: 'channels/watch', + withCredential: true + }, + { + name: 'channels/unwatch', + withCredential: true + }, + { + name: 'channels' + }, ]; export default endpoints; diff --git a/src/api/endpoints/aggregation/posts.ts b/src/server/api/endpoints/aggregation/posts.ts similarity index 68% rename from src/api/endpoints/aggregation/posts.ts rename to src/server/api/endpoints/aggregation/posts.ts index 48ee225129..cc2a48b53d 100644 --- a/src/api/endpoints/aggregation/posts.ts +++ b/src/server/api/endpoints/aggregation/posts.ts @@ -2,10 +2,10 @@ * Module dependencies */ import $ from 'cafy'; -import Post from '../../models/post'; +import Note from '../../../../models/note'; /** - * Aggregate posts + * Aggregate notes * * @param {any} params * @return {Promise<any>} @@ -15,28 +15,28 @@ module.exports = params => new Promise(async (res, rej) => { const [limit = 365, limitErr] = $(params.limit).optional.number().range(1, 365).$; if (limitErr) return rej('invalid limit param'); - const datas = await Post + const datas = await Note .aggregate([ { $project: { - repost_id: '$repost_id', - reply_to_id: '$reply_to_id', - created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST + renoteId: '$renoteId', + replyId: '$replyId', + createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST }}, { $project: { date: { - year: { $year: '$created_at' }, - month: { $month: '$created_at' }, - day: { $dayOfMonth: '$created_at' } + year: { $year: '$createdAt' }, + month: { $month: '$createdAt' }, + day: { $dayOfMonth: '$createdAt' } }, type: { $cond: { - if: { $ne: ['$repost_id', null] }, - then: 'repost', + if: { $ne: ['$renoteId', null] }, + then: 'renote', else: { $cond: { - if: { $ne: ['$reply_to_id', null] }, + if: { $ne: ['$replyId', null] }, then: 'reply', - else: 'post' + else: 'note' } } } @@ -59,8 +59,8 @@ module.exports = params => new Promise(async (res, rej) => { data.date = data._id; delete data._id; - data.posts = (data.data.filter(x => x.type == 'post')[0] || { count: 0 }).count; - data.reposts = (data.data.filter(x => x.type == 'repost')[0] || { count: 0 }).count; + data.notes = (data.data.filter(x => x.type == 'note')[0] || { count: 0 }).count; + data.renotes = (data.data.filter(x => x.type == 'renote')[0] || { count: 0 }).count; data.replies = (data.data.filter(x => x.type == 'reply')[0] || { count: 0 }).count; delete data.data; @@ -79,8 +79,8 @@ module.exports = params => new Promise(async (res, rej) => { graph.push(data); } else { graph.push({ - posts: 0, - reposts: 0, + notes: 0, + renotes: 0, replies: 0 }); } diff --git a/src/api/endpoints/aggregation/users.ts b/src/server/api/endpoints/aggregation/users.ts similarity index 81% rename from src/api/endpoints/aggregation/users.ts rename to src/server/api/endpoints/aggregation/users.ts index 9eb2d035ec..19776ed297 100644 --- a/src/api/endpoints/aggregation/users.ts +++ b/src/server/api/endpoints/aggregation/users.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../models/user'; +import User from '../../../../models/user'; /** * Aggregate users @@ -17,11 +17,14 @@ module.exports = params => new Promise(async (res, rej) => { const users = await User .find({}, { - _id: false, - created_at: true, - deleted_at: true - }, { - sort: { created_at: -1 } + sort: { + _id: -1 + }, + fields: { + _id: false, + createdAt: true, + deletedAt: true + } }); const graph = []; @@ -41,11 +44,11 @@ module.exports = params => new Promise(async (res, rej) => { // day = day.getTime(); const total = users.filter(u => - u.created_at < dayEnd && (u.deleted_at == null || u.deleted_at > dayEnd) + u.createdAt < dayEnd && (u.deletedAt == null || u.deletedAt > dayEnd) ).length; const created = users.filter(u => - u.created_at < dayEnd && u.created_at > dayStart + u.createdAt < dayEnd && u.createdAt > dayStart ).length; graph.push({ diff --git a/src/api/endpoints/aggregation/users/activity.ts b/src/server/api/endpoints/aggregation/users/activity.ts similarity index 67% rename from src/api/endpoints/aggregation/users/activity.ts rename to src/server/api/endpoints/aggregation/users/activity.ts index 5a3e78c441..318cce77a5 100644 --- a/src/api/endpoints/aggregation/users/activity.ts +++ b/src/server/api/endpoints/aggregation/users/activity.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../../models/user'; -import Post from '../../../models/post'; +import User from '../../../../../models/user'; +import Note from '../../../../../models/note'; // TODO: likeやfollowも集計 @@ -18,9 +18,9 @@ module.exports = (params) => new Promise(async (res, rej) => { const [limit = 365, limitErr] = $(params.limit).optional.number().range(1, 365).$; if (limitErr) return rej('invalid limit param'); - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); // Lookup user const user = await User.findOne({ @@ -35,29 +35,29 @@ module.exports = (params) => new Promise(async (res, rej) => { return rej('user not found'); } - const datas = await Post + const datas = await Note .aggregate([ - { $match: { user_id: user._id } }, + { $match: { userId: user._id } }, { $project: { - repost_id: '$repost_id', - reply_to_id: '$reply_to_id', - created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST + renoteId: '$renoteId', + replyId: '$replyId', + createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST }}, { $project: { date: { - year: { $year: '$created_at' }, - month: { $month: '$created_at' }, - day: { $dayOfMonth: '$created_at' } + year: { $year: '$createdAt' }, + month: { $month: '$createdAt' }, + day: { $dayOfMonth: '$createdAt' } }, type: { $cond: { - if: { $ne: ['$repost_id', null] }, - then: 'repost', + if: { $ne: ['$renoteId', null] }, + then: 'renote', else: { $cond: { - if: { $ne: ['$reply_to_id', null] }, + if: { $ne: ['$replyId', null] }, then: 'reply', - else: 'post' + else: 'note' } } } @@ -80,8 +80,8 @@ module.exports = (params) => new Promise(async (res, rej) => { data.date = data._id; delete data._id; - data.posts = (data.data.filter(x => x.type == 'post')[0] || { count: 0 }).count; - data.reposts = (data.data.filter(x => x.type == 'repost')[0] || { count: 0 }).count; + data.notes = (data.data.filter(x => x.type == 'note')[0] || { count: 0 }).count; + data.renotes = (data.data.filter(x => x.type == 'renote')[0] || { count: 0 }).count; data.replies = (data.data.filter(x => x.type == 'reply')[0] || { count: 0 }).count; delete data.data; @@ -105,8 +105,8 @@ module.exports = (params) => new Promise(async (res, rej) => { month: day.getMonth() + 1, // In JavaScript, month is zero-based. day: day.getDate() }, - posts: 0, - reposts: 0, + notes: 0, + renotes: 0, replies: 0 }); } diff --git a/src/server/api/endpoints/aggregation/users/followers.ts b/src/server/api/endpoints/aggregation/users/followers.ts new file mode 100644 index 0000000000..7ccb2a3066 --- /dev/null +++ b/src/server/api/endpoints/aggregation/users/followers.ts @@ -0,0 +1,64 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../../../../models/user'; +import FollowedLog from '../../../../../models/followed-log'; + +/** + * Aggregate followers of a user + * + * @param {any} params + * @return {Promise<any>} + */ +module.exports = (params) => new Promise(async (res, rej) => { + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); + + // Lookup user + const user = await User.findOne({ + _id: userId + }, { + fields: { + _id: true + } + }); + + if (user === null) { + return rej('user not found'); + } + + const today = new Date(); + const graph = []; + + today.setMinutes(0); + today.setSeconds(0); + today.setMilliseconds(0); + + let cursorDate = new Date(today.getTime()); + let cursorTime = cursorDate.setDate(new Date(today.getTime()).getDate() + 1); + + for (let i = 0; i < 30; i++) { + graph.push(FollowedLog.findOne({ + createdAt: { $lt: new Date(cursorTime / 1000) }, + userId: user._id + }, { + sort: { createdAt: -1 }, + }).then(log => { + cursorDate = new Date(today.getTime()); + cursorTime = cursorDate.setDate(today.getDate() - i); + + return { + date: { + year: cursorDate.getFullYear(), + month: cursorDate.getMonth() + 1, // In JavaScript, month is zero-based. + day: cursorDate.getDate() + }, + count: log ? log.count : 0 + }; + })); + } + + res(await Promise.all(graph)); +}); diff --git a/src/server/api/endpoints/aggregation/users/following.ts b/src/server/api/endpoints/aggregation/users/following.ts new file mode 100644 index 0000000000..45e246495b --- /dev/null +++ b/src/server/api/endpoints/aggregation/users/following.ts @@ -0,0 +1,64 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../../../../models/user'; +import FollowingLog from '../../../../../models/following-log'; + +/** + * Aggregate following of a user + * + * @param {any} params + * @return {Promise<any>} + */ +module.exports = (params) => new Promise(async (res, rej) => { + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); + + // Lookup user + const user = await User.findOne({ + _id: userId + }, { + fields: { + _id: true + } + }); + + if (user === null) { + return rej('user not found'); + } + + const today = new Date(); + const graph = []; + + today.setMinutes(0); + today.setSeconds(0); + today.setMilliseconds(0); + + let cursorDate = new Date(today.getTime()); + let cursorTime = cursorDate.setDate(new Date(today.getTime()).getDate() + 1); + + for (let i = 0; i < 30; i++) { + graph.push(FollowingLog.findOne({ + createdAt: { $lt: new Date(cursorTime / 1000) }, + userId: user._id + }, { + sort: { createdAt: -1 }, + }).then(log => { + cursorDate = new Date(today.getTime()); + cursorTime = cursorDate.setDate(today.getDate() - i); + + return { + date: { + year: cursorDate.getFullYear(), + month: cursorDate.getMonth() + 1, // In JavaScript, month is zero-based. + day: cursorDate.getDate() + }, + count: log ? log.count : 0 + }; + })); + } + + res(await Promise.all(graph)); +}); diff --git a/src/api/endpoints/aggregation/users/post.ts b/src/server/api/endpoints/aggregation/users/post.ts similarity index 63% rename from src/api/endpoints/aggregation/users/post.ts rename to src/server/api/endpoints/aggregation/users/post.ts index c964815a0c..e6170d83e2 100644 --- a/src/api/endpoints/aggregation/users/post.ts +++ b/src/server/api/endpoints/aggregation/users/post.ts @@ -2,19 +2,19 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../../models/user'; -import Post from '../../../models/post'; +import User from '../../../../../models/user'; +import Note from '../../../../../models/note'; /** - * Aggregate post of a user + * Aggregate note of a user * * @param {any} params * @return {Promise<any>} */ module.exports = (params) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); // Lookup user const user = await User.findOne({ @@ -29,29 +29,29 @@ module.exports = (params) => new Promise(async (res, rej) => { return rej('user not found'); } - const datas = await Post + const datas = await Note .aggregate([ - { $match: { user_id: user._id } }, + { $match: { userId: user._id } }, { $project: { - repost_id: '$repost_id', - reply_to_id: '$reply_to_id', - created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST + renoteId: '$renoteId', + replyId: '$replyId', + createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST }}, { $project: { date: { - year: { $year: '$created_at' }, - month: { $month: '$created_at' }, - day: { $dayOfMonth: '$created_at' } + year: { $year: '$createdAt' }, + month: { $month: '$createdAt' }, + day: { $dayOfMonth: '$createdAt' } }, type: { $cond: { - if: { $ne: ['$repost_id', null] }, - then: 'repost', + if: { $ne: ['$renoteId', null] }, + then: 'renote', else: { $cond: { - if: { $ne: ['$reply_to_id', null] }, + if: { $ne: ['$replyId', null] }, then: 'reply', - else: 'post' + else: 'note' } } } @@ -74,8 +74,8 @@ module.exports = (params) => new Promise(async (res, rej) => { data.date = data._id; delete data._id; - data.posts = (data.data.filter(x => x.type == 'post')[0] || { count: 0 }).count; - data.reposts = (data.data.filter(x => x.type == 'repost')[0] || { count: 0 }).count; + data.notes = (data.data.filter(x => x.type == 'note')[0] || { count: 0 }).count; + data.renotes = (data.data.filter(x => x.type == 'renote')[0] || { count: 0 }).count; data.replies = (data.data.filter(x => x.type == 'reply')[0] || { count: 0 }).count; delete data.data; @@ -99,8 +99,8 @@ module.exports = (params) => new Promise(async (res, rej) => { month: day.getMonth() + 1, // In JavaScript, month is zero-based. day: day.getDate() }, - posts: 0, - reposts: 0, + notes: 0, + renotes: 0, replies: 0 }); } diff --git a/src/api/endpoints/aggregation/users/reaction.ts b/src/server/api/endpoints/aggregation/users/reaction.ts similarity index 70% rename from src/api/endpoints/aggregation/users/reaction.ts rename to src/server/api/endpoints/aggregation/users/reaction.ts index 0a082ed1b7..881c7ea693 100644 --- a/src/api/endpoints/aggregation/users/reaction.ts +++ b/src/server/api/endpoints/aggregation/users/reaction.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../../models/user'; -import Reaction from '../../../models/post-reaction'; +import User from '../../../../../models/user'; +import Reaction from '../../../../../models/note-reaction'; /** * Aggregate reaction of a user @@ -12,9 +12,9 @@ import Reaction from '../../../models/post-reaction'; * @return {Promise<any>} */ module.exports = (params) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); // Lookup user const user = await User.findOne({ @@ -31,15 +31,15 @@ module.exports = (params) => new Promise(async (res, rej) => { const datas = await Reaction .aggregate([ - { $match: { user_id: user._id } }, + { $match: { userId: user._id } }, { $project: { - created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST + createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST }}, { $project: { date: { - year: { $year: '$created_at' }, - month: { $month: '$created_at' }, - day: { $dayOfMonth: '$created_at' } + year: { $year: '$createdAt' }, + month: { $month: '$createdAt' }, + day: { $dayOfMonth: '$createdAt' } } }}, { $group: { diff --git a/src/api/endpoints/app/create.ts b/src/server/api/endpoints/app/create.ts similarity index 74% rename from src/api/endpoints/app/create.ts rename to src/server/api/endpoints/app/create.ts index 498c4f144e..4a55d33f2d 100644 --- a/src/api/endpoints/app/create.ts +++ b/src/server/api/endpoints/app/create.ts @@ -3,19 +3,17 @@ */ import rndstr from 'rndstr'; import $ from 'cafy'; -import App from '../../models/app'; -import { isValidNameId } from '../../models/app'; -import serialize from '../../serializers/app'; +import App, { isValidNameId, pack } from '../../../../models/app'; /** * @swagger * /app/create: - * post: + * note: * summary: Create an application * parameters: * - $ref: "#/parameters/AccessToken" * - - * name: name_id + * name: nameId * description: Application unique name * in: formData * required: true @@ -42,7 +40,7 @@ import serialize from '../../serializers/app'; * type: string * collectionFormat: csv * - - * name: callback_url + * name: callbackUrl * description: URL called back after authentication * in: formData * required: false @@ -68,9 +66,9 @@ import serialize from '../../serializers/app'; * @return {Promise<any>} */ module.exports = async (params, user) => new Promise(async (res, rej) => { - // Get 'name_id' parameter - const [nameId, nameIdErr] = $(params.name_id).string().pipe(isValidNameId).$; - if (nameIdErr) return rej('invalid name_id param'); + // Get 'nameId' parameter + const [nameId, nameIdErr] = $(params.nameId).string().pipe(isValidNameId).$; + if (nameIdErr) return rej('invalid nameId param'); // Get 'name' parameter const [name, nameErr] = $(params.name).string().$; @@ -84,27 +82,27 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { const [permission, permissionErr] = $(params.permission).array('string').unique().$; if (permissionErr) return rej('invalid permission param'); - // Get 'callback_url' parameter - // TODO: Check $ is valid url - const [callbackUrl = null, callbackUrlErr] = $(params.callback_url).optional.nullable.string().$; - if (callbackUrlErr) return rej('invalid callback_url param'); + // Get 'callbackUrl' parameter + // TODO: Check it is valid url + const [callbackUrl = null, callbackUrlErr] = $(params.callbackUrl).optional.nullable.string().$; + if (callbackUrlErr) return rej('invalid callbackUrl param'); // Generate secret const secret = rndstr('a-zA-Z0-9', 32); // Create account const app = await App.insert({ - created_at: new Date(), - user_id: user._id, + createdAt: new Date(), + userId: user._id, name: name, - name_id: nameId, - name_id_lower: nameId.toLowerCase(), + nameId: nameId, + nameIdLower: nameId.toLowerCase(), description: description, permission: permission, - callback_url: callbackUrl, + callbackUrl: callbackUrl, secret: secret }); // Response - res(await serialize(app)); + res(await pack(app)); }); diff --git a/src/api/endpoints/app/name_id/available.ts b/src/server/api/endpoints/app/name_id/available.ts similarity index 60% rename from src/api/endpoints/app/name_id/available.ts rename to src/server/api/endpoints/app/name_id/available.ts index 3d2c710322..ec2d692412 100644 --- a/src/api/endpoints/app/name_id/available.ts +++ b/src/server/api/endpoints/app/name_id/available.ts @@ -2,17 +2,17 @@ * Module dependencies */ import $ from 'cafy'; -import App from '../../../models/app'; -import { isValidNameId } from '../../../models/app'; +import App from '../../../../../models/app'; +import { isValidNameId } from '../../../../../models/app'; /** * @swagger - * /app/name_id/available: - * post: - * summary: Check available name_id on creation an application + * /app/nameId/available: + * note: + * summary: Check available nameId on creation an application * parameters: * - - * name: name_id + * name: nameId * description: Application unique name * in: formData * required: true @@ -25,7 +25,7 @@ import { isValidNameId } from '../../../models/app'; * type: object * properties: * available: - * description: Whether name_id is available + * description: Whether nameId is available * type: boolean * * default: @@ -35,20 +35,20 @@ import { isValidNameId } from '../../../models/app'; */ /** - * Check available name_id of app + * Check available nameId of app * * @param {any} params * @return {Promise<any>} */ module.exports = async (params) => new Promise(async (res, rej) => { - // Get 'name_id' parameter - const [nameId, nameIdErr] = $(params.name_id).string().pipe(isValidNameId).$; - if (nameIdErr) return rej('invalid name_id param'); + // Get 'nameId' parameter + const [nameId, nameIdErr] = $(params.nameId).string().pipe(isValidNameId).$; + if (nameIdErr) return rej('invalid nameId param'); // Get exist const exist = await App .count({ - name_id_lower: nameId.toLowerCase() + nameIdLower: nameId.toLowerCase() }, { limit: 1 }); diff --git a/src/server/api/endpoints/app/show.ts b/src/server/api/endpoints/app/show.ts new file mode 100644 index 0000000000..99a2093b68 --- /dev/null +++ b/src/server/api/endpoints/app/show.ts @@ -0,0 +1,68 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import App, { pack } from '../../../../models/app'; + +/** + * @swagger + * /app/show: + * note: + * summary: Show an application's information + * description: Require appId or nameId + * parameters: + * - + * name: appId + * description: Application ID + * in: formData + * type: string + * - + * name: nameId + * description: Application unique name + * in: formData + * type: string + * + * responses: + * 200: + * description: Success + * schema: + * $ref: "#/definitions/Application" + * + * default: + * description: Failed + * schema: + * $ref: "#/definitions/Error" + */ + +/** + * Show an app + */ +module.exports = (params, user, app) => new Promise(async (res, rej) => { + const isSecure = user != null && app == null; + + // Get 'appId' parameter + const [appId, appIdErr] = $(params.appId).optional.id().$; + if (appIdErr) return rej('invalid appId param'); + + // Get 'nameId' parameter + const [nameId, nameIdErr] = $(params.nameId).optional.string().$; + if (nameIdErr) return rej('invalid nameId param'); + + if (appId === undefined && nameId === undefined) { + return rej('appId or nameId is required'); + } + + // Lookup app + const ap = appId !== undefined + ? await App.findOne({ _id: appId }) + : await App.findOne({ nameIdLower: nameId.toLowerCase() }); + + if (ap === null) { + return rej('app not found'); + } + + // Send response + res(await pack(ap, user, { + includeSecret: isSecure && ap.userId.equals(user._id) + })); +}); diff --git a/src/api/endpoints/auth/accept.ts b/src/server/api/endpoints/auth/accept.ts similarity index 82% rename from src/api/endpoints/auth/accept.ts rename to src/server/api/endpoints/auth/accept.ts index 4ee20a6d25..b6297d663d 100644 --- a/src/api/endpoints/auth/accept.ts +++ b/src/server/api/endpoints/auth/accept.ts @@ -4,14 +4,14 @@ import rndstr from 'rndstr'; const crypto = require('crypto'); import $ from 'cafy'; -import App from '../../models/app'; -import AuthSess from '../../models/auth-session'; -import AccessToken from '../../models/access-token'; +import App from '../../../../models/app'; +import AuthSess from '../../../../models/auth-session'; +import AccessToken from '../../../../models/access-token'; /** * @swagger * /auth/accept: - * post: + * note: * summary: Accept a session * parameters: * - $ref: "#/parameters/NativeToken" @@ -56,14 +56,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Fetch exist access token const exist = await AccessToken.findOne({ - app_id: session.app_id, - user_id: user._id, + appId: session.appId, + userId: user._id, }); if (exist === null) { // Lookup app const app = await App.findOne({ - _id: session.app_id + _id: session.appId }); // Generate Hash @@ -73,9 +73,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Insert access token doc await AccessToken.insert({ - created_at: new Date(), - app_id: session.app_id, - user_id: user._id, + createdAt: new Date(), + appId: session.appId, + userId: user._id, token: accessToken, hash: hash }); @@ -84,7 +84,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Update session await AuthSess.update(session._id, { $set: { - user_id: user._id + userId: user._id } }); diff --git a/src/api/endpoints/auth/session/generate.ts b/src/server/api/endpoints/auth/session/generate.ts similarity index 76% rename from src/api/endpoints/auth/session/generate.ts rename to src/server/api/endpoints/auth/session/generate.ts index 510382247e..7c475dbe26 100644 --- a/src/api/endpoints/auth/session/generate.ts +++ b/src/server/api/endpoints/auth/session/generate.ts @@ -3,18 +3,18 @@ */ import * as uuid from 'uuid'; import $ from 'cafy'; -import App from '../../../models/app'; -import AuthSess from '../../../models/auth-session'; -import config from '../../../../conf'; +import App from '../../../../../models/app'; +import AuthSess from '../../../../../models/auth-session'; +import config from '../../../../../config'; /** * @swagger * /auth/session/generate: - * post: + * note: * summary: Generate a session * parameters: * - - * name: app_secret + * name: appSecret * description: App Secret * in: formData * required: true @@ -45,9 +45,9 @@ import config from '../../../../conf'; * @return {Promise<any>} */ module.exports = (params) => new Promise(async (res, rej) => { - // Get 'app_secret' parameter - const [appSecret, appSecretErr] = $(params.app_secret).string().$; - if (appSecretErr) return rej('invalid app_secret param'); + // Get 'appSecret' parameter + const [appSecret, appSecretErr] = $(params.appSecret).string().$; + if (appSecretErr) return rej('invalid appSecret param'); // Lookup app const app = await App.findOne({ @@ -63,8 +63,8 @@ module.exports = (params) => new Promise(async (res, rej) => { // Create session token document const doc = await AuthSess.insert({ - created_at: new Date(), - app_id: app._id, + createdAt: new Date(), + appId: app._id, token: token }); diff --git a/src/api/endpoints/auth/session/show.ts b/src/server/api/endpoints/auth/session/show.ts similarity index 86% rename from src/api/endpoints/auth/session/show.ts rename to src/server/api/endpoints/auth/session/show.ts index ede8a67634..f7f0b087b7 100644 --- a/src/api/endpoints/auth/session/show.ts +++ b/src/server/api/endpoints/auth/session/show.ts @@ -2,13 +2,12 @@ * Module dependencies */ import $ from 'cafy'; -import AuthSess from '../../../models/auth-session'; -import serialize from '../../../serializers/auth-session'; +import AuthSess, { pack } from '../../../../../models/auth-session'; /** * @swagger * /auth/session/show: - * post: + * note: * summary: Show a session information * parameters: * - @@ -24,17 +23,17 @@ import serialize from '../../../serializers/auth-session'; * schema: * type: object * properties: - * created_at: + * createdAt: * type: string * format: date-time * description: Date and time of the session creation - * app_id: + * appId: * type: string * description: Application ID * token: * type: string * description: Session Token - * user_id: + * userId: * type: string * description: ID of user who create the session * app: @@ -67,5 +66,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { } // Response - res(await serialize(session, user)); + res(await pack(session, user)); }); diff --git a/src/api/endpoints/auth/session/userkey.ts b/src/server/api/endpoints/auth/session/userkey.ts similarity index 74% rename from src/api/endpoints/auth/session/userkey.ts rename to src/server/api/endpoints/auth/session/userkey.ts index afd3250b04..ddb67cb451 100644 --- a/src/api/endpoints/auth/session/userkey.ts +++ b/src/server/api/endpoints/auth/session/userkey.ts @@ -2,19 +2,19 @@ * Module dependencies */ import $ from 'cafy'; -import App from '../../../models/app'; -import AuthSess from '../../../models/auth-session'; -import AccessToken from '../../../models/access-token'; -import serialize from '../../../serializers/user'; +import App from '../../../../../models/app'; +import AuthSess from '../../../../../models/auth-session'; +import AccessToken from '../../../../../models/access-token'; +import { pack } from '../../../../../models/user'; /** * @swagger * /auth/session/userkey: - * post: + * note: * summary: Get an access token(userkey) * parameters: * - - * name: app_secret + * name: appSecret * description: App Secret * in: formData * required: true @@ -50,9 +50,9 @@ import serialize from '../../../serializers/user'; * @return {Promise<any>} */ module.exports = (params) => new Promise(async (res, rej) => { - // Get 'app_secret' parameter - const [appSecret, appSecretErr] = $(params.app_secret).string().$; - if (appSecretErr) return rej('invalid app_secret param'); + // Get 'appSecret' parameter + const [appSecret, appSecretErr] = $(params.appSecret).string().$; + if (appSecretErr) return rej('invalid appSecret param'); // Lookup app const app = await App.findOne({ @@ -71,21 +71,21 @@ module.exports = (params) => new Promise(async (res, rej) => { const session = await AuthSess .findOne({ token: token, - app_id: app._id + appId: app._id }); if (session === null) { return rej('session not found'); } - if (session.user_id == null) { + if (session.userId == null) { return rej('this session is not allowed yet'); } // Lookup access token const accessToken = await AccessToken.findOne({ - app_id: app._id, - user_id: session.user_id + appId: app._id, + userId: session.userId }); // Delete session @@ -101,8 +101,8 @@ module.exports = (params) => new Promise(async (res, rej) => { // Response res({ - access_token: accessToken.token, - user: await serialize(session.user_id, null, { + accessToken: accessToken.token, + user: await pack(session.userId, null, { detail: true }) }); diff --git a/src/server/api/endpoints/channels.ts b/src/server/api/endpoints/channels.ts new file mode 100644 index 0000000000..582e6ba43b --- /dev/null +++ b/src/server/api/endpoints/channels.ts @@ -0,0 +1,58 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Channel, { pack } from '../../../models/channel'; + +/** + * Get all channels + * + * @param {any} params + * @param {any} me + * @return {Promise<any>} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); + + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); + + // Check if both of sinceId and untilId is specified + if (sinceId && untilId) { + return rej('cannot set sinceId and untilId'); + } + + // Construct query + const sort = { + _id: -1 + }; + const query = {} as any; + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } + + // Issue query + const channels = await Channel + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(channels.map(async channel => + await pack(channel, me)))); +}); diff --git a/src/server/api/endpoints/channels/create.ts b/src/server/api/endpoints/channels/create.ts new file mode 100644 index 0000000000..0f0f558c8a --- /dev/null +++ b/src/server/api/endpoints/channels/create.ts @@ -0,0 +1,39 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Channel from '../../../../models/channel'; +import Watching from '../../../../models/channel-watching'; +import { pack } from '../../../../models/channel'; + +/** + * Create a channel + * + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'title' parameter + const [title, titleErr] = $(params.title).string().range(1, 100).$; + if (titleErr) return rej('invalid title param'); + + // Create a channel + const channel = await Channel.insert({ + createdAt: new Date(), + userId: user._id, + title: title, + index: 0, + watchingCount: 1 + }); + + // Response + res(await pack(channel)); + + // Create Watching + await Watching.insert({ + createdAt: new Date(), + userId: user._id, + channelId: channel._id + }); +}); diff --git a/src/server/api/endpoints/channels/notes.ts b/src/server/api/endpoints/channels/notes.ts new file mode 100644 index 0000000000..d636aa0d10 --- /dev/null +++ b/src/server/api/endpoints/channels/notes.ts @@ -0,0 +1,78 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import { default as Channel, IChannel } from '../../../../models/channel'; +import Note, { pack } from '../../../../models/note'; + +/** + * Show a notes of a channel + * + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 1000, limitErr] = $(params.limit).optional.number().range(1, 1000).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); + + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); + + // Check if both of sinceId and untilId is specified + if (sinceId && untilId) { + return rej('cannot set sinceId and untilId'); + } + + // Get 'channelId' parameter + const [channelId, channelIdErr] = $(params.channelId).id().$; + if (channelIdErr) return rej('invalid channelId param'); + + // Fetch channel + const channel: IChannel = await Channel.findOne({ + _id: channelId + }); + + if (channel === null) { + return rej('channel not found'); + } + + //#region Construct query + const sort = { + _id: -1 + }; + + const query = { + channelId: channel._id + } as any; + + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } + //#endregion Construct query + + // Issue query + const notes = await Note + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(notes.map(async (note) => + await pack(note, user) + ))); +}); diff --git a/src/server/api/endpoints/channels/show.ts b/src/server/api/endpoints/channels/show.ts new file mode 100644 index 0000000000..3ce9ce4745 --- /dev/null +++ b/src/server/api/endpoints/channels/show.ts @@ -0,0 +1,30 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Channel, { IChannel, pack } from '../../../../models/channel'; + +/** + * Show a channel + * + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'channelId' parameter + const [channelId, channelIdErr] = $(params.channelId).id().$; + if (channelIdErr) return rej('invalid channelId param'); + + // Fetch channel + const channel: IChannel = await Channel.findOne({ + _id: channelId + }); + + if (channel === null) { + return rej('channel not found'); + } + + // Serialize + res(await pack(channel, user)); +}); diff --git a/src/server/api/endpoints/channels/unwatch.ts b/src/server/api/endpoints/channels/unwatch.ts new file mode 100644 index 0000000000..8220b90b68 --- /dev/null +++ b/src/server/api/endpoints/channels/unwatch.ts @@ -0,0 +1,60 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Channel from '../../../../models/channel'; +import Watching from '../../../../models/channel-watching'; + +/** + * Unwatch a channel + * + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'channelId' parameter + const [channelId, channelIdErr] = $(params.channelId).id().$; + if (channelIdErr) return rej('invalid channelId param'); + + //#region Fetch channel + const channel = await Channel.findOne({ + _id: channelId + }); + + if (channel === null) { + return rej('channel not found'); + } + //#endregion + + //#region Check whether not watching + const exist = await Watching.findOne({ + userId: user._id, + channelId: channel._id, + deletedAt: { $exists: false } + }); + + if (exist === null) { + return rej('already not watching'); + } + //#endregion + + // Delete watching + await Watching.update({ + _id: exist._id + }, { + $set: { + deletedAt: new Date() + } + }); + + // Send response + res(); + + // Decrement watching count + Channel.update(channel._id, { + $inc: { + watchingCount: -1 + } + }); +}); diff --git a/src/server/api/endpoints/channels/watch.ts b/src/server/api/endpoints/channels/watch.ts new file mode 100644 index 0000000000..6906282a54 --- /dev/null +++ b/src/server/api/endpoints/channels/watch.ts @@ -0,0 +1,58 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Channel from '../../../../models/channel'; +import Watching from '../../../../models/channel-watching'; + +/** + * Watch a channel + * + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'channelId' parameter + const [channelId, channelIdErr] = $(params.channelId).id().$; + if (channelIdErr) return rej('invalid channelId param'); + + //#region Fetch channel + const channel = await Channel.findOne({ + _id: channelId + }); + + if (channel === null) { + return rej('channel not found'); + } + //#endregion + + //#region Check whether already watching + const exist = await Watching.findOne({ + userId: user._id, + channelId: channel._id, + deletedAt: { $exists: false } + }); + + if (exist !== null) { + return rej('already watching'); + } + //#endregion + + // Create Watching + await Watching.insert({ + createdAt: new Date(), + userId: user._id, + channelId: channel._id + }); + + // Send response + res(); + + // Increment watching count + Channel.update(channel._id, { + $inc: { + watchingCount: 1 + } + }); +}); diff --git a/src/api/endpoints/drive.ts b/src/server/api/endpoints/drive.ts similarity index 70% rename from src/api/endpoints/drive.ts rename to src/server/api/endpoints/drive.ts index 41ad6301d7..d77ab2bbb0 100644 --- a/src/api/endpoints/drive.ts +++ b/src/server/api/endpoints/drive.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import DriveFile from '../models/drive-file'; +import DriveFile from '../../../models/drive-file'; /** * Get drive information @@ -14,16 +14,16 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Calculate drive usage const usage = ((await DriveFile .aggregate([ - { $match: { user_id: user._id } }, + { $match: { 'metadata.userId': user._id } }, { $project: { - datasize: true + length: true } }, { $group: { _id: null, - usage: { $sum: '$datasize' } + usage: { $sum: '$length' } } } ]))[0] || { @@ -31,7 +31,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }).usage; res({ - capacity: user.drive_capacity, + capacity: user.driveCapacity, usage: usage }); }); diff --git a/src/server/api/endpoints/drive/files.ts b/src/server/api/endpoints/drive/files.ts new file mode 100644 index 0000000000..63d69d145a --- /dev/null +++ b/src/server/api/endpoints/drive/files.ts @@ -0,0 +1,73 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import DriveFile, { pack } from '../../../../models/drive-file'; + +/** + * Get drive files + * + * @param {any} params + * @param {any} user + * @param {any} app + * @return {Promise<any>} + */ +module.exports = async (params, user, app) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) throw 'invalid limit param'; + + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) throw 'invalid sinceId param'; + + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) throw 'invalid untilId param'; + + // Check if both of sinceId and untilId is specified + if (sinceId && untilId) { + throw 'cannot set sinceId and untilId'; + } + + // Get 'folderId' parameter + const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$; + if (folderIdErr) throw 'invalid folderId param'; + + // Get 'type' parameter + const [type, typeErr] = $(params.type).optional.string().match(/^[a-zA-Z\/\-\*]+$/).$; + if (typeErr) throw 'invalid type param'; + + // Construct query + const sort = { + _id: -1 + }; + const query = { + 'metadata.userId': user._id, + 'metadata.folderId': folderId + } as any; + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } + if (type) { + query.contentType = new RegExp(`^${type.replace(/\*/g, '.+?')}$`); + } + + // Issue query + const files = await DriveFile + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + const _files = await Promise.all(files.map(file => pack(file))); + return _files; +}; diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts new file mode 100644 index 0000000000..df0bd0a0d3 --- /dev/null +++ b/src/server/api/endpoints/drive/files/create.ts @@ -0,0 +1,51 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import { validateFileName, pack } from '../../../../../models/drive-file'; +import create from '../../../../../services/drive/add-file'; + +/** + * Create a file + * + * @param {any} file + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = async (file, params, user): Promise<any> => { + if (file == null) { + throw 'file is required'; + } + + // Get 'name' parameter + let name = file.originalname; + if (name !== undefined && name !== null) { + name = name.trim(); + if (name.length === 0) { + name = null; + } else if (name === 'blob') { + name = null; + } else if (!validateFileName(name)) { + throw 'invalid name'; + } + } else { + name = null; + } + + // Get 'folderId' parameter + const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$; + if (folderIdErr) throw 'invalid folderId param'; + + try { + // Create file + const driveFile = await create(user, file.path, name, null, folderId); + + // Serialize + return pack(driveFile); + } catch (e) { + console.error(e); + + throw e; + } +}; diff --git a/src/api/endpoints/drive/files/find.ts b/src/server/api/endpoints/drive/files/find.ts similarity index 53% rename from src/api/endpoints/drive/files/find.ts rename to src/server/api/endpoints/drive/files/find.ts index cd0b33f2ca..0ab6e5d3e3 100644 --- a/src/api/endpoints/drive/files/find.ts +++ b/src/server/api/endpoints/drive/files/find.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFile from '../../../models/drive-file'; -import serialize from '../../../serializers/drive-file'; +import DriveFile, { pack } from '../../../../../models/drive-file'; /** * Find a file(s) @@ -17,23 +16,19 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [name, nameErr] = $(params.name).string().$; if (nameErr) return rej('invalid name param'); - // Get 'folder_id' parameter - const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; - if (folderIdErr) return rej('invalid folder_id param'); + // Get 'folderId' parameter + const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$; + if (folderIdErr) return rej('invalid folderId param'); // Issue query const files = await DriveFile .find({ - name: name, - user_id: user._id, - folder_id: folderId - }, { - fields: { - data: false - } + filename: name, + 'metadata.userId': user._id, + 'metadata.folderId': folderId }); // Serialize res(await Promise.all(files.map(async file => - await serialize(file)))); + await pack(file)))); }); diff --git a/src/server/api/endpoints/drive/files/show.ts b/src/server/api/endpoints/drive/files/show.ts new file mode 100644 index 0000000000..3398f24541 --- /dev/null +++ b/src/server/api/endpoints/drive/files/show.ts @@ -0,0 +1,36 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import DriveFile, { pack } from '../../../../../models/drive-file'; + +/** + * Show a file + * + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = async (params, user) => { + // Get 'fileId' parameter + const [fileId, fileIdErr] = $(params.fileId).id().$; + if (fileIdErr) throw 'invalid fileId param'; + + // Fetch file + const file = await DriveFile + .findOne({ + _id: fileId, + 'metadata.userId': user._id + }); + + if (file === null) { + throw 'file-not-found'; + } + + // Serialize + const _file = await pack(file, { + detail: true + }); + + return _file; +}; diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts new file mode 100644 index 0000000000..c783ad8b3b --- /dev/null +++ b/src/server/api/endpoints/drive/files/update.ts @@ -0,0 +1,75 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import DriveFolder from '../../../../../models/drive-folder'; +import DriveFile, { validateFileName, pack } from '../../../../../models/drive-file'; +import { publishDriveStream } from '../../../../../publishers/stream'; + +/** + * Update a file + * + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'fileId' parameter + const [fileId, fileIdErr] = $(params.fileId).id().$; + if (fileIdErr) return rej('invalid fileId param'); + + // Fetch file + const file = await DriveFile + .findOne({ + _id: fileId, + 'metadata.userId': user._id + }); + + if (file === null) { + return rej('file-not-found'); + } + + // Get 'name' parameter + const [name, nameErr] = $(params.name).optional.string().pipe(validateFileName).$; + if (nameErr) return rej('invalid name param'); + if (name) file.filename = name; + + // Get 'folderId' parameter + const [folderId, folderIdErr] = $(params.folderId).optional.nullable.id().$; + if (folderIdErr) return rej('invalid folderId param'); + + if (folderId !== undefined) { + if (folderId === null) { + file.metadata.folderId = null; + } else { + // Fetch folder + const folder = await DriveFolder + .findOne({ + _id: folderId, + userId: user._id + }); + + if (folder === null) { + return rej('folder-not-found'); + } + + file.metadata.folderId = folder._id; + } + } + + await DriveFile.update(file._id, { + $set: { + filename: file.filename, + 'metadata.folderId': file.metadata.folderId + } + }); + + // Serialize + const fileObj = await pack(file); + + // Response + res(fileObj); + + // Publish file_updated event + publishDriveStream(user._id, 'file_updated', fileObj); +}); diff --git a/src/server/api/endpoints/drive/files/upload_from_url.ts b/src/server/api/endpoints/drive/files/upload_from_url.ts new file mode 100644 index 0000000000..8a426c0efc --- /dev/null +++ b/src/server/api/endpoints/drive/files/upload_from_url.ts @@ -0,0 +1,22 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import { pack } from '../../../../../models/drive-file'; +import uploadFromUrl from '../../../../../services/drive/upload-from-url'; + +/** + * Create a file from a URL + */ +module.exports = async (params, user): Promise<any> => { + // Get 'url' parameter + // TODO: Validate this url + const [url, urlErr] = $(params.url).string().$; + if (urlErr) throw 'invalid url param'; + + // Get 'folderId' parameter + const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$; + if (folderIdErr) throw 'invalid folderId param'; + + return pack(await uploadFromUrl(url, user, folderId)); +}; diff --git a/src/server/api/endpoints/drive/folders.ts b/src/server/api/endpoints/drive/folders.ts new file mode 100644 index 0000000000..489e47912e --- /dev/null +++ b/src/server/api/endpoints/drive/folders.ts @@ -0,0 +1,66 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import DriveFolder, { pack } from '../../../../models/drive-folder'; + +/** + * Get drive folders + * + * @param {any} params + * @param {any} user + * @param {any} app + * @return {Promise<any>} + */ +module.exports = (params, user, app) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); + + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); + + // Check if both of sinceId and untilId is specified + if (sinceId && untilId) { + return rej('cannot set sinceId and untilId'); + } + + // Get 'folderId' parameter + const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$; + if (folderIdErr) return rej('invalid folderId param'); + + // Construct query + const sort = { + _id: -1 + }; + const query = { + userId: user._id, + parentId: folderId + } as any; + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } + + // Issue query + const folders = await DriveFolder + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(folders.map(async folder => + await pack(folder)))); +}); diff --git a/src/api/endpoints/drive/folders/create.ts b/src/server/api/endpoints/drive/folders/create.ts similarity index 54% rename from src/api/endpoints/drive/folders/create.ts rename to src/server/api/endpoints/drive/folders/create.ts index 8c875db164..f34d0019d7 100644 --- a/src/api/endpoints/drive/folders/create.ts +++ b/src/server/api/endpoints/drive/folders/create.ts @@ -2,10 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFolder from '../../../models/drive-folder'; -import { isValidFolderName } from '../../../models/drive-folder'; -import serialize from '../../../serializers/drive-folder'; -import event from '../../../event'; +import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder'; +import { publishDriveStream } from '../../../../../publishers/stream'; /** * Create drive folder @@ -19,9 +17,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [name = '無題のフォルダー', nameErr] = $(params.name).optional.string().pipe(isValidFolderName).$; if (nameErr) return rej('invalid name param'); - // Get 'parent_id' parameter - const [parentId = null, parentIdErr] = $(params.parent_id).optional.nullable.id().$; - if (parentIdErr) return rej('invalid parent_id param'); + // Get 'parentId' parameter + const [parentId = null, parentIdErr] = $(params.parentId).optional.nullable.id().$; + if (parentIdErr) return rej('invalid parentId param'); // If the parent folder is specified let parent = null; @@ -30,7 +28,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { parent = await DriveFolder .findOne({ _id: parentId, - user_id: user._id + userId: user._id }); if (parent === null) { @@ -40,18 +38,18 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Create folder const folder = await DriveFolder.insert({ - created_at: new Date(), + createdAt: new Date(), name: name, - parent_id: parent !== null ? parent._id : null, - user_id: user._id + parentId: parent !== null ? parent._id : null, + userId: user._id }); // Serialize - const folderObj = await serialize(folder); + const folderObj = await pack(folder); // Response res(folderObj); - // Publish drive_folder_created event - event(user._id, 'drive_folder_created', folderObj); + // Publish folder_created event + publishDriveStream(user._id, 'folder_created', folderObj); }); diff --git a/src/api/endpoints/drive/folders/find.ts b/src/server/api/endpoints/drive/folders/find.ts similarity index 52% rename from src/api/endpoints/drive/folders/find.ts rename to src/server/api/endpoints/drive/folders/find.ts index cdf055839a..04dc38f87f 100644 --- a/src/api/endpoints/drive/folders/find.ts +++ b/src/server/api/endpoints/drive/folders/find.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFolder from '../../../models/drive-folder'; -import serialize from '../../../serializers/drive-folder'; +import DriveFolder, { pack } from '../../../../../models/drive-folder'; /** * Find a folder(s) @@ -17,19 +16,18 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [name, nameErr] = $(params.name).string().$; if (nameErr) return rej('invalid name param'); - // Get 'parent_id' parameter - const [parentId = null, parentIdErr] = $(params.parent_id).optional.nullable.id().$; - if (parentIdErr) return rej('invalid parent_id param'); + // Get 'parentId' parameter + const [parentId = null, parentIdErr] = $(params.parentId).optional.nullable.id().$; + if (parentIdErr) return rej('invalid parentId param'); // Issue query const folders = await DriveFolder .find({ name: name, - user_id: user._id, - parent_id: parentId + userId: user._id, + parentId: parentId }); // Serialize - res(await Promise.all(folders.map(async folder => - await serialize(folder)))); + res(await Promise.all(folders.map(folder => pack(folder)))); }); diff --git a/src/api/endpoints/drive/folders/show.ts b/src/server/api/endpoints/drive/folders/show.ts similarity index 56% rename from src/api/endpoints/drive/folders/show.ts rename to src/server/api/endpoints/drive/folders/show.ts index 9b1c04ca3c..b432f5a50a 100644 --- a/src/api/endpoints/drive/folders/show.ts +++ b/src/server/api/endpoints/drive/folders/show.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFolder from '../../../models/drive-folder'; -import serialize from '../../../serializers/drive-folder'; +import DriveFolder, { pack } from '../../../../../models/drive-folder'; /** * Show a folder @@ -13,15 +12,15 @@ import serialize from '../../../serializers/drive-folder'; * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'folder_id' parameter - const [folderId, folderIdErr] = $(params.folder_id).id().$; - if (folderIdErr) return rej('invalid folder_id param'); + // Get 'folderId' parameter + const [folderId, folderIdErr] = $(params.folderId).id().$; + if (folderIdErr) return rej('invalid folderId param'); // Get folder const folder = await DriveFolder .findOne({ _id: folderId, - user_id: user._id + userId: user._id }); if (folder === null) { @@ -29,7 +28,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { } // Serialize - res(await serialize(folder, { + res(await pack(folder, { detail: true })); }); diff --git a/src/api/endpoints/drive/folders/update.ts b/src/server/api/endpoints/drive/folders/update.ts similarity index 57% rename from src/api/endpoints/drive/folders/update.ts rename to src/server/api/endpoints/drive/folders/update.ts index eec2757878..dd7e8f5c86 100644 --- a/src/api/endpoints/drive/folders/update.ts +++ b/src/server/api/endpoints/drive/folders/update.ts @@ -2,10 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFolder from '../../../models/drive-folder'; -import { isValidFolderName } from '../../../models/drive-folder'; -import serialize from '../../../serializers/drive-file'; -import event from '../../../event'; +import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder'; +import { publishDriveStream } from '../../../../../publishers/stream'; /** * Update a folder @@ -15,15 +13,15 @@ import event from '../../../event'; * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'folder_id' parameter - const [folderId, folderIdErr] = $(params.folder_id).id().$; - if (folderIdErr) return rej('invalid folder_id param'); + // Get 'folderId' parameter + const [folderId, folderIdErr] = $(params.folderId).id().$; + if (folderIdErr) return rej('invalid folderId param'); // Fetch folder const folder = await DriveFolder .findOne({ _id: folderId, - user_id: user._id + userId: user._id }); if (folder === null) { @@ -35,18 +33,18 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (nameErr) return rej('invalid name param'); if (name) folder.name = name; - // Get 'parent_id' parameter - const [parentId, parentIdErr] = $(params.parent_id).optional.nullable.id().$; - if (parentIdErr) return rej('invalid parent_id param'); + // Get 'parentId' parameter + const [parentId, parentIdErr] = $(params.parentId).optional.nullable.id().$; + if (parentIdErr) return rej('invalid parentId param'); if (parentId !== undefined) { if (parentId === null) { - folder.parent_id = null; + folder.parentId = null; } else { // Get parent folder const parent = await DriveFolder .findOne({ _id: parentId, - user_id: user._id + userId: user._id }); if (parent === null) { @@ -60,25 +58,25 @@ module.exports = (params, user) => new Promise(async (res, rej) => { _id: folderId }, { _id: true, - parent_id: true + parentId: true }); if (folder2._id.equals(folder._id)) { return true; - } else if (folder2.parent_id) { - return await checkCircle(folder2.parent_id); + } else if (folder2.parentId) { + return await checkCircle(folder2.parentId); } else { return false; } } - if (parent.parent_id !== null) { - if (await checkCircle(parent.parent_id)) { + if (parent.parentId !== null) { + if (await checkCircle(parent.parentId)) { return rej('detected-circular-definition'); } } - folder.parent_id = parent._id; + folder.parentId = parent._id; } } @@ -86,16 +84,16 @@ module.exports = (params, user) => new Promise(async (res, rej) => { DriveFolder.update(folder._id, { $set: { name: folder.name, - parent_id: folder.parent_id + parentId: folder.parentId } }); // Serialize - const folderObj = await serialize(folder); + const folderObj = await pack(folder); // Response res(folderObj); - // Publish drive_folder_updated event - event(user._id, 'drive_folder_updated', folderObj); + // Publish folder_updated event + publishDriveStream(user._id, 'folder_updated', folderObj); }); diff --git a/src/api/endpoints/drive/stream.ts b/src/server/api/endpoints/drive/stream.ts similarity index 56% rename from src/api/endpoints/drive/stream.ts rename to src/server/api/endpoints/drive/stream.ts index 32f7ac7e0a..02313aa37b 100644 --- a/src/api/endpoints/drive/stream.ts +++ b/src/server/api/endpoints/drive/stream.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFile from '../../models/drive-file'; -import serialize from '../../serializers/drive-file'; +import DriveFile, { pack } from '../../../../models/drive-file'; /** * Get drive stream @@ -17,17 +16,17 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; if (limitErr) return rej('invalid limit param'); - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); - // Check if both of since_id and max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); + // Check if both of sinceId and untilId is specified + if (sinceId && untilId) { + return rej('cannot set sinceId and untilId'); } // Get 'type' parameter @@ -39,33 +38,30 @@ module.exports = (params, user) => new Promise(async (res, rej) => { _id: -1 }; const query = { - user_id: user._id + 'metadata.userId': user._id } as any; if (sinceId) { sort._id = 1; query._id = { $gt: sinceId }; - } else if (maxId) { + } else if (untilId) { query._id = { - $lt: maxId + $lt: untilId }; } if (type) { - query.type = new RegExp(`^${type.replace(/\*/g, '.+?')}$`); + query.contentType = new RegExp(`^${type.replace(/\*/g, '.+?')}$`); } // Issue query const files = await DriveFile .find(query, { - fields: { - data: false - }, limit: limit, sort: sort }); // Serialize res(await Promise.all(files.map(async file => - await serialize(file)))); + await pack(file)))); }); diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts new file mode 100644 index 0000000000..27e5eb31db --- /dev/null +++ b/src/server/api/endpoints/following/create.ts @@ -0,0 +1,53 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../../../models/user'; +import Following from '../../../../models/following'; +import create from '../../../../services/following/create'; + +/** + * Follow a user + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + const follower = user; + + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); + + // 自分自身 + if (user._id.equals(userId)) { + return rej('followee is yourself'); + } + + // Get followee + const followee = await User.findOne({ + _id: userId + }, { + fields: { + data: false, + profile: false + } + }); + + if (followee === null) { + return rej('user not found'); + } + + // Check if already following + const exist = await Following.findOne({ + followerId: follower._id, + followeeId: followee._id + }); + + if (exist !== null) { + return rej('already following'); + } + + // Create following + create(follower, followee); + + // Send response + res(); +}); diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts new file mode 100644 index 0000000000..ca0703ca22 --- /dev/null +++ b/src/server/api/endpoints/following/delete.ts @@ -0,0 +1,53 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../../../models/user'; +import Following from '../../../../models/following'; +import deleteFollowing from '../../../../services/following/delete'; + +/** + * Unfollow a user + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + const follower = user; + + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); + + // Check if the followee is yourself + if (user._id.equals(userId)) { + return rej('followee is yourself'); + } + + // Get followee + const followee = await User.findOne({ + _id: userId + }, { + fields: { + data: false, + 'profile': false + } + }); + + if (followee === null) { + return rej('user not found'); + } + + // Check not following + const exist = await Following.findOne({ + followerId: follower._id, + followeeId: followee._id + }); + + if (exist === null) { + return rej('already not following'); + } + + // Delete following + deleteFollowing(follower, followee); + + // Send response + res(); +}); diff --git a/src/server/api/endpoints/i.ts b/src/server/api/endpoints/i.ts new file mode 100644 index 0000000000..379c3c4d88 --- /dev/null +++ b/src/server/api/endpoints/i.ts @@ -0,0 +1,24 @@ +/** + * Module dependencies + */ +import User, { pack } from '../../../models/user'; + +/** + * Show myself + */ +module.exports = (params, user, app) => new Promise(async (res, rej) => { + const isSecure = user != null && app == null; + + // Serialize + res(await pack(user, user, { + detail: true, + includeSecrets: isSecure + })); + + // Update lastUsedAt + User.update({ _id: user._id }, { + $set: { + lastUsedAt: new Date() + } + }); +}); diff --git a/src/server/api/endpoints/i/2fa/done.ts b/src/server/api/endpoints/i/2fa/done.ts new file mode 100644 index 0000000000..3e824feffd --- /dev/null +++ b/src/server/api/endpoints/i/2fa/done.ts @@ -0,0 +1,37 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import * as speakeasy from 'speakeasy'; +import User from '../../../../../models/user'; + +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'token' parameter + const [token, tokenErr] = $(params.token).string().$; + if (tokenErr) return rej('invalid token param'); + + const _token = token.replace(/\s/g, ''); + + if (user.twoFactorTempSecret == null) { + return rej('二段階認証の設定が開始されていません'); + } + + const verified = (speakeasy as any).totp.verify({ + secret: user.twoFactorTempSecret, + encoding: 'base32', + token: _token + }); + + if (!verified) { + return rej('not verified'); + } + + await User.update(user._id, { + $set: { + 'twoFactorSecret': user.twoFactorTempSecret, + 'twoFactorEnabled': true + } + }); + + res(); +}); diff --git a/src/server/api/endpoints/i/2fa/register.ts b/src/server/api/endpoints/i/2fa/register.ts new file mode 100644 index 0000000000..bed64a2545 --- /dev/null +++ b/src/server/api/endpoints/i/2fa/register.ts @@ -0,0 +1,48 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import * as bcrypt from 'bcryptjs'; +import * as speakeasy from 'speakeasy'; +import * as QRCode from 'qrcode'; +import User from '../../../../../models/user'; +import config from '../../../../../config'; + +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'password' parameter + const [password, passwordErr] = $(params.password).string().$; + if (passwordErr) return rej('invalid password param'); + + // Compare password + const same = await bcrypt.compare(password, user.password); + + if (!same) { + return rej('incorrect password'); + } + + // Generate user's secret key + const secret = speakeasy.generateSecret({ + length: 32 + }); + + await User.update(user._id, { + $set: { + twoFactorTempSecret: secret.base32 + } + }); + + // Get the data URL of the authenticator URL + QRCode.toDataURL(speakeasy.otpauthURL({ + secret: secret.base32, + encoding: 'base32', + label: user.username, + issuer: config.host + }), (err, data_url) => { + res({ + qr: data_url, + secret: secret.base32, + label: user.username, + issuer: config.host + }); + }); +}); diff --git a/src/server/api/endpoints/i/2fa/unregister.ts b/src/server/api/endpoints/i/2fa/unregister.ts new file mode 100644 index 0000000000..f9d7a25f53 --- /dev/null +++ b/src/server/api/endpoints/i/2fa/unregister.ts @@ -0,0 +1,28 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import * as bcrypt from 'bcryptjs'; +import User from '../../../../../models/user'; + +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'password' parameter + const [password, passwordErr] = $(params.password).string().$; + if (passwordErr) return rej('invalid password param'); + + // Compare password + const same = await bcrypt.compare(password, user.password); + + if (!same) { + return rej('incorrect password'); + } + + await User.update(user._id, { + $set: { + 'twoFactorSecret': null, + 'twoFactorEnabled': false + } + }); + + res(); +}); diff --git a/src/api/endpoints/i/authorized_apps.ts b/src/server/api/endpoints/i/authorized_apps.ts similarity index 85% rename from src/api/endpoints/i/authorized_apps.ts rename to src/server/api/endpoints/i/authorized_apps.ts index 807ca5b1e7..82fd2d2516 100644 --- a/src/api/endpoints/i/authorized_apps.ts +++ b/src/server/api/endpoints/i/authorized_apps.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import AccessToken from '../../models/access-token'; -import serialize from '../../serializers/app'; +import AccessToken from '../../../../models/access-token'; +import { pack } from '../../../../models/app'; /** * Get authorized apps of my account @@ -28,7 +28,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Get tokens const tokens = await AccessToken .find({ - user_id: user._id + userId: user._id }, { limit: limit, skip: offset, @@ -39,5 +39,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(tokens.map(async token => - await serialize(token.app_id)))); + await pack(token.appId)))); }); diff --git a/src/server/api/endpoints/i/change_password.ts b/src/server/api/endpoints/i/change_password.ts new file mode 100644 index 0000000000..57415083f1 --- /dev/null +++ b/src/server/api/endpoints/i/change_password.ts @@ -0,0 +1,42 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import * as bcrypt from 'bcryptjs'; +import User from '../../../../models/user'; + +/** + * Change password + * + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'currentPasword' parameter + const [currentPassword, currentPasswordErr] = $(params.currentPasword).string().$; + if (currentPasswordErr) return rej('invalid currentPasword param'); + + // Get 'newPassword' parameter + const [newPassword, newPasswordErr] = $(params.newPassword).string().$; + if (newPasswordErr) return rej('invalid newPassword param'); + + // Compare password + const same = await bcrypt.compare(currentPassword, user.password); + + if (!same) { + return rej('incorrect password'); + } + + // Generate hash of password + const salt = await bcrypt.genSalt(8); + const hash = await bcrypt.hash(newPassword, salt); + + await User.update(user._id, { + $set: { + 'password': hash + } + }); + + res(); +}); diff --git a/src/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts similarity index 86% rename from src/api/endpoints/i/favorites.ts rename to src/server/api/endpoints/i/favorites.ts index a66eaa7546..b40f2b3887 100644 --- a/src/api/endpoints/i/favorites.ts +++ b/src/server/api/endpoints/i/favorites.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import Favorite from '../../models/favorite'; -import serialize from '../../serializers/post'; +import Favorite from '../../../../models/favorite'; +import { pack } from '../../../../models/note'; /** * Get followers of a user @@ -28,7 +28,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Get favorites const favorites = await Favorite .find({ - user_id: user._id + userId: user._id }, { limit: limit, skip: offset, @@ -39,6 +39,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(favorites.map(async favorite => - await serialize(favorite.post) + await pack(favorite.noteId) ))); }); diff --git a/src/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts similarity index 50% rename from src/api/endpoints/i/notifications.ts rename to src/server/api/endpoints/i/notifications.ts index 5575fb7412..3b4899682d 100644 --- a/src/api/endpoints/i/notifications.ts +++ b/src/server/api/endpoints/i/notifications.ts @@ -2,16 +2,14 @@ * Module dependencies */ import $ from 'cafy'; -import Notification from '../../models/notification'; -import serialize from '../../serializers/notification'; +import Notification from '../../../../models/notification'; +import Mute from '../../../../models/mute'; +import { pack } from '../../../../models/notification'; import getFriends from '../../common/get-friends'; +import read from '../../common/read-notification'; /** * Get notifications - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'following' parameter @@ -19,9 +17,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { $(params.following).optional.boolean().$; if (followingError) return rej('invalid following param'); - // Get 'mark_as_read' parameter - const [markAsRead = true, markAsReadErr] = $(params.mark_as_read).optional.boolean().$; - if (markAsReadErr) return rej('invalid mark_as_read param'); + // Get 'markAsRead' parameter + const [markAsRead = true, markAsReadErr] = $(params.markAsRead).optional.boolean().$; + if (markAsReadErr) return rej('invalid markAsRead param'); // Get 'type' parameter const [type, typeErr] = $(params.type).optional.array('string').unique().$; @@ -31,21 +29,31 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; if (limitErr) return rej('invalid limit param'); - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); - // Check if both of since_id and max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); + // Check if both of sinceId and untilId is specified + if (sinceId && untilId) { + return rej('cannot set sinceId and untilId'); } + const mute = await Mute.find({ + muterId: user._id, + deletedAt: { $exists: false } + }); + const query = { - notifiee_id: user._id + notifieeId: user._id, + $and: [{ + notifierId: { + $nin: mute.map(m => m.muteeId) + } + }] } as any; const sort = { @@ -53,12 +61,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }; if (following) { - // ID list of the user $self and other users who the user follows + // ID list of the user itself and other users who the user follows const followingIds = await getFriends(user._id); - query.notifier_id = { - $in: followingIds - }; + query.$and.push({ + notifierId: { + $in: followingIds + } + }); } if (type) { @@ -72,9 +82,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { query._id = { $gt: sinceId }; - } else if (maxId) { + } else if (untilId) { query._id = { - $lt: maxId + $lt: untilId }; } @@ -87,21 +97,10 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(notifications.map(async notification => - await serialize(notification)))); + await pack(notification)))); // Mark as read all if (notifications.length > 0 && markAsRead) { - const ids = notifications - .filter(x => x.is_read == false) - .map(x => x._id); - - // Update documents - await Notification.update({ - _id: { $in: ids } - }, { - $set: { is_read: true } - }, { - multi: true - }); + read(user._id, notifications); } }); diff --git a/src/server/api/endpoints/i/pin.ts b/src/server/api/endpoints/i/pin.ts new file mode 100644 index 0000000000..909a6fdbde --- /dev/null +++ b/src/server/api/endpoints/i/pin.ts @@ -0,0 +1,44 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../../../models/user'; +import Note from '../../../../models/note'; +import { pack } from '../../../../models/user'; + +/** + * Pin note + * + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); + + // Fetch pinee + const note = await Note.findOne({ + _id: noteId, + userId: user._id + }); + + if (note === null) { + return rej('note not found'); + } + + await User.update(user._id, { + $set: { + pinnedNoteId: note._id + } + }); + + // Serialize + const iObj = await pack(user, user, { + detail: true + }); + + // Send response + res(iObj); +}); diff --git a/src/server/api/endpoints/i/regenerate_token.ts b/src/server/api/endpoints/i/regenerate_token.ts new file mode 100644 index 0000000000..f9e92c1797 --- /dev/null +++ b/src/server/api/endpoints/i/regenerate_token.ts @@ -0,0 +1,42 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import * as bcrypt from 'bcryptjs'; +import User from '../../../../models/user'; +import event from '../../../../publishers/stream'; +import generateUserToken from '../../common/generate-native-user-token'; + +/** + * Regenerate native token + * + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'password' parameter + const [password, passwordErr] = $(params.password).string().$; + if (passwordErr) return rej('invalid password param'); + + // Compare password + const same = await bcrypt.compare(password, user.password); + + if (!same) { + return rej('incorrect password'); + } + + // Generate secret + const secret = generateUserToken(); + + await User.update(user._id, { + $set: { + 'token': secret + } + }); + + res(); + + // Publish event + event(user._id, 'my_token_regenerated'); +}); diff --git a/src/api/endpoints/i/signin_history.ts b/src/server/api/endpoints/i/signin_history.ts similarity index 55% rename from src/api/endpoints/i/signin_history.ts rename to src/server/api/endpoints/i/signin_history.ts index 1a6e50c7c8..931b9e2252 100644 --- a/src/api/endpoints/i/signin_history.ts +++ b/src/server/api/endpoints/i/signin_history.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import Signin from '../../models/signin'; -import serialize from '../../serializers/signin'; +import Signin, { pack } from '../../../../models/signin'; /** * Get signin history of my account @@ -17,21 +16,21 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; if (limitErr) return rej('invalid limit param'); - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); - // Check if both of since_id and max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); + // Check if both of sinceId and untilId is specified + if (sinceId && untilId) { + return rej('cannot set sinceId and untilId'); } const query = { - user_id: user._id + userId: user._id } as any; const sort = { @@ -43,9 +42,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { query._id = { $gt: sinceId }; - } else if (maxId) { + } else if (untilId) { query._id = { - $lt: maxId + $lt: untilId }; } @@ -58,5 +57,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(history.map(async record => - await serialize(record)))); + await pack(record)))); }); diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts new file mode 100644 index 0000000000..f3c9d777b5 --- /dev/null +++ b/src/server/api/endpoints/i/update.ts @@ -0,0 +1,77 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../../../models/user'; +import event from '../../../../publishers/stream'; + +/** + * Update myself + */ +module.exports = async (params, user, app) => new Promise(async (res, rej) => { + const isSecure = user != null && app == null; + + // Get 'name' parameter + const [name, nameErr] = $(params.name).optional.nullable.string().pipe(isValidName).$; + if (nameErr) return rej('invalid name param'); + if (name) user.name = name; + + // Get 'description' parameter + const [description, descriptionErr] = $(params.description).optional.nullable.string().pipe(isValidDescription).$; + if (descriptionErr) return rej('invalid description param'); + if (description !== undefined) user.description = description; + + // Get 'location' parameter + const [location, locationErr] = $(params.location).optional.nullable.string().pipe(isValidLocation).$; + if (locationErr) return rej('invalid location param'); + if (location !== undefined) user.profile.location = location; + + // Get 'birthday' parameter + const [birthday, birthdayErr] = $(params.birthday).optional.nullable.string().pipe(isValidBirthday).$; + if (birthdayErr) return rej('invalid birthday param'); + if (birthday !== undefined) user.profile.birthday = birthday; + + // Get 'avatarId' parameter + const [avatarId, avatarIdErr] = $(params.avatarId).optional.id().$; + if (avatarIdErr) return rej('invalid avatarId param'); + if (avatarId) user.avatarId = avatarId; + + // Get 'bannerId' parameter + const [bannerId, bannerIdErr] = $(params.bannerId).optional.id().$; + if (bannerIdErr) return rej('invalid bannerId param'); + if (bannerId) user.bannerId = bannerId; + + // Get 'isBot' parameter + const [isBot, isBotErr] = $(params.isBot).optional.boolean().$; + if (isBotErr) return rej('invalid isBot param'); + if (isBot != null) user.isBot = isBot; + + // Get 'autoWatch' parameter + const [autoWatch, autoWatchErr] = $(params.autoWatch).optional.boolean().$; + if (autoWatchErr) return rej('invalid autoWatch param'); + if (autoWatch != null) user.settings.autoWatch = autoWatch; + + await User.update(user._id, { + $set: { + name: user.name, + description: user.description, + avatarId: user.avatarId, + bannerId: user.bannerId, + profile: user.profile, + isBot: user.isBot, + settings: user.settings + } + }); + + // Serialize + const iObj = await pack(user, user, { + detail: true, + includeSecrets: isSecure + }); + + // Send response + res(iObj); + + // Publish i updated event + event(user._id, 'i_updated', iObj); +}); diff --git a/src/server/api/endpoints/i/update_client_setting.ts b/src/server/api/endpoints/i/update_client_setting.ts new file mode 100644 index 0000000000..b0d5db5ec2 --- /dev/null +++ b/src/server/api/endpoints/i/update_client_setting.ts @@ -0,0 +1,43 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User, { pack } from '../../../../models/user'; +import event from '../../../../publishers/stream'; + +/** + * Update myself + * + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'name' parameter + const [name, nameErr] = $(params.name).string().$; + if (nameErr) return rej('invalid name param'); + + // Get 'value' parameter + const [value, valueErr] = $(params.value).nullable.any().$; + if (valueErr) return rej('invalid value param'); + + const x = {}; + x[`clientSettings.${name}`] = value; + + await User.update(user._id, { + $set: x + }); + + // Serialize + user.clientSettings[name] = value; + const iObj = await pack(user, user, { + detail: true, + includeSecrets: true + }); + + // Send response + res(iObj); + + // Publish i updated event + event(user._id, 'i_updated', iObj); +}); diff --git a/src/server/api/endpoints/i/update_home.ts b/src/server/api/endpoints/i/update_home.ts new file mode 100644 index 0000000000..ce7661ede0 --- /dev/null +++ b/src/server/api/endpoints/i/update_home.ts @@ -0,0 +1,60 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../../../models/user'; +import event from '../../../../publishers/stream'; + +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'home' parameter + const [home, homeErr] = $(params.home).optional.array().each( + $().strict.object() + .have('name', $().string()) + .have('id', $().string()) + .have('place', $().string()) + .have('data', $().object())).$; + if (homeErr) return rej('invalid home param'); + + // Get 'id' parameter + const [id, idErr] = $(params.id).optional.string().$; + if (idErr) return rej('invalid id param'); + + // Get 'data' parameter + const [data, dataErr] = $(params.data).optional.object().$; + if (dataErr) return rej('invalid data param'); + + if (home) { + await User.update(user._id, { + $set: { + 'clientSettings.home': home + } + }); + + res(); + + event(user._id, 'home_updated', { + home + }); + } else { + if (id == null && data == null) return rej('you need to set id and data params if home param unset'); + + const _home = user.clientSettings.home; + const widget = _home.find(w => w.id == id); + + if (widget == null) return rej('widget not found'); + + widget.data = data; + + await User.update(user._id, { + $set: { + 'clientSettings.home': _home + } + }); + + res(); + + event(user._id, 'home_updated', { + id, data + }); + } +}); diff --git a/src/server/api/endpoints/i/update_mobile_home.ts b/src/server/api/endpoints/i/update_mobile_home.ts new file mode 100644 index 0000000000..b710e2f330 --- /dev/null +++ b/src/server/api/endpoints/i/update_mobile_home.ts @@ -0,0 +1,59 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../../../models/user'; +import event from '../../../../publishers/stream'; + +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'home' parameter + const [home, homeErr] = $(params.home).optional.array().each( + $().strict.object() + .have('name', $().string()) + .have('id', $().string()) + .have('data', $().object())).$; + if (homeErr) return rej('invalid home param'); + + // Get 'id' parameter + const [id, idErr] = $(params.id).optional.string().$; + if (idErr) return rej('invalid id param'); + + // Get 'data' parameter + const [data, dataErr] = $(params.data).optional.object().$; + if (dataErr) return rej('invalid data param'); + + if (home) { + await User.update(user._id, { + $set: { + 'clientSettings.mobileHome': home + } + }); + + res(); + + event(user._id, 'mobile_home_updated', { + home + }); + } else { + if (id == null && data == null) return rej('you need to set id and data params if home param unset'); + + const _home = user.clientSettings.mobileHome || []; + const widget = _home.find(w => w.id == id); + + if (widget == null) return rej('widget not found'); + + widget.data = data; + + await User.update(user._id, { + $set: { + 'clientSettings.mobileHome': _home + } + }); + + res(); + + event(user._id, 'mobile_home_updated', { + id, data + }); + } +}); diff --git a/src/api/endpoints/messaging/history.ts b/src/server/api/endpoints/messaging/history.ts similarity index 59% rename from src/api/endpoints/messaging/history.ts rename to src/server/api/endpoints/messaging/history.ts index 5f7c9276dd..e42d34f21a 100644 --- a/src/api/endpoints/messaging/history.ts +++ b/src/server/api/endpoints/messaging/history.ts @@ -2,8 +2,9 @@ * Module dependencies */ import $ from 'cafy'; -import History from '../../models/messaging-history'; -import serialize from '../../serializers/messaging-message'; +import History from '../../../../models/messaging-history'; +import Mute from '../../../../models/mute'; +import { pack } from '../../../../models/messaging-message'; /** * Show messaging history @@ -17,18 +18,26 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; if (limitErr) return rej('invalid limit param'); + const mute = await Mute.find({ + muterId: user._id, + deletedAt: { $exists: false } + }); + // Get history const history = await History .find({ - user_id: user._id + userId: user._id, + partnerId: { + $nin: mute.map(m => m.muteeId) + } }, { limit: limit, sort: { - updated_at: -1 + updatedAt: -1 } }); // Serialize res(await Promise.all(history.map(async h => - await serialize(h.message, user)))); + await pack(h.messageId, user)))); }); diff --git a/src/api/endpoints/messaging/messages.ts b/src/server/api/endpoints/messaging/messages.ts similarity index 51% rename from src/api/endpoints/messaging/messages.ts rename to src/server/api/endpoints/messaging/messages.ts index 7b270924eb..092eab0562 100644 --- a/src/api/endpoints/messaging/messages.ts +++ b/src/server/api/endpoints/messaging/messages.ts @@ -2,9 +2,9 @@ * Module dependencies */ import $ from 'cafy'; -import Message from '../../models/messaging-message'; -import User from '../../models/user'; -import serialize from '../../serializers/messaging-message'; +import Message from '../../../../models/messaging-message'; +import User from '../../../../models/user'; +import { pack } from '../../../../models/messaging-message'; import read from '../../common/read-messaging-message'; /** @@ -15,9 +15,9 @@ import read from '../../common/read-messaging-message'; * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [recipientId, recipientIdErr] = $(params.user_id).id().$; - if (recipientIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [recipientId, recipientIdErr] = $(params.userId).id().$; + if (recipientIdErr) return rej('invalid userId param'); // Fetch recipient const recipient = await User.findOne({ @@ -32,34 +32,34 @@ module.exports = (params, user) => new Promise(async (res, rej) => { return rej('user not found'); } - // Get 'mark_as_read' parameter - const [markAsRead = true, markAsReadErr] = $(params.mark_as_read).optional.boolean().$; - if (markAsReadErr) return rej('invalid mark_as_read param'); + // Get 'markAsRead' parameter + const [markAsRead = true, markAsReadErr] = $(params.markAsRead).optional.boolean().$; + if (markAsReadErr) return rej('invalid markAsRead param'); // Get 'limit' parameter const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; if (limitErr) return rej('invalid limit param'); - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); - // Check if both of since_id and max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); + // Check if both of sinceId and untilId is specified + if (sinceId && untilId) { + return rej('cannot set sinceId and untilId'); } const query = { $or: [{ - user_id: user._id, - recipient_id: recipient._id + userId: user._id, + recipientId: recipient._id }, { - user_id: recipient._id, - recipient_id: user._id + userId: recipient._id, + recipientId: user._id }] } as any; @@ -72,9 +72,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { query._id = { $gt: sinceId }; - } else if (maxId) { + } else if (untilId) { query._id = { - $lt: maxId + $lt: untilId }; } @@ -87,7 +87,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(messages.map(async message => - await serialize(message, user, { + await pack(message, user, { populateRecipient: false })))); diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts new file mode 100644 index 0000000000..085e75e6cf --- /dev/null +++ b/src/server/api/endpoints/messaging/messages/create.ts @@ -0,0 +1,160 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Message from '../../../../../models/messaging-message'; +import { isValidText } from '../../../../../models/messaging-message'; +import History from '../../../../../models/messaging-history'; +import User from '../../../../../models/user'; +import Mute from '../../../../../models/mute'; +import DriveFile from '../../../../../models/drive-file'; +import { pack } from '../../../../../models/messaging-message'; +import publishUserStream from '../../../../../publishers/stream'; +import { publishMessagingStream, publishMessagingIndexStream } from '../../../../../publishers/stream'; +import pushSw from '../../../../../publishers/push-sw'; +import html from '../../../../../text/html'; +import parse from '../../../../../text/parse'; +import config from '../../../../../config'; + +/** + * Create a message + * + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'userId' parameter + const [recipientId, recipientIdErr] = $(params.userId).id().$; + if (recipientIdErr) return rej('invalid userId param'); + + // Myself + if (recipientId.equals(user._id)) { + return rej('cannot send message to myself'); + } + + // Fetch recipient + const recipient = await User.findOne({ + _id: recipientId + }, { + fields: { + _id: true + } + }); + + if (recipient === null) { + return rej('user not found'); + } + + // Get 'text' parameter + const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$; + if (textErr) return rej('invalid text'); + + // Get 'fileId' parameter + const [fileId, fileIdErr] = $(params.fileId).optional.id().$; + if (fileIdErr) return rej('invalid fileId param'); + + let file = null; + if (fileId !== undefined) { + file = await DriveFile.findOne({ + _id: fileId, + 'metadata.userId': user._id + }); + + if (file === null) { + return rej('file not found'); + } + } + + // テキストが無いかつ添付ファイルも無かったらエラー + if (text === undefined && file === null) { + return rej('text or file is required'); + } + + // メッセージを作成 + const message = await Message.insert({ + createdAt: new Date(), + fileId: file ? file._id : undefined, + recipientId: recipient._id, + text: text ? text : undefined, + textHtml: text ? html(parse(text)) : undefined, + userId: user._id, + isRead: false + }); + + // Serialize + const messageObj = await pack(message); + + // Reponse + res(messageObj); + + // 自分のストリーム + publishMessagingStream(message.userId, message.recipientId, 'message', messageObj); + publishMessagingIndexStream(message.userId, 'message', messageObj); + publishUserStream(message.userId, 'messaging_message', messageObj); + + // 相手のストリーム + publishMessagingStream(message.recipientId, message.userId, 'message', messageObj); + publishMessagingIndexStream(message.recipientId, 'message', messageObj); + publishUserStream(message.recipientId, 'messaging_message', messageObj); + + // 3秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する + setTimeout(async () => { + const freshMessage = await Message.findOne({ _id: message._id }, { isRead: true }); + if (!freshMessage.isRead) { + //#region ただしミュートされているなら発行しない + const mute = await Mute.find({ + muterId: recipient._id, + deletedAt: { $exists: false } + }); + const mutedUserIds = mute.map(m => m.muteeId.toString()); + if (mutedUserIds.indexOf(user._id.toString()) != -1) { + return; + } + //#endregion + + publishUserStream(message.recipientId, 'unread_messaging_message', messageObj); + pushSw(message.recipientId, 'unread_messaging_message', messageObj); + } + }, 3000); + + // Register to search database + if (message.text && config.elasticsearch.enable) { + const es = require('../../../db/elasticsearch'); + + es.index({ + index: 'misskey', + type: 'messaging_message', + id: message._id.toString(), + body: { + text: message.text + } + }); + } + + // 履歴作成(自分) + History.update({ + userId: user._id, + partnerId: recipient._id + }, { + updatedAt: new Date(), + userId: user._id, + partnerId: recipient._id, + messageId: message._id + }, { + upsert: true + }); + + // 履歴作成(相手) + History.update({ + userId: recipient._id, + partnerId: user._id + }, { + updatedAt: new Date(), + userId: recipient._id, + partnerId: user._id, + messageId: message._id + }, { + upsert: true + }); +}); diff --git a/src/server/api/endpoints/messaging/unread.ts b/src/server/api/endpoints/messaging/unread.ts new file mode 100644 index 0000000000..30d59dd8bd --- /dev/null +++ b/src/server/api/endpoints/messaging/unread.ts @@ -0,0 +1,33 @@ +/** + * Module dependencies + */ +import Message from '../../../../models/messaging-message'; +import Mute from '../../../../models/mute'; + +/** + * Get count of unread messages + * + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + const mute = await Mute.find({ + muterId: user._id, + deletedAt: { $exists: false } + }); + const mutedUserIds = mute.map(m => m.muteeId); + + const count = await Message + .count({ + userId: { + $nin: mutedUserIds + }, + recipientId: user._id, + isRead: false + }); + + res({ + count: count + }); +}); diff --git a/src/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts similarity index 80% rename from src/api/endpoints/meta.ts rename to src/server/api/endpoints/meta.ts index a3f1d50329..f6a276a2b7 100644 --- a/src/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -2,13 +2,14 @@ * Module dependencies */ import * as os from 'os'; -import version from '../../version'; -import config from '../../conf'; +import version from '../../../version'; +import config from '../../../config'; +import Meta from '../../../models/meta'; /** * @swagger * /meta: - * post: + * note: * summary: Show the misskey's information * responses: * 200: @@ -34,21 +35,21 @@ import config from '../../conf'; /** * Show core info - * - * @param {any} params - * @return {Promise<any>} */ module.exports = (params) => new Promise(async (res, rej) => { + const meta: any = (await Meta.findOne()) || {}; + res({ maintainer: config.maintainer, version: version, - secure: config.https.enable, + secure: config.https != null, machine: os.hostname(), os: os.platform(), node: process.version, cpu: { model: os.cpus()[0].model, cores: os.cpus().length - } + }, + broadcasts: meta.broadcasts }); }); diff --git a/src/server/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts new file mode 100644 index 0000000000..19894d07af --- /dev/null +++ b/src/server/api/endpoints/mute/create.ts @@ -0,0 +1,61 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../../../models/user'; +import Mute from '../../../../models/mute'; + +/** + * Mute a user + * + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + const muter = user; + + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); + + // 自分自身 + if (user._id.equals(userId)) { + return rej('mutee is yourself'); + } + + // Get mutee + const mutee = await User.findOne({ + _id: userId + }, { + fields: { + data: false, + 'profile': false + } + }); + + if (mutee === null) { + return rej('user not found'); + } + + // Check if already muting + const exist = await Mute.findOne({ + muterId: muter._id, + muteeId: mutee._id, + deletedAt: { $exists: false } + }); + + if (exist !== null) { + return rej('already muting'); + } + + // Create mute + await Mute.insert({ + createdAt: new Date(), + muterId: muter._id, + muteeId: mutee._id, + }); + + // Send response + res(); +}); diff --git a/src/server/api/endpoints/mute/delete.ts b/src/server/api/endpoints/mute/delete.ts new file mode 100644 index 0000000000..10096352ba --- /dev/null +++ b/src/server/api/endpoints/mute/delete.ts @@ -0,0 +1,63 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../../../models/user'; +import Mute from '../../../../models/mute'; + +/** + * Unmute a user + * + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + const muter = user; + + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); + + // Check if the mutee is yourself + if (user._id.equals(userId)) { + return rej('mutee is yourself'); + } + + // Get mutee + const mutee = await User.findOne({ + _id: userId + }, { + fields: { + data: false, + 'profile': false + } + }); + + if (mutee === null) { + return rej('user not found'); + } + + // Check not muting + const exist = await Mute.findOne({ + muterId: muter._id, + muteeId: mutee._id, + deletedAt: { $exists: false } + }); + + if (exist === null) { + return rej('already not muting'); + } + + // Delete mute + await Mute.update({ + _id: exist._id + }, { + $set: { + deletedAt: new Date() + } + }); + + // Send response + res(); +}); diff --git a/src/server/api/endpoints/mute/list.ts b/src/server/api/endpoints/mute/list.ts new file mode 100644 index 0000000000..bd80401445 --- /dev/null +++ b/src/server/api/endpoints/mute/list.ts @@ -0,0 +1,73 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Mute from '../../../../models/mute'; +import { pack } from '../../../../models/user'; +import getFriends from '../../common/get-friends'; + +/** + * Get muted users of a user + * + * @param {any} params + * @param {any} me + * @return {Promise<any>} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'iknow' parameter + const [iknow = false, iknowErr] = $(params.iknow).optional.boolean().$; + if (iknowErr) return rej('invalid iknow param'); + + // Get 'limit' parameter + const [limit = 30, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'cursor' parameter + const [cursor = null, cursorErr] = $(params.cursor).optional.id().$; + if (cursorErr) return rej('invalid cursor param'); + + // Construct query + const query = { + muterId: me._id, + deletedAt: { $exists: false } + } as any; + + if (iknow) { + // Get my friends + const myFriends = await getFriends(me._id); + + query.muteeId = { + $in: myFriends + }; + } + + // カーソルが指定されている場合 + if (cursor) { + query._id = { + $lt: cursor + }; + } + + // Get mutes + const mutes = await Mute + .find(query, { + limit: limit + 1, + sort: { _id: -1 } + }); + + // 「次のページ」があるかどうか + const inStock = mutes.length === limit + 1; + if (inStock) { + mutes.pop(); + } + + // Serialize + const users = await Promise.all(mutes.map(async m => + await pack(m.muteeId, me, { detail: true }))); + + // Response + res({ + users: users, + next: inStock ? mutes[mutes.length - 1]._id : null, + }); +}); diff --git a/src/api/endpoints/my/apps.ts b/src/server/api/endpoints/my/apps.ts similarity index 85% rename from src/api/endpoints/my/apps.ts rename to src/server/api/endpoints/my/apps.ts index eb9c758768..2a3f8bcd7a 100644 --- a/src/api/endpoints/my/apps.ts +++ b/src/server/api/endpoints/my/apps.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import App from '../../models/app'; -import serialize from '../../serializers/app'; +import App, { pack } from '../../../../models/app'; /** * Get my apps @@ -22,7 +21,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (offsetErr) return rej('invalid offset param'); const query = { - user_id: user._id + userId: user._id }; // Execute query @@ -37,5 +36,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Reply res(await Promise.all(apps.map(async app => - await serialize(app)))); + await pack(app)))); }); diff --git a/src/server/api/endpoints/notes.ts b/src/server/api/endpoints/notes.ts new file mode 100644 index 0000000000..a70ac0588f --- /dev/null +++ b/src/server/api/endpoints/notes.ts @@ -0,0 +1,94 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Note, { pack } from '../../../models/note'; + +/** + * Get all notes + */ +module.exports = (params) => new Promise(async (res, rej) => { + // Get 'reply' parameter + const [reply, replyErr] = $(params.reply).optional.boolean().$; + if (replyErr) return rej('invalid reply param'); + + // Get 'renote' parameter + const [renote, renoteErr] = $(params.renote).optional.boolean().$; + if (renoteErr) return rej('invalid renote param'); + + // Get 'media' parameter + const [media, mediaErr] = $(params.media).optional.boolean().$; + if (mediaErr) return rej('invalid media param'); + + // Get 'poll' parameter + const [poll, pollErr] = $(params.poll).optional.boolean().$; + if (pollErr) return rej('invalid poll param'); + + // Get 'bot' parameter + //const [bot, botErr] = $(params.bot).optional.boolean().$; + //if (botErr) return rej('invalid bot param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); + + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); + + // Check if both of sinceId and untilId is specified + if (sinceId && untilId) { + return rej('cannot set sinceId and untilId'); + } + + // Construct query + const sort = { + _id: -1 + }; + const query = {} as any; + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } + + if (reply != undefined) { + query.replyId = reply ? { $exists: true, $ne: null } : null; + } + + if (renote != undefined) { + query.renoteId = renote ? { $exists: true, $ne: null } : null; + } + + if (media != undefined) { + query.mediaIds = media ? { $exists: true, $ne: null } : []; + } + + if (poll != undefined) { + query.poll = poll ? { $exists: true, $ne: null } : null; + } + + // TODO + //if (bot != undefined) { + // query.isBot = bot; + //} + + // Issue query + const notes = await Note + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(notes.map(note => pack(note)))); +}); diff --git a/src/api/endpoints/posts/context.ts b/src/server/api/endpoints/notes/context.ts similarity index 54% rename from src/api/endpoints/posts/context.ts rename to src/server/api/endpoints/notes/context.ts index cd5f15f481..2caf742d26 100644 --- a/src/api/endpoints/posts/context.ts +++ b/src/server/api/endpoints/notes/context.ts @@ -2,20 +2,19 @@ * Module dependencies */ import $ from 'cafy'; -import Post from '../../models/post'; -import serialize from '../../serializers/post'; +import Note, { pack } from '../../../../models/note'; /** - * Show a context of a post + * Show a context of a note * * @param {any} params * @param {any} user * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); // Get 'limit' parameter const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; @@ -25,13 +24,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; if (offsetErr) return rej('invalid offset param'); - // Lookup post - const post = await Post.findOne({ - _id: postId + // Lookup note + const note = await Note.findOne({ + _id: noteId }); - if (post === null) { - return rej('post not found'); + if (note === null) { + return rej('note not found'); } const context = []; @@ -39,7 +38,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { async function get(id) { i++; - const p = await Post.findOne({ _id: id }); + const p = await Note.findOne({ _id: id }); if (i > offset) { context.push(p); @@ -49,16 +48,16 @@ module.exports = (params, user) => new Promise(async (res, rej) => { return; } - if (p.reply_to_id) { - await get(p.reply_to_id); + if (p.replyId) { + await get(p.replyId); } } - if (post.reply_to_id) { - await get(post.reply_to_id); + if (note.replyId) { + await get(note.replyId); } // Serialize - res(await Promise.all(context.map(async post => - await serialize(post, user)))); + res(await Promise.all(context.map(async note => + await pack(note, user)))); }); diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts new file mode 100644 index 0000000000..7e79912b1b --- /dev/null +++ b/src/server/api/endpoints/notes/create.ts @@ -0,0 +1,251 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import deepEqual = require('deep-equal'); +import Note, { INote, isValidText, isValidCw, pack } from '../../../../models/note'; +import { ILocalUser } from '../../../../models/user'; +import Channel, { IChannel } from '../../../../models/channel'; +import DriveFile from '../../../../models/drive-file'; +import create from '../../../../services/note/create'; +import { IApp } from '../../../../models/app'; + +/** + * Create a note + * + * @param {any} params + * @param {any} user + * @param {any} app + * @return {Promise<any>} + */ +module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res, rej) => { + // Get 'visibility' parameter + const [visibility = 'public', visibilityErr] = $(params.visibility).optional.string().or(['public', 'unlisted', 'private', 'direct']).$; + if (visibilityErr) return rej('invalid visibility'); + + // Get 'text' parameter + const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$; + if (textErr) return rej('invalid text'); + + // Get 'cw' parameter + const [cw, cwErr] = $(params.cw).optional.string().pipe(isValidCw).$; + if (cwErr) return rej('invalid cw'); + + // Get 'viaMobile' parameter + const [viaMobile = false, viaMobileErr] = $(params.viaMobile).optional.boolean().$; + if (viaMobileErr) return rej('invalid viaMobile'); + + // Get 'tags' parameter + const [tags = [], tagsErr] = $(params.tags).optional.array('string').unique().eachQ(t => t.range(1, 32)).$; + if (tagsErr) return rej('invalid tags'); + + // Get 'geo' parameter + const [geo, geoErr] = $(params.geo).optional.nullable.strict.object() + .have('coordinates', $().array().length(2) + .item(0, $().number().range(-180, 180)) + .item(1, $().number().range(-90, 90))) + .have('altitude', $().nullable.number()) + .have('accuracy', $().nullable.number()) + .have('altitudeAccuracy', $().nullable.number()) + .have('heading', $().nullable.number().range(0, 360)) + .have('speed', $().nullable.number()) + .$; + if (geoErr) return rej('invalid geo'); + + // Get 'mediaIds' parameter + const [mediaIds, mediaIdsErr] = $(params.mediaIds).optional.array('id').unique().range(1, 4).$; + if (mediaIdsErr) return rej('invalid mediaIds'); + + let files = []; + if (mediaIds !== undefined) { + // Fetch files + // forEach だと途中でエラーなどがあっても return できないので + // 敢えて for を使っています。 + for (const mediaId of mediaIds) { + // Fetch file + // SELECT _id + const entity = await DriveFile.findOne({ + _id: mediaId, + 'metadata.userId': user._id + }); + + if (entity === null) { + return rej('file not found'); + } else { + files.push(entity); + } + } + } else { + files = null; + } + + // Get 'renoteId' parameter + const [renoteId, renoteIdErr] = $(params.renoteId).optional.id().$; + if (renoteIdErr) return rej('invalid renoteId'); + + let renote: INote = null; + let isQuote = false; + if (renoteId !== undefined) { + // Fetch renote to note + renote = await Note.findOne({ + _id: renoteId + }); + + if (renote == null) { + return rej('renoteee is not found'); + } else if (renote.renoteId && !renote.text && !renote.mediaIds) { + return rej('cannot renote to renote'); + } + + // Fetch recently note + const latestNote = await Note.findOne({ + userId: user._id + }, { + sort: { + _id: -1 + } + }); + + isQuote = text != null || files != null; + + // 直近と同じRenote対象かつ引用じゃなかったらエラー + if (latestNote && + latestNote.renoteId && + latestNote.renoteId.equals(renote._id) && + !isQuote) { + return rej('cannot renote same note that already reposted in your latest note'); + } + + // 直近がRenote対象かつ引用じゃなかったらエラー + if (latestNote && + latestNote._id.equals(renote._id) && + !isQuote) { + return rej('cannot renote your latest note'); + } + } + + // Get 'replyId' parameter + const [replyId, replyIdErr] = $(params.replyId).optional.id().$; + if (replyIdErr) return rej('invalid replyId'); + + let reply: INote = null; + if (replyId !== undefined) { + // Fetch reply + reply = await Note.findOne({ + _id: replyId + }); + + if (reply === null) { + return rej('in reply to note is not found'); + } + + // 返信対象が引用でないRenoteだったらエラー + if (reply.renoteId && !reply.text && !reply.mediaIds) { + return rej('cannot reply to renote'); + } + } + + // Get 'channelId' parameter + const [channelId, channelIdErr] = $(params.channelId).optional.id().$; + if (channelIdErr) return rej('invalid channelId'); + + let channel: IChannel = null; + if (channelId !== undefined) { + // Fetch channel + channel = await Channel.findOne({ + _id: channelId + }); + + if (channel === null) { + return rej('channel not found'); + } + + // 返信対象の投稿がこのチャンネルじゃなかったらダメ + if (reply && !channelId.equals(reply.channelId)) { + return rej('チャンネル内部からチャンネル外部の投稿に返信することはできません'); + } + + // Renote対象の投稿がこのチャンネルじゃなかったらダメ + if (renote && !channelId.equals(renote.channelId)) { + return rej('チャンネル内部からチャンネル外部の投稿をRenoteすることはできません'); + } + + // 引用ではないRenoteはダメ + if (renote && !isQuote) { + return rej('チャンネル内部では引用ではないRenoteをすることはできません'); + } + } else { + // 返信対象の投稿がチャンネルへの投稿だったらダメ + if (reply && reply.channelId != null) { + return rej('チャンネル外部からチャンネル内部の投稿に返信することはできません'); + } + + // Renote対象の投稿がチャンネルへの投稿だったらダメ + if (renote && renote.channelId != null) { + return rej('チャンネル外部からチャンネル内部の投稿をRenoteすることはできません'); + } + } + + // Get 'poll' parameter + const [poll, pollErr] = $(params.poll).optional.strict.object() + .have('choices', $().array('string') + .unique() + .range(2, 10) + .each(c => c.length > 0 && c.length < 50)) + .$; + if (pollErr) return rej('invalid poll'); + + if (poll) { + (poll as any).choices = (poll as any).choices.map((choice, i) => ({ + id: i, // IDを付与 + text: choice.trim(), + votes: 0 + })); + } + + // テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー + if (text === undefined && files === null && renote === null && poll === undefined) { + return rej('text, mediaIds, renoteId or poll is required'); + } + + // 直近の投稿と重複してたらエラー + // TODO: 直近の投稿が一日前くらいなら重複とは見なさない + if (user.latestNote) { + if (deepEqual({ + text: user.latestNote.text, + reply: user.latestNote.replyId ? user.latestNote.replyId.toString() : null, + renote: user.latestNote.renoteId ? user.latestNote.renoteId.toString() : null, + mediaIds: (user.latestNote.mediaIds || []).map(id => id.toString()) + }, { + text: text, + reply: reply ? reply._id.toString() : null, + renote: renote ? renote._id.toString() : null, + mediaIds: (files || []).map(file => file._id.toString()) + })) { + return rej('duplicate'); + } + } + + // 投稿を作成 + const note = await create(user, { + createdAt: new Date(), + media: files, + poll: poll, + text: text, + reply, + renote, + cw: cw, + tags: tags, + app: app, + viaMobile: viaMobile, + visibility, + geo + }); + + const noteObj = await pack(note, user); + + // Reponse + res({ + createdNote: noteObj + }); +}); diff --git a/src/server/api/endpoints/notes/favorites/create.ts b/src/server/api/endpoints/notes/favorites/create.ts new file mode 100644 index 0000000000..c8e7f52426 --- /dev/null +++ b/src/server/api/endpoints/notes/favorites/create.ts @@ -0,0 +1,48 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Favorite from '../../../../../models/favorite'; +import Note from '../../../../../models/note'; + +/** + * Favorite a note + * + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); + + // Get favoritee + const note = await Note.findOne({ + _id: noteId + }); + + if (note === null) { + return rej('note not found'); + } + + // if already favorited + const exist = await Favorite.findOne({ + noteId: note._id, + userId: user._id + }); + + if (exist !== null) { + return rej('already favorited'); + } + + // Create favorite + await Favorite.insert({ + createdAt: new Date(), + noteId: note._id, + userId: user._id + }); + + // Send response + res(); +}); diff --git a/src/api/endpoints/posts/favorites/delete.ts b/src/server/api/endpoints/notes/favorites/delete.ts similarity index 51% rename from src/api/endpoints/posts/favorites/delete.ts rename to src/server/api/endpoints/notes/favorites/delete.ts index c4fe7d3234..92aceb343b 100644 --- a/src/api/endpoints/posts/favorites/delete.ts +++ b/src/server/api/endpoints/notes/favorites/delete.ts @@ -2,34 +2,34 @@ * Module dependencies */ import $ from 'cafy'; -import Favorite from '../../../models/favorite'; -import Post from '../../../models/post'; +import Favorite from '../../../../../models/favorite'; +import Note from '../../../../../models/note'; /** - * Unfavorite a post + * Unfavorite a note * * @param {any} params * @param {any} user * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); // Get favoritee - const post = await Post.findOne({ - _id: postId + const note = await Note.findOne({ + _id: noteId }); - if (post === null) { - return rej('post not found'); + if (note === null) { + return rej('note not found'); } // if already favorited const exist = await Favorite.findOne({ - post_id: post._id, - user_id: user._id + noteId: note._id, + userId: user._id }); if (exist === null) { @@ -37,7 +37,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { } // Delete favorite - await Favorite.deleteOne({ + await Favorite.remove({ _id: exist._id }); diff --git a/src/api/endpoints/posts/mentions.ts b/src/server/api/endpoints/notes/mentions.ts similarity index 62% rename from src/api/endpoints/posts/mentions.ts rename to src/server/api/endpoints/notes/mentions.ts index 0ebe8be503..c507acbaec 100644 --- a/src/api/endpoints/posts/mentions.ts +++ b/src/server/api/endpoints/notes/mentions.ts @@ -2,9 +2,9 @@ * Module dependencies */ import $ from 'cafy'; -import Post from '../../models/post'; +import Note from '../../../../models/note'; import getFriends from '../../common/get-friends'; -import serialize from '../../serializers/post'; +import { pack } from '../../../../models/note'; /** * Get mentions of myself @@ -23,17 +23,17 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; if (limitErr) return rej('invalid limit param'); - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); - // Check if both of since_id and max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); + // Check if both of sinceId and untilId is specified + if (sinceId && untilId) { + return rej('cannot set sinceId and untilId'); } // Construct query @@ -48,7 +48,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (following) { const followingIds = await getFriends(user._id); - query.user_id = { + query.userId = { $in: followingIds }; } @@ -58,14 +58,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => { query._id = { $gt: sinceId }; - } else if (maxId) { + } else if (untilId) { query._id = { - $lt: maxId + $lt: untilId }; } // Issue query - const mentions = await Post + const mentions = await Note .find(query, { limit: limit, sort: sort @@ -73,6 +73,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(mentions.map(async mention => - await serialize(mention, user) + await pack(mention, user) ))); }); diff --git a/src/api/endpoints/posts/polls/recommendation.ts b/src/server/api/endpoints/notes/polls/recommendation.ts similarity index 68% rename from src/api/endpoints/posts/polls/recommendation.ts rename to src/server/api/endpoints/notes/polls/recommendation.ts index 9c92d6cac4..cb530ea2cf 100644 --- a/src/api/endpoints/posts/polls/recommendation.ts +++ b/src/server/api/endpoints/notes/polls/recommendation.ts @@ -2,9 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import Vote from '../../../models/poll-vote'; -import Post from '../../../models/post'; -import serialize from '../../../serializers/post'; +import Vote from '../../../../../models/poll-vote'; +import Note, { pack } from '../../../../../models/note'; /** * Get recommended polls @@ -24,22 +23,22 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Get votes const votes = await Vote.find({ - user_id: user._id + userId: user._id }, { fields: { _id: false, - post_id: true + noteId: true } }); - const nin = votes && votes.length != 0 ? votes.map(v => v.post_id) : []; + const nin = votes && votes.length != 0 ? votes.map(v => v.noteId) : []; - const posts = await Post + const notes = await Note .find({ _id: { $nin: nin }, - user_id: { + userId: { $ne: user._id }, poll: { @@ -55,6 +54,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }); // Serialize - res(await Promise.all(posts.map(async post => - await serialize(post, user, { detail: true })))); + res(await Promise.all(notes.map(async note => + await pack(note, user, { detail: true })))); }); diff --git a/src/server/api/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts new file mode 100644 index 0000000000..03d94da60d --- /dev/null +++ b/src/server/api/endpoints/notes/polls/vote.ts @@ -0,0 +1,115 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Vote from '../../../../../models/poll-vote'; +import Note from '../../../../../models/note'; +import Watching from '../../../../../models/note-watching'; +import watch from '../../../../../services/note/watch'; +import { publishNoteStream } from '../../../../../publishers/stream'; +import notify from '../../../../../publishers/notify'; + +/** + * Vote poll of a note + * + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); + + // Get votee + const note = await Note.findOne({ + _id: noteId + }); + + if (note === null) { + return rej('note not found'); + } + + if (note.poll == null) { + return rej('poll not found'); + } + + // Get 'choice' parameter + const [choice, choiceError] = + $(params.choice).number() + .pipe(c => note.poll.choices.some(x => x.id == c)) + .$; + if (choiceError) return rej('invalid choice param'); + + // if already voted + const exist = await Vote.findOne({ + noteId: note._id, + userId: user._id + }); + + if (exist !== null) { + return rej('already voted'); + } + + // Create vote + await Vote.insert({ + createdAt: new Date(), + noteId: note._id, + userId: user._id, + choice: choice + }); + + // Send response + res(); + + const inc = {}; + inc[`poll.choices.${findWithAttr(note.poll.choices, 'id', choice)}.votes`] = 1; + + // Increment votes count + await Note.update({ _id: note._id }, { + $inc: inc + }); + + publishNoteStream(note._id, 'poll_voted'); + + // Notify + notify(note.userId, user._id, 'poll_vote', { + noteId: note._id, + choice: choice + }); + + // Fetch watchers + Watching + .find({ + noteId: note._id, + userId: { $ne: user._id }, + // 削除されたドキュメントは除く + deletedAt: { $exists: false } + }, { + fields: { + userId: true + } + }) + .then(watchers => { + watchers.forEach(watcher => { + notify(watcher.userId, user._id, 'poll_vote', { + noteId: note._id, + choice: choice + }); + }); + }); + + // この投稿をWatchする + if (user.settings.autoWatch !== false) { + watch(user._id, note); + } +}); + +function findWithAttr(array, attr, value) { + for (let i = 0; i < array.length; i += 1) { + if (array[i][attr] === value) { + return i; + } + } + return -1; +} diff --git a/src/api/endpoints/posts/reactions.ts b/src/server/api/endpoints/notes/reactions.ts similarity index 63% rename from src/api/endpoints/posts/reactions.ts rename to src/server/api/endpoints/notes/reactions.ts index eab5d9b258..bbff97bb0a 100644 --- a/src/api/endpoints/posts/reactions.ts +++ b/src/server/api/endpoints/notes/reactions.ts @@ -2,21 +2,20 @@ * Module dependencies */ import $ from 'cafy'; -import Post from '../../models/post'; -import Reaction from '../../models/post-reaction'; -import serialize from '../../serializers/post-reaction'; +import Note from '../../../../models/note'; +import Reaction, { pack } from '../../../../models/note-reaction'; /** - * Show reactions of a post + * Show reactions of a note * * @param {any} params * @param {any} user * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); // Get 'limit' parameter const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; @@ -30,20 +29,20 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [sort = 'desc', sortError] = $(params.sort).optional.string().or('desc asc').$; if (sortError) return rej('invalid sort param'); - // Lookup post - const post = await Post.findOne({ - _id: postId + // Lookup note + const note = await Note.findOne({ + _id: noteId }); - if (post === null) { - return rej('post not found'); + if (note === null) { + return rej('note not found'); } // Issue query const reactions = await Reaction .find({ - post_id: post._id, - deleted_at: { $exists: false } + noteId: note._id, + deletedAt: { $exists: false } }, { limit: limit, skip: offset, @@ -54,5 +53,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(reactions.map(async reaction => - await serialize(reaction, user)))); + await pack(reaction, user)))); }); diff --git a/src/server/api/endpoints/notes/reactions/create.ts b/src/server/api/endpoints/notes/reactions/create.ts new file mode 100644 index 0000000000..c80c5416b1 --- /dev/null +++ b/src/server/api/endpoints/notes/reactions/create.ts @@ -0,0 +1,46 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Note from '../../../../../models/note'; +import create from '../../../../../services/note/reaction/create'; + +/** + * React to a note + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); + + // Get 'reaction' parameter + const [reaction, reactionErr] = $(params.reaction).string().or([ + 'like', + 'love', + 'laugh', + 'hmm', + 'surprise', + 'congrats', + 'angry', + 'confused', + 'pudding' + ]).$; + if (reactionErr) return rej('invalid reaction param'); + + // Fetch reactee + const note = await Note.findOne({ + _id: noteId + }); + + if (note === null) { + return rej('note not found'); + } + + try { + await create(user, note, reaction); + } catch (e) { + rej(e); + } + + res(); +}); diff --git a/src/server/api/endpoints/notes/reactions/delete.ts b/src/server/api/endpoints/notes/reactions/delete.ts new file mode 100644 index 0000000000..b5d738b8ff --- /dev/null +++ b/src/server/api/endpoints/notes/reactions/delete.ts @@ -0,0 +1,60 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Reaction from '../../../../../models/note-reaction'; +import Note from '../../../../../models/note'; +// import event from '../../../publishers/stream'; + +/** + * Unreact to a note + * + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); + + // Fetch unreactee + const note = await Note.findOne({ + _id: noteId + }); + + if (note === null) { + return rej('note not found'); + } + + // if already unreacted + const exist = await Reaction.findOne({ + noteId: note._id, + userId: user._id, + deletedAt: { $exists: false } + }); + + if (exist === null) { + return rej('never reacted'); + } + + // Delete reaction + await Reaction.update({ + _id: exist._id + }, { + $set: { + deletedAt: new Date() + } + }); + + // Send response + res(); + + const dec = {}; + dec[`reactionCounts.${exist.reaction}`] = -1; + + // Decrement reactions count + Note.update({ _id: note._id }, { + $inc: dec + }); +}); diff --git a/src/api/endpoints/posts/replies.ts b/src/server/api/endpoints/notes/replies.ts similarity index 61% rename from src/api/endpoints/posts/replies.ts rename to src/server/api/endpoints/notes/replies.ts index 89f4d99841..88d9ff329a 100644 --- a/src/api/endpoints/posts/replies.ts +++ b/src/server/api/endpoints/notes/replies.ts @@ -2,20 +2,19 @@ * Module dependencies */ import $ from 'cafy'; -import Post from '../../models/post'; -import serialize from '../../serializers/post'; +import Note, { pack } from '../../../../models/note'; /** - * Show a replies of a post + * Show a replies of a note * * @param {any} params * @param {any} user * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); // Get 'limit' parameter const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; @@ -29,18 +28,18 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [sort = 'desc', sortError] = $(params.sort).optional.string().or('desc asc').$; if (sortError) return rej('invalid sort param'); - // Lookup post - const post = await Post.findOne({ - _id: postId + // Lookup note + const note = await Note.findOne({ + _id: noteId }); - if (post === null) { - return rej('post not found'); + if (note === null) { + return rej('note not found'); } // Issue query - const replies = await Post - .find({ reply_to_id: post._id }, { + const replies = await Note + .find({ replyId: note._id }, { limit: limit, skip: offset, sort: { @@ -49,6 +48,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }); // Serialize - res(await Promise.all(replies.map(async post => - await serialize(post, user)))); + res(await Promise.all(replies.map(async note => + await pack(note, user)))); }); diff --git a/src/server/api/endpoints/notes/reposts.ts b/src/server/api/endpoints/notes/reposts.ts new file mode 100644 index 0000000000..9dfc2c3cb5 --- /dev/null +++ b/src/server/api/endpoints/notes/reposts.ts @@ -0,0 +1,73 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Note, { pack } from '../../../../models/note'; + +/** + * Show a renotes of a note + * + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); + + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); + + // Check if both of sinceId and untilId is specified + if (sinceId && untilId) { + return rej('cannot set sinceId and untilId'); + } + + // Lookup note + const note = await Note.findOne({ + _id: noteId + }); + + if (note === null) { + return rej('note not found'); + } + + // Construct query + const sort = { + _id: -1 + }; + const query = { + renoteId: note._id + } as any; + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } + + // Issue query + const renotes = await Note + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(renotes.map(async note => + await pack(note, user)))); +}); diff --git a/src/server/api/endpoints/notes/search.ts b/src/server/api/endpoints/notes/search.ts new file mode 100644 index 0000000000..bfa17b000e --- /dev/null +++ b/src/server/api/endpoints/notes/search.ts @@ -0,0 +1,364 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +const escapeRegexp = require('escape-regexp'); +import Note from '../../../../models/note'; +import User from '../../../../models/user'; +import Mute from '../../../../models/mute'; +import getFriends from '../../common/get-friends'; +import { pack } from '../../../../models/note'; + +/** + * Search a note + * + * @param {any} params + * @param {any} me + * @return {Promise<any>} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'text' parameter + const [text, textError] = $(params.text).optional.string().$; + if (textError) return rej('invalid text param'); + + // Get 'includeUserIds' parameter + const [includeUserIds = [], includeUserIdsErr] = $(params.includeUserIds).optional.array('id').$; + if (includeUserIdsErr) return rej('invalid includeUserIds param'); + + // Get 'excludeUserIds' parameter + const [excludeUserIds = [], excludeUserIdsErr] = $(params.excludeUserIds).optional.array('id').$; + if (excludeUserIdsErr) return rej('invalid excludeUserIds param'); + + // Get 'includeUserUsernames' parameter + const [includeUserUsernames = [], includeUserUsernamesErr] = $(params.includeUserUsernames).optional.array('string').$; + if (includeUserUsernamesErr) return rej('invalid includeUserUsernames param'); + + // Get 'excludeUserUsernames' parameter + const [excludeUserUsernames = [], excludeUserUsernamesErr] = $(params.excludeUserUsernames).optional.array('string').$; + if (excludeUserUsernamesErr) return rej('invalid excludeUserUsernames param'); + + // Get 'following' parameter + const [following = null, followingErr] = $(params.following).optional.nullable.boolean().$; + if (followingErr) return rej('invalid following param'); + + // Get 'mute' parameter + const [mute = 'mute_all', muteErr] = $(params.mute).optional.string().$; + if (muteErr) return rej('invalid mute param'); + + // Get 'reply' parameter + const [reply = null, replyErr] = $(params.reply).optional.nullable.boolean().$; + if (replyErr) return rej('invalid reply param'); + + // Get 'renote' parameter + const [renote = null, renoteErr] = $(params.renote).optional.nullable.boolean().$; + if (renoteErr) return rej('invalid renote param'); + + // Get 'media' parameter + const [media = null, mediaErr] = $(params.media).optional.nullable.boolean().$; + if (mediaErr) return rej('invalid media param'); + + // Get 'poll' parameter + const [poll = null, pollErr] = $(params.poll).optional.nullable.boolean().$; + if (pollErr) return rej('invalid poll param'); + + // Get 'sinceDate' parameter + const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$; + if (sinceDateErr) throw 'invalid sinceDate param'; + + // Get 'untilDate' parameter + const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$; + if (untilDateErr) throw 'invalid untilDate param'; + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 30).$; + if (limitErr) return rej('invalid limit param'); + + let includeUsers = includeUserIds; + if (includeUserUsernames != null) { + const ids = (await Promise.all(includeUserUsernames.map(async (username) => { + const _user = await User.findOne({ + usernameLower: username.toLowerCase() + }); + return _user ? _user._id : null; + }))).filter(id => id != null); + includeUsers = includeUsers.concat(ids); + } + + let excludeUsers = excludeUserIds; + if (excludeUserUsernames != null) { + const ids = (await Promise.all(excludeUserUsernames.map(async (username) => { + const _user = await User.findOne({ + usernameLower: username.toLowerCase() + }); + return _user ? _user._id : null; + }))).filter(id => id != null); + excludeUsers = excludeUsers.concat(ids); + } + + search(res, rej, me, text, includeUsers, excludeUsers, following, + mute, reply, renote, media, poll, sinceDate, untilDate, offset, limit); +}); + +async function search( + res, rej, me, text, includeUserIds, excludeUserIds, following, + mute, reply, renote, media, poll, sinceDate, untilDate, offset, max) { + + let q: any = { + $and: [] + }; + + const push = x => q.$and.push(x); + + if (text) { + // 完全一致検索 + if (/"""(.+?)"""/.test(text)) { + const x = text.match(/"""(.+?)"""/)[1]; + push({ + text: x + }); + } else { + const tags = text.split(' ').filter(x => x[0] == '#'); + if (tags) { + push({ + $and: tags.map(x => ({ + tags: x + })) + }); + } + + push({ + $and: text.split(' ').map(x => ({ + // キーワードが-で始まる場合そのキーワードを除外する + text: x[0] == '-' ? { + $not: new RegExp(escapeRegexp(x.substr(1))) + } : new RegExp(escapeRegexp(x)) + })) + }); + } + } + + if (includeUserIds && includeUserIds.length != 0) { + push({ + userId: { + $in: includeUserIds + } + }); + } else if (excludeUserIds && excludeUserIds.length != 0) { + push({ + userId: { + $nin: excludeUserIds + } + }); + } + + if (following != null && me != null) { + const ids = await getFriends(me._id, false); + push({ + userId: following ? { + $in: ids + } : { + $nin: ids.concat(me._id) + } + }); + } + + if (me != null) { + const mutes = await Mute.find({ + muterId: me._id, + deletedAt: { $exists: false } + }); + const mutedUserIds = mutes.map(m => m.muteeId); + + switch (mute) { + case 'mute_all': + push({ + userId: { + $nin: mutedUserIds + }, + '_reply.userId': { + $nin: mutedUserIds + }, + '_renote.userId': { + $nin: mutedUserIds + } + }); + break; + case 'mute_related': + push({ + '_reply.userId': { + $nin: mutedUserIds + }, + '_renote.userId': { + $nin: mutedUserIds + } + }); + break; + case 'mute_direct': + push({ + userId: { + $nin: mutedUserIds + } + }); + break; + case 'direct_only': + push({ + userId: { + $in: mutedUserIds + } + }); + break; + case 'related_only': + push({ + $or: [{ + '_reply.userId': { + $in: mutedUserIds + } + }, { + '_renote.userId': { + $in: mutedUserIds + } + }] + }); + break; + case 'all_only': + push({ + $or: [{ + userId: { + $in: mutedUserIds + } + }, { + '_reply.userId': { + $in: mutedUserIds + } + }, { + '_renote.userId': { + $in: mutedUserIds + } + }] + }); + break; + } + } + + if (reply != null) { + if (reply) { + push({ + replyId: { + $exists: true, + $ne: null + } + }); + } else { + push({ + $or: [{ + replyId: { + $exists: false + } + }, { + replyId: null + }] + }); + } + } + + if (renote != null) { + if (renote) { + push({ + renoteId: { + $exists: true, + $ne: null + } + }); + } else { + push({ + $or: [{ + renoteId: { + $exists: false + } + }, { + renoteId: null + }] + }); + } + } + + if (media != null) { + if (media) { + push({ + mediaIds: { + $exists: true, + $ne: null + } + }); + } else { + push({ + $or: [{ + mediaIds: { + $exists: false + } + }, { + mediaIds: null + }] + }); + } + } + + if (poll != null) { + if (poll) { + push({ + poll: { + $exists: true, + $ne: null + } + }); + } else { + push({ + $or: [{ + poll: { + $exists: false + } + }, { + poll: null + }] + }); + } + } + + if (sinceDate) { + push({ + createdAt: { + $gt: new Date(sinceDate) + } + }); + } + + if (untilDate) { + push({ + createdAt: { + $lt: new Date(untilDate) + } + }); + } + + if (q.$and.length == 0) { + q = {}; + } + + // Search notes + const notes = await Note + .find(q, { + sort: { + _id: -1 + }, + limit: max, + skip: offset + }); + + // Serialize + res(await Promise.all(notes.map(async note => + await pack(note, me)))); +} diff --git a/src/server/api/endpoints/notes/show.ts b/src/server/api/endpoints/notes/show.ts new file mode 100644 index 0000000000..67cdc3038b --- /dev/null +++ b/src/server/api/endpoints/notes/show.ts @@ -0,0 +1,32 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Note, { pack } from '../../../../models/note'; + +/** + * Show a note + * + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'noteId' parameter + const [noteId, noteIdErr] = $(params.noteId).id().$; + if (noteIdErr) return rej('invalid noteId param'); + + // Get note + const note = await Note.findOne({ + _id: noteId + }); + + if (note === null) { + return rej('note not found'); + } + + // Serialize + res(await pack(note, user, { + detail: true + })); +}); diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts new file mode 100644 index 0000000000..5263cfb2aa --- /dev/null +++ b/src/server/api/endpoints/notes/timeline.ts @@ -0,0 +1,132 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import rap from '@prezzemolo/rap'; +import Note from '../../../../models/note'; +import Mute from '../../../../models/mute'; +import ChannelWatching from '../../../../models/channel-watching'; +import getFriends from '../../common/get-friends'; +import { pack } from '../../../../models/note'; + +/** + * Get timeline of myself + * + * @param {any} params + * @param {any} user + * @param {any} app + * @return {Promise<any>} + */ +module.exports = async (params, user, app) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) throw 'invalid limit param'; + + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) throw 'invalid sinceId param'; + + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) throw 'invalid untilId param'; + + // Get 'sinceDate' parameter + const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$; + if (sinceDateErr) throw 'invalid sinceDate param'; + + // Get 'untilDate' parameter + const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$; + if (untilDateErr) throw 'invalid untilDate param'; + + // Check if only one of sinceId, untilId, sinceDate, untilDate specified + if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) { + throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; + } + + const { followingIds, watchingChannelIds, mutedUserIds } = await rap({ + // ID list of the user itself and other users who the user follows + followingIds: getFriends(user._id), + + // Watchしているチャンネルを取得 + watchingChannelIds: ChannelWatching.find({ + userId: user._id, + // 削除されたドキュメントは除く + deletedAt: { $exists: false } + }).then(watches => watches.map(w => w.channelId)), + + // ミュートしているユーザーを取得 + mutedUserIds: Mute.find({ + muterId: user._id, + // 削除されたドキュメントは除く + deletedAt: { $exists: false } + }).then(ms => ms.map(m => m.muteeId)) + }); + + //#region Construct query + const sort = { + _id: -1 + }; + + const query = { + $or: [{ + // フォローしている人のタイムラインへの投稿 + userId: { + $in: followingIds + }, + // 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る + $or: [{ + channelId: { + $exists: false + } + }, { + channelId: null + }] + }, { + // Watchしているチャンネルへの投稿 + channelId: { + $in: watchingChannelIds + } + }], + // mute + userId: { + $nin: mutedUserIds + }, + '_reply.userId': { + $nin: mutedUserIds + }, + '_renote.userId': { + $nin: mutedUserIds + }, + } as any; + + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } else if (sinceDate) { + sort._id = 1; + query.createdAt = { + $gt: new Date(sinceDate) + }; + } else if (untilDate) { + query.createdAt = { + $lt: new Date(untilDate) + }; + } + //#endregion + + // Issue query + const timeline = await Note + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + return await Promise.all(timeline.map(note => pack(note, user))); +}; diff --git a/src/api/endpoints/posts/trend.ts b/src/server/api/endpoints/notes/trend.ts similarity index 67% rename from src/api/endpoints/posts/trend.ts rename to src/server/api/endpoints/notes/trend.ts index 3277206d26..48ecd5b843 100644 --- a/src/api/endpoints/posts/trend.ts +++ b/src/server/api/endpoints/notes/trend.ts @@ -3,11 +3,10 @@ */ const ms = require('ms'); import $ from 'cafy'; -import Post from '../../models/post'; -import serialize from '../../serializers/post'; +import Note, { pack } from '../../../../models/note'; /** - * Get trend posts + * Get trend notes * * @param {any} params * @param {any} user @@ -26,9 +25,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const [reply, replyErr] = $(params.reply).optional.boolean().$; if (replyErr) return rej('invalid reply param'); - // Get 'repost' parameter - const [repost, repostErr] = $(params.repost).optional.boolean().$; - if (repostErr) return rej('invalid repost param'); + // Get 'renote' parameter + const [renote, renoteErr] = $(params.renote).optional.boolean().$; + if (renoteErr) return rej('invalid renote param'); // Get 'media' parameter const [media, mediaErr] = $(params.media).optional.boolean().$; @@ -39,24 +38,24 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (pollErr) return rej('invalid poll param'); const query = { - created_at: { + createdAt: { $gte: new Date(Date.now() - ms('1days')) }, - repost_count: { + renoteCount: { $gt: 0 } } as any; if (reply != undefined) { - query.reply_to_id = reply ? { $exists: true, $ne: null } : null; + query.replyId = reply ? { $exists: true, $ne: null } : null; } - if (repost != undefined) { - query.repost_id = repost ? { $exists: true, $ne: null } : null; + if (renote != undefined) { + query.renoteId = renote ? { $exists: true, $ne: null } : null; } if (media != undefined) { - query.media_ids = media ? { $exists: true, $ne: null } : null; + query.mediaIds = media ? { $exists: true, $ne: null } : null; } if (poll != undefined) { @@ -64,17 +63,17 @@ module.exports = (params, user) => new Promise(async (res, rej) => { } // Issue query - const posts = await Post + const notes = await Note .find(query, { limit: limit, skip: offset, sort: { - repost_count: -1, + renoteCount: -1, _id: -1 } }); // Serialize - res(await Promise.all(posts.map(async post => - await serialize(post, user, { detail: true })))); + res(await Promise.all(notes.map(async note => + await pack(note, user, { detail: true })))); }); diff --git a/src/server/api/endpoints/notifications/get_unread_count.ts b/src/server/api/endpoints/notifications/get_unread_count.ts new file mode 100644 index 0000000000..283ecd63b1 --- /dev/null +++ b/src/server/api/endpoints/notifications/get_unread_count.ts @@ -0,0 +1,33 @@ +/** + * Module dependencies + */ +import Notification from '../../../../models/notification'; +import Mute from '../../../../models/mute'; + +/** + * Get count of unread notifications + * + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + const mute = await Mute.find({ + muterId: user._id, + deletedAt: { $exists: false } + }); + const mutedUserIds = mute.map(m => m.muteeId); + + const count = await Notification + .count({ + notifieeId: user._id, + notifierId: { + $nin: mutedUserIds + }, + isRead: false + }); + + res({ + count: count + }); +}); diff --git a/src/server/api/endpoints/notifications/mark_as_read_all.ts b/src/server/api/endpoints/notifications/mark_as_read_all.ts new file mode 100644 index 0000000000..01c9145837 --- /dev/null +++ b/src/server/api/endpoints/notifications/mark_as_read_all.ts @@ -0,0 +1,32 @@ +/** + * Module dependencies + */ +import Notification from '../../../../models/notification'; +import event from '../../../../publishers/stream'; + +/** + * Mark as read all notifications + * + * @param {any} params + * @param {any} user + * @return {Promise<any>} + */ +module.exports = (params, user) => new Promise(async (res, rej) => { + // Update documents + await Notification.update({ + notifieeId: user._id, + isRead: false + }, { + $set: { + isRead: true + } + }, { + multi: true + }); + + // Response + res(); + + // 全ての通知を読みましたよというイベントを発行 + event(user._id, 'read_all_notifications'); +}); diff --git a/src/server/api/endpoints/othello/games.ts b/src/server/api/endpoints/othello/games.ts new file mode 100644 index 0000000000..d05c1c2585 --- /dev/null +++ b/src/server/api/endpoints/othello/games.ts @@ -0,0 +1,62 @@ +import $ from 'cafy'; +import OthelloGame, { pack } from '../../../../models/othello-game'; + +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'my' parameter + const [my = false, myErr] = $(params.my).optional.boolean().$; + if (myErr) return rej('invalid my param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); + + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); + + // Check if both of sinceId and untilId is specified + if (sinceId && untilId) { + return rej('cannot set sinceId and untilId'); + } + + const q: any = my ? { + isStarted: true, + $or: [{ + user1Id: user._id + }, { + user2Id: user._id + }] + } : { + isStarted: true + }; + + const sort = { + _id: -1 + }; + + if (sinceId) { + sort._id = 1; + q._id = { + $gt: sinceId + }; + } else if (untilId) { + q._id = { + $lt: untilId + }; + } + + // Fetch games + const games = await OthelloGame.find(q, { + sort, + limit + }); + + // Reponse + res(Promise.all(games.map(async (g) => await pack(g, user, { + detail: false + })))); +}); diff --git a/src/server/api/endpoints/othello/games/show.ts b/src/server/api/endpoints/othello/games/show.ts new file mode 100644 index 0000000000..dd886936d4 --- /dev/null +++ b/src/server/api/endpoints/othello/games/show.ts @@ -0,0 +1,32 @@ +import $ from 'cafy'; +import OthelloGame, { pack } from '../../../../../models/othello-game'; +import Othello from '../../../../../othello/core'; + +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'gameId' parameter + const [gameId, gameIdErr] = $(params.gameId).id().$; + if (gameIdErr) return rej('invalid gameId param'); + + const game = await OthelloGame.findOne({ _id: gameId }); + + if (game == null) { + return rej('game not found'); + } + + const o = new Othello(game.settings.map, { + isLlotheo: game.settings.isLlotheo, + canPutEverywhere: game.settings.canPutEverywhere, + loopedBoard: game.settings.loopedBoard + }); + + game.logs.forEach(log => { + o.put(log.color, log.pos); + }); + + const packed = await pack(game, user); + + res(Object.assign({ + board: o.board, + turn: o.turn + }, packed)); +}); diff --git a/src/server/api/endpoints/othello/invitations.ts b/src/server/api/endpoints/othello/invitations.ts new file mode 100644 index 0000000000..4761537614 --- /dev/null +++ b/src/server/api/endpoints/othello/invitations.ts @@ -0,0 +1,15 @@ +import Matching, { pack as packMatching } from '../../../../models/othello-matching'; + +module.exports = (params, user) => new Promise(async (res, rej) => { + // Find session + const invitations = await Matching.find({ + childId: user._id + }, { + sort: { + _id: -1 + } + }); + + // Reponse + res(Promise.all(invitations.map(async (i) => await packMatching(i, user)))); +}); diff --git a/src/server/api/endpoints/othello/match.ts b/src/server/api/endpoints/othello/match.ts new file mode 100644 index 0000000000..d9274f8f9c --- /dev/null +++ b/src/server/api/endpoints/othello/match.ts @@ -0,0 +1,95 @@ +import $ from 'cafy'; +import Matching, { pack as packMatching } from '../../../../models/othello-matching'; +import OthelloGame, { pack as packGame } from '../../../../models/othello-game'; +import User from '../../../../models/user'; +import publishUserStream, { publishOthelloStream } from '../../../../publishers/stream'; +import { eighteight } from '../../../../othello/maps'; + +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'userId' parameter + const [childId, childIdErr] = $(params.userId).id().$; + if (childIdErr) return rej('invalid userId param'); + + // Myself + if (childId.equals(user._id)) { + return rej('invalid userId param'); + } + + // Find session + const exist = await Matching.findOne({ + parentId: childId, + childId: user._id + }); + + if (exist) { + // Destroy session + Matching.remove({ + _id: exist._id + }); + + // Create game + const game = await OthelloGame.insert({ + createdAt: new Date(), + user1Id: exist.parentId, + user2Id: user._id, + user1Accepted: false, + user2Accepted: false, + isStarted: false, + isEnded: false, + logs: [], + settings: { + map: eighteight.data, + bw: 'random', + isLlotheo: false + } + }); + + // Reponse + res(await packGame(game, user)); + + publishOthelloStream(exist.parentId, 'matched', await packGame(game, exist.parentId)); + + const other = await Matching.count({ + childId: user._id + }); + + if (other == 0) { + publishUserStream(user._id, 'othello_no_invites'); + } + } else { + // Fetch child + const child = await User.findOne({ + _id: childId + }, { + fields: { + _id: true + } + }); + + if (child === null) { + return rej('user not found'); + } + + // 以前のセッションはすべて削除しておく + await Matching.remove({ + parentId: user._id + }); + + // セッションを作成 + const matching = await Matching.insert({ + createdAt: new Date(), + parentId: user._id, + childId: child._id + }); + + // Reponse + res(); + + const packed = await packMatching(matching, child); + + // 招待 + publishOthelloStream(child._id, 'invited', packed); + + publishUserStream(child._id, 'othello_invited', packed); + } +}); diff --git a/src/server/api/endpoints/othello/match/cancel.ts b/src/server/api/endpoints/othello/match/cancel.ts new file mode 100644 index 0000000000..562e691061 --- /dev/null +++ b/src/server/api/endpoints/othello/match/cancel.ts @@ -0,0 +1,9 @@ +import Matching from '../../../../../models/othello-matching'; + +module.exports = (params, user) => new Promise(async (res, rej) => { + await Matching.remove({ + parentId: user._id + }); + + res(); +}); diff --git a/src/api/endpoints/stats.ts b/src/server/api/endpoints/stats.ts similarity index 70% rename from src/api/endpoints/stats.ts rename to src/server/api/endpoints/stats.ts index a6084cd17a..52e5195484 100644 --- a/src/api/endpoints/stats.ts +++ b/src/server/api/endpoints/stats.ts @@ -1,13 +1,13 @@ /** * Module dependencies */ -import Post from '../models/post'; -import User from '../models/user'; +import Note from '../../../models/note'; +import User from '../../../models/user'; /** * @swagger * /stats: - * post: + * note: * summary: Show the misskey's statistics * responses: * 200: @@ -15,10 +15,10 @@ import User from '../models/user'; * schema: * type: object * properties: - * posts_count: - * description: count of all posts of misskey + * notesCount: + * description: count of all notes of misskey * type: number - * users_count: + * usersCount: * description: count of all users of misskey * type: number * @@ -35,14 +35,14 @@ import User from '../models/user'; * @return {Promise<any>} */ module.exports = params => new Promise(async (res, rej) => { - const postsCount = await Post + const notesCount = await Note .count(); const usersCount = await User .count(); res({ - posts_count: postsCount, - users_count: usersCount + notesCount: notesCount, + usersCount: usersCount }); }); diff --git a/src/server/api/endpoints/sw/register.ts b/src/server/api/endpoints/sw/register.ts new file mode 100644 index 0000000000..3fe0bda4ee --- /dev/null +++ b/src/server/api/endpoints/sw/register.ts @@ -0,0 +1,44 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Subscription from '../../../../models/sw-subscription'; + +/** + * subscribe service worker + */ +module.exports = async (params, user, app) => new Promise(async (res, rej) => { + // Get 'endpoint' parameter + const [endpoint, endpointErr] = $(params.endpoint).string().$; + if (endpointErr) return rej('invalid endpoint param'); + + // Get 'auth' parameter + const [auth, authErr] = $(params.auth).string().$; + if (authErr) return rej('invalid auth param'); + + // Get 'publickey' parameter + const [publickey, publickeyErr] = $(params.publickey).string().$; + if (publickeyErr) return rej('invalid publickey param'); + + // if already subscribed + const exist = await Subscription.findOne({ + userId: user._id, + endpoint: endpoint, + auth: auth, + publickey: publickey, + deletedAt: { $exists: false } + }); + + if (exist !== null) { + return res(); + } + + await Subscription.insert({ + userId: user._id, + endpoint: endpoint, + auth: auth, + publickey: publickey + }); + + res(); +}); diff --git a/src/api/endpoints/username/available.ts b/src/server/api/endpoints/username/available.ts similarity index 76% rename from src/api/endpoints/username/available.ts rename to src/server/api/endpoints/username/available.ts index 3be7bcba32..bd27c37de0 100644 --- a/src/api/endpoints/username/available.ts +++ b/src/server/api/endpoints/username/available.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../models/user'; -import { validateUsername } from '../../models/user'; +import User from '../../../../models/user'; +import { validateUsername } from '../../../../models/user'; /** * Check available username @@ -19,7 +19,8 @@ module.exports = async (params) => new Promise(async (res, rej) => { // Get exist const exist = await User .count({ - username_lower: username.toLowerCase() + host: null, + usernameLower: username.toLowerCase() }, { limit: 1 }); diff --git a/src/server/api/endpoints/users.ts b/src/server/api/endpoints/users.ts new file mode 100644 index 0000000000..e82d72748c --- /dev/null +++ b/src/server/api/endpoints/users.ts @@ -0,0 +1,56 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User, { pack } from '../../../models/user'; + +/** + * Lists all users + * + * @param {any} params + * @param {any} me + * @return {Promise<any>} + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'offset' parameter + const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; + if (offsetErr) return rej('invalid offset param'); + + // Get 'sort' parameter + const [sort, sortError] = $(params.sort).optional.string().or('+follower|-follower').$; + if (sortError) return rej('invalid sort param'); + + // Construct query + let _sort; + if (sort) { + if (sort == '+follower') { + _sort = { + followersCount: -1 + }; + } else if (sort == '-follower') { + _sort = { + followersCount: 1 + }; + } + } else { + _sort = { + _id: -1 + }; + } + + // Issue query + const users = await User + .find({}, { + limit: limit, + sort: _sort, + skip: offset + }); + + // Serialize + res(await Promise.all(users.map(async user => + await pack(user, me)))); +}); diff --git a/src/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts similarity index 80% rename from src/api/endpoints/users/followers.ts rename to src/server/api/endpoints/users/followers.ts index 4905323ba5..0222313e81 100644 --- a/src/api/endpoints/users/followers.ts +++ b/src/server/api/endpoints/users/followers.ts @@ -2,9 +2,9 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../models/user'; -import Following from '../../models/following'; -import serialize from '../../serializers/user'; +import User from '../../../../models/user'; +import Following from '../../../../models/following'; +import { pack } from '../../../../models/user'; import getFriends from '../../common/get-friends'; /** @@ -15,9 +15,9 @@ import getFriends from '../../common/get-friends'; * @return {Promise<any>} */ module.exports = (params, me) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); // Get 'iknow' parameter const [iknow = false, iknowErr] = $(params.iknow).optional.boolean().$; @@ -46,8 +46,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Construct query const query = { - followee_id: user._id, - deleted_at: { $exists: false } + followeeId: user._id } as any; // ログインしていてかつ iknow フラグがあるとき @@ -55,7 +54,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Get my friends const myFriends = await getFriends(me._id); - query.follower_id = { + query.followerId = { $in: myFriends }; } @@ -82,7 +81,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Serialize const users = await Promise.all(following.map(async f => - await serialize(f.follower_id, me, { detail: true }))); + await pack(f.followerId, me, { detail: true }))); // Response res({ diff --git a/src/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts similarity index 80% rename from src/api/endpoints/users/following.ts rename to src/server/api/endpoints/users/following.ts index dc2ff49bbe..2372f57fbe 100644 --- a/src/api/endpoints/users/following.ts +++ b/src/server/api/endpoints/users/following.ts @@ -2,9 +2,9 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../models/user'; -import Following from '../../models/following'; -import serialize from '../../serializers/user'; +import User from '../../../../models/user'; +import Following from '../../../../models/following'; +import { pack } from '../../../../models/user'; import getFriends from '../../common/get-friends'; /** @@ -15,9 +15,9 @@ import getFriends from '../../common/get-friends'; * @return {Promise<any>} */ module.exports = (params, me) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); // Get 'iknow' parameter const [iknow = false, iknowErr] = $(params.iknow).optional.boolean().$; @@ -46,8 +46,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Construct query const query = { - follower_id: user._id, - deleted_at: { $exists: false } + followerId: user._id } as any; // ログインしていてかつ iknow フラグがあるとき @@ -55,7 +54,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Get my friends const myFriends = await getFriends(me._id); - query.followee_id = { + query.followeeId = { $in: myFriends }; } @@ -82,7 +81,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Serialize const users = await Promise.all(following.map(async f => - await serialize(f.followee_id, me, { detail: true }))); + await pack(f.followeeId, me, { detail: true }))); // Response res({ diff --git a/src/server/api/endpoints/users/get_frequently_replied_users.ts b/src/server/api/endpoints/users/get_frequently_replied_users.ts new file mode 100644 index 0000000000..7a98f44e98 --- /dev/null +++ b/src/server/api/endpoints/users/get_frequently_replied_users.ts @@ -0,0 +1,99 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Note from '../../../../models/note'; +import User, { pack } from '../../../../models/user'; + +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).id().$; + if (userIdErr) return rej('invalid userId param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Lookup user + const user = await User.findOne({ + _id: userId + }, { + fields: { + _id: true + } + }); + + if (user === null) { + return rej('user not found'); + } + + // Fetch recent notes + const recentNotes = await Note.find({ + userId: user._id, + replyId: { + $exists: true, + $ne: null + } + }, { + sort: { + _id: -1 + }, + limit: 1000, + fields: { + _id: false, + replyId: true + } + }); + + // 投稿が少なかったら中断 + if (recentNotes.length === 0) { + return res([]); + } + + const replyTargetNotes = await Note.find({ + _id: { + $in: recentNotes.map(p => p.replyId) + }, + userId: { + $ne: user._id + } + }, { + fields: { + _id: false, + userId: true + } + }); + + const repliedUsers = {}; + + // Extract replies from recent notes + replyTargetNotes.forEach(note => { + const userId = note.userId.toString(); + if (repliedUsers[userId]) { + repliedUsers[userId]++; + } else { + repliedUsers[userId] = 1; + } + }); + + // Calc peak + let peak = 0; + Object.keys(repliedUsers).forEach(user => { + if (repliedUsers[user] > peak) peak = repliedUsers[user]; + }); + + // Sort replies by frequency + const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]); + + // Extract top replied users + const topRepliedUsers = repliedUsersSorted.slice(0, limit); + + // Make replies object (includes weights) + const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({ + user: await pack(user, me, { detail: true }), + weight: repliedUsers[user] / peak + }))); + + // Response + res(repliesObj); +}); diff --git a/src/server/api/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts new file mode 100644 index 0000000000..e91b75e1d3 --- /dev/null +++ b/src/server/api/endpoints/users/notes.ts @@ -0,0 +1,133 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import getHostLower from '../../common/get-host-lower'; +import Note, { pack } from '../../../../models/note'; +import User from '../../../../models/user'; + +/** + * Get notes of a user + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).optional.id().$; + if (userIdErr) return rej('invalid userId param'); + + // Get 'username' parameter + const [username, usernameErr] = $(params.username).optional.string().$; + if (usernameErr) return rej('invalid username param'); + + if (userId === undefined && username === undefined) { + return rej('userId or pair of username and host is required'); + } + + // Get 'host' parameter + const [host, hostErr] = $(params.host).optional.string().$; + if (hostErr) return rej('invalid host param'); + + if (userId === undefined && host === undefined) { + return rej('userId or pair of username and host is required'); + } + + // Get 'includeReplies' parameter + const [includeReplies = true, includeRepliesErr] = $(params.includeReplies).optional.boolean().$; + if (includeRepliesErr) return rej('invalid includeReplies param'); + + // Get 'withMedia' parameter + const [withMedia = false, withMediaErr] = $(params.withMedia).optional.boolean().$; + if (withMediaErr) return rej('invalid withMedia param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + if (sinceIdErr) return rej('invalid sinceId param'); + + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + if (untilIdErr) return rej('invalid untilId param'); + + // Get 'sinceDate' parameter + const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$; + if (sinceDateErr) throw 'invalid sinceDate param'; + + // Get 'untilDate' parameter + const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$; + if (untilDateErr) throw 'invalid untilDate param'; + + // Check if only one of sinceId, untilId, sinceDate, untilDate specified + if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) { + throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; + } + + const q = userId !== undefined + ? { _id: userId } + : { usernameLower: username.toLowerCase(), hostLower: getHostLower(host) } ; + + // Lookup user + const user = await User.findOne(q, { + fields: { + _id: true + } + }); + + if (user === null) { + return rej('user not found'); + } + + //#region Construct query + const sort = { + _id: -1 + }; + + const query = { + userId: user._id + } as any; + + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } else if (sinceDate) { + sort._id = 1; + query.createdAt = { + $gt: new Date(sinceDate) + }; + } else if (untilDate) { + query.createdAt = { + $lt: new Date(untilDate) + }; + } + + if (!includeReplies) { + query.replyId = null; + } + + if (withMedia) { + query.mediaIds = { + $exists: true, + $ne: [] + }; + } + //#endregion + + // Issue query + const notes = await Note + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + res(await Promise.all(notes.map(async (note) => + await pack(note, me) + ))); +}); diff --git a/src/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts similarity index 78% rename from src/api/endpoints/users/recommendation.ts rename to src/server/api/endpoints/users/recommendation.ts index 731d68a7b1..2de22da13e 100644 --- a/src/api/endpoints/users/recommendation.ts +++ b/src/server/api/endpoints/users/recommendation.ts @@ -3,8 +3,7 @@ */ const ms = require('ms'); import $ from 'cafy'; -import User from '../../models/user'; -import serialize from '../../serializers/user'; +import User, { pack } from '../../../../models/user'; import getFriends from '../../common/get-friends'; /** @@ -31,18 +30,24 @@ module.exports = (params, me) => new Promise(async (res, rej) => { _id: { $nin: followingIds }, - last_used_at: { - $gte: new Date(Date.now() - ms('7days')) - } + $or: [ + { + 'lastUsedAt': { + $gte: new Date(Date.now() - ms('7days')) + } + }, { + host: { $ne: null } + } + ] }, { limit: limit, skip: offset, sort: { - followers_count: -1 + followersCount: -1 } }); // Serialize res(await Promise.all(users.map(async user => - await serialize(user, me, { detail: true })))); + await pack(user, me, { detail: true })))); }); diff --git a/src/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts similarity index 87% rename from src/api/endpoints/users/search.ts rename to src/server/api/endpoints/users/search.ts index 73a5db47e2..da30f47c2a 100644 --- a/src/api/endpoints/users/search.ts +++ b/src/server/api/endpoints/users/search.ts @@ -3,9 +3,8 @@ */ import * as mongo from 'mongodb'; import $ from 'cafy'; -import User from '../../models/user'; -import serialize from '../../serializers/user'; -import config from '../../../conf'; +import User, { pack } from '../../../../models/user'; +import config from '../../../../config'; const escapeRegexp = require('escape-regexp'); /** @@ -42,7 +41,7 @@ async function byNative(res, rej, me, query, offset, max) { const users = await User .find({ $or: [{ - username_lower: new RegExp(escapedQuery.replace('@', '').toLowerCase()) + usernameLower: new RegExp(escapedQuery.replace('@', '').toLowerCase()) }, { name: new RegExp(escapedQuery) }] @@ -52,7 +51,7 @@ async function byNative(res, rej, me, query, offset, max) { // Serialize res(await Promise.all(users.map(async user => - await serialize(user, me, { detail: true })))); + await pack(user, me, { detail: true })))); } // Search by Elasticsearch @@ -94,6 +93,6 @@ async function byElasticsearch(res, rej, me, query, offset, max) { // Serialize res(await Promise.all(users.map(async user => - await serialize(user, me, { detail: true })))); + await pack(user, me, { detail: true })))); }); } diff --git a/src/api/endpoints/users/search_by_username.ts b/src/server/api/endpoints/users/search_by_username.ts similarity index 81% rename from src/api/endpoints/users/search_by_username.ts rename to src/server/api/endpoints/users/search_by_username.ts index 7f2f42f0a6..5f6ececff9 100644 --- a/src/api/endpoints/users/search_by_username.ts +++ b/src/server/api/endpoints/users/search_by_username.ts @@ -2,8 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../models/user'; -import serialize from '../../serializers/user'; +import User, { pack } from '../../../../models/user'; /** * Search a user by username @@ -27,7 +26,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { const users = await User .find({ - username_lower: new RegExp(query.toLowerCase()) + usernameLower: new RegExp(query.toLowerCase()) }, { limit: limit, skip: offset @@ -35,5 +34,5 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Serialize res(await Promise.all(users.map(async user => - await serialize(user, me, { detail: true })))); + await pack(user, me, { detail: true })))); }); diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts new file mode 100644 index 0000000000..7e7f5dc488 --- /dev/null +++ b/src/server/api/endpoints/users/show.ts @@ -0,0 +1,56 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User, { pack } from '../../../../models/user'; +import resolveRemoteUser from '../../../../remote/resolve-user'; + +const cursorOption = { fields: { data: false } }; + +/** + * Show a user + */ +module.exports = (params, me) => new Promise(async (res, rej) => { + let user; + + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).optional.id().$; + if (userIdErr) return rej('invalid userId param'); + + // Get 'username' parameter + const [username, usernameErr] = $(params.username).optional.string().$; + if (usernameErr) return rej('invalid username param'); + + // Get 'host' parameter + const [host, hostErr] = $(params.host).nullable.optional.string().$; + if (hostErr) return rej('invalid host param'); + + if (userId === undefined && typeof username !== 'string') { + return rej('userId or pair of username and host is required'); + } + + // Lookup user + if (typeof host === 'string') { + try { + user = await resolveRemoteUser(username, host, cursorOption); + } catch (e) { + console.warn(`failed to resolve remote user: ${e}`); + return rej('failed to resolve remote user'); + } + } else { + const q = userId !== undefined + ? { _id: userId } + : { usernameLower: username.toLowerCase(), host: null }; + + user = await User.findOne(q, cursorOption); + + if (user === null) { + return rej('user not found'); + } + } + + // Send response + res(await pack(user, me, { + detail: true + })); +}); diff --git a/src/api/server.ts b/src/server/api/index.ts similarity index 65% rename from src/api/server.ts rename to src/server/api/index.ts index c98167eb3e..5fbacd8a0e 100644 --- a/src/api/server.ts +++ b/src/server/api/index.ts @@ -7,7 +7,6 @@ import * as bodyParser from 'body-parser'; import * as cors from 'cors'; import * as multer from 'multer'; -// import authenticate from './authenticate'; import endpoints from './endpoints'; /** @@ -19,11 +18,14 @@ app.disable('x-powered-by'); app.set('etag', false); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json({ - type: ['application/json', 'text/plain'] -})); -app.use(cors({ - origin: true + type: ['application/json', 'text/plain'], + verify: (req, res, buf, encoding) => { + if (buf && buf.length) { + (req as any).rawBody = buf.toString(encoding || 'utf8'); + } + } })); +app.use(cors()); app.get('/', (req, res) => { res.send('YEE HAW'); @@ -35,7 +37,7 @@ app.get('/', (req, res) => { endpoints.forEach(endpoint => endpoint.withFile ? app.post(`/${endpoint.name}`, - endpoint.withFile ? multer({ dest: 'uploads/' }).single('file') : null, + endpoint.withFile ? multer({ storage: multer.diskStorage({}) }).single('file') : null, require('./api-handler').default.bind(null, endpoint)) : app.post(`/${endpoint.name}`, require('./api-handler').default.bind(null, endpoint)) @@ -44,14 +46,9 @@ endpoints.forEach(endpoint => app.post('/signup', require('./private/signup').default); app.post('/signin', require('./private/signin').default); -app.use((req, res, next) => { - // req.headers['cookie'] は常に string ですが、型定義の都合上 - // string | string[] になっているので string を明示しています - res.locals.user = ((req.headers['cookie'] as string || '').match(/i=(!\w+)/) || [null, null])[1]; - next(); -}); - require('./service/github')(app); require('./service/twitter')(app); +require('./bot/interfaces/line')(app); + module.exports = app; diff --git a/src/api/limitter.ts b/src/server/api/limitter.ts similarity index 75% rename from src/api/limitter.ts rename to src/server/api/limitter.ts index 10c50c3403..b84e16ecde 100644 --- a/src/api/limitter.ts +++ b/src/server/api/limitter.ts @@ -1,12 +1,13 @@ import * as Limiter from 'ratelimiter'; import * as debug from 'debug'; -import limiterDB from '../db/redis'; +import limiterDB from '../../db/redis'; import { Endpoint } from './endpoints'; -import { IAuthContext } from './authenticate'; +import getAcct from '../../acct/render'; +import { IUser } from '../../models/user'; const log = debug('misskey:limitter'); -export default (endpoint: Endpoint, ctx: IAuthContext) => new Promise((ok, reject) => { +export default (endpoint: Endpoint, user: IUser) => new Promise((ok, reject) => { const limitation = endpoint.limit; const key = limitation.hasOwnProperty('key') @@ -31,7 +32,7 @@ export default (endpoint: Endpoint, ctx: IAuthContext) => new Promise((ok, rejec // Short-term limit function min() { const minIntervalLimiter = new Limiter({ - id: `${ctx.user._id}:${key}:min`, + id: `${user._id}:${key}:min`, duration: limitation.minInterval, max: 1, db: limiterDB @@ -42,7 +43,7 @@ export default (endpoint: Endpoint, ctx: IAuthContext) => new Promise((ok, rejec return reject('ERR'); } - log(`@${ctx.user.username} ${endpoint.name} min remaining: ${info.remaining}`); + log(`@${getAcct(user)} ${endpoint.name} min remaining: ${info.remaining}`); if (info.remaining === 0) { reject('BRIEF_REQUEST_INTERVAL'); @@ -59,7 +60,7 @@ export default (endpoint: Endpoint, ctx: IAuthContext) => new Promise((ok, rejec // Long term limit function max() { const limiter = new Limiter({ - id: `${ctx.user._id}:${key}`, + id: `${user._id}:${key}`, duration: limitation.duration, max: limitation.max, db: limiterDB @@ -70,7 +71,7 @@ export default (endpoint: Endpoint, ctx: IAuthContext) => new Promise((ok, rejec return reject('ERR'); } - log(`@${ctx.user.username} ${endpoint.name} max remaining: ${info.remaining}`); + log(`@${getAcct(user)} ${endpoint.name} max remaining: ${info.remaining}`); if (info.remaining === 0) { reject('RATE_LIMIT_EXCEEDED'); diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts new file mode 100644 index 0000000000..665ee21ebd --- /dev/null +++ b/src/server/api/private/signin.ts @@ -0,0 +1,89 @@ +import * as express from 'express'; +import * as bcrypt from 'bcryptjs'; +import * as speakeasy from 'speakeasy'; +import User, { ILocalUser } from '../../../models/user'; +import Signin, { pack } from '../../../models/signin'; +import event from '../../../publishers/stream'; +import signin from '../common/signin'; +import config from '../../../config'; + +export default async (req: express.Request, res: express.Response) => { + res.header('Access-Control-Allow-Origin', config.url); + res.header('Access-Control-Allow-Credentials', 'true'); + + const username = req.body['username']; + const password = req.body['password']; + const token = req.body['token']; + + if (typeof username != 'string') { + res.sendStatus(400); + return; + } + + if (typeof password != 'string') { + res.sendStatus(400); + return; + } + + if (token != null && typeof token != 'string') { + res.sendStatus(400); + return; + } + + // Fetch user + const user = await User.findOne({ + usernameLower: username.toLowerCase(), + host: null + }, { + fields: { + data: false, + 'profile': false + } + }) as ILocalUser; + + if (user === null) { + res.status(404).send({ + error: 'user not found' + }); + return; + } + + // Compare password + const same = await bcrypt.compare(password, user.password); + + if (same) { + if (user.twoFactorEnabled) { + const verified = (speakeasy as any).totp.verify({ + secret: user.twoFactorSecret, + encoding: 'base32', + token: token + }); + + if (verified) { + signin(res, user, false); + } else { + res.status(400).send({ + error: 'invalid token' + }); + } + } else { + signin(res, user, false); + } + } else { + res.status(400).send({ + error: 'incorrect password' + }); + } + + // Append signin history + const record = await Signin.insert({ + createdAt: new Date(), + userId: user._id, + ip: req.ip, + headers: req.headers, + success: same + }); + + // Publish signin event + event(user._id, 'signin', await pack(record)); +}; diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts new file mode 100644 index 0000000000..f441e1b754 --- /dev/null +++ b/src/server/api/private/signup.ts @@ -0,0 +1,147 @@ +import * as uuid from 'uuid'; +import * as express from 'express'; +import * as bcrypt from 'bcryptjs'; +import { generate as generateKeypair } from '../../../crypto_key'; +import recaptcha = require('recaptcha-promise'); +import User, { IUser, validateUsername, validatePassword, pack } from '../../../models/user'; +import generateUserToken from '../common/generate-native-user-token'; +import config from '../../../config'; + +recaptcha.init({ + secret_key: config.recaptcha.secret_key +}); + +const home = { + left: [ + 'profile', + 'calendar', + 'activity', + 'rss', + 'trends', + 'photo-stream', + 'version' + ], + right: [ + 'broadcast', + 'notifications', + 'users', + 'polls', + 'server', + 'donation', + 'nav', + 'tips' + ] +}; + +export default async (req: express.Request, res: express.Response) => { + // Verify recaptcha + // ただしテスト時はこの機構は障害となるため無効にする + if (process.env.NODE_ENV !== 'test') { + const success = await recaptcha(req.body['g-recaptcha-response']); + + if (!success) { + res.status(400).send('recaptcha-failed'); + return; + } + } + + const username = req.body['username']; + const password = req.body['password']; + + // Validate username + if (!validateUsername(username)) { + res.sendStatus(400); + return; + } + + // Validate password + if (!validatePassword(password)) { + res.sendStatus(400); + return; + } + + // Fetch exist user that same username + const usernameExist = await User + .count({ + usernameLower: username.toLowerCase(), + host: null + }, { + limit: 1 + }); + + // Check username already used + if (usernameExist !== 0) { + res.sendStatus(400); + return; + } + + // Generate hash of password + const salt = await bcrypt.genSalt(8); + const hash = await bcrypt.hash(password, salt); + + // Generate secret + const secret = generateUserToken(); + + //#region Construct home data + const homeData = []; + + home.left.forEach(widget => { + homeData.push({ + name: widget, + id: uuid(), + place: 'left', + data: {} + }); + }); + + home.right.forEach(widget => { + homeData.push({ + name: widget, + id: uuid(), + place: 'right', + data: {} + }); + }); + //#endregion + + // Create account + const account: IUser = await User.insert({ + avatarId: null, + bannerId: null, + createdAt: new Date(), + description: null, + followersCount: 0, + followingCount: 0, + name: null, + notesCount: 0, + driveCapacity: 1024 * 1024 * 128, // 128MiB + username: username, + usernameLower: username.toLowerCase(), + host: null, + hostLower: null, + keypair: generateKeypair(), + token: secret, + email: null, + links: null, + password: hash, + profile: { + bio: null, + birthday: null, + blood: null, + gender: null, + handedness: null, + height: null, + location: null, + weight: null + }, + settings: { + autoWatch: true + }, + clientSettings: { + home: homeData + } + }); + + // Response + res.send(await pack(account)); +}; diff --git a/src/api/service/github.ts b/src/server/api/service/github.ts similarity index 91% rename from src/api/service/github.ts rename to src/server/api/service/github.ts index a631808ba5..bc8d3c6a7d 100644 --- a/src/api/service/github.ts +++ b/src/server/api/service/github.ts @@ -2,14 +2,16 @@ import * as EventEmitter from 'events'; import * as express from 'express'; import * as request from 'request'; const crypto = require('crypto'); -import User from '../models/user'; -import config from '../../conf'; + +import User from '../../../models/user'; +import createNote from '../../../services/note/create'; +import config from '../../../config'; module.exports = async (app: express.Application) => { if (config.github_bot == null) return; const bot = await User.findOne({ - username_lower: config.github_bot.username.toLowerCase() + usernameLower: config.github_bot.username.toLowerCase() }); if (bot == null) { @@ -17,7 +19,7 @@ module.exports = async (app: express.Application) => { return; } - const post = text => require('../endpoints/posts/create')({ text }, bot); + const post = text => createNote(bot, { text }); const handler = new EventEmitter(); @@ -111,12 +113,12 @@ module.exports = async (app: express.Application) => { handler.on('watch', event => { const sender = event.sender; - post(`Starred by **${sender.login}**`); + post(`⭐️ Starred by **${sender.login}** ⭐️`); }); handler.on('fork', event => { const repo = event.forkee; - post(`Forked:\n${repo.html_url}`); + post(`🍴 Forked:\n${repo.html_url} 🍴`); }); handler.on('pull_request', event => { diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts new file mode 100644 index 0000000000..e5239fa171 --- /dev/null +++ b/src/server/api/service/twitter.ts @@ -0,0 +1,176 @@ +import * as express from 'express'; +import * as cookie from 'cookie'; +import * as uuid from 'uuid'; +// import * as Twitter from 'twitter'; +// const Twitter = require('twitter'); +import autwh from 'autwh'; +import redis from '../../../db/redis'; +import User, { pack } from '../../../models/user'; +import event from '../../../publishers/stream'; +import config from '../../../config'; +import signin from '../common/signin'; + +module.exports = (app: express.Application) => { + function getUserToken(req: express.Request) { + // req.headers['cookie'] は常に string ですが、型定義の都合上 + // string | string[] になっているので string を明示しています + return ((req.headers['cookie'] as string || '').match(/i=(!\w+)/) || [null, null])[1]; + } + + function compareOrigin(req: express.Request) { + function normalizeUrl(url: string) { + return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url; + } + + // req.headers['referer'] は常に string ですが、型定義の都合上 + // string | string[] になっているので string を明示しています + const referer = req.headers['referer'] as string; + + return (normalizeUrl(referer) == normalizeUrl(config.url)); + } + + app.get('/disconnect/twitter', async (req, res): Promise<any> => { + if (!compareOrigin(req)) { + res.status(400).send('invalid origin'); + return; + } + + const userToken = getUserToken(req); + if (userToken == null) return res.send('plz signin'); + + const user = await User.findOneAndUpdate({ + host: null, + 'token': userToken + }, { + $set: { + 'twitter': null + } + }); + + res.send(`Twitterの連携を解除しました :v:`); + + // Publish i updated event + event(user._id, 'i_updated', await pack(user, user, { + detail: true, + includeSecrets: true + })); + }); + + if (config.twitter == null) { + app.get('/connect/twitter', (req, res) => { + res.send('現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)'); + }); + + app.get('/signin/twitter', (req, res) => { + res.send('現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)'); + }); + + return; + } + + const twAuth = autwh({ + consumerKey: config.twitter.consumer_key, + consumerSecret: config.twitter.consumer_secret, + callbackUrl: `${config.url}/api/tw/cb` + }); + + app.get('/connect/twitter', async (req, res): Promise<any> => { + if (!compareOrigin(req)) { + res.status(400).send('invalid origin'); + return; + } + + const userToken = getUserToken(req); + if (userToken == null) return res.send('plz signin'); + + const ctx = await twAuth.begin(); + redis.set(userToken, JSON.stringify(ctx)); + res.redirect(ctx.url); + }); + + app.get('/signin/twitter', async (req, res): Promise<any> => { + const ctx = await twAuth.begin(); + + const sessid = uuid(); + + redis.set(sessid, JSON.stringify(ctx)); + + const expires = 1000 * 60 * 60; // 1h + res.cookie('signin_with_twitter_session_id', sessid, { + path: '/', + domain: `.${config.host}`, + secure: config.url.substr(0, 5) === 'https', + httpOnly: true, + expires: new Date(Date.now() + expires), + maxAge: expires + }); + + res.redirect(ctx.url); + }); + + app.get('/tw/cb', (req, res): any => { + const userToken = getUserToken(req); + + if (userToken == null) { + // req.headers['cookie'] は常に string ですが、型定義の都合上 + // string | string[] になっているので string を明示しています + const cookies = cookie.parse((req.headers['cookie'] as string || '')); + + const sessid = cookies['signin_with_twitter_session_id']; + + if (sessid == undefined) { + res.status(400).send('invalid session'); + return; + } + + redis.get(sessid, async (_, ctx) => { + const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier); + + const user = await User.findOne({ + host: null, + 'twitter.userId': result.userId + }); + + if (user == null) { + res.status(404).send(`@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`); + return; + } + + signin(res, user, true); + }); + } else { + const verifier = req.query.oauth_verifier; + + if (verifier == null) { + res.status(400).send('invalid session'); + return; + } + + redis.get(userToken, async (_, ctx) => { + const result = await twAuth.done(JSON.parse(ctx), verifier); + + const user = await User.findOneAndUpdate({ + host: null, + 'token': userToken + }, { + $set: { + 'twitter': { + accessToken: result.accessToken, + accessTokenSecret: result.accessTokenSecret, + userId: result.userId, + screenName: result.screenName + } + } + }); + + res.send(`Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`); + + // Publish i updated event + event(user._id, 'i_updated', await pack(user, user, { + detail: true, + includeSecrets: true + })); + }); + } + }); +}; diff --git a/src/server/api/stream/channel.ts b/src/server/api/stream/channel.ts new file mode 100644 index 0000000000..cb04278237 --- /dev/null +++ b/src/server/api/stream/channel.ts @@ -0,0 +1,14 @@ +import * as websocket from 'websocket'; +import * as redis from 'redis'; +import { ParsedUrlQuery } from 'querystring'; + +export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient): void { + const q = request.resourceURL.query as ParsedUrlQuery; + const channel = q.channel; + + // Subscribe channel stream + subscriber.subscribe(`misskey:channel-stream:${channel}`); + subscriber.on('message', (_, data) => { + connection.send(data); + }); +} diff --git a/src/server/api/stream/drive.ts b/src/server/api/stream/drive.ts new file mode 100644 index 0000000000..c97ab80dcc --- /dev/null +++ b/src/server/api/stream/drive.ts @@ -0,0 +1,10 @@ +import * as websocket from 'websocket'; +import * as redis from 'redis'; + +export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void { + // Subscribe drive stream + subscriber.subscribe(`misskey:drive-stream:${user._id}`); + subscriber.on('message', (_, data) => { + connection.send(data); + }); +} diff --git a/src/server/api/stream/home.ts b/src/server/api/stream/home.ts new file mode 100644 index 0000000000..e9c0924f31 --- /dev/null +++ b/src/server/api/stream/home.ts @@ -0,0 +1,113 @@ +import * as websocket from 'websocket'; +import * as redis from 'redis'; +import * as debug from 'debug'; + +import User, { IUser } from '../../../models/user'; +import Mute from '../../../models/mute'; +import { pack as packNote } from '../../../models/note'; +import readNotification from '../common/read-notification'; +import call from '../call'; +import { IApp } from '../../../models/app'; + +const log = debug('misskey'); + +export default async function( + request: websocket.request, + connection: websocket.connection, + subscriber: redis.RedisClient, + user: IUser, + app: IApp +) { + // Subscribe Home stream channel + subscriber.subscribe(`misskey:user-stream:${user._id}`); + + const mute = await Mute.find({ + muterId: user._id, + deletedAt: { $exists: false } + }); + const mutedUserIds = mute.map(m => m.muteeId.toString()); + + subscriber.on('message', async (channel, data) => { + switch (channel.split(':')[1]) { + case 'user-stream': + try { + const x = JSON.parse(data); + + if (x.type == 'note') { + if (mutedUserIds.indexOf(x.body.userId) != -1) { + return; + } + if (x.body.reply != null && mutedUserIds.indexOf(x.body.reply.userId) != -1) { + return; + } + if (x.body.renote != null && mutedUserIds.indexOf(x.body.renote.userId) != -1) { + return; + } + } else if (x.type == 'notification') { + if (mutedUserIds.indexOf(x.body.userId) != -1) { + return; + } + } + + connection.send(data); + } catch (e) { + connection.send(data); + } + break; + case 'note-stream': + const noteId = channel.split(':')[2]; + log(`RECEIVED: ${noteId} ${data} by @${user.username}`); + const note = await packNote(noteId, user, { + detail: true + }); + connection.send(JSON.stringify({ + type: 'note-updated', + body: { + note: note + } + })); + break; + } + }); + + connection.on('message', data => { + const msg = JSON.parse(data.utf8Data); + + switch (msg.type) { + case 'api': + call(msg.endpoint, user, app, msg.data).then(res => { + connection.send(JSON.stringify({ + type: `api-res:${msg.id}`, + body: { res } + })); + }).catch(e => { + connection.send(JSON.stringify({ + type: `api-res:${msg.id}`, + body: { e } + })); + }); + break; + + case 'alive': + // Update lastUsedAt + User.update({ _id: user._id }, { + $set: { + 'lastUsedAt': new Date() + } + }); + break; + + case 'read_notification': + if (!msg.id) return; + readNotification(user._id, msg.id); + break; + + case 'capture': + if (!msg.id) return; + const noteId = msg.id; + log(`CAPTURE: ${noteId} by @${user.username}`); + subscriber.subscribe(`misskey:note-stream:${noteId}`); + break; + } + }); +} diff --git a/src/server/api/stream/messaging-index.ts b/src/server/api/stream/messaging-index.ts new file mode 100644 index 0000000000..c1b2fbc806 --- /dev/null +++ b/src/server/api/stream/messaging-index.ts @@ -0,0 +1,10 @@ +import * as websocket from 'websocket'; +import * as redis from 'redis'; + +export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void { + // Subscribe messaging index stream + subscriber.subscribe(`misskey:messaging-index-stream:${user._id}`); + subscriber.on('message', (_, data) => { + connection.send(data); + }); +} diff --git a/src/api/stream/messaging.ts b/src/server/api/stream/messaging.ts similarity index 64% rename from src/api/stream/messaging.ts rename to src/server/api/stream/messaging.ts index 3f505cfafa..3e6c2cd509 100644 --- a/src/api/stream/messaging.ts +++ b/src/server/api/stream/messaging.ts @@ -1,9 +1,11 @@ import * as websocket from 'websocket'; import * as redis from 'redis'; import read from '../common/read-messaging-message'; +import { ParsedUrlQuery } from 'querystring'; -export default function messagingStream(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void { - const otherparty = request.resourceURL.query.otherparty; +export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void { + const q = request.resourceURL.query as ParsedUrlQuery; + const otherparty = q.otherparty as string; // Subscribe messaging stream subscriber.subscribe(`misskey:messaging-stream:${user._id}-${otherparty}`); diff --git a/src/server/api/stream/othello-game.ts b/src/server/api/stream/othello-game.ts new file mode 100644 index 0000000000..841e542610 --- /dev/null +++ b/src/server/api/stream/othello-game.ts @@ -0,0 +1,333 @@ +import * as websocket from 'websocket'; +import * as redis from 'redis'; +import * as CRC32 from 'crc-32'; +import OthelloGame, { pack } from '../../../models/othello-game'; +import { publishOthelloGameStream } from '../../../publishers/stream'; +import Othello from '../../../othello/core'; +import * as maps from '../../../othello/maps'; +import { ParsedUrlQuery } from 'querystring'; + +export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user?: any): void { + const q = request.resourceURL.query as ParsedUrlQuery; + const gameId = q.game; + + // Subscribe game stream + subscriber.subscribe(`misskey:othello-game-stream:${gameId}`); + subscriber.on('message', (_, data) => { + connection.send(data); + }); + + connection.on('message', async (data) => { + const msg = JSON.parse(data.utf8Data); + + switch (msg.type) { + case 'accept': + accept(true); + break; + + case 'cancel-accept': + accept(false); + break; + + case 'update-settings': + if (msg.settings == null) return; + updateSettings(msg.settings); + break; + + case 'init-form': + if (msg.body == null) return; + initForm(msg.body); + break; + + case 'update-form': + if (msg.id == null || msg.value === undefined) return; + updateForm(msg.id, msg.value); + break; + + case 'message': + if (msg.body == null) return; + message(msg.body); + break; + + case 'set': + if (msg.pos == null) return; + set(msg.pos); + break; + + case 'check': + if (msg.crc32 == null) return; + check(msg.crc32); + break; + } + }); + + async function updateSettings(settings) { + const game = await OthelloGame.findOne({ _id: gameId }); + + if (game.isStarted) return; + if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; + if (game.user1Id.equals(user._id) && game.user1Accepted) return; + if (game.user2Id.equals(user._id) && game.user2Accepted) return; + + await OthelloGame.update({ _id: gameId }, { + $set: { + settings + } + }); + + publishOthelloGameStream(gameId, 'update-settings', settings); + } + + async function initForm(form) { + const game = await OthelloGame.findOne({ _id: gameId }); + + if (game.isStarted) return; + if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; + + const set = game.user1Id.equals(user._id) ? { + form1: form + } : { + form2: form + }; + + await OthelloGame.update({ _id: gameId }, { + $set: set + }); + + publishOthelloGameStream(gameId, 'init-form', { + userId: user._id, + form + }); + } + + async function updateForm(id, value) { + const game = await OthelloGame.findOne({ _id: gameId }); + + if (game.isStarted) return; + if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; + + const form = game.user1Id.equals(user._id) ? game.form2 : game.form1; + + const item = form.find(i => i.id == id); + + if (item == null) return; + + item.value = value; + + const set = game.user1Id.equals(user._id) ? { + form2: form + } : { + form1: form + }; + + await OthelloGame.update({ _id: gameId }, { + $set: set + }); + + publishOthelloGameStream(gameId, 'update-form', { + userId: user._id, + id, + value + }); + } + + async function message(message) { + message.id = Math.random(); + publishOthelloGameStream(gameId, 'message', { + userId: user._id, + message + }); + } + + async function accept(accept: boolean) { + const game = await OthelloGame.findOne({ _id: gameId }); + + if (game.isStarted) return; + + let bothAccepted = false; + + if (game.user1Id.equals(user._id)) { + await OthelloGame.update({ _id: gameId }, { + $set: { + user1Accepted: accept + } + }); + + publishOthelloGameStream(gameId, 'change-accepts', { + user1: accept, + user2: game.user2Accepted + }); + + if (accept && game.user2Accepted) bothAccepted = true; + } else if (game.user2Id.equals(user._id)) { + await OthelloGame.update({ _id: gameId }, { + $set: { + user2Accepted: accept + } + }); + + publishOthelloGameStream(gameId, 'change-accepts', { + user1: game.user1Accepted, + user2: accept + }); + + if (accept && game.user1Accepted) bothAccepted = true; + } else { + return; + } + + if (bothAccepted) { + // 3秒後、まだacceptされていたらゲーム開始 + setTimeout(async () => { + const freshGame = await OthelloGame.findOne({ _id: gameId }); + if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return; + if (!freshGame.user1Accepted || !freshGame.user2Accepted) return; + + let bw: number; + if (freshGame.settings.bw == 'random') { + bw = Math.random() > 0.5 ? 1 : 2; + } else { + bw = freshGame.settings.bw as number; + } + + function getRandomMap() { + const mapCount = Object.entries(maps).length; + const rnd = Math.floor(Math.random() * mapCount); + return Object.entries(maps).find((x, i) => i == rnd)[1].data; + } + + const map = freshGame.settings.map != null ? freshGame.settings.map : getRandomMap(); + + await OthelloGame.update({ _id: gameId }, { + $set: { + startedAt: new Date(), + isStarted: true, + black: bw, + 'settings.map': map + } + }); + + //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 + const o = new Othello(map, { + isLlotheo: freshGame.settings.isLlotheo, + canPutEverywhere: freshGame.settings.canPutEverywhere, + loopedBoard: freshGame.settings.loopedBoard + }); + + if (o.isEnded) { + let winner; + if (o.winner === true) { + winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id; + } else if (o.winner === false) { + winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id; + } else { + winner = null; + } + + await OthelloGame.update({ + _id: gameId + }, { + $set: { + isEnded: true, + winnerId: winner + } + }); + + publishOthelloGameStream(gameId, 'ended', { + winnerId: winner, + game: await pack(gameId, user) + }); + } + //#endregion + + publishOthelloGameStream(gameId, 'started', await pack(gameId, user)); + }, 3000); + } + } + + // 石を打つ + async function set(pos) { + const game = await OthelloGame.findOne({ _id: gameId }); + + if (!game.isStarted) return; + if (game.isEnded) return; + if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; + + const o = new Othello(game.settings.map, { + isLlotheo: game.settings.isLlotheo, + canPutEverywhere: game.settings.canPutEverywhere, + loopedBoard: game.settings.loopedBoard + }); + + game.logs.forEach(log => { + o.put(log.color, log.pos); + }); + + const myColor = + (game.user1Id.equals(user._id) && game.black == 1) || (game.user2Id.equals(user._id) && game.black == 2) + ? true + : false; + + if (!o.canPut(myColor, pos)) return; + o.put(myColor, pos); + + let winner; + if (o.isEnded) { + if (o.winner === true) { + winner = game.black == 1 ? game.user1Id : game.user2Id; + } else if (o.winner === false) { + winner = game.black == 1 ? game.user2Id : game.user1Id; + } else { + winner = null; + } + } + + const log = { + at: new Date(), + color: myColor, + pos + }; + + const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()); + + await OthelloGame.update({ + _id: gameId + }, { + $set: { + crc32, + isEnded: o.isEnded, + winnerId: winner + }, + $push: { + logs: log + } + }); + + publishOthelloGameStream(gameId, 'set', Object.assign(log, { + next: o.turn + })); + + if (o.isEnded) { + publishOthelloGameStream(gameId, 'ended', { + winnerId: winner, + game: await pack(gameId, user) + }); + } + } + + async function check(crc32) { + const game = await OthelloGame.findOne({ _id: gameId }); + + if (!game.isStarted) return; + + // 互換性のため + if (game.crc32 == null) return; + + if (crc32 !== game.crc32) { + connection.send(JSON.stringify({ + type: 'rescue', + body: await pack(game, user) + })); + } + } +} diff --git a/src/server/api/stream/othello.ts b/src/server/api/stream/othello.ts new file mode 100644 index 0000000000..fa62b05836 --- /dev/null +++ b/src/server/api/stream/othello.ts @@ -0,0 +1,29 @@ +import * as mongo from 'mongodb'; +import * as websocket from 'websocket'; +import * as redis from 'redis'; +import Matching, { pack } from '../../../models/othello-matching'; +import publishUserStream from '../../../publishers/stream'; + +export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void { + // Subscribe othello stream + subscriber.subscribe(`misskey:othello-stream:${user._id}`); + subscriber.on('message', (_, data) => { + connection.send(data); + }); + + connection.on('message', async (data) => { + const msg = JSON.parse(data.utf8Data); + + switch (msg.type) { + case 'ping': + if (msg.id == null) return; + const matching = await Matching.findOne({ + parentId: user._id, + childId: new mongo.ObjectID(msg.id) + }); + if (matching == null) return; + publishUserStream(matching.childId, 'othello_invited', await pack(matching, matching.childId)); + break; + } + }); +} diff --git a/src/server/api/stream/requests.ts b/src/server/api/stream/requests.ts new file mode 100644 index 0000000000..d7bb5e6c5c --- /dev/null +++ b/src/server/api/stream/requests.ts @@ -0,0 +1,19 @@ +import * as websocket from 'websocket'; +import Xev from 'xev'; + +const ev = new Xev(); + +export default function(request: websocket.request, connection: websocket.connection): void { + const onRequest = request => { + connection.send(JSON.stringify({ + type: 'request', + body: request + })); + }; + + ev.addListener('request', onRequest); + + connection.on('close', () => { + ev.removeListener('request', onRequest); + }); +} diff --git a/src/api/stream/server.ts b/src/server/api/stream/server.ts similarity index 70% rename from src/api/stream/server.ts rename to src/server/api/stream/server.ts index 6de5337499..4ca2ad1b10 100644 --- a/src/api/stream/server.ts +++ b/src/server/api/stream/server.ts @@ -3,7 +3,7 @@ import Xev from 'xev'; const ev = new Xev(); -export default function homeStream(request: websocket.request, connection: websocket.connection): void { +export default function(request: websocket.request, connection: websocket.connection): void { const onStats = stats => { connection.send(JSON.stringify({ type: 'stats', @@ -14,7 +14,6 @@ export default function homeStream(request: websocket.request, connection: webso ev.addListener('stats', onStats); connection.on('close', () => { - console.log('yooo'); ev.removeListener('stats', onStats); }); } diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts new file mode 100644 index 0000000000..d586d7c08f --- /dev/null +++ b/src/server/api/streaming.ts @@ -0,0 +1,81 @@ +import * as http from 'http'; +import * as websocket from 'websocket'; +import * as redis from 'redis'; +import config from '../../config'; + +import homeStream from './stream/home'; +import driveStream from './stream/drive'; +import messagingStream from './stream/messaging'; +import messagingIndexStream from './stream/messaging-index'; +import othelloGameStream from './stream/othello-game'; +import othelloStream from './stream/othello'; +import serverStream from './stream/server'; +import requestsStream from './stream/requests'; +import channelStream from './stream/channel'; +import { ParsedUrlQuery } from 'querystring'; +import authenticate from './authenticate'; + +module.exports = (server: http.Server) => { + /** + * Init websocket server + */ + const ws = new websocket.server({ + httpServer: server + }); + + ws.on('request', async (request) => { + const connection = request.accept(); + + if (request.resourceURL.pathname === '/server') { + serverStream(request, connection); + return; + } + + if (request.resourceURL.pathname === '/requests') { + requestsStream(request, connection); + return; + } + + // Connect to Redis + const subscriber = redis.createClient( + config.redis.port, config.redis.host); + + connection.on('close', () => { + subscriber.unsubscribe(); + subscriber.quit(); + }); + + if (request.resourceURL.pathname === '/channel') { + channelStream(request, connection, subscriber); + return; + } + + const q = request.resourceURL.query as ParsedUrlQuery; + const [user, app] = await authenticate(q.i as string); + + if (request.resourceURL.pathname === '/othello-game') { + othelloGameStream(request, connection, subscriber, user); + return; + } + + if (user == null) { + connection.send('authentication-failed'); + connection.close(); + return; + } + + const channel = + request.resourceURL.pathname === '/' ? homeStream : + request.resourceURL.pathname === '/drive' ? driveStream : + request.resourceURL.pathname === '/messaging' ? messagingStream : + request.resourceURL.pathname === '/messaging-index' ? messagingIndexStream : + request.resourceURL.pathname === '/othello' ? othelloStream : + null; + + if (channel !== null) { + channel(request, connection, subscriber, user, app); + } else { + connection.close(); + } + }); +}; diff --git a/src/file/assets/avatar.jpg b/src/server/file/assets/avatar.jpg similarity index 100% rename from src/file/assets/avatar.jpg rename to src/server/file/assets/avatar.jpg diff --git a/src/file/assets/bad-egg.png b/src/server/file/assets/bad-egg.png similarity index 100% rename from src/file/assets/bad-egg.png rename to src/server/file/assets/bad-egg.png diff --git a/src/file/assets/dummy.png b/src/server/file/assets/dummy.png similarity index 100% rename from src/file/assets/dummy.png rename to src/server/file/assets/dummy.png diff --git a/src/server/file/assets/not-an-image.png b/src/server/file/assets/not-an-image.png new file mode 100644 index 0000000000..bf98b293f7 Binary files /dev/null and b/src/server/file/assets/not-an-image.png differ diff --git a/src/server/file/assets/thumbnail-not-available.png b/src/server/file/assets/thumbnail-not-available.png new file mode 100644 index 0000000000..f960ce4d00 Binary files /dev/null and b/src/server/file/assets/thumbnail-not-available.png differ diff --git a/src/server/file/index.ts b/src/server/file/index.ts new file mode 100644 index 0000000000..8d21b0ba46 --- /dev/null +++ b/src/server/file/index.ts @@ -0,0 +1,167 @@ +/** + * File Server + */ + +import * as fs from 'fs'; +import * as express from 'express'; +import * as bodyParser from 'body-parser'; +import * as cors from 'cors'; +import * as mongodb from 'mongodb'; +import * as _gm from 'gm'; +import * as stream from 'stream'; + +import DriveFile, { getGridFSBucket } from '../../models/drive-file'; + +const gm = _gm.subClass({ + imageMagick: true +}); + +/** + * Init app + */ +const app = express(); + +app.disable('x-powered-by'); +app.locals.cache = true; +app.use(bodyParser.urlencoded({ extended: true })); +app.use(cors()); + +/** + * Statics + */ +app.use('/assets', express.static(`${__dirname}/assets`, { + maxAge: 1000 * 60 * 60 * 24 * 365 // 一年 +})); + +app.get('/', (req, res) => { + res.send('yee haw'); +}); + +app.get('/default-avatar.jpg', (req, res) => { + const file = fs.createReadStream(`${__dirname}/assets/avatar.jpg`); + send(file, 'image/jpeg', req, res); +}); + +app.get('/app-default.jpg', (req, res) => { + const file = fs.createReadStream(`${__dirname}/assets/dummy.png`); + send(file, 'image/png', req, res); +}); + +interface ISend { + contentType: string; + stream: stream.Readable; +} + +function thumbnail(data: stream.Readable, type: string, resize: number): ISend { + const readable: stream.Readable = (() => { + // 画像ではない場合 + if (!/^image\/.*$/.test(type)) { + // 使わないことにしたストリームはしっかり取り壊しておく + data.destroy(); + return fs.createReadStream(`${__dirname}/assets/not-an-image.png`); + } + + const imageType = type.split('/')[1]; + + // 画像でもPNGかJPEGでないならダメ + if (imageType != 'png' && imageType != 'jpeg') { + // 使わないことにしたストリームはしっかり取り壊しておく + data.destroy(); + return fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`); + } + + return data; + })(); + + let g = gm(readable); + + if (resize) { + g = g.resize(resize, resize); + } + + const stream = g + .compress('jpeg') + .quality(80) + .interlace('line') + .stream(); + + return { + contentType: 'image/jpeg', + stream + }; +} + +const commonReadableHandlerGenerator = (req: express.Request, res: express.Response) => (e: Error): void => { + console.dir(e); + req.destroy(); + res.destroy(e); +}; + +function send(readable: stream.Readable, type: string, req: express.Request, res: express.Response): void { + readable.on('error', commonReadableHandlerGenerator(req, res)); + + const data = ((): ISend => { + if (req.query.thumbnail !== undefined) { + return thumbnail(readable, type, req.query.size); + } + return { + contentType: type, + stream: readable + }; + })(); + + if (readable !== data.stream) { + data.stream.on('error', commonReadableHandlerGenerator(req, res)); + } + + if (req.query.download !== undefined) { + res.header('Content-Disposition', 'attachment'); + } + + res.header('Content-Type', data.contentType); + + data.stream.pipe(res); + + data.stream.on('end', () => { + res.end(); + }); +} + +async function sendFileById(req: express.Request, res: express.Response): Promise<void> { + // Validate id + if (!mongodb.ObjectID.isValid(req.params.id)) { + res.status(400).send('incorrect id'); + return; + } + + const fileId = new mongodb.ObjectID(req.params.id); + + // Fetch (drive) file + const file = await DriveFile.findOne({ _id: fileId }); + + // validate name + if (req.params.name !== undefined && req.params.name !== file.filename) { + res.status(404).send('there is no file has given name'); + return; + } + + if (file == null) { + res.status(404).sendFile(`${__dirname}/assets/dummy.png`); + return; + } + + const bucket = await getGridFSBucket(); + + const readable = bucket.openDownloadStream(fileId); + + send(readable, file.contentType, req, res); +} + +/** + * Routing + */ + +app.get('/:id', sendFileById); +app.get('/:id/:name', sendFileById); + +module.exports = app; diff --git a/src/server/index.ts b/src/server/index.ts new file mode 100644 index 0000000000..abb8992da5 --- /dev/null +++ b/src/server/index.ts @@ -0,0 +1,94 @@ +/** + * Core Server + */ + +import * as fs from 'fs'; +import * as http from 'http'; +import * as https from 'https'; +import * as express from 'express'; +import * as morgan from 'morgan'; +import Accesses from 'accesses'; + +import activityPub from './activitypub'; +import webFinger from './webfinger'; +import log from './log-request'; +import config from '../config'; + +/** + * Init app + */ +const app = express(); +app.disable('x-powered-by'); +app.set('trust proxy', 'loopback'); + +// Log +if (config.accesses && config.accesses.enable) { + const accesses = new Accesses({ + appName: 'Misskey', + port: config.accesses.port + }); + + app.use(accesses.express); +} + +app.use(morgan(process.env.NODE_ENV == 'production' ? 'combined' : 'dev', { + // create a write stream (in append mode) + stream: config.accesslog ? fs.createWriteStream(config.accesslog) : null +})); + +app.use((req, res, next) => { + log(req); + next(); +}); + +// Drop request when without 'Host' header +app.use((req, res, next) => { + if (!req.headers['host']) { + res.sendStatus(400); + } else { + next(); + } +}); + +// 互換性のため +app.post('/meta', (req, res) => { + res.header('Access-Control-Allow-Origin', '*'); + res.json({ + version: 'nighthike' + }); +}); + +/** + * Register modules + */ +app.use('/api', require('./api')); +app.use('/files', require('./file')); +app.use(activityPub); +app.use(webFinger); +app.use(require('./web')); + +function createServer() { + if (config.https) { + const certs = {}; + Object.keys(config.https).forEach(k => { + certs[k] = fs.readFileSync(config.https[k]); + }); + return https.createServer(certs, app); + } else { + return http.createServer(app); + } +} + +export default () => new Promise(resolve => { + const server = createServer(); + + /** + * Steaming + */ + require('./api/streaming')(server); + + /** + * Server listen + */ + server.listen(config.port, resolve); +}); diff --git a/src/server/log-request.ts b/src/server/log-request.ts new file mode 100644 index 0000000000..e431aa271d --- /dev/null +++ b/src/server/log-request.ts @@ -0,0 +1,21 @@ +import * as crypto from 'crypto'; +import * as express from 'express'; +import * as proxyAddr from 'proxy-addr'; +import Xev from 'xev'; + +const ev = new Xev(); + +export default function(req: express.Request) { + const ip = proxyAddr(req, () => true); + + const md5 = crypto.createHash('md5'); + md5.update(ip); + const hashedIp = md5.digest('hex').substr(0, 3); + + ev.emit('request', { + ip: hashedIp, + method: req.method, + hostname: req.hostname, + path: req.originalUrl + }); +} diff --git a/src/server/web/docs.ts b/src/server/web/docs.ts new file mode 100644 index 0000000000..889532e17e --- /dev/null +++ b/src/server/web/docs.ts @@ -0,0 +1,24 @@ +/** + * Docs Server + */ + +import * as path from 'path'; +import * as express from 'express'; + +const docs = path.resolve(`${__dirname}/../../client/docs/`); + +/** + * Init app + */ +const app = express(); +app.disable('x-powered-by'); + +app.use('/assets', express.static(`${docs}/assets`)); + +/** + * Routing + */ +app.get(/^\/([a-z_\-\/]+?)$/, (req, res) => + res.sendFile(`${docs}/${req.params[0]}.html`)); + +module.exports = app; diff --git a/src/server/web/index.ts b/src/server/web/index.ts new file mode 100644 index 0000000000..5b1b6409b9 --- /dev/null +++ b/src/server/web/index.ts @@ -0,0 +1,64 @@ +/** + * Web Client Server + */ + +import * as path from 'path'; +import ms = require('ms'); + +// express modules +import * as express from 'express'; +import * as bodyParser from 'body-parser'; +import * as favicon from 'serve-favicon'; +import * as compression from 'compression'; + +const client = path.resolve(`${__dirname}/../../client/`); + +// Create server +const app = express(); +app.disable('x-powered-by'); + +app.use('/docs', require('./docs')); + +app.use(bodyParser.urlencoded({ extended: true })); +app.use(bodyParser.json({ + type: ['application/json', 'text/plain'] +})); +app.use(compression()); + +app.use((req, res, next) => { + res.header('X-Frame-Options', 'DENY'); + next(); +}); + +//#region static assets + +app.use(favicon(`${client}/assets/favicon.ico`)); +app.get('/apple-touch-icon.png', (req, res) => res.sendFile(`${client}/assets/apple-touch-icon.png`)); +app.use('/assets', express.static(`${client}/assets`, { + maxAge: ms('7 days') +})); +app.use('/assets/*.js', (req, res) => res.sendFile(`${client}/assets/404.js`)); +app.use('/assets', (req, res) => { + res.sendStatus(404); +}); + +// ServiceWroker +app.get(/^\/sw\.(.+?)\.js$/, (req, res) => + res.sendFile(`${client}/assets/sw.${req.params[0]}.js`)); + +// Manifest +app.get('/manifest.json', (req, res) => + res.sendFile(`${client}/assets/manifest.json`)); + +//#endregion + +app.get(/\/api:url/, require('./url-preview')); + +// Render base html for all requests +app.get('*', (req, res) => { + res.sendFile(path.resolve(`${client}/app/base.html`), { + maxAge: ms('7 days') + }); +}); + +module.exports = app; diff --git a/src/web/service/url-preview.ts b/src/server/web/url-preview.ts similarity index 100% rename from src/web/service/url-preview.ts rename to src/server/web/url-preview.ts diff --git a/src/server/webfinger.ts b/src/server/webfinger.ts new file mode 100644 index 0000000000..dbf0999f3e --- /dev/null +++ b/src/server/webfinger.ts @@ -0,0 +1,53 @@ +import * as express from 'express'; + +import config from '../config'; +import parseAcct from '../acct/parse'; +import User from '../models/user'; + +const app = express.Router(); + +app.get('/.well-known/webfinger', async (req, res) => { + if (typeof req.query.resource !== 'string') { + return res.sendStatus(400); + } + + const resourceLower = req.query.resource.toLowerCase(); + const webPrefix = config.url.toLowerCase() + '/@'; + let acctLower; + + if (resourceLower.startsWith(webPrefix)) { + acctLower = resourceLower.slice(webPrefix.length); + } else if (resourceLower.startsWith('acct:')) { + acctLower = resourceLower.slice('acct:'.length); + } else { + acctLower = resourceLower; + } + + const parsedAcctLower = parseAcct(acctLower); + if (![null, config.host.toLowerCase()].includes(parsedAcctLower.host)) { + return res.sendStatus(422); + } + + const user = await User.findOne({ usernameLower: parsedAcctLower.username, host: null }); + if (user === null) { + return res.sendStatus(404); + } + + return res.json({ + subject: `acct:${user.username}@${config.host}`, + links: [{ + rel: 'self', + type: 'application/activity+json', + href: `${config.url}/users/${user._id}` + }, { + rel: 'http://webfinger.net/rel/profile-page', + type: 'text/html', + href: `${config.url}/@${user.username}` + }, { + rel: 'http://ostatus.org/schema/1.0/subscribe', + template: `${config.url}/authorize-follow?acct={uri}` + }] + }); +}); + +export default app; diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts new file mode 100644 index 0000000000..30aae24ba6 --- /dev/null +++ b/src/services/drive/add-file.ts @@ -0,0 +1,298 @@ +import { Buffer } from 'buffer'; +import * as fs from 'fs'; +import * as tmp from 'tmp'; +import * as stream from 'stream'; + +import * as mongodb from 'mongodb'; +import * as crypto from 'crypto'; +import * as _gm from 'gm'; +import * as debug from 'debug'; +import fileType = require('file-type'); +import prominence = require('prominence'); + +import DriveFile, { IMetadata, getGridFSBucket, IDriveFile } from '../../models/drive-file'; +import DriveFolder from '../../models/drive-folder'; +import { pack } from '../../models/drive-file'; +import event, { publishDriveStream } from '../../publishers/stream'; +import getAcct from '../../acct/render'; +import { IUser } from '../../models/user'; + +const gm = _gm.subClass({ + imageMagick: true +}); + +const log = debug('misskey:drive:add-file'); + +const tmpFile = (): Promise<[string, any]> => new Promise((resolve, reject) => { + tmp.file((e, path, fd, cleanup) => { + if (e) return reject(e); + resolve([path, cleanup]); + }); +}); + +const addToGridFS = (name: string, readable: stream.Readable, type: string, metadata: any): Promise<any> => + getGridFSBucket() + .then(bucket => new Promise((resolve, reject) => { + const writeStream = bucket.openUploadStream(name, { contentType: type, metadata }); + writeStream.once('finish', resolve); + writeStream.on('error', reject); + readable.pipe(writeStream); + })); + +const addFile = async ( + user: IUser, + path: string, + name: string = null, + comment: string = null, + folderId: mongodb.ObjectID = null, + force: boolean = false, + uri: string = null +): Promise<IDriveFile> => { + log(`registering ${name} (user: ${getAcct(user)}, path: ${path})`); + + // Calculate hash, get content type and get file size + const [hash, [mime, ext], size] = await Promise.all([ + // hash + ((): Promise<string> => new Promise((res, rej) => { + const readable = fs.createReadStream(path); + const hash = crypto.createHash('md5'); + const chunks = []; + readable + .on('error', rej) + .pipe(hash) + .on('error', rej) + .on('data', (chunk) => chunks.push(chunk)) + .on('end', () => { + const buffer = Buffer.concat(chunks); + res(buffer.toString('hex')); + }); + }))(), + // mime + ((): Promise<[string, string | null]> => new Promise((res, rej) => { + const readable = fs.createReadStream(path); + readable + .on('error', rej) + .once('data', (buffer: Buffer) => { + readable.destroy(); + const type = fileType(buffer); + if (type) { + return res([type.mime, type.ext]); + } else { + // 種類が同定できなかったら application/octet-stream にする + return res(['application/octet-stream', null]); + } + }); + }))(), + // size + ((): Promise<number> => new Promise((res, rej) => { + fs.stat(path, (err, stats) => { + if (err) return rej(err); + res(stats.size); + }); + }))() + ]); + + log(`hash: ${hash}, mime: ${mime}, ext: ${ext}, size: ${size}`); + + // detect name + const detectedName: string = name || (ext ? `untitled.${ext}` : 'untitled'); + + if (!force) { + // Check if there is a file with the same hash + const much = await DriveFile.findOne({ + md5: hash, + 'metadata.userId': user._id + }); + + if (much !== null) { + log('file with same hash is found'); + return much; + } else { + log('file with same hash is not found'); + } + } + + const [wh, averageColor, folder] = await Promise.all([ + // Width and height (when image) + (async () => { + // 画像かどうか + if (!/^image\/.*$/.test(mime)) { + return null; + } + + const imageType = mime.split('/')[1]; + + // 画像でもPNGかJPEGかGIFでないならスキップ + if (imageType != 'png' && imageType != 'jpeg' && imageType != 'gif') { + return null; + } + + log('calculate image width and height...'); + + // Calculate width and height + const g = gm(fs.createReadStream(path), name); + const size = await prominence(g).size(); + + log(`image width and height is calculated: ${size.width}, ${size.height}`); + + return [size.width, size.height]; + })(), + // average color (when image) + (async () => { + // 画像かどうか + if (!/^image\/.*$/.test(mime)) { + return null; + } + + const imageType = mime.split('/')[1]; + + // 画像でもPNGかJPEGでないならスキップ + if (imageType != 'png' && imageType != 'jpeg') { + return null; + } + + log('calculate average color...'); + + const buffer = await prominence(gm(fs.createReadStream(path), name) + .setFormat('ppm') + .resize(1, 1)) // 1pxのサイズに縮小して平均色を取得するというハック + .toBuffer(); + + const r = buffer.readUInt8(buffer.length - 3); + const g = buffer.readUInt8(buffer.length - 2); + const b = buffer.readUInt8(buffer.length - 1); + + log(`average color is calculated: ${r}, ${g}, ${b}`); + + return [r, g, b]; + })(), + // folder + (async () => { + if (!folderId) { + return null; + } + const driveFolder = await DriveFolder.findOne({ + _id: folderId, + userId: user._id + }); + if (!driveFolder) { + throw 'folder-not-found'; + } + return driveFolder; + })(), + // usage checker + (async () => { + // Calculate drive usage + const usage = await DriveFile + .aggregate([{ + $match: { 'metadata.userId': user._id } + }, { + $project: { + length: true + } + }, { + $group: { + _id: null, + usage: { $sum: '$length' } + } + }]) + .then((aggregates: any[]) => { + if (aggregates.length > 0) { + return aggregates[0].usage; + } + return 0; + }); + + log(`drive usage is ${usage}`); + + // If usage limit exceeded + if (usage + size > user.driveCapacity) { + throw 'no-free-space'; + } + })() + ]); + + const readable = fs.createReadStream(path); + + const properties = {}; + + if (wh) { + properties['width'] = wh[0]; + properties['height'] = wh[1]; + } + + if (averageColor) { + properties['avgColor'] = averageColor; + } + + const metadata = { + userId: user._id, + folderId: folder !== null ? folder._id : null, + comment: comment, + properties: properties + } as IMetadata; + + if (uri !== null) { + metadata.uri = uri; + } + + return addToGridFS(detectedName, readable, mime, metadata); +}; + +/** + * Add file to drive + * + * @param user User who wish to add file + * @param file File path or readableStream + * @param comment Comment + * @param type File type + * @param folderId Folder ID + * @param force If set to true, forcibly upload the file even if there is a file with the same hash. + * @return Object that represents added file + */ +export default (user: any, file: string | stream.Readable, ...args) => new Promise<any>((resolve, reject) => { + const isStream = typeof file === 'object' && typeof file.read === 'function'; + + // Get file path + new Promise<[string, any]>((res, rej) => { + if (typeof file === 'string') { + res([file, null]); + } else if (isStream) { + tmpFile() + .then(([path, cleanup]) => { + const readable: stream.Readable = file; + const writable = fs.createWriteStream(path); + readable + .on('error', rej) + .on('end', () => { + res([path, cleanup]); + }) + .pipe(writable) + .on('error', rej); + }) + .catch(rej); + } else { + rej(new Error('un-compatible file.')); + } + }) + .then(([path, cleanup]) => new Promise<IDriveFile>((res, rej) => { + addFile(user, path, ...args) + .then(file => { + res(file); + if (cleanup) cleanup(); + }) + .catch(rej); + })) + .then(file => { + log(`drive file has been created ${file._id}`); + + resolve(file); + + pack(file).then(packedFile => { + // Publish drive_file_created event + event(user._id, 'drive_file_created', packedFile); + publishDriveStream(user._id, 'file_created', packedFile); + }); + }) + .catch(reject); +}); diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts new file mode 100644 index 0000000000..08e0397706 --- /dev/null +++ b/src/services/drive/upload-from-url.ts @@ -0,0 +1,61 @@ +import * as URL from 'url'; +import { IDriveFile, validateFileName } from '../../models/drive-file'; +import create from './add-file'; +import * as debug from 'debug'; +import * as tmp from 'tmp'; +import * as fs from 'fs'; +import * as request from 'request'; + +const log = debug('misskey:drive:upload-from-url'); + +export default async (url, user, folderId = null, uri = null): Promise<IDriveFile> => { + log(`REQUESTED: ${url}`); + + let name = URL.parse(url).pathname.split('/').pop(); + if (!validateFileName(name)) { + name = null; + } + + log(`name: ${name}`); + + // Create temp file + const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { + tmp.file((e, path, fd, cleanup) => { + if (e) return rej(e); + res([path, cleanup]); + }); + }); + + // write content at URL to temp file + await new Promise((res, rej) => { + const writable = fs.createWriteStream(path); + request(url) + .on('error', rej) + .on('end', () => { + writable.close(); + res(); + }) + .pipe(writable) + .on('error', rej); + }); + + let driveFile: IDriveFile; + let error; + + try { + driveFile = await create(user, path, name, null, folderId, false, uri); + log(`created: ${driveFile._id}`); + } catch (e) { + error = e; + log(`failed: ${e}`); + } + + // clean-up + cleanup(); + + if (error) { + throw error; + } else { + return driveFile; + } +}; diff --git a/src/services/following/create.ts b/src/services/following/create.ts new file mode 100644 index 0000000000..31e3be19ed --- /dev/null +++ b/src/services/following/create.ts @@ -0,0 +1,72 @@ +import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user'; +import Following from '../../models/following'; +import FollowingLog from '../../models/following-log'; +import FollowedLog from '../../models/followed-log'; +import event from '../../publishers/stream'; +import notify from '../../publishers/notify'; +import context from '../../remote/activitypub/renderer/context'; +import renderFollow from '../../remote/activitypub/renderer/follow'; +import renderAccept from '../../remote/activitypub/renderer/accept'; +import { deliver } from '../../queue'; + +export default async function(follower: IUser, followee: IUser, activity?) { + const following = await Following.insert({ + createdAt: new Date(), + followerId: follower._id, + followeeId: followee._id + }); + + //#region Increment following count + User.update({ _id: follower._id }, { + $inc: { + followingCount: 1 + } + }); + + FollowingLog.insert({ + createdAt: following.createdAt, + userId: follower._id, + count: follower.followingCount + 1 + }); + //#endregion + + //#region Increment followers count + User.update({ _id: followee._id }, { + $inc: { + followersCount: 1 + } + }); + FollowedLog.insert({ + createdAt: following.createdAt, + userId: followee._id, + count: followee.followersCount + 1 + }); + //#endregion + + // Publish follow event + if (isLocalUser(follower)) { + packUser(followee, follower).then(packed => event(follower._id, 'follow', packed)); + } + + // Publish followed event + if (isLocalUser(followee)) { + packUser(follower, followee).then(packed => event(followee._id, 'followed', packed)), + + // 通知を作成 + notify(followee._id, follower._id, 'follow'); + } + + if (isLocalUser(follower) && isRemoteUser(followee)) { + const content = renderFollow(follower, followee); + content['@context'] = context; + + deliver(follower, content, followee.inbox).save(); + } + + if (isRemoteUser(follower) && isLocalUser(followee)) { + const content = renderAccept(activity); + content['@context'] = context; + + deliver(followee, content, follower.inbox).save(); + } +} diff --git a/src/services/following/delete.ts b/src/services/following/delete.ts new file mode 100644 index 0000000000..d79bf64f53 --- /dev/null +++ b/src/services/following/delete.ts @@ -0,0 +1,64 @@ +import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user'; +import Following from '../../models/following'; +import FollowingLog from '../../models/following-log'; +import FollowedLog from '../../models/followed-log'; +import event from '../../publishers/stream'; +import context from '../../remote/activitypub/renderer/context'; +import renderFollow from '../../remote/activitypub/renderer/follow'; +import renderUndo from '../../remote/activitypub/renderer/undo'; +import { deliver } from '../../queue'; + +export default async function(follower: IUser, followee: IUser, activity?) { + const following = await Following.findOne({ + followerId: follower._id, + followeeId: followee._id + }); + + if (following == null) { + console.warn('フォロー解除がリクエストされましたがフォローしていませんでした'); + return; + } + + Following.remove({ + _id: following._id + }); + + //#region Decrement following count + User.update({ _id: follower._id }, { + $inc: { + followingCount: -1 + } + }); + + FollowingLog.insert({ + createdAt: following.createdAt, + userId: follower._id, + count: follower.followingCount - 1 + }); + //#endregion + + //#region Decrement followers count + User.update({ _id: followee._id }, { + $inc: { + followersCount: -1 + } + }); + FollowedLog.insert({ + createdAt: following.createdAt, + userId: followee._id, + count: followee.followersCount - 1 + }); + //#endregion + + // Publish unfollow event + if (isLocalUser(follower)) { + packUser(followee, follower).then(packed => event(follower._id, 'unfollow', packed)); + } + + if (isLocalUser(follower) && isRemoteUser(followee)) { + const content = renderUndo(renderFollow(follower, followee)); + content['@context'] = context; + + deliver(follower, content, followee.inbox).save(); + } +} diff --git a/src/services/note/create.ts b/src/services/note/create.ts new file mode 100644 index 0000000000..8f0b84bccd --- /dev/null +++ b/src/services/note/create.ts @@ -0,0 +1,365 @@ +import Note, { pack, INote } from '../../models/note'; +import User, { isLocalUser, IUser, isRemoteUser } from '../../models/user'; +import stream from '../../publishers/stream'; +import Following from '../../models/following'; +import { deliver } from '../../queue'; +import renderNote from '../../remote/activitypub/renderer/note'; +import renderCreate from '../../remote/activitypub/renderer/create'; +import renderAnnounce from '../../remote/activitypub/renderer/announce'; +import context from '../../remote/activitypub/renderer/context'; +import { IDriveFile } from '../../models/drive-file'; +import notify from '../../publishers/notify'; +import NoteWatching from '../../models/note-watching'; +import watch from './watch'; +import Mute from '../../models/mute'; +import pushSw from '../../publishers/push-sw'; +import event from '../../publishers/stream'; +import parse from '../../text/parse'; +import html from '../../text/html'; +import { IApp } from '../../models/app'; + +export default async (user: IUser, data: { + createdAt?: Date; + text?: string; + reply?: INote; + renote?: INote; + media?: IDriveFile[]; + geo?: any; + poll?: any; + viaMobile?: boolean; + tags?: string[]; + cw?: string; + visibility?: string; + uri?: string; + app?: IApp; +}, silent = false) => new Promise<INote>(async (res, rej) => { + if (data.createdAt == null) data.createdAt = new Date(); + if (data.visibility == null) data.visibility = 'public'; + if (data.viaMobile == null) data.viaMobile = false; + + const tags = data.tags || []; + + let tokens = null; + + if (data.text) { + // Analyze + tokens = parse(data.text); + + // Extract hashtags + const hashtags = tokens + .filter(t => t.type == 'hashtag') + .map(t => t.hashtag); + + hashtags.forEach(tag => { + if (tags.indexOf(tag) == -1) { + tags.push(tag); + } + }); + } + + const insert: any = { + createdAt: data.createdAt, + mediaIds: data.media ? data.media.map(file => file._id) : [], + replyId: data.reply ? data.reply._id : null, + renoteId: data.renote ? data.renote._id : null, + text: data.text, + textHtml: tokens === null ? null : html(tokens), + poll: data.poll, + cw: data.cw, + tags, + userId: user._id, + viaMobile: data.viaMobile, + geo: data.geo || null, + appId: data.app ? data.app._id : null, + visibility: data.visibility, + + // 以下非正規化データ + _reply: data.reply ? { userId: data.reply.userId } : null, + _renote: data.renote ? { userId: data.renote.userId } : null, + _user: { + host: user.host, + hostLower: user.hostLower, + inbox: isRemoteUser(user) ? user.inbox : undefined + } + }; + + if (data.uri != null) insert.uri = data.uri; + + // 投稿を作成 + const note = await Note.insert(insert); + + res(note); + + User.update({ _id: user._id }, { + // Increment notes count + $inc: { + notesCount: 1 + }, + // Update latest note + $set: { + latestNote: note + } + }); + + // Serialize + const noteObj = await pack(note); + + // タイムラインへの投稿 + if (note.channelId == null) { + // Publish event to myself's stream + if (isLocalUser(user)) { + stream(note.userId, 'note', noteObj); + } + + // Fetch all followers + const followers = await Following.aggregate([{ + $lookup: { + from: 'users', + localField: 'followerId', + foreignField: '_id', + as: 'user' + } + }, { + $match: { + followeeId: note.userId + } + }], { + _id: false + }); + + if (!silent) { + const render = async () => { + const content = data.renote && data.text == null + ? renderAnnounce(data.renote.uri ? data.renote.uri : await renderNote(data.renote)) + : renderCreate(await renderNote(note)); + content['@context'] = context; + return content; + }; + + // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送 + if (data.reply && isLocalUser(user) && isRemoteUser(data.reply._user)) { + deliver(user, await render(), data.reply._user.inbox).save(); + } + + // 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送 + if (data.renote && isLocalUser(user) && isRemoteUser(data.renote._user)) { + deliver(user, await render(), data.renote._user.inbox).save(); + } + + Promise.all(followers.map(async follower => { + follower = follower.user[0]; + + if (isLocalUser(follower)) { + // Publish event to followers stream + stream(follower._id, 'note', noteObj); + } else { + // フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信 + if (isLocalUser(user)) { + deliver(user, await render(), follower.inbox).save(); + } + } + })); + } + } + + // チャンネルへの投稿 + /* TODO + if (note.channelId) { + promises.push( + // Increment channel index(notes count) + Channel.update({ _id: note.channelId }, { + $inc: { + index: 1 + } + }), + + // Publish event to channel + promisedNoteObj.then(noteObj => { + publishChannelStream(note.channelId, 'note', noteObj); + }), + + Promise.all([ + promisedNoteObj, + + // Get channel watchers + ChannelWatching.find({ + channelId: note.channelId, + // 削除されたドキュメントは除く + deletedAt: { $exists: false } + }) + ]).then(([noteObj, watches]) => { + // チャンネルの視聴者(のタイムライン)に配信 + watches.forEach(w => { + stream(w.userId, 'note', noteObj); + }); + }) + ); + }*/ + + const mentions = []; + + async function addMention(mentionee, reason) { + // Reject if already added + if (mentions.some(x => x.equals(mentionee))) return; + + // Add mention + mentions.push(mentionee); + + // Publish event + if (!user._id.equals(mentionee)) { + const mentioneeMutes = await Mute.find({ + muter_id: mentionee, + deleted_at: { $exists: false } + }); + const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString()); + if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) { + event(mentionee, reason, noteObj); + pushSw(mentionee, reason, noteObj); + } + } + } + + // If has in reply to note + if (data.reply) { + // Increment replies count + Note.update({ _id: data.reply._id }, { + $inc: { + repliesCount: 1 + } + }); + + // (自分自身へのリプライでない限りは)通知を作成 + notify(data.reply.userId, user._id, 'reply', { + noteId: note._id + }); + + // Fetch watchers + NoteWatching.find({ + noteId: data.reply._id, + userId: { $ne: user._id }, + // 削除されたドキュメントは除く + deletedAt: { $exists: false } + }, { + fields: { + userId: true + } + }).then(watchers => { + watchers.forEach(watcher => { + notify(watcher.userId, user._id, 'reply', { + noteId: note._id + }); + }); + }); + + // この投稿をWatchする + if (isLocalUser(user) && user.settings.autoWatch !== false) { + watch(user._id, data.reply); + } + + // Add mention + addMention(data.reply.userId, 'reply'); + } + + // If it is renote + if (data.renote) { + // Notify + const type = data.text ? 'quote' : 'renote'; + notify(data.renote.userId, user._id, type, { + noteId: note._id + }); + + // Fetch watchers + NoteWatching.find({ + noteId: data.renote._id, + userId: { $ne: user._id } + }, { + fields: { + userId: true + } + }).then(watchers => { + watchers.forEach(watcher => { + notify(watcher.userId, user._id, type, { + noteId: note._id + }); + }); + }); + + // この投稿をWatchする + if (isLocalUser(user) && user.settings.autoWatch !== false) { + watch(user._id, data.renote); + } + + // If it is quote renote + if (data.text) { + // Add mention + addMention(data.renote.userId, 'quote'); + } else { + // Publish event + if (!user._id.equals(data.renote.userId)) { + event(data.renote.userId, 'renote', noteObj); + } + } + + // 今までで同じ投稿をRenoteしているか + const existRenote = await Note.findOne({ + userId: user._id, + renoteId: data.renote._id, + _id: { + $ne: note._id + } + }); + + if (!existRenote) { + // Update renoteee status + Note.update({ _id: data.renote._id }, { + $inc: { + renoteCount: 1 + } + }); + } + } + + // If has text content + if (data.text) { + // Extract an '@' mentions + const atMentions = tokens + .filter(t => t.type == 'mention') + .map(m => m.username) + // Drop dupulicates + .filter((v, i, s) => s.indexOf(v) == i); + + // Resolve all mentions + await Promise.all(atMentions.map(async mention => { + // Fetch mentioned user + // SELECT _id + const mentionee = await User + .findOne({ + usernameLower: mention.toLowerCase() + }, { _id: true }); + + // When mentioned user not found + if (mentionee == null) return; + + // 既に言及されたユーザーに対する返信や引用renoteの場合も無視 + if (data.reply && data.reply.userId.equals(mentionee._id)) return; + if (data.renote && data.renote.userId.equals(mentionee._id)) return; + + // Add mention + addMention(mentionee._id, 'mention'); + + // Create notification + notify(mentionee._id, user._id, 'mention', { + noteId: note._id + }); + })); + } + + // Append mentions data + if (mentions.length > 0) { + Note.update({ _id: note._id }, { + $set: { + mentions + } + }); + } +}); diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts new file mode 100644 index 0000000000..88158034f3 --- /dev/null +++ b/src/services/note/reaction/create.ts @@ -0,0 +1,94 @@ +import { IUser, pack as packUser, isLocalUser, isRemoteUser } from '../../../models/user'; +import Note, { INote, pack as packNote } from '../../../models/note'; +import NoteReaction from '../../../models/note-reaction'; +import { publishNoteStream } from '../../../publishers/stream'; +import notify from '../../../publishers/notify'; +import pushSw from '../../../publishers/push-sw'; +import NoteWatching from '../../../models/note-watching'; +import watch from '../watch'; +import renderLike from '../../../remote/activitypub/renderer/like'; +import { deliver } from '../../../queue'; +import context from '../../../remote/activitypub/renderer/context'; + +export default async (user: IUser, note: INote, reaction: string) => new Promise(async (res, rej) => { + // Myself + if (note.userId.equals(user._id)) { + return rej('cannot react to my note'); + } + + // if already reacted + const exist = await NoteReaction.findOne({ + noteId: note._id, + userId: user._id + }); + + if (exist !== null) { + return rej('already reacted'); + } + + // Create reaction + await NoteReaction.insert({ + createdAt: new Date(), + noteId: note._id, + userId: user._id, + reaction + }); + + res(); + + const inc = {}; + inc[`reactionCounts.${reaction}`] = 1; + + // Increment reactions count + await Note.update({ _id: note._id }, { + $inc: inc + }); + + publishNoteStream(note._id, 'reacted'); + + // Notify + notify(note.userId, user._id, 'reaction', { + noteId: note._id, + reaction: reaction + }); + + pushSw(note.userId, 'reaction', { + user: await packUser(user, note.userId), + note: await packNote(note, note.userId), + reaction: reaction + }); + + // Fetch watchers + NoteWatching + .find({ + noteId: note._id, + userId: { $ne: user._id } + }, { + fields: { + userId: true + } + }) + .then(watchers => { + watchers.forEach(watcher => { + notify(watcher.userId, user._id, 'reaction', { + noteId: note._id, + reaction: reaction + }); + }); + }); + + // ユーザーがローカルユーザーかつ自動ウォッチ設定がオンならばこの投稿をWatchする + if (isLocalUser(user) && user.settings.autoWatch !== false) { + watch(user._id, note); + } + + //#region 配信 + // リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送 + if (isLocalUser(user) && isRemoteUser(note._user)) { + const content = renderLike(user, note); + content['@context'] = context; + + deliver(user, content, note._user.inbox).save(); + } + //#endregion +}); diff --git a/src/services/note/watch.ts b/src/services/note/watch.ts new file mode 100644 index 0000000000..28fb7a32c2 --- /dev/null +++ b/src/services/note/watch.ts @@ -0,0 +1,26 @@ +import * as mongodb from 'mongodb'; +import Watching from '../../models/note-watching'; + +export default async (me: mongodb.ObjectID, note: object) => { + // 自分の投稿はwatchできない + if (me.equals((note as any).userId)) { + return; + } + + // if watching now + const exist = await Watching.findOne({ + noteId: (note as any)._id, + userId: me, + deletedAt: { $exists: false } + }); + + if (exist !== null) { + return; + } + + await Watching.insert({ + createdAt: new Date(), + noteId: (note as any)._id, + userId: me + }); +}; diff --git a/src/text/html.ts b/src/text/html.ts new file mode 100644 index 0000000000..797f3b3f33 --- /dev/null +++ b/src/text/html.ts @@ -0,0 +1,83 @@ +import { lib as emojilib } from 'emojilib'; +import { JSDOM } from 'jsdom'; + +const handlers = { + bold({ document }, { bold }) { + const b = document.createElement('b'); + b.textContent = bold; + document.body.appendChild(b); + }, + + code({ document }, { code }) { + const pre = document.createElement('pre'); + const inner = document.createElement('code'); + inner.innerHTML = code; + pre.appendChild(inner); + document.body.appendChild(pre); + }, + + emoji({ document }, { content, emoji }) { + const found = emojilib[emoji]; + const node = document.createTextNode(found ? found.char : content); + document.body.appendChild(node); + }, + + hashtag({ document }, { hashtag }) { + const a = document.createElement('a'); + a.href = '/search?q=#' + hashtag; + a.textContent = hashtag; + }, + + 'inline-code'({ document }, { code }) { + const element = document.createElement('code'); + element.textContent = code; + document.body.appendChild(element); + }, + + link({ document }, { url, title }) { + const a = document.createElement('a'); + a.href = url; + a.textContent = title; + document.body.appendChild(a); + }, + + mention({ document }, { content }) { + const a = document.createElement('a'); + a.href = '/' + content; + a.textContent = content; + document.body.appendChild(a); + }, + + quote({ document }, { quote }) { + const blockquote = document.createElement('blockquote'); + blockquote.textContent = quote; + document.body.appendChild(blockquote); + }, + + text({ document }, { content }) { + for (const text of content.split('\n')) { + const node = document.createTextNode(text); + document.body.appendChild(node); + + const br = document.createElement('br'); + document.body.appendChild(br); + } + }, + + url({ document }, { url }) { + const a = document.createElement('a'); + a.href = url; + a.textContent = url; + document.body.appendChild(a); + } +}; + +export default tokens => { + const { window } = new JSDOM(''); + + for (const token of tokens) { + handlers[token.type](window, token); + } + + return `<p>${window.document.body.innerHTML}</p>`; +}; diff --git a/src/api/common/text/core/syntax-highlighter.ts b/src/text/parse/core/syntax-highlighter.ts similarity index 100% rename from src/api/common/text/core/syntax-highlighter.ts rename to src/text/parse/core/syntax-highlighter.ts diff --git a/src/api/common/text/elements/bold.ts b/src/text/parse/elements/bold.ts similarity index 100% rename from src/api/common/text/elements/bold.ts rename to src/text/parse/elements/bold.ts diff --git a/src/api/common/text/elements/code.ts b/src/text/parse/elements/code.ts similarity index 100% rename from src/api/common/text/elements/code.ts rename to src/text/parse/elements/code.ts diff --git a/src/api/common/text/elements/emoji.ts b/src/text/parse/elements/emoji.ts similarity index 100% rename from src/api/common/text/elements/emoji.ts rename to src/text/parse/elements/emoji.ts diff --git a/src/api/common/text/elements/hashtag.ts b/src/text/parse/elements/hashtag.ts similarity index 100% rename from src/api/common/text/elements/hashtag.ts rename to src/text/parse/elements/hashtag.ts diff --git a/src/api/common/text/elements/inline-code.ts b/src/text/parse/elements/inline-code.ts similarity index 100% rename from src/api/common/text/elements/inline-code.ts rename to src/text/parse/elements/inline-code.ts diff --git a/src/api/common/text/elements/link.ts b/src/text/parse/elements/link.ts similarity index 100% rename from src/api/common/text/elements/link.ts rename to src/text/parse/elements/link.ts diff --git a/src/text/parse/elements/mention.ts b/src/text/parse/elements/mention.ts new file mode 100644 index 0000000000..2ad2788300 --- /dev/null +++ b/src/text/parse/elements/mention.ts @@ -0,0 +1,17 @@ +/** + * Mention + */ +import parseAcct from '../../../acct/parse'; + +module.exports = text => { + const match = text.match(/^@[a-z0-9_]+(?:@[a-z0-9\.\-]+[a-z0-9])?/i); + if (!match) return null; + const mention = match[0]; + const { username, host } = parseAcct(mention.substr(1)); + return { + type: 'mention', + content: mention, + username, + host + }; +}; diff --git a/src/text/parse/elements/quote.ts b/src/text/parse/elements/quote.ts new file mode 100644 index 0000000000..cc8cfffdc4 --- /dev/null +++ b/src/text/parse/elements/quote.ts @@ -0,0 +1,14 @@ +/** + * Quoted text + */ + +module.exports = text => { + const match = text.match(/^"([\s\S]+?)\n"/); + if (!match) return null; + const quote = match[0]; + return { + type: 'quote', + content: quote, + quote: quote.substr(1, quote.length - 2).trim(), + }; +}; diff --git a/src/api/common/text/elements/url.ts b/src/text/parse/elements/url.ts similarity index 100% rename from src/api/common/text/elements/url.ts rename to src/text/parse/elements/url.ts diff --git a/src/api/common/text/index.ts b/src/text/parse/index.ts similarity index 83% rename from src/api/common/text/index.ts rename to src/text/parse/index.ts index 47127e8646..b958da81b0 100644 --- a/src/api/common/text/index.ts +++ b/src/text/parse/index.ts @@ -10,10 +10,11 @@ const elements = [ require('./elements/hashtag'), require('./elements/code'), require('./elements/inline-code'), + require('./elements/quote'), require('./elements/emoji') ]; -export default (source: string) => { +export default (source: string): any[] => { if (source == '') { return null; @@ -33,12 +34,12 @@ export default (source: string) => { // パース while (source != '') { const parsed = elements.some(el => { - let tokens = el(source, i); - if (tokens) { - if (!Array.isArray(tokens)) { - tokens = [tokens]; + let _tokens = el(source, i); + if (_tokens) { + if (!Array.isArray(_tokens)) { + _tokens = [_tokens]; } - tokens.forEach(push); + _tokens.forEach(push); return true; } else { return false; diff --git a/src/utils/cli/progressbar.ts b/src/utils/cli/progressbar.ts index 4afb4b0904..72496fdedc 100644 --- a/src/utils/cli/progressbar.ts +++ b/src/utils/cli/progressbar.ts @@ -1,6 +1,6 @@ import { EventEmitter } from 'events'; import * as readline from 'readline'; -import * as chalk from 'chalk'; +import chalk from 'chalk'; /** * Progress bar diff --git a/src/utils/dependencyInfo.ts b/src/utils/dependencyInfo.ts index 818fa3136c..89af0d20fb 100644 --- a/src/utils/dependencyInfo.ts +++ b/src/utils/dependencyInfo.ts @@ -11,7 +11,7 @@ export default class { public showAll(): void { this.show('MongoDB', 'mongo --version', x => x.match(/^MongoDB shell version:? (.*)\r?\n/)); this.show('Redis', 'redis-server --version', x => x.match(/v=([0-9\.]*)/)); - this.show('GraphicsMagick', 'gm -version', x => x.match(/^GraphicsMagick ([0-9\.]*) .*/)); + this.show('ImageMagick', 'magick -version', x => x.match(/^Version: ImageMagick (.+?)\r?\n/)); } public show(serviceName: string, command: string, transform: (x: string) => RegExpMatchArray): void { diff --git a/src/utils/logger.ts b/src/utils/logger.ts index ecfacbc952..fae1042c39 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,8 +1,8 @@ -import * as chalk from 'chalk'; +import chalk, { Chalk } from 'chalk'; export type LogLevel = 'Error' | 'Warn' | 'Info'; -function toLevelColor(level: LogLevel): chalk.ChalkStyle { +function toLevelColor(level: LogLevel): Chalk { switch (level) { case 'Error': return chalk.red; case 'Warn': return chalk.yellow; diff --git a/src/utils/type.ts b/src/utils/type.ts new file mode 100644 index 0000000000..ba6ea0be77 --- /dev/null +++ b/src/utils/type.ts @@ -0,0 +1,3 @@ +// https://github.com/Microsoft/TypeScript/issues/12215 +export type Diff<T extends string, U extends string> = ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T]; +export type Omit<T, K extends keyof T> = { [P in Diff<keyof T, K>]: T[P] }; diff --git a/src/version.ts b/src/version.ts index 2b4c1320e7..d379b57f8f 100644 --- a/src/version.ts +++ b/src/version.ts @@ -2,6 +2,6 @@ * Version */ -const meta = require('../package.json'); +const meta = require('../version.json'); export default meta.version as string; diff --git a/src/web/app/auth/script.js b/src/web/app/auth/script.js deleted file mode 100644 index fe7f9befe8..0000000000 --- a/src/web/app/auth/script.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Authorize Form - */ - -// Style -import './style.styl'; - -import * as riot from 'riot'; -require('./tags'); -import init from '../init'; - -document.title = 'Misskey | アプリの連携'; - -/** - * init - */ -init(me => { - mount(document.createElement('mk-index')); -}); - -function mount(content) { - riot.mount(document.getElementById('app').appendChild(content)); -} diff --git a/src/web/app/auth/tags/form.tag b/src/web/app/auth/tags/form.tag deleted file mode 100644 index 4a236f7594..0000000000 --- a/src/web/app/auth/tags/form.tag +++ /dev/null @@ -1,130 +0,0 @@ -<mk-form> - <header> - <h1><i>{ app.name }</i>があなたの<b>アカウント</b>に<b>アクセス</b>することを<b>許可</b>しますか?</h1><img src={ app.icon_url + '?thumbnail&size=64' }/> - </header> - <div class="app"> - <section> - <h2>{ app.name }</h2> - <p class="nid">{ app.name_id }</p> - <p class="description">{ app.description }</p> - </section> - <section> - <h2>このアプリは次の権限を要求しています:</h2> - <ul> - <virtual each={ p in app.permission }> - <li if={ p == 'account-read' }>アカウントの情報を見る。</li> - <li if={ p == 'account-write' }>アカウントの情報を操作する。</li> - <li if={ p == 'post-write' }>投稿する。</li> - <li if={ p == 'like-write' }>いいねしたりいいね解除する。</li> - <li if={ p == 'following-write' }>フォローしたりフォロー解除する。</li> - <li if={ p == 'drive-read' }>ドライブを見る。</li> - <li if={ p == 'drive-write' }>ドライブを操作する。</li> - <li if={ p == 'notification-read' }>通知を見る。</li> - <li if={ p == 'notification-write' }>通知を操作する。</li> - </virtual> - </ul> - </section> - </div> - <div class="action"> - <button onclick={ cancel }>キャンセル</button> - <button onclick={ accept }>アクセスを許可</button> - </div> - <style> - :scope - display block - - > header - > h1 - margin 0 - padding 32px 32px 20px 32px - font-size 24px - font-weight normal - color #777 - - i - color #77aeca - - &:before - content '「' - - &:after - content '」' - - b - color #666 - - > img - display block - z-index 1 - width 84px - height 84px - margin 0 auto -38px auto - border solid 5px #fff - border-radius 100% - box-shadow 0 2px 2px rgba(0, 0, 0, 0.1) - - > .app - padding 44px 16px 0 16px - color #555 - background #eee - box-shadow 0 2px 2px rgba(0, 0, 0, 0.1) inset - - &:after - content '' - display block - clear both - - > section - float left - width 50% - padding 8px - text-align left - - > h2 - margin 0 - font-size 16px - color #777 - - > .action - padding 16px - - > button - margin 0 8px - - @media (max-width 600px) - > header - > img - box-shadow none - - > .app - box-shadow none - - @media (max-width 500px) - > header - > h1 - font-size 16px - - </style> - <script> - this.mixin('api'); - - this.session = this.opts.session; - this.app = this.session.app; - - this.cancel = () => { - this.api('auth/deny', { - token: this.session.token - }).then(() => { - this.trigger('denied'); - }); - }; - - this.accept = () => { - this.api('auth/accept', { - token: this.session.token - }).then(() => { - this.trigger('accepted'); - }); - }; - </script> -</mk-form> diff --git a/src/web/app/auth/tags/index.js b/src/web/app/auth/tags/index.js deleted file mode 100644 index 42dffe67d9..0000000000 --- a/src/web/app/auth/tags/index.js +++ /dev/null @@ -1,2 +0,0 @@ -require('./index.tag'); -require('./form.tag'); diff --git a/src/web/app/auth/tags/index.tag b/src/web/app/auth/tags/index.tag deleted file mode 100644 index e71214f4a3..0000000000 --- a/src/web/app/auth/tags/index.tag +++ /dev/null @@ -1,143 +0,0 @@ -<mk-index> - <main if={ SIGNIN }> - <p class="fetching" if={ fetching }>読み込み中<mk-ellipsis/></p> - <mk-form ref="form" if={ state == 'waiting' } session={ session }/> - <div class="denied" if={ state == 'denied' }> - <h1>アプリケーションの連携をキャンセルしました。</h1> - <p>このアプリがあなたのアカウントにアクセスすることはありません。</p> - </div> - <div class="accepted" if={ state == 'accepted' }> - <h1>{ session.app.is_authorized ? 'このアプリは既に連携済みです' : 'アプリケーションの連携を許可しました'}</h1> - <p if={ session.app.callback_url }>アプリケーションに戻っています<mk-ellipsis/></p> - <p if={ !session.app.callback_url }>アプリケーションに戻って、やっていってください。</p> - </div> - <div class="error" if={ state == 'fetch-session-error' }> - <p>セッションが存在しません。</p> - </div> - </main> - <main class="signin" if={ !SIGNIN }> - <h1>サインインしてください</h1> - <mk-signin/> - </main> - <footer><img src="/assets/auth/logo.svg" alt="Misskey"/></footer> - <style> - :scope - display block - - > main - width 100% - max-width 500px - margin 0 auto - text-align center - background #fff - box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2) - - > .fetching - margin 0 - padding 32px - color #555 - - > div - padding 64px - - > h1 - margin 0 0 8px 0 - padding 0 - font-size 20px - font-weight normal - - > p - margin 0 - color #555 - - &.denied > h1 - color #e65050 - - &.accepted > h1 - color #54af7c - - &.signin - padding 32px 32px 16px 32px - - > h1 - margin 0 0 22px 0 - padding 0 - font-size 20px - font-weight normal - color #555 - - @media (max-width 600px) - max-width none - box-shadow none - - @media (max-width 500px) - > div - > h1 - font-size 16px - - > footer - > img - display block - width 64px - height 64px - margin 0 auto - - </style> - <script> - this.mixin('i'); - this.mixin('api'); - - this.state = null; - this.fetching = true; - - this.token = window.location.href.split('/').pop(); - - this.on('mount', () => { - if (!this.SIGNIN) return; - - // Fetch session - this.api('auth/session/show', { - token: this.token - }).then(session => { - this.session = session; - this.fetching = false; - - // 既に連携していた場合 - if (this.session.app.is_authorized) { - this.api('auth/accept', { - token: this.session.token - }).then(() => { - this.accepted(); - }); - } else { - this.update({ - state: 'waiting' - }); - - this.refs.form.on('denied', () => { - this.update({ - state: 'denied' - }); - }); - - this.refs.form.on('accepted', this.accepted); - } - }).catch(error => { - this.update({ - fetching: false, - state: 'fetch-session-error' - }); - }); - }); - - this.accepted = () => { - this.update({ - state: 'accepted' - }); - - if (this.session.app.callback_url) { - location.href = this.session.app.callback_url + '?token=' + this.session.token; - } - }; - </script> -</mk-index> diff --git a/src/web/app/common/mixins/api.js b/src/web/app/common/mixins/api.js deleted file mode 100644 index 42d96db559..0000000000 --- a/src/web/app/common/mixins/api.js +++ /dev/null @@ -1,8 +0,0 @@ -import * as riot from 'riot'; -import api from '../scripts/api'; - -export default me => { - riot.mixin('api', { - api: api.bind(null, me ? me.token : null) - }); -}; diff --git a/src/web/app/common/mixins/i.js b/src/web/app/common/mixins/i.js deleted file mode 100644 index 5225147766..0000000000 --- a/src/web/app/common/mixins/i.js +++ /dev/null @@ -1,20 +0,0 @@ -import * as riot from 'riot'; - -export default me => { - riot.mixin('i', { - init: function() { - this.I = me; - this.SIGNIN = me != null; - - if (this.SIGNIN) { - this.on('mount', () => { - me.on('updated', this.update); - }); - this.on('unmount', () => { - me.off('updated', this.update); - }); - } - }, - me: me - }); -}; diff --git a/src/web/app/common/mixins/index.js b/src/web/app/common/mixins/index.js deleted file mode 100644 index 9718ee949b..0000000000 --- a/src/web/app/common/mixins/index.js +++ /dev/null @@ -1,9 +0,0 @@ -import activateMe from './i'; -import activateApi from './api'; -import activateStream from './stream'; - -export default (me, stream) => { - activateMe(me); - activateApi(me); - activateStream(stream); -}; diff --git a/src/web/app/common/mixins/stream.js b/src/web/app/common/mixins/stream.js deleted file mode 100644 index 4706042b04..0000000000 --- a/src/web/app/common/mixins/stream.js +++ /dev/null @@ -1,5 +0,0 @@ -import * as riot from 'riot'; - -export default stream => { - riot.mixin('stream', { stream }); -}; diff --git a/src/web/app/common/scripts/api.js b/src/web/app/common/scripts/api.js deleted file mode 100644 index 4855f736c7..0000000000 --- a/src/web/app/common/scripts/api.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * API Request - */ - -import CONFIG from './config'; - -let spinner = null; -let pending = 0; - -/** - * Send a request to API - * @param {string|Object} i Credential - * @param {string} endpoint Endpoint - * @param {any} [data={}] Data - * @return {Promise<any>} Response - */ -export default (i, endpoint, data = {}) => { - if (++pending === 1) { - spinner = document.createElement('div'); - spinner.setAttribute('id', 'wait'); - document.body.appendChild(spinner); - } - - // Append the credential - if (i != null) data.i = typeof i === 'object' ? i.token : i; - - return new Promise((resolve, reject) => { - // Send request - fetch(endpoint.indexOf('://') > -1 ? endpoint : `${CONFIG.apiUrl}/${endpoint}`, { - method: 'POST', - body: JSON.stringify(data), - credentials: endpoint === 'signin' ? 'include' : 'omit' - }).then(res => { - if (--pending === 0) spinner.parentNode.removeChild(spinner); - if (res.status === 200) { - res.json().then(resolve); - } else if (res.status === 204) { - resolve(); - } else { - res.json().then(err => { - reject(err.error); - }); - } - }).catch(reject); - }); -}; diff --git a/src/web/app/common/scripts/bytes-to-size.js b/src/web/app/common/scripts/bytes-to-size.js deleted file mode 100644 index af0268dbd0..0000000000 --- a/src/web/app/common/scripts/bytes-to-size.js +++ /dev/null @@ -1,6 +0,0 @@ -export default (bytes, digits = 0) => { - var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; - if (bytes == 0) return '0Byte'; - var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); - return (bytes / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i]; -}; diff --git a/src/web/app/common/scripts/check-for-update.js b/src/web/app/common/scripts/check-for-update.js deleted file mode 100644 index 7cb7839d29..0000000000 --- a/src/web/app/common/scripts/check-for-update.js +++ /dev/null @@ -1,14 +0,0 @@ -import CONFIG from './config'; - -export default function() { - fetch(CONFIG.apiUrl + '/meta', { - method: 'POST' - }).then(res => { - res.json().then(meta => { - if (meta.version != VERSION) { - localStorage.setItem('should-refresh', 'true'); - alert('%i18n:common.update-available%'.replace('{newer}', meta.version).replace('{current}', VERSION)); - } - }); - }); -}; diff --git a/src/web/app/common/scripts/config.js b/src/web/app/common/scripts/config.js deleted file mode 100644 index 75a7abba29..0000000000 --- a/src/web/app/common/scripts/config.js +++ /dev/null @@ -1,23 +0,0 @@ -const Url = new URL(location.href); - -const isRoot = Url.host.split('.')[0] == 'misskey'; - -const host = isRoot ? Url.host : Url.host.substring(Url.host.indexOf('.') + 1, Url.host.length); -const scheme = Url.protocol; -const url = `${scheme}//${host}`; -const apiUrl = `${scheme}//api.${host}`; -const devUrl = `${scheme}//dev.${host}`; -const aboutUrl = `${scheme}//about.${host}`; -const statsUrl = `${scheme}//stats.${host}`; -const statusUrl = `${scheme}//status.${host}`; - -export default { - host, - scheme, - url, - apiUrl, - devUrl, - aboutUrl, - statsUrl, - statusUrl -}; diff --git a/src/web/app/common/scripts/generate-default-userdata.js b/src/web/app/common/scripts/generate-default-userdata.js deleted file mode 100644 index 1200563e1a..0000000000 --- a/src/web/app/common/scripts/generate-default-userdata.js +++ /dev/null @@ -1,45 +0,0 @@ -import uuid from './uuid'; - -const home = { - left: [ - 'profile', - 'calendar', - 'rss-reader', - 'photo-stream', - 'version' - ], - right: [ - 'broadcast', - 'notifications', - 'user-recommendation', - 'donation', - 'nav', - 'tips' - ] -}; - -export default () => { - const homeData = []; - - home.left.forEach(widget => { - homeData.push({ - name: widget, - id: uuid(), - place: 'left' - }); - }); - - home.right.forEach(widget => { - homeData.push({ - name: widget, - id: uuid(), - place: 'right' - }); - }); - - const data = { - home: JSON.stringify(homeData) - }; - - return data; -}; diff --git a/src/web/app/common/scripts/get-post-summary.js b/src/web/app/common/scripts/get-post-summary.js deleted file mode 100644 index 83eda8f6b4..0000000000 --- a/src/web/app/common/scripts/get-post-summary.js +++ /dev/null @@ -1,35 +0,0 @@ -const summarize = post => { - let summary = post.text ? post.text : ''; - - // メディアが添付されているとき - if (post.media) { - summary += ` (${post.media.length}つのメディア)`; - } - - // 投票が添付されているとき - if (post.poll) { - summary += ' (投票)'; - } - - // 返信のとき - if (post.reply_to_id) { - if (post.reply_to) { - summary += ` RE: ${summarize(post.reply_to)}`; - } else { - summary += ' RE: ...'; - } - } - - // Repostのとき - if (post.repost_id) { - if (post.repost) { - summary += ` RP: ${summarize(post.repost)}`; - } else { - summary += ' RP: ...'; - } - } - - return summary.trim(); -}; - -export default summarize; diff --git a/src/web/app/common/scripts/home-stream.js b/src/web/app/common/scripts/home-stream.js deleted file mode 100644 index 24f13cd291..0000000000 --- a/src/web/app/common/scripts/home-stream.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -import Stream from './stream'; - -/** - * Home stream connection - */ -class Connection extends Stream { - constructor(me) { - super('', { - i: me.token - }); - - this.on('i_updated', me.update); - } -} - -export default Connection; diff --git a/src/web/app/common/scripts/is-promise.js b/src/web/app/common/scripts/is-promise.js deleted file mode 100644 index 3b4cd70b49..0000000000 --- a/src/web/app/common/scripts/is-promise.js +++ /dev/null @@ -1 +0,0 @@ -export default x => typeof x.then == 'function'; diff --git a/src/web/app/common/scripts/messaging-stream.js b/src/web/app/common/scripts/messaging-stream.js deleted file mode 100644 index 261525d5f6..0000000000 --- a/src/web/app/common/scripts/messaging-stream.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; - -import Stream from './stream'; - -/** - * Messaging stream connection - */ -class Connection extends Stream { - constructor(me, otherparty) { - super('messaging', { - i: me.token, - otherparty - }); - - this.on('_connected_', () => { - this.send({ - i: me.token - }); - }); - } -} - -export default Connection; diff --git a/src/web/app/common/scripts/server-stream.js b/src/web/app/common/scripts/server-stream.js deleted file mode 100644 index a1c466b35d..0000000000 --- a/src/web/app/common/scripts/server-stream.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -import Stream from './stream'; - -/** - * Server stream connection - */ -class Connection extends Stream { - constructor() { - super('server'); - } -} - -export default Connection; diff --git a/src/web/app/common/scripts/signout.js b/src/web/app/common/scripts/signout.js deleted file mode 100644 index 6c95cfbc9c..0000000000 --- a/src/web/app/common/scripts/signout.js +++ /dev/null @@ -1,7 +0,0 @@ -import CONFIG from './config'; - -export default () => { - localStorage.removeItem('me'); - document.cookie = `i=; domain=.${CONFIG.host}; expires=Thu, 01 Jan 1970 00:00:01 GMT;`; - location.href = '/'; -}; diff --git a/src/web/app/common/scripts/stream.js b/src/web/app/common/scripts/stream.js deleted file mode 100644 index 981118b5de..0000000000 --- a/src/web/app/common/scripts/stream.js +++ /dev/null @@ -1,100 +0,0 @@ -'use strict'; - -const ReconnectingWebSocket = require('reconnecting-websocket'); -import * as riot from 'riot'; -import CONFIG from './config'; - -/** - * Misskey stream connection - */ -class Connection { - constructor(endpoint, params) { - // BIND ----------------------------------- - this.onOpen = this.onOpen.bind(this); - this.onClose = this.onClose.bind(this); - this.onMessage = this.onMessage.bind(this); - this.send = this.send.bind(this); - this.close = this.close.bind(this); - // ---------------------------------------- - - riot.observable(this); - - this.state = 'initializing'; - this.buffer = []; - - const host = CONFIG.apiUrl.replace('http', 'ws'); - const query = params - ? Object.keys(params) - .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])) - .join('&') - : null; - - this.socket = new ReconnectingWebSocket(`${host}/${endpoint}${query ? '?' + query : ''}`); - this.socket.addEventListener('open', this.onOpen); - this.socket.addEventListener('close', this.onClose); - this.socket.addEventListener('message', this.onMessage); - } - - /** - * Callback of when open connection - * @private - */ - onOpen() { - this.state = 'connected'; - this.trigger('_connected_'); - - // バッファーを処理 - const _buffer = [].concat(this.buffer); // Shallow copy - this.buffer = []; // Clear buffer - _buffer.forEach(message => { - this.send(message); // Resend each buffered messages - }); - } - - /** - * Callback of when close connection - * @private - */ - onClose() { - this.state = 'reconnecting'; - this.trigger('_closed_'); - } - - /** - * Callback of when received a message from connection - * @private - */ - onMessage(message) { - try { - const msg = JSON.parse(message.data); - if (msg.type) this.trigger(msg.type, msg.body); - } catch(e) { - // noop - } - } - - /** - * Send a message to connection - * @public - */ - send(message) { - // まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する - if (this.state != 'connected') { - this.buffer.push(message); - return; - }; - - this.socket.send(JSON.stringify(message)); - } - - /** - * Close this connection - * @public - */ - close() { - this.socket.removeEventListener('open', this.onOpen); - this.socket.removeEventListener('message', this.onMessage); - } -} - -export default Connection; diff --git a/src/web/app/common/scripts/text-compiler.js b/src/web/app/common/scripts/text-compiler.js deleted file mode 100644 index 0a9b8022df..0000000000 --- a/src/web/app/common/scripts/text-compiler.js +++ /dev/null @@ -1,47 +0,0 @@ -import * as riot from 'riot'; -const pictograph = require('pictograph'); -import CONFIG from './config'; - -const escape = text => - text - .replace(/>/g, '>') - .replace(/</g, '<'); - -export default (tokens, shouldBreak) => { - if (shouldBreak == null) { - shouldBreak = true; - } - - const me = riot.mixin('i').me; - - let text = tokens.map(token => { - switch (token.type) { - case 'text': - return escape(token.content) - .replace(/(\r\n|\n|\r)/g, shouldBreak ? '<br>' : ' '); - case 'bold': - return `<strong>${escape(token.bold)}</strong>`; - case 'url': - return `<mk-url href="${escape(token.content)}" target="_blank"></mk-url>`; - case 'link': - return `<a class="link" href="${escape(token.url)}" target="_blank" title="${escape(token.url)}">${escape(token.title)}</a>`; - case 'mention': - return `<a href="${CONFIG.url + '/' + escape(token.username)}" target="_blank" data-user-preview="${token.content}" ${me && me.username == token.username ? 'data-is-me' : ''}>${token.content}</a>`; - case 'hashtag': // TODO - return `<a>${escape(token.content)}</a>`; - case 'code': - return `<pre><code>${token.html}</code></pre>`; - case 'inline-code': - return `<code>${token.html}</code>`; - case 'emoji': - return pictograph.dic[token.emoji] || token.content; - } - }).join(''); - - // Remove needless whitespaces - text = text - .replace(/ <code>/g, '<code>').replace(/<\/code> /g, '</code>') - .replace(/<br><code><pre>/g, '<code><pre>').replace(/<\/code><\/pre><br>/g, '</code></pre>'); - - return text; -}; diff --git a/src/web/app/common/scripts/uuid.js b/src/web/app/common/scripts/uuid.js deleted file mode 100644 index ff016e18ad..0000000000 --- a/src/web/app/common/scripts/uuid.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Generate a UUID - */ -export default () => { - let uuid = ''; - - for (let i = 0; i < 32; i++) { - const random = Math.random() * 16 | 0; - - if (i == 8 || i == 12 || i == 16 || i == 20) { - uuid += '-'; - } - - uuid += (i == 12 ? 4 : (i == 16 ? (random & 3 | 8) : random)).toString(16); - } - - return uuid; -}; diff --git a/src/web/app/common/tags/activity-table.tag b/src/web/app/common/tags/activity-table.tag deleted file mode 100644 index 6331e7c9c3..0000000000 --- a/src/web/app/common/tags/activity-table.tag +++ /dev/null @@ -1,58 +0,0 @@ -<mk-activity-table> - <svg if={ data } ref="canvas" viewBox="0 0 53 7" preserveAspectRatio="none"> - <rect each={ data } width="1" height="1" - riot-x={ x } riot-y={ date.weekday } - rx="1" ry="1" - fill={ color } - style="transform: scale({ v });"/> - <rect class="today" width="1" height="1" - riot-x={ data[data.length - 1].x } riot-y={ data[data.length - 1].date.weekday } - rx="1" ry="1" - fill="none" - stroke-width="0.1" - stroke="#f73520"/> - </svg> - <style> - :scope - display block - max-width 600px - margin 0 auto - background #fff - - > svg - display block - - > rect - transform-origin center - - </style> - <script> - this.mixin('api'); - - this.user = this.opts.user; - - this.on('mount', () => { - this.api('aggregation/users/activity', { - user_id: this.user.id - }).then(data => { - data.forEach(d => d.total = d.posts + d.replies + d.reposts); - this.peak = Math.max.apply(null, data.map(d => d.total)) / 2; - let x = 0; - data.reverse().forEach(d => { - d.x = x; - d.date.weekday = (new Date(d.date.year, d.date.month - 1, d.date.day)).getDay(); - - d.v = d.total / this.peak; - if (d.v > 1) d.v = 1; - const ch = d.date.weekday == 0 || d.date.weekday == 6 ? 275 : 170; - const cs = d.v * 100; - const cl = 15 + ((1 - d.v) * 80); - d.color = `hsl(${ch}, ${cs}%, ${cl}%)`; - - if (d.date.weekday == 6) x++; - }); - this.update({ data }); - }); - }); - </script> -</mk-activity-table> diff --git a/src/web/app/common/tags/api-info.tag b/src/web/app/common/tags/api-info.tag deleted file mode 100644 index 612f20a7a8..0000000000 --- a/src/web/app/common/tags/api-info.tag +++ /dev/null @@ -1,27 +0,0 @@ -<mk-api-info> - <p>Token:<code>{ I.token }</code></p> - <p>APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。</p> - <p>アカウントを乗っ取られてしまう可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。</p> - <p>万が一このトークンが漏れたりその可能性がある場合は - <button class="regenerate" onclick={ regenerateToken }>トークンを再生成</button>できます。(副作用として、ログインしているすべてのデバイスでログアウトが発生します) - </p> - <style> - :scope - display block - color #4a535a - - code - padding 4px - background #eee - - .regenerate - display inline - color $theme-color - - &:hover - text-decoration underline - </style> - <script> - this.mixin('i'); - </script> -</mk-api-info> diff --git a/src/web/app/common/tags/authorized-apps.tag b/src/web/app/common/tags/authorized-apps.tag deleted file mode 100644 index 0078a18636..0000000000 --- a/src/web/app/common/tags/authorized-apps.tag +++ /dev/null @@ -1,33 +0,0 @@ -<mk-authorized-apps> - <p class="none" if={ !fetching && apps.length == 0 }>%i18n:common.tags.mk-authorized-apps.no-apps%</p> - <div class="apps" if={ apps.length != 0 }> - <div each={ app in apps }> - <p><b>{ app.name }</b></p> - <p>{ app.description }</p> - </div> - </div> - <style> - :scope - display block - - > .apps - > div - padding 16px 0 0 0 - border-bottom solid 1px #eee - - </style> - <script> - this.mixin('api'); - - this.apps = []; - this.fetching = true; - - this.on('mount', () => { - this.api('i/authorized_apps').then(apps => { - this.apps = apps; - this.fetching = false; - this.update(); - }); - }); - </script> -</mk-authorized-apps> diff --git a/src/web/app/common/tags/copyright.tag b/src/web/app/common/tags/copyright.tag deleted file mode 100644 index 9c3f1f648b..0000000000 --- a/src/web/app/common/tags/copyright.tag +++ /dev/null @@ -1,7 +0,0 @@ -<mk-copyright> - <span>(c) syuilo 2014-2017</span> - <style> - :scope - display block - </style> -</mk-copyright> diff --git a/src/web/app/common/tags/ellipsis.tag b/src/web/app/common/tags/ellipsis.tag deleted file mode 100644 index 97ef745d02..0000000000 --- a/src/web/app/common/tags/ellipsis.tag +++ /dev/null @@ -1,24 +0,0 @@ -<mk-ellipsis><span>.</span><span>.</span><span>.</span> - <style> - :scope - display inline - - > span - animation ellipsis 1.4s infinite ease-in-out both - - &:nth-child(1) - animation-delay 0s - - &:nth-child(2) - animation-delay 0.16s - - &:nth-child(3) - animation-delay 0.32s - - @keyframes ellipsis - 0%, 80%, 100% - opacity 1 - 40% - opacity 0 - </style> -</mk-ellipsis> diff --git a/src/web/app/common/tags/error.tag b/src/web/app/common/tags/error.tag deleted file mode 100644 index e4e0272a49..0000000000 --- a/src/web/app/common/tags/error.tag +++ /dev/null @@ -1,57 +0,0 @@ -<mk-error> - <img src="/assets/error.jpg" alt=""/> - <h1>%i18n:common.tags.mk-error.title%</h1> - <p class="text">%i18n:common.tags.mk-error.description%</p> - <p class="thanks">%i18n:common.tags.mk-error.thanks%</p> - <style> - :scope - display block - width 100% - padding 32px 18px - text-align center - - > img - display block - height 200px - margin 0 auto - pointer-events none - user-select none - - > h1 - display block - margin 1.25em auto 0.65em auto - font-size 1.5em - color #555 - - > .text - display block - margin 0 auto - max-width 600px - font-size 1em - color #666 - - > .thanks - display block - margin 2em auto 0 auto - padding 2em 0 0 0 - max-width 600px - font-size 0.9em - font-style oblique - color #aaa - border-top solid 1px #eee - - @media (max-width 500px) - padding 24px 18px - font-size 80% - - > img - height 150px - - </style> - <script> - this.on('mount', () => { - document.title = 'Oops!'; - document.documentElement.style.background = '#f8f8f8'; - }); - </script> -</mk-error> diff --git a/src/web/app/common/tags/file-type-icon.tag b/src/web/app/common/tags/file-type-icon.tag deleted file mode 100644 index 55c473bcd4..0000000000 --- a/src/web/app/common/tags/file-type-icon.tag +++ /dev/null @@ -1,10 +0,0 @@ -<mk-file-type-icon> - <i class="fa fa-file-image-o" if={ kind == 'image' }></i> - <style> - :scope - display inline - </style> - <script> - this.kind = this.opts.type.split('/')[0]; - </script> -</mk-file-type-icon> diff --git a/src/web/app/common/tags/forkit.tag b/src/web/app/common/tags/forkit.tag deleted file mode 100644 index 55d5731081..0000000000 --- a/src/web/app/common/tags/forkit.tag +++ /dev/null @@ -1,40 +0,0 @@ -<mk-forkit><a href="https://github.com/syuilo/misskey" target="_blank" title="%i18n:common.tags.mk-forkit.open-github-link%" aria-label="%i18n:common.tags.mk-forkit.open-github-link%"> - <svg width="80" height="80" viewBox="0 0 250 250" aria-hidden="aria-hidden"> - <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path> - <path class="octo-arm" d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor"></path> - <path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor"></path> - </svg></a> - <style> - :scope - display block - position absolute - top 0 - right 0 - - > a - display block - - > svg - display block - //fill #151513 - //color #fff - fill $theme-color - color $theme-color-foreground - - .octo-arm - transform-origin 130px 106px - - &:hover - .octo-arm - animation octocat-wave 560ms ease-in-out - - @keyframes octocat-wave - 0%, 100% - transform rotate(0) - 20%, 60% - transform rotate(-25deg) - 40%, 80% - transform rotate(10deg) - - </style> -</mk-forkit> diff --git a/src/web/app/common/tags/index.js b/src/web/app/common/tags/index.js deleted file mode 100644 index 5dc4ef4546..0000000000 --- a/src/web/app/common/tags/index.js +++ /dev/null @@ -1,30 +0,0 @@ -require('./error.tag'); -require('./url.tag'); -require('./url-preview.tag'); -require('./time.tag'); -require('./file-type-icon.tag'); -require('./uploader.tag'); -require('./ellipsis.tag'); -require('./raw.tag'); -require('./number.tag'); -require('./special-message.tag'); -require('./signin.tag'); -require('./signup.tag'); -require('./forkit.tag'); -require('./introduction.tag'); -require('./copyright.tag'); -require('./signin-history.tag'); -require('./api-info.tag'); -require('./twitter-setting.tag'); -require('./authorized-apps.tag'); -require('./poll.tag'); -require('./poll-editor.tag'); -require('./messaging/room.tag'); -require('./messaging/message.tag'); -require('./messaging/index.tag'); -require('./messaging/form.tag'); -require('./stream-indicator.tag'); -require('./activity-table.tag'); -require('./reaction-picker.tag'); -require('./reactions-viewer.tag'); -require('./reaction-icon.tag'); diff --git a/src/web/app/common/tags/introduction.tag b/src/web/app/common/tags/introduction.tag deleted file mode 100644 index fa1b1e247a..0000000000 --- a/src/web/app/common/tags/introduction.tag +++ /dev/null @@ -1,25 +0,0 @@ -<mk-introduction> - <article> - <h1>Misskeyとは?</h1> - <p><ruby>Misskey<rt>みすきー</rt></ruby>は、<a href="http://syuilo.com" target="_blank">syuilo</a>が2014年くらいから<a href="https://github.com/syuilo/misskey" target="_blank">オープンソースで</a>開発・運営を行っている、ミニブログベースのSNSです。</p> - <p>無料で誰でも利用でき、広告も掲載していません。</p> - <p><a href={ CONFIG.aboutUrl } target="_blank">もっと知りたい方はこちら</a></p> - </article> - <style> - :scope - display block - - h1 - margin 0 - text-align center - font-size 1.2em - - p - margin 16px 0 - - &:last-child - margin 0 - text-align center - - </style> -</mk-introduction> diff --git a/src/web/app/common/tags/messaging/form.tag b/src/web/app/common/tags/messaging/form.tag deleted file mode 100644 index a839bad7fa..0000000000 --- a/src/web/app/common/tags/messaging/form.tag +++ /dev/null @@ -1,175 +0,0 @@ -<mk-messaging-form> - <textarea ref="text" onkeypress={ onkeypress } onpaste={ onpaste } placeholder="%i18n:common.input-message-here%"></textarea> - <div class="files"></div> - <mk-uploader ref="uploader"/> - <button class="send" onclick={ send } disabled={ sending } title="%i18n:common.send%"> - <i class="fa fa-paper-plane" if={ !sending }></i><i class="fa fa-spinner fa-spin" if={ sending }></i> - </button> - <button class="attach-from-local" type="button" title="%i18n:common.tags.mk-messaging-form.attach-from-local%"> - <i class="fa fa-upload"></i> - </button> - <button class="attach-from-drive" type="button" title="%i18n:common.tags.mk-messaging-form.attach-from-drive%"> - <i class="fa fa-folder-open"></i> - </button> - <input name="file" type="file" accept="image/*"/> - <style> - :scope - display block - - > textarea - cursor auto - display block - width 100% - min-width 100% - max-width 100% - height 64px - margin 0 - padding 8px - font-size 1em - color #000 - outline none - border none - border-top solid 1px #eee - border-radius 0 - box-shadow none - background transparent - - > .send - position absolute - bottom 0 - right 0 - margin 0 - padding 10px 14px - line-height 1em - font-size 1em - color #aaa - transition color 0.1s ease - - &:hover - color $theme-color - - &:active - color darken($theme-color, 10%) - transition color 0s ease - - .files - display block - margin 0 - padding 0 8px - list-style none - - &:after - content '' - display block - clear both - - > li - display block - float left - margin 4px - padding 0 - width 64px - height 64px - background-color #eee - background-repeat no-repeat - background-position center center - background-size cover - cursor move - - &:hover - > .remove - display block - - > .remove - display none - position absolute - right -6px - top -6px - margin 0 - padding 0 - background transparent - outline none - border none - border-radius 0 - box-shadow none - cursor pointer - - .attach-from-local - .attach-from-drive - margin 0 - padding 10px 14px - line-height 1em - font-size 1em - font-weight normal - text-decoration none - color #aaa - transition color 0.1s ease - - &:hover - color $theme-color - - &:active - color darken($theme-color, 10%) - transition color 0s ease - - input[type=file] - display none - - </style> - <script> - this.mixin('api'); - - this.onpaste = e => { - const data = e.clipboardData; - const items = data.items; - for (const item of items) { - if (item.kind == 'file') { - this.upload(item.getAsFile()); - } - } - }; - - this.onkeypress = e => { - if ((e.which == 10 || e.which == 13) && e.ctrlKey) { - this.send(); - } - }; - - this.selectFile = () => { - this.refs.file.click(); - }; - - this.selectFileFromDrive = () => { - const browser = document.body.appendChild(document.createElement('mk-select-file-from-drive-window')); - const event = riot.observable(); - riot.mount(browser, { - multiple: true, - event: event - }); - event.one('selected', files => { - files.forEach(this.addFile); - }); - }; - - this.send = () => { - this.sending = true; - this.api('messaging/messages/create', { - user_id: this.opts.user.id, - text: this.refs.text.value - }).then(message => { - this.clear(); - }).catch(err => { - console.error(err); - }).then(() => { - this.sending = false; - this.update(); - }); - }; - - this.clear = () => { - this.refs.text.value = ''; - this.files = []; - this.update(); - }; - </script> -</mk-messaging-form> diff --git a/src/web/app/common/tags/messaging/index.tag b/src/web/app/common/tags/messaging/index.tag deleted file mode 100644 index 731c9da2c7..0000000000 --- a/src/web/app/common/tags/messaging/index.tag +++ /dev/null @@ -1,383 +0,0 @@ -<mk-messaging> - <div class="search"> - <div class="form"> - <label for="search-input"><i class="fa fa-search"></i></label> - <input ref="search" type="search" oninput={ search } onkeydown={ onSearchKeydown } placeholder="%i18n:common.tags.mk-messaging.search-user%"/> - </div> - <div class="result"> - <ol class="users" if={ searchResult.length > 0 } ref="searchResult"> - <li each={ user, i in searchResult } onkeydown={ parent.onSearchResultKeydown.bind(null, i) } onclick={ user._click } tabindex="-1"> - <img class="avatar" src={ user.avatar_url + '?thumbnail&size=32' } alt=""/> - <span class="name">{ user.name }</span> - <span class="username">@{ user.username }</span> - </li> - </ol> - </div> - </div> - <div class="history" if={ history.length > 0 }> - <virtual each={ history }> - <a class="user" data-is-me={ is_me } data-is-read={ is_read } onclick={ _click }> - <div> - <img class="avatar" src={ (is_me ? recipient.avatar_url : user.avatar_url) + '?thumbnail&size=64' } alt=""/> - <header> - <span class="name">{ is_me ? recipient.name : user.name }</span> - <span class="username">{ '@' + (is_me ? recipient.username : user.username ) }</span> - <mk-time time={ created_at }/> - </header> - <div class="body"> - <p class="text"><span class="me" if={ is_me }>%i18n:common.tags.mk-messaging.you%:</span>{ text }</p> - </div> - </div> - </a> - </virtual> - </div> - <p class="no-history" if={ history.length == 0 }>%i18n:common.tags.mk-messaging.no-history%</p> - <style> - :scope - display block - - > .search - display block - position -webkit-sticky - position sticky - top 0 - left 0 - z-index 1 - width 100% - background #fff - box-shadow 0 0px 2px rgba(0, 0, 0, 0.2) - - > .form - padding 8px - background #f7f7f7 - - > label - display block - position absolute - top 0 - left 8px - z-index 1 - height 100% - width 38px - pointer-events none - - > i - display block - position absolute - top 0 - right 0 - bottom 0 - left 0 - width 1em - height 1em - margin auto - color #555 - - > input - margin 0 - padding 0 12px 0 38px - width 100% - font-size 1em - line-height 38px - color #000 - outline none - border solid 1px #eee - border-radius 5px - box-shadow none - transition color 0.5s ease, border 0.5s ease - - &:hover - border solid 1px #ddd - transition border 0.2s ease - - &:focus - color darken($theme-color, 20%) - border solid 1px $theme-color - transition color 0, border 0 - - > .result - display block - top 0 - left 0 - z-index 2 - width 100% - margin 0 - padding 0 - background #fff - - > .users - margin 0 - padding 0 - list-style none - - > li - display inline-block - z-index 1 - width 100% - padding 8px 32px - vertical-align top - white-space nowrap - overflow hidden - color rgba(0, 0, 0, 0.8) - text-decoration none - transition none - cursor pointer - - &:hover - &:focus - color #fff - background $theme-color - - .name - color #fff - - .username - color #fff - - &:active - color #fff - background darken($theme-color, 10%) - - .name - color #fff - - .username - color #fff - - .avatar - vertical-align middle - min-width 32px - min-height 32px - max-width 32px - max-height 32px - margin 0 8px 0 0 - border-radius 6px - - .name - margin 0 8px 0 0 - /*font-weight bold*/ - font-weight normal - color rgba(0, 0, 0, 0.8) - - .username - font-weight normal - color rgba(0, 0, 0, 0.3) - - - > .history - - > a - display block - text-decoration none - background #fff - border-bottom solid 1px #eee - - * - pointer-events none - user-select none - - &:hover - background #fafafa - - > .avatar - filter saturate(200%) - - &:active - background #eee - - &[data-is-read] - &[data-is-me] - opacity 0.8 - - &:not([data-is-me]):not([data-is-read]) - > div - background-image url("/assets/unread.svg") - background-repeat no-repeat - background-position 0 center - - &:after - content "" - display block - clear both - - > div - max-width 500px - margin 0 auto - padding 20px 30px - - &:after - content "" - display block - clear both - - > header - margin-bottom 2px - white-space nowrap - overflow hidden - - > .name - text-align left - display inline - margin 0 - padding 0 - font-size 1em - color rgba(0, 0, 0, 0.9) - font-weight bold - transition all 0.1s ease - - > .username - text-align left - margin 0 0 0 8px - color rgba(0, 0, 0, 0.5) - - > mk-time - position absolute - top 0 - right 0 - display inline - color rgba(0, 0, 0, 0.5) - font-size 80% - - > .avatar - float left - width 54px - height 54px - margin 0 16px 0 0 - border-radius 8px - transition all 0.1s ease - - > .body - - > .text - display block - margin 0 0 0 0 - padding 0 - overflow hidden - overflow-wrap break-word - font-size 1.1em - color rgba(0, 0, 0, 0.8) - - .me - color rgba(0, 0, 0, 0.4) - - > .image - display block - max-width 100% - max-height 512px - - > .no-history - margin 0 - padding 2em 1em - text-align center - color #999 - font-weight 500 - - // TODO: element base media query - @media (max-width 400px) - > .search - > .result - > .users - > li - padding 8px 16px - - > .history - > a - &:not([data-is-me]):not([data-is-read]) - > div - background-image none - border-left solid 4px #3aa2dc - - > div - padding 16px - font-size 14px - - > .avatar - margin 0 12px 0 0 - - </style> - <script> - this.mixin('i'); - this.mixin('api'); - - this.searchResult = []; - - this.on('mount', () => { - this.api('messaging/history').then(history => { - this.isLoading = false; - history.forEach(message => { - message.is_me = message.user_id == this.I.id - message._click = () => { - this.trigger('navigate-user', message.is_me ? message.recipient : message.user); - }; - }); - this.history = history; - this.update(); - }); - }); - - this.search = () => { - const q = this.refs.search.value; - if (q == '') { - this.searchResult = []; - return; - } - this.api('users/search', { - query: q, - max: 5 - }).then(users => { - users.forEach(user => { - user._click = () => { - this.trigger('navigate-user', user); - this.searchResult = []; - }; - }); - this.update({ - searchResult: users - }); - }); - }; - - this.onSearchKeydown = e => { - switch (e.which) { - case 9: // [TAB] - case 40: // [↓] - e.preventDefault(); - e.stopPropagation(); - this.refs.searchResult.childNodes[0].focus(); - break; - } - }; - - this.onSearchResultKeydown = (i, e) => { - const cancel = () => { - e.preventDefault(); - e.stopPropagation(); - }; - switch (true) { - case e.which == 10: // [ENTER] - case e.which == 13: // [ENTER] - cancel(); - this.searchResult[i]._click(); - break; - - case e.which == 27: // [ESC] - cancel(); - this.refs.search.focus(); - break; - - case e.which == 9 && e.shiftKey: // [TAB] + [Shift] - case e.which == 38: // [↑] - cancel(); - (this.refs.searchResult.childNodes[i].previousElementSibling || this.refs.searchResult.childNodes[this.searchResult.length - 1]).focus(); - break; - - case e.which == 9: // [TAB] - case e.which == 40: // [↓] - cancel(); - (this.refs.searchResult.childNodes[i].nextElementSibling || this.refs.searchResult.childNodes[0]).focus(); - break; - } - }; - - </script> -</mk-messaging> diff --git a/src/web/app/common/tags/messaging/message.tag b/src/web/app/common/tags/messaging/message.tag deleted file mode 100644 index d6db9070e2..0000000000 --- a/src/web/app/common/tags/messaging/message.tag +++ /dev/null @@ -1,238 +0,0 @@ -<mk-messaging-message data-is-me={ message.is_me }> - <a class="avatar-anchor" href={ '/' + message.user.username } title={ message.user.username } target="_blank"> - <img class="avatar" src={ message.user.avatar_url + '?thumbnail&size=80' } alt=""/> - </a> - <div class="content-container"> - <div class="balloon"> - <p class="read" if={ message.is_me && message.is_read }>%i18n:common.tags.mk-messaging-message.is-read%</p> - <button class="delete-button" if={ message.is_me } title="%i18n:common.delete%"><img src="/assets/desktop/messaging/delete.png" alt="Delete"/></button> - <div class="content" if={ !message.is_deleted }> - <div ref="text"></div> - <div class="image" if={ message.file }><img src={ message.file.url } alt="image" title={ message.file.name }/></div> - </div> - <div class="content" if={ message.is_deleted }> - <p class="is-deleted">%i18n:common.tags.mk-messaging-message.deleted%</p> - </div> - </div> - <footer> - <mk-time time={ message.created_at }/><i class="fa fa-pencil is-edited" if={ message.is_edited }></i> - </footer> - </div> - <style> - :scope - $me-balloon-color = #23A7B6 - - display block - padding 10px 12px 10px 12px - background-color transparent - - &:after - content "" - display block - clear both - - > .avatar-anchor - display block - - > .avatar - display block - min-width 54px - min-height 54px - max-width 54px - max-height 54px - margin 0 - border-radius 8px - transition all 0.1s ease - - > .content-container - display block - margin 0 12px - padding 0 - max-width calc(100% - 78px) - - > .balloon - display block - float inherit - margin 0 - padding 0 - max-width 100% - min-height 38px - border-radius 16px - - &:before - content "" - pointer-events none - display block - position absolute - top 12px - - &:hover - > .delete-button - display block - - > .delete-button - display none - position absolute - z-index 1 - top -4px - right -4px - margin 0 - padding 0 - cursor pointer - outline none - border none - border-radius 0 - box-shadow none - background transparent - - > img - vertical-align bottom - width 16px - height 16px - cursor pointer - - > .read - user-select none - display block - position absolute - z-index 1 - bottom -4px - left -12px - margin 0 - color rgba(0, 0, 0, 0.5) - font-size 11px - - > .content - - > .is-deleted - display block - margin 0 - padding 0 - overflow hidden - overflow-wrap break-word - font-size 1em - color rgba(0, 0, 0, 0.5) - - > [ref='text'] - display block - margin 0 - padding 8px 16px - overflow hidden - overflow-wrap break-word - font-size 1em - color rgba(0, 0, 0, 0.8) - - &, * - user-select text - cursor auto - - & + .file - &.image - > img - border-radius 0 0 16px 16px - - > .file - &.image - > img - display block - max-width 100% - max-height 512px - border-radius 16px - - > footer - display block - clear both - margin 0 - padding 2px - font-size 10px - color rgba(0, 0, 0, 0.4) - - > .is-edited - margin-left 4px - - &:not([data-is-me='true']) - > .avatar-anchor - float left - - > .content-container - float left - - > .balloon - background #eee - - &:before - left -14px - border-top solid 8px transparent - border-right solid 8px #eee - border-bottom solid 8px transparent - border-left solid 8px transparent - - > footer - text-align left - - &[data-is-me='true'] - > .avatar-anchor - float right - - > .content-container - float right - - > .balloon - background $me-balloon-color - - &:before - right -14px - left auto - border-top solid 8px transparent - border-right solid 8px transparent - border-bottom solid 8px transparent - border-left solid 8px $me-balloon-color - - > .content - - > p.is-deleted - color rgba(255, 255, 255, 0.5) - - > [ref='text'] - &, * - color #fff !important - - > footer - text-align right - - &[data-is-deleted='true'] - > .content-container - opacity 0.5 - - </style> - <script> - import compile from '../../../common/scripts/text-compiler'; - - this.mixin('i'); - - this.message = this.opts.message; - this.message.is_me = this.message.user.id == this.I.id; - - this.on('mount', () => { - if (this.message.text) { - const tokens = this.message.ast; - - this.refs.text.innerHTML = compile(tokens); - - this.refs.text.children.forEach(e => { - if (e.tagName == 'MK-URL') riot.mount(e); - }); - - // URLをプレビュー - tokens - .filter(t => t.type == 'link') - .map(t => { - const el = this.refs.text.appendChild(document.createElement('mk-url-preview')); - riot.mount(el, { - url: t.content - }); - }); - } - }); - </script> -</mk-messaging-message> diff --git a/src/web/app/common/tags/messaging/room.tag b/src/web/app/common/tags/messaging/room.tag deleted file mode 100644 index b1082e26be..0000000000 --- a/src/web/app/common/tags/messaging/room.tag +++ /dev/null @@ -1,319 +0,0 @@ -<mk-messaging-room> - <div class="stream"> - <p class="init" if={ init }><i class="fa fa-spinner fa-spin"></i>%i18n:common.loading%</p> - <p class="empty" if={ !init && messages.length == 0 }><i class="fa fa-info-circle"></i>%i18n:common.tags.mk-messaging-room.empty%</p> - <p class="no-history" if={ !init && messages.length > 0 && !moreMessagesIsInStock }><i class="fa fa-flag"></i>%i18n:common.tags.mk-messaging-room.no-history%</p> - <button class="more { fetching: fetchingMoreMessages }" if={ moreMessagesIsInStock } onclick={ fetchMoreMessages } disabled={ fetchingMoreMessages }> - <i class="fa fa-spinner fa-pulse fa-fw" if={ fetchingMoreMessages }></i>{ fetchingMoreMessages ? '%i18n:common.loading%' : '%i18n:common.tags.mk-messaging-room.more%' } - </button> - <virtual each={ message, i in messages }> - <mk-messaging-message message={ message }/> - <p class="date" if={ i != messages.length - 1 && message._date != messages[i + 1]._date }><span>{ messages[i + 1]._datetext }</span></p> - </virtual> - </div> - <footer> - <div ref="notifications"></div> - <div class="grippie" title="%i18n:common.tags.mk-messaging-room.resize-form%"></div> - <mk-messaging-form user={ user }/> - </footer> - <style> - :scope - display block - - > .stream - max-width 600px - margin 0 auto - - > .init - width 100% - margin 0 - padding 16px 8px 8px 8px - text-align center - font-size 0.8em - color rgba(0, 0, 0, 0.4) - - i - margin-right 4px - - > .empty - width 100% - margin 0 - padding 16px 8px 8px 8px - text-align center - font-size 0.8em - color rgba(0, 0, 0, 0.4) - - i - margin-right 4px - - > .no-history - display block - margin 0 - padding 16px - text-align center - font-size 0.8em - color rgba(0, 0, 0, 0.4) - - i - margin-right 4px - - > .more - display block - margin 16px auto - padding 0 12px - line-height 24px - color #fff - background rgba(0, 0, 0, 0.3) - border-radius 12px - - &:hover - background rgba(0, 0, 0, 0.4) - - &:active - background rgba(0, 0, 0, 0.5) - - &.fetching - cursor wait - - > i - margin-right 4px - - > .message - // something - - > .date - display block - margin 8px 0 - text-align center - - &:before - content '' - display block - position absolute - height 1px - width 90% - top 16px - left 0 - right 0 - margin 0 auto - background rgba(0, 0, 0, 0.1) - - > span - display inline-block - margin 0 - padding 0 16px - //font-weight bold - line-height 32px - color rgba(0, 0, 0, 0.3) - background #fff - - > footer - position -webkit-sticky - position sticky - z-index 2 - bottom 0 - width 100% - max-width 600px - margin 0 auto - padding 0 - background rgba(255, 255, 255, 0.95) - background-clip content-box - - > [ref='notifications'] - position absolute - top -48px - width 100% - padding 8px 0 - text-align center - - &:empty - display none - - > p - display inline-block - margin 0 - padding 0 12px 0 28px - cursor pointer - line-height 32px - font-size 12px - color $theme-color-foreground - background $theme-color - border-radius 16px - transition opacity 1s ease - - > i - position absolute - top 0 - left 10px - line-height 32px - font-size 16px - - > .grippie - height 10px - margin-top -10px - background transparent - cursor ns-resize - - &:hover - //background rgba(0, 0, 0, 0.1) - - &:active - //background rgba(0, 0, 0, 0.2) - - </style> - <script> - import MessagingStreamConnection from '../../scripts/messaging-stream'; - - this.mixin('i'); - this.mixin('api'); - - this.user = this.opts.user; - this.init = true; - this.sending = false; - this.messages = []; - this.isNaked = this.opts.isNaked; - - this.connection = new MessagingStreamConnection(this.I, this.user.id); - - this.on('mount', () => { - this.connection.on('message', this.onMessage); - this.connection.on('read', this.onRead); - - document.addEventListener('visibilitychange', this.onVisibilitychange); - - this.fetchMessages().then(() => { - this.init = false; - this.update(); - this.scrollToBottom(); - }); - }); - - this.on('unmount', () => { - this.connection.off('message', this.onMessage); - this.connection.off('read', this.onRead); - this.connection.close(); - - document.removeEventListener('visibilitychange', this.onVisibilitychange); - }); - - this.on('update', () => { - this.messages.forEach(message => { - const date = (new Date(message.created_at)).getDate(); - const month = (new Date(message.created_at)).getMonth() + 1; - message._date = date; - message._datetext = month + '月 ' + date + '日'; - }); - }); - - this.onMessage = (message) => { - const isBottom = this.isBottom(); - - this.messages.push(message); - if (message.user_id != this.I.id && !document.hidden) { - this.connection.send({ - type: 'read', - id: message.id - }); - } - this.update(); - - if (isBottom) { - // Scroll to bottom - this.scrollToBottom(); - } else if (message.user_id != this.I.id) { - // Notify - this.notify('%i18n:common.tags.mk-messaging-room.new-message%'); - } - }; - - this.onRead = ids => { - if (!Array.isArray(ids)) ids = [ids]; - ids.forEach(id => { - if (this.messages.some(x => x.id == id)) { - const exist = this.messages.map(x => x.id).indexOf(id); - this.messages[exist].is_read = true; - this.update(); - } - }); - }; - - this.fetchMoreMessages = () => { - this.update({ - fetchingMoreMessages: true - }); - this.fetchMessages().then(() => { - this.update({ - fetchingMoreMessages: false - }); - }); - }; - - this.fetchMessages = () => new Promise((resolve, reject) => { - const max = this.moreMessagesIsInStock ? 20 : 10; - - this.api('messaging/messages', { - user_id: this.user.id, - limit: max + 1, - max_id: this.moreMessagesIsInStock ? this.messages[0].id : undefined - }).then(messages => { - if (messages.length == max + 1) { - this.moreMessagesIsInStock = true; - messages.pop(); - } else { - this.moreMessagesIsInStock = false; - } - - this.messages.unshift.apply(this.messages, messages.reverse()); - this.update(); - - resolve(); - }); - }); - - this.isBottom = () => { - const asobi = 32; - const current = this.isNaked - ? window.scrollY + window.innerHeight - : this.root.scrollTop + this.root.offsetHeight; - const max = this.isNaked - ? document.body.offsetHeight - : this.root.scrollHeight; - return current > (max - asobi); - }; - - this.scrollToBottom = () => { - if (this.isNaked) { - window.scroll(0, document.body.offsetHeight); - } else { - this.root.scrollTop = this.root.scrollHeight; - } - }; - - this.notify = message => { - const n = document.createElement('p'); - n.innerHTML = '<i class="fa fa-arrow-circle-down"></i>' + message; - n.onclick = () => { - this.scrollToBottom(); - n.parentNode.removeChild(n); - }; - this.refs.notifications.appendChild(n); - - setTimeout(() => { - n.style.opacity = 0; - setTimeout(() => n.parentNode.removeChild(n), 1000); - }, 4000); - }; - - this.onVisibilitychange = () => { - if (document.hidden) return; - this.messages.forEach(message => { - if (message.user_id !== this.I.id && !message.is_read) { - this.connection.send({ - type: 'read', - id: message.id - }); - } - }); - }; - </script> -</mk-messaging-room> diff --git a/src/web/app/common/tags/number.tag b/src/web/app/common/tags/number.tag deleted file mode 100644 index 7afb8b3983..0000000000 --- a/src/web/app/common/tags/number.tag +++ /dev/null @@ -1,16 +0,0 @@ -<mk-number> - <style> - :scope - display inline - </style> - <script> - this.on('mount', () => { - let value = this.opts.value; - const max = this.opts.max; - - if (max != null && value > max) value = max; - - this.root.innerHTML = value.toLocaleString(); - }); - </script> -</mk-number> diff --git a/src/web/app/common/tags/poll-editor.tag b/src/web/app/common/tags/poll-editor.tag deleted file mode 100644 index 0ee224f33c..0000000000 --- a/src/web/app/common/tags/poll-editor.tag +++ /dev/null @@ -1,121 +0,0 @@ -<mk-poll-editor> - <p class="caution" if={ choices.length < 2 }> - <i class="fa fa-exclamation-triangle"></i>%i18n:common.tags.mk-poll-editor.no-only-one-choice% - </p> - <ul ref="choices"> - <li each={ choice, i in choices }> - <input value={ choice } oninput={ oninput.bind(null, i) } placeholder={ '%i18n:common.tags.mk-poll-editor.choice-n%'.replace('{}', i + 1) }> - <button onclick={ remove.bind(null, i) } title="%i18n:common.tags.mk-poll-editor.remove%"> - <i class="fa fa-times"></i> - </button> - </li> - </ul> - <button class="add" if={ choices.length < 10 } onclick={ add }>%i18n:common.tags.mk-poll-editor.add%</button> - <button class="destroy" onclick={ destroy } title="%i18n:common.tags.mk-poll-editor.destroy%"> - <i class="fa fa-times"></i> - </button> - <style> - :scope - display block - padding 8px - - > .caution - margin 0 0 8px 0 - font-size 0.8em - color #f00 - - > i - margin-right 4px - - > ul - display block - margin 0 - padding 0 - list-style none - - > li - display block - margin 8px 0 - padding 0 - width 100% - - &:first-child - margin-top 0 - - &:last-child - margin-bottom 0 - - > input - padding 6px - border solid 1px rgba($theme-color, 0.1) - border-radius 4px - - &:hover - border-color rgba($theme-color, 0.2) - - &:focus - border-color rgba($theme-color, 0.5) - - > button - padding 4px 8px - color rgba($theme-color, 0.4) - - &:hover - color rgba($theme-color, 0.6) - - &:active - color darken($theme-color, 30%) - - > .add - margin 8px 0 0 0 - vertical-align top - color $theme-color - - > .destroy - position absolute - top 0 - right 0 - padding 4px 8px - color rgba($theme-color, 0.4) - - &:hover - color rgba($theme-color, 0.6) - - &:active - color darken($theme-color, 30%) - - </style> - <script> - this.choices = ['', '']; - - this.oninput = (i, e) => { - this.choices[i] = e.target.value; - }; - - this.add = () => { - this.choices.push(''); - this.update(); - this.refs.choices.childNodes[this.choices.length - 1].childNodes[0].focus(); - }; - - this.remove = (i) => { - this.choices = this.choices.filter((_, _i) => _i != i); - this.update(); - }; - - this.destroy = () => { - this.opts.ondestroy(); - }; - - this.get = () => { - return { - choices: this.choices.filter(choice => choice != '') - } - }; - - this.set = data => { - if (data.choices.length == 0) return; - this.choices = data.choices; - }; - </script> -</mk-poll-editor> diff --git a/src/web/app/common/tags/poll.tag b/src/web/app/common/tags/poll.tag deleted file mode 100644 index 52614cf709..0000000000 --- a/src/web/app/common/tags/poll.tag +++ /dev/null @@ -1,109 +0,0 @@ -<mk-poll data-is-voted={ isVoted }> - <ul> - <li each={ poll.choices } onclick={ vote.bind(null, id) } class={ voted: voted } title={ !parent.isVoted ? '%i18n:common.tags.mk-poll.vote-to%'.replace('{}', text) : '' }> - <div class="backdrop" style={ 'width:' + (parent.result ? (votes / parent.total * 100) : 0) + '%' }></div> - <span> - <i class="fa fa-check" if={ is_voted }></i> - { text } - <span class="votes" if={ parent.result }>({ '%i18n:common.tags.mk-poll.vote-count%'.replace('{}', votes) })</span> - </span> - </li> - </ul> - <p if={ total > 0 }> - <span>{ '%i18n:common.tags.mk-poll.total-users%'.replace('{}', total) }</span> - ・ - <a if={ !isVoted } onclick={ toggleResult }>{ result ? '%i18n:common.tags.mk-poll.vote%' : '%i18n:common.tags.mk-poll.show-result%' }</a> - <span if={ isVoted }>%i18n:common.tags.mk-poll.voted%</span> - </p> - <style> - :scope - display block - - > ul - display block - margin 0 - padding 0 - list-style none - - > li - display block - margin 4px 0 - padding 4px 8px - width 100% - border solid 1px #eee - border-radius 4px - overflow hidden - cursor pointer - - &:hover - background rgba(0, 0, 0, 0.05) - - &:active - background rgba(0, 0, 0, 0.1) - - > .backdrop - position absolute - top 0 - left 0 - height 100% - background $theme-color - transition width 1s ease - - > .votes - margin-left 4px - - > p - a - color inherit - - &[data-is-voted] - > ul > li - cursor default - - &:hover - background transparent - - &:active - background transparent - - </style> - <script> - this.mixin('api'); - - this.init = post => { - this.post = post; - this.poll = this.post.poll; - this.total = this.poll.choices.reduce((a, b) => a + b.votes, 0); - this.isVoted = this.poll.choices.some(c => c.is_voted); - this.result = this.isVoted; - this.update(); - }; - - this.init(this.opts.post); - - this.toggleResult = () => { - this.result = !this.result; - }; - - this.vote = id => { - if (this.poll.choices.some(c => c.is_voted)) return; - this.api('posts/polls/vote', { - post_id: this.post.id, - choice: id - }).then(() => { - this.poll.choices.forEach(c => { - if (c.id == id) { - c.votes++; - c.is_voted = true; - } - }); - this.update({ - poll: this.poll, - isVoted: true, - result: true, - total: this.total + 1 - }); - }); - }; - </script> -</mk-poll> diff --git a/src/web/app/common/tags/raw.tag b/src/web/app/common/tags/raw.tag deleted file mode 100644 index e1285694e4..0000000000 --- a/src/web/app/common/tags/raw.tag +++ /dev/null @@ -1,9 +0,0 @@ -<mk-raw> - <style> - :scope - display inline - </style> - <script> - this.root.innerHTML = this.opts.content; - </script> -</mk-raw> diff --git a/src/web/app/common/tags/reaction-icon.tag b/src/web/app/common/tags/reaction-icon.tag deleted file mode 100644 index 0127293917..0000000000 --- a/src/web/app/common/tags/reaction-icon.tag +++ /dev/null @@ -1,21 +0,0 @@ -<mk-reaction-icon> - <virtual if={ opts.reaction == 'like' }><img src="/assets/reactions/like.png" alt="%i18n:common.reactions.like%"></virtual> - <virtual if={ opts.reaction == 'love' }><img src="/assets/reactions/love.png" alt="%i18n:common.reactions.love%"></virtual> - <virtual if={ opts.reaction == 'laugh' }><img src="/assets/reactions/laugh.png" alt="%i18n:common.reactions.laugh%"></virtual> - <virtual if={ opts.reaction == 'hmm' }><img src="/assets/reactions/hmm.png" alt="%i18n:common.reactions.hmm%"></virtual> - <virtual if={ opts.reaction == 'surprise' }><img src="/assets/reactions/surprise.png" alt="%i18n:common.reactions.surprise%"></virtual> - <virtual if={ opts.reaction == 'congrats' }><img src="/assets/reactions/congrats.png" alt="%i18n:common.reactions.congrats%"></virtual> - <virtual if={ opts.reaction == 'angry' }><img src="/assets/reactions/angry.png" alt="%i18n:common.reactions.angry%"></virtual> - <virtual if={ opts.reaction == 'confused' }><img src="/assets/reactions/confused.png" alt="%i18n:common.reactions.confused%"></virtual> - <virtual if={ opts.reaction == 'pudding' }><img src="/assets/reactions/pudding.png" alt="%i18n:common.reactions.pudding%"></virtual> - - <style> - :scope - display inline - - img - vertical-align middle - width 1em - height 1em - </style> -</mk-reaction-icon> diff --git a/src/web/app/common/tags/reaction-picker.tag b/src/web/app/common/tags/reaction-picker.tag deleted file mode 100644 index 458d16ec71..0000000000 --- a/src/web/app/common/tags/reaction-picker.tag +++ /dev/null @@ -1,184 +0,0 @@ -<mk-reaction-picker> - <div class="backdrop" ref="backdrop" onclick={ close }></div> - <div class="popover { compact: opts.compact }" ref="popover"> - <p if={ !opts.compact }>{ title }</p> - <div> - <button onclick={ react.bind(null, 'like') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="1" title="%i18n:common.reactions.like%"><mk-reaction-icon reaction='like'/></button> - <button onclick={ react.bind(null, 'love') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="2" title="%i18n:common.reactions.love%"><mk-reaction-icon reaction='love'/></button> - <button onclick={ react.bind(null, 'laugh') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="3" title="%i18n:common.reactions.laugh%"><mk-reaction-icon reaction='laugh'/></button> - <button onclick={ react.bind(null, 'hmm') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="4" title="%i18n:common.reactions.hmm%"><mk-reaction-icon reaction='hmm'/></button> - <button onclick={ react.bind(null, 'surprise') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="5" title="%i18n:common.reactions.surprise%"><mk-reaction-icon reaction='surprise'/></button> - <button onclick={ react.bind(null, 'congrats') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="6" title="%i18n:common.reactions.congrats%"><mk-reaction-icon reaction='congrats'/></button> - <button onclick={ react.bind(null, 'angry') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="4" title="%i18n:common.reactions.angry%"><mk-reaction-icon reaction='angry'/></button> - <button onclick={ react.bind(null, 'confused') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="5" title="%i18n:common.reactions.confused%"><mk-reaction-icon reaction='confused'/></button> - <button onclick={ react.bind(null, 'pudding') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="6" title="%i18n:common.reactions.pudding%"><mk-reaction-icon reaction='pudding'/></button> - </div> - </div> - <style> - $border-color = rgba(27, 31, 35, 0.15) - - :scope - display block - position initial - - > .backdrop - position fixed - top 0 - left 0 - z-index 10000 - width 100% - height 100% - background rgba(0, 0, 0, 0.1) - opacity 0 - - > .popover - position absolute - z-index 10001 - background #fff - border 1px solid $border-color - border-radius 4px - box-shadow 0 3px 12px rgba(27, 31, 35, 0.15) - transform scale(0.5) - opacity 0 - - $balloon-size = 16px - - &:not(.compact) - margin-top $balloon-size - transform-origin center -($balloon-size) - - &:before - content "" - display block - position absolute - top -($balloon-size * 2) - left s('calc(50% - %s)', $balloon-size) - border-top solid $balloon-size transparent - border-left solid $balloon-size transparent - border-right solid $balloon-size transparent - border-bottom solid $balloon-size $border-color - - &:after - content "" - display block - position absolute - top -($balloon-size * 2) + 1.5px - left s('calc(50% - %s)', $balloon-size) - border-top solid $balloon-size transparent - border-left solid $balloon-size transparent - border-right solid $balloon-size transparent - border-bottom solid $balloon-size #fff - - > p - display block - margin 0 - padding 8px 10px - font-size 14px - color #586069 - border-bottom solid 1px #e1e4e8 - - > div - padding 4px - width 240px - text-align center - - > button - width 40px - height 40px - font-size 24px - border-radius 2px - - &:hover - background #eee - - &:active - background $theme-color - box-shadow inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15) - - </style> - <script> - import anime from 'animejs'; - - this.mixin('api'); - - this.post = this.opts.post; - this.source = this.opts.source; - - const placeholder = '%i18n:common.tags.mk-reaction-picker.choose-reaction%'; - - this.title = placeholder; - - this.onmouseover = e => { - this.update({ - title: e.target.title - }); - }; - - this.onmouseout = () => { - this.update({ - title: placeholder - }); - }; - - this.on('mount', () => { - const rect = this.source.getBoundingClientRect(); - const width = this.refs.popover.offsetWidth; - const height = this.refs.popover.offsetHeight; - if (this.opts.compact) { - const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); - const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2); - this.refs.popover.style.left = (x - (width / 2)) + 'px'; - this.refs.popover.style.top = (y - (height / 2)) + 'px'; - } else { - const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); - const y = rect.top + window.pageYOffset + this.source.offsetHeight; - this.refs.popover.style.left = (x - (width / 2)) + 'px'; - this.refs.popover.style.top = y + 'px'; - } - - anime({ - targets: this.refs.backdrop, - opacity: 1, - duration: 100, - easing: 'linear' - }); - - anime({ - targets: this.refs.popover, - opacity: 1, - scale: [0.5, 1], - duration: 500 - }); - }); - - this.react = reaction => { - this.api('posts/reactions/create', { - post_id: this.post.id, - reaction: reaction - }).then(() => { - if (this.opts.cb) this.opts.cb(); - this.unmount(); - }); - }; - - this.close = () => { - this.refs.backdrop.style.pointerEvents = 'none'; - anime({ - targets: this.refs.backdrop, - opacity: 0, - duration: 200, - easing: 'linear' - }); - - this.refs.popover.style.pointerEvents = 'none'; - anime({ - targets: this.refs.popover, - opacity: 0, - scale: 0.5, - duration: 200, - easing: 'easeInBack', - complete: () => this.unmount() - }); - }; - </script> -</mk-reaction-picker> diff --git a/src/web/app/common/tags/reactions-viewer.tag b/src/web/app/common/tags/reactions-viewer.tag deleted file mode 100644 index 50fb023f70..0000000000 --- a/src/web/app/common/tags/reactions-viewer.tag +++ /dev/null @@ -1,46 +0,0 @@ -<mk-reactions-viewer> - <virtual if={ reactions }> - <span if={ reactions.like }><mk-reaction-icon reaction='like'/><span>{ reactions.like }</span></span> - <span if={ reactions.love }><mk-reaction-icon reaction='love'/><span>{ reactions.love }</span></span> - <span if={ reactions.laugh }><mk-reaction-icon reaction='laugh'/><span>{ reactions.laugh }</span></span> - <span if={ reactions.hmm }><mk-reaction-icon reaction='hmm'/><span>{ reactions.hmm }</span></span> - <span if={ reactions.surprise }><mk-reaction-icon reaction='surprise'/><span>{ reactions.surprise }</span></span> - <span if={ reactions.congrats }><mk-reaction-icon reaction='congrats'/><span>{ reactions.congrats }</span></span> - <span if={ reactions.angry }><mk-reaction-icon reaction='angry'/><span>{ reactions.angry }</span></span> - <span if={ reactions.confused }><mk-reaction-icon reaction='confused'/><span>{ reactions.confused }</span></span> - <span if={ reactions.pudding }><mk-reaction-icon reaction='pudding'/><span>{ reactions.pudding }</span></span> - </virtual> - <style> - :scope - display block - border-top dashed 1px #eee - border-bottom dashed 1px #eee - margin 4px 0 - - &:empty - display none - - > span - margin-right 8px - - > mk-reaction-icon - font-size 1.4em - - > span - margin-left 4px - font-size 1.2em - color #444 - - </style> - <script> - this.post = this.opts.post; - - this.on('mount', () => { - this.update(); - }); - - this.on('update', () => { - this.reactions = this.post.reaction_counts; - }); - </script> -</mk-reactions-viewer> diff --git a/src/web/app/common/tags/signin-history.tag b/src/web/app/common/tags/signin-history.tag deleted file mode 100644 index 9c96746249..0000000000 --- a/src/web/app/common/tags/signin-history.tag +++ /dev/null @@ -1,78 +0,0 @@ -<mk-signin-history> - <div class="records" if={ history.length != 0 }> - <div each={ history }> - <mk-time time={ created_at }/> - <header><i class="fa fa-check" if={ success }></i><i class="fa fa-times" if={ !success }></i><span class="ip">{ ip }</span></header> - <pre><code>{ JSON.stringify(headers, null, ' ') }</code></pre> - </div> - </div> - <style> - :scope - display block - - > .records - > div - padding 16px 0 0 0 - border-bottom solid 1px #eee - - > header - - > i - margin-right 8px - - &.fa-check - color #0fda82 - - &.fa-times - color #ff3100 - - > .ip - display inline-block - color #444 - background #f8f8f8 - - > mk-time - position absolute - top 16px - right 0 - color #777 - - > pre - overflow auto - max-height 100px - - > code - white-space pre-wrap - word-break break-all - color #4a535a - - </style> - <script> - this.mixin('i'); - this.mixin('api'); - this.mixin('stream'); - - this.history = []; - this.fetching = true; - - this.on('mount', () => { - this.api('i/signin_history').then(history => { - this.update({ - fetching: false, - history: history - }); - }); - - this.stream.on('signin', this.onSignin); - }); - - this.on('unmount', () => { - this.stream.off('signin', this.onSignin); - }); - - this.onSignin = signin => { - this.history.unshift(signin); - this.update(); - }; - </script> -</mk-signin-history> diff --git a/src/web/app/common/tags/signin.tag b/src/web/app/common/tags/signin.tag deleted file mode 100644 index d369c0621c..0000000000 --- a/src/web/app/common/tags/signin.tag +++ /dev/null @@ -1,146 +0,0 @@ -<mk-signin> - <form class={ signing: signing } onsubmit={ onsubmit }> - <label class="user-name"> - <input ref="username" type="text" pattern="^[a-zA-Z0-9-]+$" placeholder="%i18n:common.tags.mk-signin.username%" autofocus="autofocus" required="required" oninput={ oninput }/><i class="fa fa-at"></i> - </label> - <label class="password"> - <input ref="password" type="password" placeholder="%i18n:common.tags.mk-signin.password%" required="required"/><i class="fa fa-lock"></i> - </label> - <button type="submit" disabled={ signing }>{ signing ? '%i18n:common.tags.mk-signin.signing-in%' : '%i18n:common.tags.mk-signin.signin%' }</button> - </form> - <style> - :scope - display block - - > form - display block - z-index 2 - - &.signing - &, * - cursor wait !important - - label - display block - margin 12px 0 - - i - display block - pointer-events none - position absolute - bottom 0 - top 0 - left 0 - z-index 1 - margin auto - padding 0 16px - height 1em - color #898786 - - input[type=text] - input[type=password] - user-select text - display inline-block - cursor auto - padding 0 0 0 38px - margin 0 - width 100% - line-height 44px - font-size 1em - color rgba(0, 0, 0, 0.7) - background #fff - outline none - border solid 1px #eee - border-radius 4px - - &:hover - background rgba(255, 255, 255, 0.7) - border-color #ddd - - & + i - color #797776 - - &:focus - background #fff - border-color #ccc - - & + i - color #797776 - - [type=submit] - cursor pointer - padding 16px - margin -6px 0 0 0 - width 100% - font-size 1.2em - color rgba(0, 0, 0, 0.5) - outline none - border none - border-radius 0 - background transparent - transition all .5s ease - - &:hover - color $theme-color - transition all .2s ease - - &:focus - color $theme-color - transition all .2s ease - - &:active - color darken($theme-color, 30%) - transition all .2s ease - - &:disabled - opacity 0.7 - - </style> - <script> - this.mixin('api'); - - this.user = null; - this.signing = false; - - this.oninput = () => { - this.api('users/show', { - username: this.refs.username.value - }).then(user => { - this.user = user; - this.trigger('user', user); - this.update(); - }); - }; - - this.onsubmit = e => { - e.preventDefault(); - - if (this.refs.username.value == '') { - this.refs.username.focus(); - return false; - } - if (this.refs.password.value == '') { - this.refs.password.focus(); - return false; - } - - this.update({ - signing: true - }); - - this.api('signin', { - username: this.refs.username.value, - password: this.refs.password.value - }).then(() => { - location.reload(); - }).catch(() => { - alert('something happened'); - this.update({ - signing: false - }); - }); - - return false; - }; - </script> -</mk-signin> diff --git a/src/web/app/common/tags/signup.tag b/src/web/app/common/tags/signup.tag deleted file mode 100644 index 0359f4fab9..0000000000 --- a/src/web/app/common/tags/signup.tag +++ /dev/null @@ -1,309 +0,0 @@ -<mk-signup> - <form onsubmit={ onsubmit } autocomplete="off"> - <label class="username"> - <p class="caption"><i class="fa fa-at"></i>%i18n:common.tags.mk-signup.username%</p> - <input ref="username" type="text" pattern="^[a-zA-Z0-9-]{3,20}$" placeholder="a~z、A~Z、0~9、-" autocomplete="off" required="required" onkeyup={ onChangeUsername }/> - <p class="profile-page-url-preview" if={ refs.username.value != '' && username-state != 'invalidFormat' && username-state != 'minRange' && username-state != 'maxRange' }>{ '/' + refs.username.value }</p> - <p class="info" if={ usernameState == 'wait' } style="color:#999"><i class="fa fa-fw fa-spinner fa-pulse"></i>%i18n:common.tags.mk-signup.checking%</p> - <p class="info" if={ usernameState == 'ok' } style="color:#3CB7B5"><i class="fa fa-fw fa-check"></i>%i18n:common.tags.mk-signup.available%</p> - <p class="info" if={ usernameState == 'unavailable' } style="color:#FF1161"><i class="fa fa-fw fa-exclamation-triangle"></i>%i18n:common.tags.mk-signup.unavailable%</p> - <p class="info" if={ usernameState == 'error' } style="color:#FF1161"><i class="fa fa-fw fa-exclamation-triangle"></i>%i18n:common.tags.mk-signup.error%</p> - <p class="info" if={ usernameState == 'invalid-format' } style="color:#FF1161"><i class="fa fa-fw fa-exclamation-triangle"></i>%i18n:common.tags.mk-signup.invalid-format%</p> - <p class="info" if={ usernameState == 'min-range' } style="color:#FF1161"><i class="fa fa-fw fa-exclamation-triangle"></i>%i18n:common.tags.mk-signup.too-short%</p> - <p class="info" if={ usernameState == 'max-range' } style="color:#FF1161"><i class="fa fa-fw fa-exclamation-triangle"></i>%i18n:common.tags.mk-signup.too-long%</p> - </label> - <label class="password"> - <p class="caption"><i class="fa fa-lock"></i>%i18n:common.tags.mk-signup.password%</p> - <input ref="password" type="password" placeholder="%i18n:common.tags.mk-signup.password-placeholder%" autocomplete="off" required="required" onkeyup={ onChangePassword }/> - <div class="meter" if={ passwordStrength != '' } data-strength={ passwordStrength }> - <div class="value" ref="passwordMetar"></div> - </div> - <p class="info" if={ passwordStrength == 'low' } style="color:#FF1161"><i class="fa fa-fw fa-exclamation-triangle"></i>%i18n:common.tags.mk-signup.weak-password%</p> - <p class="info" if={ passwordStrength == 'medium' } style="color:#3CB7B5"><i class="fa fa-fw fa-check"></i>%i18n:common.tags.mk-signup.normal-password%</p> - <p class="info" if={ passwordStrength == 'high' } style="color:#3CB7B5"><i class="fa fa-fw fa-check"></i>%i18n:common.tags.mk-signup.strong-password%</p> - </label> - <label class="retype-password"> - <p class="caption"><i class="fa fa-lock"></i>%i18n:common.tags.mk-signup.password%(%i18n:common.tags.mk-signup.retype%)</p> - <input ref="passwordRetype" type="password" placeholder="%i18n:common.tags.mk-signup.retype-placeholder%" autocomplete="off" required="required" onkeyup={ onChangePasswordRetype }/> - <p class="info" if={ passwordRetypeState == 'match' } style="color:#3CB7B5"><i class="fa fa-fw fa-check"></i>%i18n:common.tags.mk-signup.password-matched%</p> - <p class="info" if={ passwordRetypeState == 'not-match' } style="color:#FF1161"><i class="fa fa-fw fa-exclamation-triangle"></i>%i18n:common.tags.mk-signup.password-not-matched%</p> - </label> - <label class="recaptcha"> - <p class="caption"><i class="fa fa-toggle-on" if={ recaptchaed }></i><i class="fa fa-toggle-off" if={ !recaptchaed }></i>%i18n:common.tags.mk-signup.recaptcha%</p> - <div if={ recaptcha } class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" data-sitekey={ recaptcha.siteKey }></div> - </label> - <label class="agree-tou"> - <input name="agree-tou" type="checkbox" autocomplete="off" required="required"/> - <p><a href="https://github.com/syuilo/misskey/blob/master/src/docs/tou.md" target="_blank">利用規約</a>に同意する</p> - </label> - <button onclick={ onsubmit }>%i18n:common.tags.mk-signup.create%</button> - </form> - <style> - :scope - display block - min-width 302px - overflow hidden - - > form - - label - display block - margin 16px 0 - - > .caption - margin 0 0 4px 0 - color #828888 - font-size 0.95em - - > i - margin-right 0.25em - color #96adac - - > .info - display block - margin 4px 0 - font-size 0.8em - - > i - margin-right 0.3em - - &.username - .profile-page-url-preview - display block - margin 4px 8px 0 4px - font-size 0.8em - color #888 - - &:empty - display none - - &:not(:empty) + .info - margin-top 0 - - &.password - .meter - display block - margin-top 8px - width 100% - height 8px - - &[data-strength=''] - display none - - &[data-strength='low'] - > .value - background #d73612 - - &[data-strength='medium'] - > .value - background #d7ca12 - - &[data-strength='high'] - > .value - background #61bb22 - - > .value - display block - width 0% - height 100% - background transparent - border-radius 4px - transition all 0.1s ease - - [type=text], [type=password] - user-select text - display inline-block - cursor auto - padding 0 12px - margin 0 - width 100% - line-height 44px - font-size 1em - color #333 !important - background #fff !important - outline none - border solid 1px rgba(0, 0, 0, 0.1) - border-radius 4px - box-shadow 0 0 0 114514px #fff inset - transition all .3s ease - - &:hover - border-color rgba(0, 0, 0, 0.2) - transition all .1s ease - - &:focus - color $theme-color !important - border-color $theme-color - box-shadow 0 0 0 1024px #fff inset, 0 0 0 4px rgba($theme-color, 10%) - transition all 0s ease - - &:disabled - opacity 0.5 - - .agree-tou - padding 4px - border-radius 4px - - &:hover - background #f4f4f4 - - &:active - background #eee - - &, * - cursor pointer - - p - display inline - color #555 - - button - margin 0 0 32px 0 - padding 16px - width 100% - font-size 1em - color #fff - background $theme-color - border-radius 3px - - &:hover - background lighten($theme-color, 5%) - - &:active - background darken($theme-color, 5%) - - </style> - <script> - this.mixin('api'); - const getPasswordStrength = require('syuilo-password-strength'); - - this.usernameState = null; - this.passwordStrength = ''; - this.passwordRetypeState = null; - this.recaptchaed = false; - - window.onEecaptchaed = () => { - this.recaptchaed = true; - this.update(); - }; - - window.onRecaptchaExpired = () => { - this.recaptchaed = false; - this.update(); - }; - - this.on('mount', () => { - fetch('/config.json').then(res => { - res.json().then(conf => { - this.update({ - recaptcha: { - siteKey: conf.recaptcha.siteKey - } - }); - - const head = document.getElementsByTagName('head')[0]; - const script = document.createElement('script'); - script.setAttribute('src', 'https://www.google.com/recaptcha/api.js'); - head.appendChild(script); - }); - }); - }); - - this.onChangeUsername = () => { - const username = this.refs.username.value; - - if (username == '') { - this.update({ - usernameState: null - }); - return; - } - - const err = - !username.match(/^[a-zA-Z0-9\-]+$/) ? 'invalid-format' : - username.length < 3 ? 'min-range' : - username.length > 20 ? 'max-range' : - null; - - if (err) { - this.update({ - usernameState: err - }); - return; - } - - this.update({ - usernameState: 'wait' - }); - - this.api('username/available', { - username: username - }).then(result => { - this.update({ - usernameState: result.available ? 'ok' : 'unavailable' - }); - }).catch(err => { - this.update({ - usernameState: 'error' - }); - }); - }; - - this.onChangePassword = () => { - const password = this.refs.password.value; - - if (password == '') { - this.passwordStrength = ''; - return; - } - - const strength = getPasswordStrength(password); - this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low'; - this.update(); - this.refs.passwordMetar.style.width = `${strength * 100}%`; - }; - - this.onChangePasswordRetype = () => { - const password = this.refs.password.value; - const retypedPassword = this.refs.passwordRetype.value; - - if (retypedPassword == '') { - this.passwordRetypeState = null; - return; - } - - this.passwordRetypeState = password == retypedPassword ? 'match' : 'not-match'; - }; - - this.onsubmit = e => { - e.preventDefault(); - - const username = this.refs.username.value; - const password = this.refs.password.value; - - const locker = document.body.appendChild(document.createElement('mk-locker')); - - this.api('signup', { - username: username, - password: password, - 'g-recaptcha-response': grecaptcha.getResponse() - }).then(() => { - this.api('signin', { - username: username, - password: password - }).then(() => { - location.href = '/'; - }); - }).catch(() => { - alert('%i18n:common.tags.mk-signup.some-error%'); - - grecaptcha.reset(); - this.recaptchaed = false; - - locker.parentNode.removeChild(locker); - }); - - return false; - }; - </script> -</mk-signup> diff --git a/src/web/app/common/tags/special-message.tag b/src/web/app/common/tags/special-message.tag deleted file mode 100644 index 6643b1324a..0000000000 --- a/src/web/app/common/tags/special-message.tag +++ /dev/null @@ -1,27 +0,0 @@ -<mk-special-message> - <p if={ m == 1 && d == 1 }>%i18n:common.tags.mk-special-message.new-year%</p> - <p if={ m == 12 && d == 25 }>%i18n:common.tags.mk-special-message.christmas%</p> - <style> - :scope - display block - - &:empty - display none - - > p - margin 0 - padding 4px - text-align center - font-size 14px - font-weight bold - text-transform uppercase - color #fff - background #ff1036 - - </style> - <script> - const now = new Date(); - this.d = now.getDate(); - this.m = now.getMonth() + 1; - </script> -</mk-special-message> diff --git a/src/web/app/common/tags/stream-indicator.tag b/src/web/app/common/tags/stream-indicator.tag deleted file mode 100644 index ea1c437035..0000000000 --- a/src/web/app/common/tags/stream-indicator.tag +++ /dev/null @@ -1,71 +0,0 @@ -<mk-stream-indicator> - <p if={ stream.state == 'initializing' }> - <i class="fa fa-spinner fa-spin"></i> - <span>%i18n:common.tags.mk-stream-indicator.connecting%<mk-ellipsis/></span> - </p> - <p if={ stream.state == 'reconnecting' }> - <i class="fa fa-spinner fa-spin"></i> - <span>%i18n:common.tags.mk-stream-indicator.reconnecting%<mk-ellipsis/></span> - </p> - <p if={ stream.state == 'connected' }> - <i class="fa fa-check"></i> - <span>%i18n:common.tags.mk-stream-indicator.connected%</span> - </p> - <style> - :scope - display block - pointer-events none - position fixed - z-index 16384 - bottom 8px - right 8px - margin 0 - padding 6px 12px - font-size 0.9em - color #fff - background rgba(0, 0, 0, 0.8) - border-radius 4px - - > p - display block - margin 0 - - > i - margin-right 0.25em - - </style> - <script> - import anime from 'animejs'; - - this.mixin('i'); - this.mixin('stream'); - - this.on('before-mount', () => { - if (this.stream.state == 'connected') { - this.root.style.opacity = 0; - } - }); - - this.stream.on('_connected_', () => { - this.update(); - setTimeout(() => { - anime({ - targets: this.root, - opacity: 0, - easing: 'linear', - duration: 200 - }); - }, 1000); - }); - - this.stream.on('_closed_', () => { - this.update(); - anime({ - targets: this.root, - opacity: 1, - easing: 'linear', - duration: 100 - }); - }); - </script> -</mk-stream-indicator> diff --git a/src/web/app/common/tags/time.tag b/src/web/app/common/tags/time.tag deleted file mode 100644 index b0d7d24533..0000000000 --- a/src/web/app/common/tags/time.tag +++ /dev/null @@ -1,50 +0,0 @@ -<mk-time> - <time datetime={ opts.time }> - <span if={ mode == 'relative' }>{ relative }</span> - <span if={ mode == 'absolute' }>{ absolute }</span> - <span if={ mode == 'detail' }>{ absolute } ({ relative })</span> - </time> - <script> - this.time = new Date(this.opts.time); - this.mode = this.opts.mode || 'relative'; - this.tickid = null; - - this.absolute = - this.time.getFullYear() + '年' + - (this.time.getMonth() + 1) + '月' + - this.time.getDate() + '日' + - ' ' + - this.time.getHours() + '時' + - this.time.getMinutes() + '分'; - - this.on('mount', () => { - if (this.mode == 'relative' || this.mode == 'detail') { - this.tick(); - this.tickid = setInterval(this.tick, 1000); - } - }); - - this.on('unmount', () => { - if (this.mode === 'relative' || this.mode === 'detail') { - clearInterval(this.tickid); - } - }); - - this.tick = () => { - const now = new Date(); - const ago = (now - this.time) / 1000/*ms*/; - this.relative = - ago >= 31536000 ? '%i18n:common.time.years_ago%' .replace('{}', ~~(ago / 31536000)) : - ago >= 2592000 ? '%i18n:common.time.months_ago%' .replace('{}', ~~(ago / 2592000)) : - ago >= 604800 ? '%i18n:common.time.weeks_ago%' .replace('{}', ~~(ago / 604800)) : - ago >= 86400 ? '%i18n:common.time.days_ago%' .replace('{}', ~~(ago / 86400)) : - ago >= 3600 ? '%i18n:common.time.hours_ago%' .replace('{}', ~~(ago / 3600)) : - ago >= 60 ? '%i18n:common.time.minutes_ago%'.replace('{}', ~~(ago / 60)) : - ago >= 10 ? '%i18n:common.time.seconds_ago%'.replace('{}', ~~(ago % 60)) : - ago >= 0 ? '%i18n:common.time.just_now%' : - ago < 0 ? '%i18n:common.time.future%' : - '%i18n:common.time.unknown%'; - this.update(); - }; - </script> -</mk-time> diff --git a/src/web/app/common/tags/twitter-setting.tag b/src/web/app/common/tags/twitter-setting.tag deleted file mode 100644 index 470426700c..0000000000 --- a/src/web/app/common/tags/twitter-setting.tag +++ /dev/null @@ -1,64 +0,0 @@ -<mk-twitter-setting> - <p>%i18n:common.tags.mk-twitter-setting.description%<a href={ CONFIG.aboutUrl + '/link-to-twitter' } target="_blank">%i18n:common.tags.mk-twitter-setting.detail%</a></p> - <p class="account" if={ I.twitter } title={ 'Twitter ID: ' + I.twitter.user_id }>%i18n:common.tags.mk-twitter-setting.connected-to%: <a href={ 'https://twitter.com/' + I.twitter.screen_name } target="_blank">@{ I.twitter.screen_name }</a></p> - <p> - <a href={ CONFIG.apiUrl + '/connect/twitter' } target="_blank" onclick={ connect }>{ I.twitter ? '%i18n:common.tags.mk-twitter-setting.reconnect%' : '%i18n:common.tags.mk-twitter-setting.connect%' }</a> - <span if={ I.twitter }> or </span> - <a href={ CONFIG.apiUrl + '/disconnect/twitter' } target="_blank" if={ I.twitter } onclick={ disconnect }>%i18n:common.tags.mk-twitter-setting.disconnect%</a> - </p> - <p class="id" if={ I.twitter }>Twitter ID: { I.twitter.user_id }</p> - <style> - :scope - display block - color #4a535a - - .account - border solid 1px #e1e8ed - border-radius 4px - padding 16px - - a - font-weight bold - color inherit - - .id - color #8899a6 - </style> - <script> - import CONFIG from '../scripts/config'; - - this.mixin('i'); - - this.form = null; - - this.on('mount', () => { - this.I.on('updated', this.onMeUpdated); - }); - - this.on('unmount', () => { - this.I.off('updated', this.onMeUpdated); - }); - - this.onMeUpdated = () => { - if (this.I.twitter) { - if (this.form) this.form.close(); - } - }; - - this.connect = e => { - e.preventDefault(); - this.form = window.open(CONFIG.apiUrl + '/connect/twitter', - 'twitter_connect_window', - 'height=570,width=520'); - return false; - }; - - this.disconnect = e => { - e.preventDefault(); - window.open(CONFIG.apiUrl + '/disconnect/twitter', - 'twitter_disconnect_window', - 'height=570,width=520'); - return false; - }; - </script> -</mk-twitter-setting> diff --git a/src/web/app/common/tags/uploader.tag b/src/web/app/common/tags/uploader.tag deleted file mode 100644 index da97957a2c..0000000000 --- a/src/web/app/common/tags/uploader.tag +++ /dev/null @@ -1,199 +0,0 @@ -<mk-uploader> - <ol if={ uploads.length > 0 }> - <li each={ uploads }> - <div class="img" style="background-image: url({ img })"></div> - <p class="name"><i class="fa fa-spinner fa-pulse"></i>{ name }</p> - <p class="status"><span class="initing" if={ progress == undefined }>%i18n:common.tags.mk-uploader.waiting%<mk-ellipsis/></span><span class="kb" if={ progress != undefined }>{ String(Math.floor(progress.value / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }<i>KB</i> / { String(Math.floor(progress.max / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }<i>KB</i></span><span class="percentage" if={ progress != undefined }>{ Math.floor((progress.value / progress.max) * 100) }</span></p> - <progress if={ progress != undefined && progress.value != progress.max } value={ progress.value } max={ progress.max }></progress> - <div class="progress initing" if={ progress == undefined }></div> - <div class="progress waiting" if={ progress != undefined && progress.value == progress.max }></div> - </li> - </ol> - <style> - :scope - display block - overflow auto - - &:empty - display none - - > ol - display block - margin 0 - padding 0 - list-style none - - > li - display block - margin 8px 0 0 0 - padding 0 - height 36px - box-shadow 0 -1px 0 rgba($theme-color, 0.1) - border-top solid 8px transparent - - &:first-child - margin 0 - box-shadow none - border-top none - - > .img - display block - position absolute - top 0 - left 0 - width 36px - height 36px - background-size cover - background-position center center - - > .name - display block - position absolute - top 0 - left 44px - margin 0 - padding 0 - max-width 256px - font-size 0.8em - color rgba($theme-color, 0.7) - white-space nowrap - text-overflow ellipsis - overflow hidden - - > i - margin-right 4px - - > .status - display block - position absolute - top 0 - right 0 - margin 0 - padding 0 - font-size 0.8em - - > .initing - color rgba($theme-color, 0.5) - - > .kb - color rgba($theme-color, 0.5) - - > .percentage - display inline-block - width 48px - text-align right - - color rgba($theme-color, 0.7) - - &:after - content '%' - - > progress - display block - position absolute - bottom 0 - right 0 - margin 0 - width calc(100% - 44px) - height 8px - background transparent - border none - border-radius 4px - overflow hidden - - &::-webkit-progress-value - background $theme-color - - &::-webkit-progress-bar - background rgba($theme-color, 0.1) - - > .progress - display block - position absolute - bottom 0 - right 0 - margin 0 - width calc(100% - 44px) - height 8px - border none - border-radius 4px - background linear-gradient( - 45deg, - lighten($theme-color, 30%) 25%, - $theme-color 25%, - $theme-color 50%, - lighten($theme-color, 30%) 50%, - lighten($theme-color, 30%) 75%, - $theme-color 75%, - $theme-color - ) - background-size 32px 32px - animation bg 1.5s linear infinite - - &.initing - opacity 0.3 - - @keyframes bg - from {background-position: 0 0;} - to {background-position: -64px 32px;} - - </style> - <script> - this.mixin('i'); - - this.uploads = []; - - this.upload = (file, folder) => { - if (folder && typeof folder == 'object') folder = folder.id; - - const id = Math.random(); - - const ctx = { - id: id, - name: file.name || 'untitled', - progress: undefined - }; - - this.uploads.push(ctx); - this.trigger('change-uploads', this.uploads); - this.update(); - - const reader = new FileReader(); - reader.onload = e => { - ctx.img = e.target.result; - this.update(); - }; - reader.readAsDataURL(file); - - const data = new FormData(); - data.append('i', this.I.token); - data.append('file', file); - - if (folder) data.append('folder_id', folder); - - const xhr = new XMLHttpRequest(); - xhr.open('POST', this.CONFIG.apiUrl + '/drive/files/create', true); - xhr.onload = e => { - const driveFile = JSON.parse(e.target.response); - - this.trigger('uploaded', driveFile); - - this.uploads = this.uploads.filter(x => x.id != id); - this.trigger('change-uploads', this.uploads); - - this.update(); - }; - - xhr.upload.onprogress = e => { - if (e.lengthComputable) { - if (ctx.progress == undefined) ctx.progress = {}; - ctx.progress.max = e.total; - ctx.progress.value = e.loaded; - this.update(); - } - }; - - xhr.send(data); - }; - </script> -</mk-uploader> diff --git a/src/web/app/common/tags/url-preview.tag b/src/web/app/common/tags/url-preview.tag deleted file mode 100644 index 7dbdd8fea2..0000000000 --- a/src/web/app/common/tags/url-preview.tag +++ /dev/null @@ -1,117 +0,0 @@ -<mk-url-preview> - <a href={ url } target="_blank" title={ url } if={ !loading }> - <div class="thumbnail" if={ thumbnail } style={ 'background-image: url(' + thumbnail + ')' }></div> - <article> - <header> - <h1>{ title }</h1> - </header> - <p>{ description }</p> - <footer> - <img class="icon" if={ icon } src={ icon }/> - <p>{ sitename }</p> - </footer> - </article> - </a> - <style> - :scope - display block - font-size 16px - - > a - display block - border solid 1px #eee - border-radius 4px - overflow hidden - - &:hover - text-decoration none - border-color #ddd - - > article > header > h1 - text-decoration underline - - > .thumbnail - position absolute - width 100px - height 100% - background-position center - background-size cover - - & + article - left 100px - width calc(100% - 100px) - - > article - padding 16px - - > header - margin-bottom 8px - - > h1 - margin 0 - font-size 1em - color #555 - - > p - margin 0 - color #777 - font-size 0.8em - - > footer - margin-top 8px - height 16px - - > img - display inline-block - width 16px - height 16px - margin-right 4px - vertical-align top - - > p - display inline-block - margin 0 - color #666 - font-size 0.8em - line-height 16px - vertical-align top - - @media (max-width 500px) - font-size 8px - - > a - border none - - > .thumbnail - width 70px - - & + article - left 70px - width calc(100% - 70px) - - > article - padding 8px - - </style> - <script> - this.mixin('api'); - - this.url = this.opts.url; - this.loading = true; - - this.on('mount', () => { - fetch('/api:url?url=' + this.url).then(res => { - res.json().then(info => { - this.title = info.title; - this.description = info.description; - this.thumbnail = info.thumbnail; - this.icon = info.icon; - this.sitename = info.sitename; - - this.loading = false; - this.update(); - }); - }); - }); - </script> -</mk-url-preview> diff --git a/src/web/app/common/tags/url.tag b/src/web/app/common/tags/url.tag deleted file mode 100644 index 330acf8210..0000000000 --- a/src/web/app/common/tags/url.tag +++ /dev/null @@ -1,48 +0,0 @@ -<mk-url><a href={ url } target={ opts.target }><span class="schema">{ schema }//</span><span class="hostname">{ hostname }</span><span class="port" if={ port != '' }>:{ port }</span><span class="pathname" if={ pathname != '' }>{ pathname }</span><span class="query">{ query }</span><span class="hash">{ hash }</span></a> - <style> - :scope - word-break break-all - - > a - &:after - content "\f14c" - display inline-block - padding-left 2px - font-family FontAwesome - font-size .9em - font-weight 400 - font-style normal - - > .schema - opacity 0.5 - - > .hostname - font-weight bold - - > .pathname - opacity 0.8 - - > .query - opacity 0.5 - - > .hash - font-style italic - - </style> - <script> - this.url = this.opts.href; - - this.on('before-mount', () => { - const url = new URL(this.url); - - this.schema = url.protocol; - this.hostname = url.hostname; - this.port = url.port; - this.pathname = url.pathname; - this.query = url.search; - this.hash = url.hash; - - this.update(); - }); - </script> -</mk-url> diff --git a/src/web/app/desktop/mixins/index.js b/src/web/app/desktop/mixins/index.js deleted file mode 100644 index a7a3eb9485..0000000000 --- a/src/web/app/desktop/mixins/index.js +++ /dev/null @@ -1 +0,0 @@ -require('./user-preview'); diff --git a/src/web/app/desktop/mixins/user-preview.js b/src/web/app/desktop/mixins/user-preview.js deleted file mode 100644 index 3f483beb3a..0000000000 --- a/src/web/app/desktop/mixins/user-preview.js +++ /dev/null @@ -1,66 +0,0 @@ -import * as riot from 'riot'; - -riot.mixin('user-preview', { - init: function() { - const scan = () => { - this.root.querySelectorAll('[data-user-preview]:not([data-user-preview-attached])') - .forEach(attach.bind(this)); - }; - this.on('mount', scan); - this.on('updated', scan); - } -}); - -function attach(el) { - el.setAttribute('data-user-preview-attached', true); - - const user = el.getAttribute('data-user-preview'); - let tag = null; - let showTimer = null; - let hideTimer = null; - - el.addEventListener('mouseover', () => { - clearTimeout(showTimer); - clearTimeout(hideTimer); - showTimer = setTimeout(show, 500); - }); - - el.addEventListener('mouseleave', () => { - clearTimeout(showTimer); - clearTimeout(hideTimer); - hideTimer = setTimeout(close, 500); - }); - - this.on('unmount', () => { - clearTimeout(showTimer); - clearTimeout(hideTimer); - close(); - }); - - const show = () => { - if (tag) return; - const preview = document.createElement('mk-user-preview'); - const rect = el.getBoundingClientRect(); - const x = rect.left + el.offsetWidth + window.pageXOffset; - const y = rect.top + window.pageYOffset; - preview.style.top = y + 'px'; - preview.style.left = x + 'px'; - preview.addEventListener('mouseover', () => { - clearTimeout(hideTimer); - }); - preview.addEventListener('mouseleave', () => { - clearTimeout(showTimer); - hideTimer = setTimeout(close, 500); - }); - tag = riot.mount(document.body.appendChild(preview), { - user: user - })[0]; - }; - - const close = () => { - if (tag) { - tag.close(); - tag = null; - } - }; -} diff --git a/src/web/app/desktop/router.js b/src/web/app/desktop/router.js deleted file mode 100644 index afa8a2dce3..0000000000 --- a/src/web/app/desktop/router.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Desktop App Router - */ - -import * as riot from 'riot'; -const route = require('page'); -let page = null; - -export default me => { - route('/', index); - route('/i>mentions', mentions); - route('/post::post', post); - route('/search::query', search); - route('/:user', user.bind(null, 'home')); - route('/:user/graphs', user.bind(null, 'graphs')); - route('/:user/:post', post); - route('*', notFound); - - function index() { - me ? home() : entrance(); - } - - function home() { - mount(document.createElement('mk-home-page')); - } - - function entrance() { - mount(document.createElement('mk-entrance')); - document.documentElement.setAttribute('data-page', 'entrance'); - } - - function mentions() { - const el = document.createElement('mk-home-page'); - el.setAttribute('mode', 'mentions'); - mount(el); - } - - function search(ctx) { - const el = document.createElement('mk-search-page'); - el.setAttribute('query', ctx.params.query); - mount(el); - } - - function user(page, ctx) { - const el = document.createElement('mk-user-page'); - el.setAttribute('user', ctx.params.user); - el.setAttribute('page', page); - mount(el); - } - - function post(ctx) { - const el = document.createElement('mk-post-page'); - el.setAttribute('post', ctx.params.post); - mount(el); - } - - function notFound() { - mount(document.createElement('mk-not-found')); - } - - riot.mixin('page', { - page: route - }); - - // EXEC - route(); -}; - -function mount(content) { - document.documentElement.removeAttribute('data-page'); - if (page) page.unmount(); - const body = document.getElementById('app'); - page = riot.mount(body.appendChild(content))[0]; -} diff --git a/src/web/app/desktop/script.js b/src/web/app/desktop/script.js deleted file mode 100644 index 2e81147943..0000000000 --- a/src/web/app/desktop/script.js +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Desktop Client - */ - -// Style -import './style.styl'; - -require('./tags'); -require('./mixins'); -import * as riot from 'riot'; -import init from '../init'; -import route from './router'; -import fuckAdBlock from './scripts/fuck-ad-block'; -import getPostSummary from '../common/scripts/get-post-summary'; - -/** - * init - */ -init(async (me, stream) => { - /** - * Fuck AD Block - */ - fuckAdBlock(); - - /** - * Init Notification - */ - if ('Notification' in window) { - // 許可を得ていなかったらリクエスト - if (Notification.permission == 'default') { - await Notification.requestPermission(); - } - - if (Notification.permission == 'granted') { - registerNotifications(stream); - } - } - - // Start routing - route(me); -}); - -function registerNotifications(stream) { - if (stream == null) return; - - stream.on('drive_file_created', file => { - const n = new Notification('ファイルがアップロードされました', { - body: file.name, - icon: file.url + '?thumbnail&size=64' - }); - setTimeout(n.close.bind(n), 5000); - }); - - stream.on('mention', post => { - const n = new Notification(`${post.user.name}さんから:`, { - body: getPostSummary(post), - icon: post.user.avatar_url + '?thumbnail&size=64' - }); - setTimeout(n.close.bind(n), 6000); - }); - - stream.on('reply', post => { - const n = new Notification(`${post.user.name}さんから返信:`, { - body: getPostSummary(post), - icon: post.user.avatar_url + '?thumbnail&size=64' - }); - setTimeout(n.close.bind(n), 6000); - }); - - stream.on('quote', post => { - const n = new Notification(`${post.user.name}さんが引用:`, { - body: getPostSummary(post), - icon: post.user.avatar_url + '?thumbnail&size=64' - }); - setTimeout(n.close.bind(n), 6000); - }); - - stream.on('unread_messaging_message', message => { - const n = new Notification(`${message.user.name}さんからメッセージ:`, { - body: message.text, // TODO: getMessagingMessageSummary(message), - icon: message.user.avatar_url + '?thumbnail&size=64' - }); - n.onclick = () => { - n.close(); - riot.mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), { - user: message.user - }); - }; - setTimeout(n.close.bind(n), 7000); - }); -} diff --git a/src/web/app/desktop/scripts/autocomplete.js b/src/web/app/desktop/scripts/autocomplete.js deleted file mode 100644 index 8ca516e2a9..0000000000 --- a/src/web/app/desktop/scripts/autocomplete.js +++ /dev/null @@ -1,130 +0,0 @@ -const getCaretCoordinates = require('textarea-caret'); -import * as riot from 'riot'; - -/** - * オートコンプリートを管理するクラス。 - */ -class Autocomplete { - - /** - * 対象のテキストエリアを与えてインスタンスを初期化します。 - */ - constructor(textarea) { - // BIND --------------------------------- - this.onInput = this.onInput.bind(this); - this.complete = this.complete.bind(this); - this.close = this.close.bind(this); - // -------------------------------------- - - this.suggestion = null; - this.textarea = textarea; - } - - /** - * このインスタンスにあるテキストエリアの入力のキャプチャを開始します。 - */ - attach() { - this.textarea.addEventListener('input', this.onInput); - } - - /** - * このインスタンスにあるテキストエリアの入力のキャプチャを解除します。 - */ - detach() { - this.textarea.removeEventListener('input', this.onInput); - this.close(); - } - - /** - * [Private] テキスト入力時 - */ - onInput() { - this.close(); - - const caret = this.textarea.selectionStart; - const text = this.textarea.value.substr(0, caret); - - const mentionIndex = text.lastIndexOf('@'); - - if (mentionIndex == -1) return; - - const username = text.substr(mentionIndex + 1); - - if (!username.match(/^[a-zA-Z0-9-]+$/)) return; - - this.open('user', username); - } - - /** - * [Private] サジェストを提示します。 - */ - open(type, q) { - // 既に開いているサジェストは閉じる - this.close(); - - // サジェスト要素作成 - const tag = document.createElement('mk-autocomplete-suggestion'); - - // ~ サジェストを表示すべき位置を計算 ~ - - const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart); - - const rect = this.textarea.getBoundingClientRect(); - - const x = rect.left + window.pageXOffset + caretPosition.left; - const y = rect.top + window.pageYOffset + caretPosition.top; - - tag.style.left = x + 'px'; - tag.style.top = y + 'px'; - - // 要素追加 - const el = document.body.appendChild(tag); - - // マウント - this.suggestion = riot.mount(el, { - textarea: this.textarea, - complete: this.complete, - close: this.close, - type: type, - q: q - })[0]; - } - - /** - * [Private] サジェストを閉じます。 - */ - close() { - if (this.suggestion == null) return; - - this.suggestion.unmount(); - this.suggestion = null; - - this.textarea.focus(); - } - - /** - * [Private] オートコンプリートする - */ - complete(user) { - this.close(); - - const value = user.username; - - const caret = this.textarea.selectionStart; - const source = this.textarea.value; - - const before = source.substr(0, caret); - const trimmedBefore = before.substring(0, before.lastIndexOf('@')); - const after = source.substr(caret); - - // 結果を挿入する - this.textarea.value = trimmedBefore + '@' + value + ' ' + after; - - // キャレットを戻す - this.textarea.focus(); - const pos = caret + value.length; - this.textarea.setSelectionRange(pos, pos); - } -} - -export default Autocomplete; diff --git a/src/web/app/desktop/scripts/dialog.js b/src/web/app/desktop/scripts/dialog.js deleted file mode 100644 index c502d3fcb8..0000000000 --- a/src/web/app/desktop/scripts/dialog.js +++ /dev/null @@ -1,16 +0,0 @@ -import * as riot from 'riot'; - -export default (title, text, buttons, canThrough, onThrough) => { - const dialog = document.body.appendChild(document.createElement('mk-dialog')); - const controller = riot.observable(); - riot.mount(dialog, { - controller: controller, - title: title, - text: text, - buttons: buttons, - canThrough: canThrough, - onThrough: onThrough - }); - controller.trigger('open'); - return controller; -}; diff --git a/src/web/app/desktop/scripts/fuck-ad-block.js b/src/web/app/desktop/scripts/fuck-ad-block.js deleted file mode 100644 index ccfc43ce6e..0000000000 --- a/src/web/app/desktop/scripts/fuck-ad-block.js +++ /dev/null @@ -1,18 +0,0 @@ -require('fuckadblock'); -import dialog from './dialog'; - -export default () => { - if (fuckAdBlock === undefined) { - adBlockDetected(); - } else { - fuckAdBlock.onDetected(adBlockDetected); - } -}; - -function adBlockDetected() { - dialog('<i class="fa fa-exclamation-triangle"></i>広告ブロッカーを無効にしてください', - '<strong>Misskeyは広告を掲載していません</strong>が、広告をブロックする機能が有効だと一部の機能が利用できなかったり、不具合が発生する場合があります。', - [{ - text: 'OK' - }]); -} diff --git a/src/web/app/desktop/scripts/input-dialog.js b/src/web/app/desktop/scripts/input-dialog.js deleted file mode 100644 index 954fabfb67..0000000000 --- a/src/web/app/desktop/scripts/input-dialog.js +++ /dev/null @@ -1,12 +0,0 @@ -import * as riot from 'riot'; - -export default (title, placeholder, defaultValue, onOk, onCancel) => { - const dialog = document.body.appendChild(document.createElement('mk-input-dialog')); - return riot.mount(dialog, { - title: title, - placeholder: placeholder, - 'default': defaultValue, - onOk: onOk, - onCancel: onCancel - }); -}; diff --git a/src/web/app/desktop/scripts/not-implemented-exception.js b/src/web/app/desktop/scripts/not-implemented-exception.js deleted file mode 100644 index dd00c7662f..0000000000 --- a/src/web/app/desktop/scripts/not-implemented-exception.js +++ /dev/null @@ -1,8 +0,0 @@ -import dialog from './dialog'; - -export default () => { - dialog('<i class="fa fa-exclamation-triangle"></i>Not implemented yet', - '要求された操作は実装されていません。<br>→<a href="https://github.com/syuilo/misskey" target="_blank">Misskeyの開発に参加する</a>', [{ - text: 'OK' - }]); -}; diff --git a/src/web/app/desktop/scripts/notify.js b/src/web/app/desktop/scripts/notify.js deleted file mode 100644 index e58a8e4d36..0000000000 --- a/src/web/app/desktop/scripts/notify.js +++ /dev/null @@ -1,8 +0,0 @@ -import * as riot from 'riot'; - -export default message => { - const notification = document.body.appendChild(document.createElement('mk-ui-notification')); - riot.mount(notification, { - message: message - }); -}; diff --git a/src/web/app/desktop/scripts/update-avatar.js b/src/web/app/desktop/scripts/update-avatar.js deleted file mode 100644 index 165c90567c..0000000000 --- a/src/web/app/desktop/scripts/update-avatar.js +++ /dev/null @@ -1,87 +0,0 @@ -import * as riot from 'riot'; -import CONFIG from '../../common/scripts/config'; -import dialog from './dialog'; -import api from '../../common/scripts/api'; - -export default (I, cb, file = null) => { - const fileSelected = file => { - const cropper = riot.mount(document.body.appendChild(document.createElement('mk-crop-window')), { - file: file, - title: 'アバターとして表示する部分を選択', - aspectRatio: 1 / 1 - })[0]; - - cropper.on('cropped', blob => { - const data = new FormData(); - data.append('i', I.token); - data.append('file', blob, file.name + '.cropped.png'); - - api(I, 'drive/folders/find', { - name: 'アイコン' - }).then(iconFolder => { - if (iconFolder.length === 0) { - api(I, 'drive/folders/create', { - name: 'アイコン' - }).then(iconFolder => { - upload(data, iconFolder); - }); - } else { - upload(data, iconFolder[0]); - } - }); - }); - - cropper.on('skipped', () => { - set(file); - }); - }; - - const upload = (data, folder) => { - const progress = riot.mount(document.body.appendChild(document.createElement('mk-progress-dialog')), { - title: '新しいアバターをアップロードしています' - })[0]; - - if (folder) data.append('folder_id', folder.id); - - const xhr = new XMLHttpRequest(); - xhr.open('POST', CONFIG.apiUrl + '/drive/files/create', true); - xhr.onload = e => { - const file = JSON.parse(e.target.response); - progress.close(); - set(file); - }; - - xhr.upload.onprogress = e => { - if (e.lengthComputable) progress.updateProgress(e.loaded, e.total); - }; - - xhr.send(data); - }; - - const set = file => { - api(I, 'i/update', { - avatar_id: file.id - }).then(i => { - dialog('<i class="fa fa-info-circle"></i>アバターを更新しました', - '新しいアバターが反映されるまで時間がかかる場合があります。', - [{ - text: 'わかった' - }]); - - if (cb) cb(i); - }); - }; - - if (file) { - fileSelected(file); - } else { - const browser = riot.mount(document.body.appendChild(document.createElement('mk-select-file-from-drive-window')), { - multiple: false, - title: '<i class="fa fa-picture-o"></i>アバターにする画像を選択' - })[0]; - - browser.one('selected', file => { - fileSelected(file); - }); - } -}; diff --git a/src/web/app/desktop/scripts/update-banner.js b/src/web/app/desktop/scripts/update-banner.js deleted file mode 100644 index d83b2bf1b1..0000000000 --- a/src/web/app/desktop/scripts/update-banner.js +++ /dev/null @@ -1,87 +0,0 @@ -import * as riot from 'riot'; -import CONFIG from '../../common/scripts/config'; -import dialog from './dialog'; -import api from '../../common/scripts/api'; - -export default (I, cb, file = null) => { - const fileSelected = file => { - const cropper = riot.mount(document.body.appendChild(document.createElement('mk-crop-window')), { - file: file, - title: 'バナーとして表示する部分を選択', - aspectRatio: 16 / 9 - })[0]; - - cropper.on('cropped', blob => { - const data = new FormData(); - data.append('i', I.token); - data.append('file', blob, file.name + '.cropped.png'); - - api(I, 'drive/folders/find', { - name: 'バナー' - }).then(iconFolder => { - if (iconFolder.length === 0) { - api(I, 'drive/folders/create', { - name: 'バナー' - }).then(iconFolder => { - upload(data, iconFolder); - }); - } else { - upload(data, iconFolder[0]); - } - }); - }); - - cropper.on('skipped', () => { - set(file); - }); - }; - - const upload = (data, folder) => { - const progress = riot.mount(document.body.appendChild(document.createElement('mk-progress-dialog')), { - title: '新しいバナーをアップロードしています' - })[0]; - - if (folder) data.append('folder_id', folder.id); - - const xhr = new XMLHttpRequest(); - xhr.open('POST', CONFIG.apiUrl + '/drive/files/create', true); - xhr.onload = e => { - const file = JSON.parse(e.target.response); - progress.close(); - set(file); - }; - - xhr.upload.onprogress = e => { - if (e.lengthComputable) progress.updateProgress(e.loaded, e.total); - }; - - xhr.send(data); - }; - - const set = file => { - api(I, 'i/update', { - banner_id: file.id - }).then(i => { - dialog('<i class="fa fa-info-circle"></i>バナーを更新しました', - '新しいバナーが反映されるまで時間がかかる場合があります。', - [{ - text: 'わかりました。' - }]); - - if (cb) cb(i); - }); - }; - - if (file) { - fileSelected(file); - } else { - const browser = riot.mount(document.body.appendChild(document.createElement('mk-select-file-from-drive-window')), { - multiple: false, - title: '<i class="fa fa-picture-o"></i>バナーにする画像を選択' - })[0]; - - browser.one('selected', file => { - fileSelected(file); - }); - } -}; diff --git a/src/web/app/desktop/style.styl b/src/web/app/desktop/style.styl deleted file mode 100644 index fa50f6ce31..0000000000 --- a/src/web/app/desktop/style.styl +++ /dev/null @@ -1,114 +0,0 @@ -@import "../base" -@import "../../../../node_modules/cropperjs/dist/cropper.css" - -*::input-placeholder - color #D8CBC5 - -* - &:focus - outline none - - &::scrollbar - width 5px - background transparent - - &:horizontal - height 5px - - &::scrollbar-button - width 0 - height 0 - background rgba(0, 0, 0, 0.2) - - &::scrollbar-piece - background transparent - - &:start - background transparent - - &::scrollbar-thumb - background rgba(0, 0, 0, 0.2) - - &:hover - background rgba(0, 0, 0, 0.4) - - &:active - background $theme-color - - &::scrollbar-corner - background rgba(0, 0, 0, 0.2) - -html - background #fdfdfd - - // ↓ workaround of https://github.com/riot/riot/issues/2134 - &[data-page='entrance'] - #wait - right auto - left 15px - -html[theme='dark'] - background #100f0f - -button - font-family sans-serif - - * - pointer-events none - - &.style-normal - &.style-primary - display block - cursor pointer - padding 0 16px - margin 0 - min-width 100px - height 40px - font-size 1em - outline none - border-radius 4px - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - &:disabled - opacity 0.7 - cursor default - - &.style-normal - color #888 - background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) - border solid 1px #e2e2e2 - - &:hover - background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) - border-color #dcdcdc - - &:active - background #ececec - border-color #dcdcdc - - &.style-primary - color $theme-color-foreground - background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) - border solid 1px lighten($theme-color, 15%) - - &:not(:disabled) - font-weight bold - - &:hover:not(:disabled) - background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) - border-color $theme-color - - &:active:not(:disabled) - background $theme-color - border-color $theme-color diff --git a/src/web/app/desktop/tags/autocomplete-suggestion.tag b/src/web/app/desktop/tags/autocomplete-suggestion.tag deleted file mode 100644 index b936360402..0000000000 --- a/src/web/app/desktop/tags/autocomplete-suggestion.tag +++ /dev/null @@ -1,197 +0,0 @@ -<mk-autocomplete-suggestion> - <ol class="users" ref="users" if={ users.length > 0 }> - <li each={ users } onclick={ parent.onClick } onkeydown={ parent.onKeydown } tabindex="-1"> - <img class="avatar" src={ avatar_url + '?thumbnail&size=32' } alt=""/> - <span class="name">{ name }</span> - <span class="username">@{ username }</span> - </li> - </ol> - <style> - :scope - display block - position absolute - z-index 65535 - margin-top calc(1em + 8px) - overflow hidden - background #fff - border solid 1px rgba(0, 0, 0, 0.1) - border-radius 4px - - > .users - display block - margin 0 - padding 4px 0 - max-height 190px - max-width 500px - overflow auto - list-style none - - > li - display block - padding 4px 12px - white-space nowrap - overflow hidden - font-size 0.9em - color rgba(0, 0, 0, 0.8) - cursor default - - &, * - user-select none - - &:hover - &[data-selected='true'] - color #fff - background $theme-color - - .name - color #fff - - .username - color #fff - - &:active - color #fff - background darken($theme-color, 10%) - - .name - color #fff - - .username - color #fff - - .avatar - vertical-align middle - min-width 28px - min-height 28px - max-width 28px - max-height 28px - margin 0 8px 0 0 - border-radius 100% - - .name - margin 0 8px 0 0 - /*font-weight bold*/ - font-weight normal - color rgba(0, 0, 0, 0.8) - - .username - font-weight normal - color rgba(0, 0, 0, 0.3) - - </style> - <script> - import contains from '../../common/scripts/contains'; - - this.mixin('api'); - - this.q = this.opts.q; - this.textarea = this.opts.textarea; - this.fetching = true; - this.users = []; - this.select = -1; - - this.on('mount', () => { - this.textarea.addEventListener('keydown', this.onKeydown); - - document.querySelectorAll('body *').forEach(el => { - el.addEventListener('mousedown', this.mousedown); - }); - - this.api('users/search_by_username', { - query: this.q, - limit: 30 - }).then(users => { - this.update({ - fetching: false, - users: users - }); - }); - }); - - this.on('unmount', () => { - this.textarea.removeEventListener('keydown', this.onKeydown); - - document.querySelectorAll('body *').forEach(el => { - el.removeEventListener('mousedown', this.mousedown); - }); - }); - - this.mousedown = e => { - if (!contains(this.root, e.target) && (this.root != e.target)) this.close(); - }; - - this.onClick = e => { - this.complete(e.item); - }; - - this.onKeydown = e => { - const cancel = () => { - e.preventDefault(); - e.stopPropagation(); - }; - - switch (e.which) { - case 10: // [ENTER] - case 13: // [ENTER] - if (this.select !== -1) { - cancel(); - this.complete(this.users[this.select]); - } else { - this.close(); - } - break; - - case 27: // [ESC] - cancel(); - this.close(); - break; - - case 38: // [↑] - if (this.select !== -1) { - cancel(); - this.selectPrev(); - } else { - this.close(); - } - break; - - case 9: // [TAB] - case 40: // [↓] - cancel(); - this.selectNext(); - break; - - default: - this.close(); - } - }; - - this.selectNext = () => { - if (++this.select >= this.users.length) this.select = 0; - this.applySelect(); - }; - - this.selectPrev = () => { - if (--this.select < 0) this.select = this.users.length - 1; - this.applySelect(); - }; - - this.applySelect = () => { - this.refs.users.children.forEach(el => { - el.removeAttribute('data-selected'); - }); - - this.refs.users.children[this.select].setAttribute('data-selected', 'true'); - this.refs.users.children[this.select].focus(); - }; - - this.complete = user => { - this.opts.complete(user); - }; - - this.close = () => { - this.opts.close(); - }; - - </script> -</mk-autocomplete-suggestion> diff --git a/src/web/app/desktop/tags/big-follow-button.tag b/src/web/app/desktop/tags/big-follow-button.tag deleted file mode 100644 index 86df2d4924..0000000000 --- a/src/web/app/desktop/tags/big-follow-button.tag +++ /dev/null @@ -1,145 +0,0 @@ -<mk-big-follow-button> - <button class={ wait: wait, follow: !user.is_following, unfollow: user.is_following } if={ !init } onclick={ onclick } disabled={ wait } title={ user.is_following ? 'フォロー解除' : 'フォローする' }><span if={ !wait && user.is_following }><i class="fa fa-minus"></i>フォロー解除</span><span if={ !wait && !user.is_following }><i class="fa fa-plus"></i>フォロー</span><i class="fa fa-spinner fa-pulse fa-fw" if={ wait }></i></button> - <div class="init" if={ init }><i class="fa fa-spinner fa-pulse fa-fw"></i></div> - <style> - :scope - display block - - > button - > .init - display block - cursor pointer - padding 0 - margin 0 - width 100% - line-height 38px - font-size 1em - outline none - border-radius 4px - - * - pointer-events none - - i - margin-right 8px - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - &.follow - color #888 - background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) - border solid 1px #e2e2e2 - - &:hover - background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) - border-color #dcdcdc - - &:active - background #ececec - border-color #dcdcdc - - &.unfollow - color $theme-color-foreground - background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) - border solid 1px lighten($theme-color, 15%) - - &:not(:disabled) - font-weight bold - - &:hover:not(:disabled) - background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) - border-color $theme-color - - &:active:not(:disabled) - background $theme-color - border-color $theme-color - - &.wait - cursor wait !important - opacity 0.7 - - </style> - <script> - import isPromise from '../../common/scripts/is-promise'; - - this.mixin('i'); - this.mixin('api'); - this.mixin('stream'); - - this.user = null; - this.userPromise = isPromise(this.opts.user) - ? this.opts.user - : Promise.resolve(this.opts.user); - this.init = true; - this.wait = false; - - this.on('mount', () => { - this.userPromise.then(user => { - this.update({ - init: false, - user: user - }); - this.stream.on('follow', this.onStreamFollow); - this.stream.on('unfollow', this.onStreamUnfollow); - }); - }); - - this.on('unmount', () => { - this.stream.off('follow', this.onStreamFollow); - this.stream.off('unfollow', this.onStreamUnfollow); - }); - - this.onStreamFollow = user => { - if (user.id == this.user.id) { - this.update({ - user: user - }); - } - }; - - this.onStreamUnfollow = user => { - if (user.id == this.user.id) { - this.update({ - user: user - }); - } - }; - - this.onclick = () => { - this.wait = true; - if (this.user.is_following) { - this.api('following/delete', { - user_id: this.user.id - }).then(() => { - this.user.is_following = false; - }).catch(err => { - console.error(err); - }).then(() => { - this.wait = false; - this.update(); - }); - } else { - this.api('following/create', { - user_id: this.user.id - }).then(() => { - this.user.is_following = true; - }).catch(err => { - console.error(err); - }).then(() => { - this.wait = false; - this.update(); - }); - } - }; - </script> -</mk-big-follow-button> diff --git a/src/web/app/desktop/tags/contextmenu.tag b/src/web/app/desktop/tags/contextmenu.tag deleted file mode 100644 index c862f3f609..0000000000 --- a/src/web/app/desktop/tags/contextmenu.tag +++ /dev/null @@ -1,138 +0,0 @@ -<mk-contextmenu> - <yield /> - <style> - :scope - $width = 240px - $item-height = 38px - $padding = 10px - - display none - position fixed - top 0 - left 0 - z-index 4096 - width $width - font-size 0.8em - background #fff - border-radius 0 4px 4px 4px - box-shadow 2px 2px 8px rgba(0, 0, 0, 0.2) - opacity 0 - - ul - display block - margin 0 - padding $padding 0 - list-style none - - li - display block - - &.separator - margin-top $padding - padding-top $padding - border-top solid 1px #eee - - &.has-child - > p - cursor default - - > i:last-child - position absolute - top 0 - right 8px - line-height $item-height - - &:hover > ul - visibility visible - - &:active - > p, a - background $theme-color - - > p, a - display block - z-index 1 - margin 0 - padding 0 32px 0 38px - line-height $item-height - color #868C8C - text-decoration none - cursor pointer - - &:hover - text-decoration none - - * - pointer-events none - - > i - width 28px - margin-left -28px - text-align center - - &:hover - > p, a - text-decoration none - background $theme-color - color $theme-color-foreground - - &:active - > p, a - text-decoration none - background darken($theme-color, 10%) - color $theme-color-foreground - - li > ul - visibility hidden - position absolute - top 0 - left $width - margin-top -($padding) - width $width - background #fff - border-radius 0 4px 4px 4px - box-shadow 2px 2px 8px rgba(0, 0, 0, 0.2) - transition visibility 0s linear 0.2s - - </style> - <script> - import anime from 'animejs'; - import contains from '../../common/scripts/contains'; - - this.root.addEventListener('contextmenu', e => { - e.preventDefault(); - }); - - this.mousedown = e => { - e.preventDefault(); - if (!contains(this.root, e.target) && (this.root != e.target)) this.close(); - return false; - }; - - this.open = pos => { - document.querySelectorAll('body *').forEach(el => { - el.addEventListener('mousedown', this.mousedown); - }); - - this.root.style.display = 'block'; - this.root.style.left = pos.x + 'px'; - this.root.style.top = pos.y + 'px'; - - anime({ - targets: this.root, - opacity: [0, 1], - duration: 100, - easing: 'linear' - }); - }; - - this.close = () => { - document.querySelectorAll('body *').forEach(el => { - el.removeEventListener('mousedown', this.mousedown); - }); - - this.trigger('closed'); - this.unmount(); - }; - </script> -</mk-contextmenu> diff --git a/src/web/app/desktop/tags/crop-window.tag b/src/web/app/desktop/tags/crop-window.tag deleted file mode 100644 index 9d984d6e57..0000000000 --- a/src/web/app/desktop/tags/crop-window.tag +++ /dev/null @@ -1,194 +0,0 @@ -<mk-crop-window> - <mk-window ref="window" is-modal={ true } width={ '800px' }><yield to="header"><i class="fa fa-crop"></i>{ parent.title }</yield> -<yield to="content"> - <div class="body"><img ref="img" src={ parent.image.url + '?thumbnail&quality=80' } alt=""/></div> - <div class="action"> - <button class="skip" onclick={ parent.skip }>クロップをスキップ</button> - <button class="cancel" onclick={ parent.cancel }>キャンセル</button> - <button class="ok" onclick={ parent.ok }>決定</button> - </div></yield> - </mk-window> - <style> - :scope - display block - - > mk-window - [data-yield='header'] - > i - margin-right 4px - - [data-yield='content'] - - > .body - > img - width 100% - max-height 400px - - .cropper-modal { - opacity: 0.8; - } - - .cropper-view-box { - outline-color: $theme-color; - } - - .cropper-line, .cropper-point { - background-color: $theme-color; - } - - .cropper-bg { - animation: cropper-bg 0.5s linear infinite; - } - - @-webkit-keyframes cropper-bg { - 0% { - background-position: 0 0; - } - - 100% { - background-position: -8px -8px; - } - } - - @-moz-keyframes cropper-bg { - 0% { - background-position: 0 0; - } - - 100% { - background-position: -8px -8px; - } - } - - @-ms-keyframes cropper-bg { - 0% { - background-position: 0 0; - } - - 100% { - background-position: -8px -8px; - } - } - - @keyframes cropper-bg { - 0% { - background-position: 0 0; - } - - 100% { - background-position: -8px -8px; - } - } - - > .action - height 72px - background lighten($theme-color, 95%) - - .ok - .cancel - .skip - display block - position absolute - bottom 16px - cursor pointer - padding 0 - margin 0 - height 40px - font-size 1em - outline none - border-radius 4px - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - &:disabled - opacity 0.7 - cursor default - - .ok - .cancel - width 120px - - .ok - right 16px - color $theme-color-foreground - background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) - border solid 1px lighten($theme-color, 15%) - - &:not(:disabled) - font-weight bold - - &:hover:not(:disabled) - background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) - border-color $theme-color - - &:active:not(:disabled) - background $theme-color - border-color $theme-color - - .cancel - .skip - color #888 - background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) - border solid 1px #e2e2e2 - - &:hover - background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) - border-color #dcdcdc - - &:active - background #ececec - border-color #dcdcdc - - .cancel - right 148px - - .skip - left 16px - width 150px - - </style> - <script> - const Cropper = require('cropperjs'); - - this.image = this.opts.file; - this.title = this.opts.title; - this.aspectRatio = this.opts.aspectRatio; - this.cropper = null; - - this.on('mount', () => { - this.img = this.refs.window.refs.img; - this.cropper = new Cropper(this.img, { - aspectRatio: this.aspectRatio, - highlight: false, - viewMode: 1 - }); - }); - - this.ok = () => { - this.cropper.getCroppedCanvas().toBlob(blob => { - this.trigger('cropped', blob); - this.refs.window.close(); - }); - }; - - this.skip = () => { - this.trigger('skipped'); - this.refs.window.close(); - }; - - this.cancel = () => { - this.trigger('canceled'); - this.refs.window.close(); - }; - </script> -</mk-crop-window> diff --git a/src/web/app/desktop/tags/dialog.tag b/src/web/app/desktop/tags/dialog.tag deleted file mode 100644 index 9905123eeb..0000000000 --- a/src/web/app/desktop/tags/dialog.tag +++ /dev/null @@ -1,141 +0,0 @@ -<mk-dialog> - <div class="bg" ref="bg" onclick={ bgClick }></div> - <div class="main" ref="main"> - <header ref="header"></header> - <div class="body" ref="body"></div> - <div class="buttons"> - <virtual each={ opts.buttons }> - <button onclick={ _onclick }>{ text }</button> - </virtual> - </div> - </div> - <style> - :scope - display block - - > .bg - display block - position fixed - z-index 8192 - top 0 - left 0 - width 100% - height 100% - background rgba(0, 0, 0, 0.7) - opacity 0 - pointer-events none - - > .main - display block - position fixed - z-index 8192 - top 20% - left 0 - right 0 - margin 0 auto 0 auto - padding 32px 42px - width 480px - background #fff - opacity 0 - - > header - margin 1em 0 - color $theme-color - // color #43A4EC - font-weight bold - - > i - margin-right 0.5em - - > .body - margin 1em 0 - color #888 - - > .buttons - > button - display inline-block - float right - margin 0 - padding 10px 10px - font-size 1.1em - font-weight normal - text-decoration none - color #888 - background transparent - outline none - border none - border-radius 0 - cursor pointer - transition color 0.1s ease - - i - margin 0 0.375em - - &:hover - color $theme-color - - &:active - color darken($theme-color, 10%) - transition color 0s ease - - </style> - <script> - import anime from 'animejs'; - - this.canThrough = opts.canThrough != null ? opts.canThrough : true; - this.opts.buttons.forEach(button => { - button._onclick = () => { - if (button.onclick) button.onclick(); - this.close(); - }; - }); - - this.on('mount', () => { - this.refs.header.innerHTML = this.opts.title; - this.refs.body.innerHTML = this.opts.text; - - this.refs.bg.style.pointerEvents = 'auto'; - anime({ - targets: this.refs.bg, - opacity: 1, - duration: 100, - easing: 'linear' - }); - - anime({ - targets: this.refs.main, - opacity: 1, - scale: [1.2, 1], - duration: 300, - easing: [ 0, 0.5, 0.5, 1 ] - }); - }); - - this.close = () => { - this.refs.bg.style.pointerEvents = 'none'; - anime({ - targets: this.refs.bg, - opacity: 0, - duration: 300, - easing: 'linear' - }); - - this.refs.main.style.pointerEvents = 'none'; - anime({ - targets: this.refs.main, - opacity: 0, - scale: 0.8, - duration: 300, - easing: [ 0.5, -0.5, 1, 0.5 ], - complete: () => this.unmount() - }); - }; - - this.bgClick = () => { - if (this.canThrough) { - if (this.opts.onThrough) this.opts.onThrough(); - this.close(); - } - }; - </script> -</mk-dialog> diff --git a/src/web/app/desktop/tags/donation.tag b/src/web/app/desktop/tags/donation.tag deleted file mode 100644 index 33f377a192..0000000000 --- a/src/web/app/desktop/tags/donation.tag +++ /dev/null @@ -1,67 +0,0 @@ -<mk-donation> - <button class="close" onclick={ close }>閉じる x</button> - <div class="message"> - <p>利用者の皆さま、</p> - <p> - 今日は、日本の皆さまにお知らせがあります。 - Misskeyの援助をお願いいたします。 - 私は独立性を守るため、一切の広告を掲載いたしません。 - 平均で約¥1,500の寄付をいただき、運営しております。 - 援助をしてくださる利用者はほんの少数です。 - お願いいたします。 - 今日、利用者の皆さまが¥300ご援助くだされば、募金活動を一時間で終了することができます。 - コーヒー1杯ほどの金額です。 - Misskeyを活用しておられるのでしたら、広告を掲載せずにもう1年活動できるよう、どうか1分だけお時間をください。 - 私は小さな非営利個人ですが、サーバー、プログラム、人件費など、世界でトップクラスのウェブサイト同等のコストがかかります。 - 利用者は何億人といますが、他の大きなサイトに比べてほんの少額の費用で運営しているのです。 - 人間の可能性、自由、そして機会。知識こそ、これらの基盤を成すものです。 - 私は、誰もが無料かつ制限なく知識に触れられるべきだと信じています。 - 募金活動を終了し、Misskeyの改善に戻れるようご援助ください。 - よろしくお願いいたします。 - </p> - </div> - <style> - :scope - display block - color #fff - background #03072C - - > .close - position absolute - top 16px - right 16px - z-index 1 - - > .message - padding 32px - font-size 1.4em - font-family serif - - > p - display block - margin 0 auto - max-width 1200px - - > p:first-child - margin-bottom 16px - - </style> - <script> - this.mixin('i'); - this.mixin('api'); - - this.close = e => { - e.preventDefault(); - e.stopPropagation(); - - this.I.data.no_donation = 'true'; - this.I.update(); - this.api('i/appdata/set', { - key: 'no_donation', - value: 'true' - }); - - this.unmount(); - }; - </script> -</mk-donation> diff --git a/src/web/app/desktop/tags/drive/base-contextmenu.tag b/src/web/app/desktop/tags/drive/base-contextmenu.tag deleted file mode 100644 index 8b01de9248..0000000000 --- a/src/web/app/desktop/tags/drive/base-contextmenu.tag +++ /dev/null @@ -1,44 +0,0 @@ -<mk-drive-browser-base-contextmenu> - <mk-contextmenu ref="ctx"> - <ul> - <li onclick={ parent.createFolder }> - <p><i class="fa fa-folder-o"></i>%i18n:desktop.tags.mk-drive-browser-base-contextmenu.create-folder%</p> - </li> - <li onclick={ parent.upload }> - <p><i class="fa fa-upload"></i>%i18n:desktop.tags.mk-drive-browser-base-contextmenu.upload%</p> - </li> - <li onclick={ parent.urlUpload }> - <p><i class="fa fa-cloud-upload"></i>%i18n:desktop.tags.mk-drive-browser-base-contextmenu.url-upload%</p> - </li> - </ul> - </mk-contextmenu> - <script> - this.browser = this.opts.browser; - - this.on('mount', () => { - this.refs.ctx.on('closed', () => { - this.trigger('closed'); - this.unmount(); - }); - }); - - this.open = pos => { - this.refs.ctx.open(pos); - }; - - this.createFolder = () => { - this.browser.createFolder(); - this.refs.ctx.close(); - }; - - this.upload = () => { - this.browser.selectLocalFile(); - this.refs.ctx.close(); - }; - - this.urlUpload = () => { - this.browser.urlUpload(); - this.refs.ctx.close(); - }; - </script> -</mk-drive-browser-base-contextmenu> diff --git a/src/web/app/desktop/tags/drive/browser-window.tag b/src/web/app/desktop/tags/drive/browser-window.tag deleted file mode 100644 index dc55371da6..0000000000 --- a/src/web/app/desktop/tags/drive/browser-window.tag +++ /dev/null @@ -1,51 +0,0 @@ -<mk-drive-browser-window> - <mk-window ref="window" is-modal={ false } width={ '800px' } height={ '500px' }> - <yield to="header"> - <p class="info" if={ parent.usage }><b>{ parent.usage.toFixed(1) }%</b> %i18n:desktop.tags.mk-drive-browser-window.used%</p> - <i class="fa fa-cloud"></i>%i18n:desktop.tags.mk-drive-browser-window.drive% - </yield> - <yield to="content"> - <mk-drive-browser multiple={ true } folder={ parent.folder }/> - </yield> - </mk-window> - <style> - :scope - > mk-window - [data-yield='header'] - > .info - position absolute - top 0 - left 16px - margin 0 - font-size 80% - - > i - margin-right 4px - - [data-yield='content'] - > mk-drive-browser - height 100% - - </style> - <script> - this.mixin('api'); - - this.folder = this.opts.folder ? this.opts.folder : null; - - this.on('mount', () => { - this.refs.window.on('closed', () => { - this.unmount(); - }); - - this.api('drive').then(info => { - this.update({ - usage: info.usage / info.capacity * 100 - }); - }); - }); - - this.close = () => { - this.refs.window.close(); - }; - </script> -</mk-drive-browser-window> diff --git a/src/web/app/desktop/tags/drive/browser.tag b/src/web/app/desktop/tags/drive/browser.tag deleted file mode 100644 index 93db0a04d7..0000000000 --- a/src/web/app/desktop/tags/drive/browser.tag +++ /dev/null @@ -1,727 +0,0 @@ -<mk-drive-browser> - <nav> - <div class="path" oncontextmenu={ pathOncontextmenu }> - <mk-drive-browser-nav-folder class={ current: folder == null } folder={ null }/> - <virtual each={ folder in hierarchyFolders }><span class="separator"><i class="fa fa-angle-right"></i></span> - <mk-drive-browser-nav-folder folder={ folder }/> - </virtual> - <span class="separator" if={ folder != null }><i class="fa fa-angle-right"></i></span> - <span class="folder current" if={ folder != null }>{ folder.name }</span> - </div> - <input class="search" type="search" placeholder=" %i18n:desktop.tags.mk-drive-browser.search%"/> - </nav> - <div class="main { uploading: uploads.length > 0, fetching: fetching }" ref="main" onmousedown={ onmousedown } ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop } oncontextmenu={ oncontextmenu }> - <div class="selection" ref="selection"></div> - <div class="contents" ref="contents"> - <div class="folders" ref="foldersContainer" if={ folders.length > 0 }> - <virtual each={ folder in folders }> - <mk-drive-browser-folder class="folder" folder={ folder }/> - </virtual> - <button if={ moreFolders }>%i18n:desktop.tags.mk-drive-browser.load-more%</button> - </div> - <div class="files" ref="filesContainer" if={ files.length > 0 }> - <virtual each={ file in files }> - <mk-drive-browser-file class="file" file={ file }/> - </virtual> - <button if={ moreFiles } onclick={ fetchMoreFiles }>%i18n:desktop.tags.mk-drive-browser.load-more%</button> - </div> - <div class="empty" if={ files.length == 0 && folders.length == 0 && !fetching }> - <p if={ draghover }>%i18n:desktop.tags.mk-drive-browser.empty-draghover%</p> - <p if={ !draghover && folder == null }><strong>%i18n:desktop.tags.mk-drive-browser.empty-drive%</strong><br/>%i18n:desktop.tags.mk-drive-browser.empty-drive-description%</p> - <p if={ !draghover && folder != null }>%i18n:desktop.tags.mk-drive-browser.empty-folder%</p> - </div> - </div> - <div class="fetching" if={ fetching }> - <div class="spinner"> - <div class="dot1"></div> - <div class="dot2"></div> - </div> - </div> - </div> - <div class="dropzone" if={ draghover }></div> - <mk-uploader ref="uploader"/> - <input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" onchange={ changeFileInput }/> - <style> - :scope - display block - - > nav - display block - z-index 2 - width 100% - overflow auto - font-size 0.9em - color #555 - background #fff - //border-bottom 1px solid #dfdfdf - box-shadow 0 1px 0 rgba(0, 0, 0, 0.05) - - &, * - user-select none - - > .path - display inline-block - vertical-align bottom - margin 0 - padding 0 8px - width calc(100% - 200px) - line-height 38px - white-space nowrap - - > * - display inline-block - margin 0 - padding 0 8px - line-height 38px - cursor pointer - - i - margin-right 4px - - * - pointer-events none - - &:hover - text-decoration underline - - &.current - font-weight bold - cursor default - - &:hover - text-decoration none - - &.separator - margin 0 - padding 0 - opacity 0.5 - cursor default - - > i - margin 0 - - > .search - display inline-block - vertical-align bottom - user-select text - cursor auto - margin 0 - padding 0 18px - width 200px - font-size 1em - line-height 38px - background transparent - outline none - //border solid 1px #ddd - border none - border-radius 0 - box-shadow none - transition color 0.5s ease, border 0.5s ease - font-family FontAwesome, sans-serif - - &[data-active='true'] - background #fff - - &::-webkit-input-placeholder, - &:-ms-input-placeholder, - &:-moz-placeholder - color $ui-control-foreground-color - - > .main - padding 8px - height calc(100% - 38px) - overflow auto - - &, * - user-select none - - &.fetching - cursor wait !important - - * - pointer-events none - - > .contents - opacity 0.5 - - &.uploading - height calc(100% - 38px - 100px) - - > .selection - display none - position absolute - z-index 128 - top 0 - left 0 - border solid 1px $theme-color - background rgba($theme-color, 0.5) - pointer-events none - - > .contents - - > .folders - &:after - content "" - display block - clear both - - > .folder - float left - - > .files - &:after - content "" - display block - clear both - - > .file - float left - - > .empty - padding 16px - text-align center - color #999 - pointer-events none - - > p - margin 0 - - > .fetching - .spinner - margin 100px auto - width 40px - height 40px - text-align center - - animation sk-rotate 2.0s infinite linear - - .dot1, .dot2 - width 60% - height 60% - display inline-block - position absolute - top 0 - background-color rgba(0, 0, 0, 0.3) - border-radius 100% - - animation sk-bounce 2.0s infinite ease-in-out - - .dot2 - top auto - bottom 0 - animation-delay -1.0s - - @keyframes sk-rotate { 100% { transform: rotate(360deg); }} - - @keyframes sk-bounce { - 0%, 100% { - transform: scale(0.0); - } 50% { - transform: scale(1.0); - } - } - - > .dropzone - position absolute - left 0 - top 38px - width 100% - height calc(100% - 38px) - border dashed 2px rgba($theme-color, 0.5) - pointer-events none - - > mk-uploader - height 100px - padding 16px - background #fff - - > input - display none - - </style> - <script> - import contains from '../../../common/scripts/contains'; - import dialog from '../../scripts/dialog'; - import inputDialog from '../../scripts/input-dialog'; - - this.mixin('i'); - this.mixin('api'); - this.mixin('stream'); - - this.files = []; - this.folders = []; - this.hierarchyFolders = []; - this.selectedFiles = []; - - this.uploads = []; - - // 現在の階層(フォルダ) - // * null でルートを表す - this.folder = null; - - this.multiple = this.opts.multiple != null ? this.opts.multiple : false; - - // ドロップされようとしているか - this.draghover = false; - - // 自信の所有するアイテムがドラッグをスタートさせたか - // (自分自身の階層にドロップできないようにするためのフラグ) - this.isDragSource = false; - - this.on('mount', () => { - this.refs.uploader.on('uploaded', file => { - this.addFile(file, true); - }); - - this.refs.uploader.on('change-uploads', uploads => { - this.update({ - uploads: uploads - }); - }); - - this.stream.on('drive_file_created', this.onStreamDriveFileCreated); - this.stream.on('drive_file_updated', this.onStreamDriveFileUpdated); - this.stream.on('drive_folder_created', this.onStreamDriveFolderCreated); - this.stream.on('drive_folder_updated', this.onStreamDriveFolderUpdated); - - if (this.opts.folder) { - this.move(this.opts.folder); - } else { - this.fetch(); - } - }); - - this.on('unmount', () => { - this.stream.off('drive_file_created', this.onStreamDriveFileCreated); - this.stream.off('drive_file_updated', this.onStreamDriveFileUpdated); - this.stream.off('drive_folder_created', this.onStreamDriveFolderCreated); - this.stream.off('drive_folder_updated', this.onStreamDriveFolderUpdated); - }); - - this.onStreamDriveFileCreated = file => { - this.addFile(file, true); - }; - - this.onStreamDriveFileUpdated = file => { - const current = this.folder ? this.folder.id : null; - if (current != file.folder_id) { - this.removeFile(file); - } else { - this.addFile(file, true); - } - }; - - this.onStreamDriveFolderCreated = folder => { - this.addFolder(folder, true); - }; - - this.onStreamDriveFolderUpdated = folder => { - const current = this.folder ? this.folder.id : null; - if (current != folder.parent_id) { - this.removeFolder(folder); - } else { - this.addFolder(folder, true); - } - }; - - this.onmousedown = e => { - if (contains(this.refs.foldersContainer, e.target) || contains(this.refs.filesContainer, e.target)) return true; - - const rect = this.refs.main.getBoundingClientRect(); - - const left = e.pageX + this.refs.main.scrollLeft - rect.left - window.pageXOffset - const top = e.pageY + this.refs.main.scrollTop - rect.top - window.pageYOffset - - const move = e => { - this.refs.selection.style.display = 'block'; - - const cursorX = e.pageX + this.refs.main.scrollLeft - rect.left - window.pageXOffset; - const cursorY = e.pageY + this.refs.main.scrollTop - rect.top - window.pageYOffset; - const w = cursorX - left; - const h = cursorY - top; - - if (w > 0) { - this.refs.selection.style.width = w + 'px'; - this.refs.selection.style.left = left + 'px'; - } else { - this.refs.selection.style.width = -w + 'px'; - this.refs.selection.style.left = cursorX + 'px'; - } - - if (h > 0) { - this.refs.selection.style.height = h + 'px'; - this.refs.selection.style.top = top + 'px'; - } else { - this.refs.selection.style.height = -h + 'px'; - this.refs.selection.style.top = cursorY + 'px'; - } - }; - - const up = e => { - document.documentElement.removeEventListener('mousemove', move); - document.documentElement.removeEventListener('mouseup', up); - - this.refs.selection.style.display = 'none'; - }; - - document.documentElement.addEventListener('mousemove', move); - document.documentElement.addEventListener('mouseup', up); - }; - - this.pathOncontextmenu = e => { - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - }; - - this.ondragover = e => { - e.preventDefault(); - e.stopPropagation(); - - // ドラッグ元が自分自身の所有するアイテムかどうか - if (!this.isDragSource) { - // ドラッグされてきたものがファイルだったら - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; - this.draghover = true; - } else { - // 自分自身にはドロップさせない - e.dataTransfer.dropEffect = 'none'; - return false; - } - }; - - this.ondragenter = e => { - e.preventDefault(); - if (!this.isDragSource) this.draghover = true; - }; - - this.ondragleave = e => { - this.draghover = false; - }; - - this.ondrop = e => { - e.preventDefault(); - e.stopPropagation(); - - this.draghover = false; - - // ドロップされてきたものがファイルだったら - if (e.dataTransfer.files.length > 0) { - e.dataTransfer.files.forEach(file => { - this.upload(file, this.folder); - }); - return false; - } - - // データ取得 - const data = e.dataTransfer.getData('text'); - if (data == null) return false; - - // パース - // TODO: JSONじゃなかったら中断 - const obj = JSON.parse(data); - - // (ドライブの)ファイルだったら - if (obj.type == 'file') { - const file = obj.id; - if (this.files.some(f => f.id == file)) return false; - this.removeFile(file); - this.api('drive/files/update', { - file_id: file, - folder_id: this.folder ? this.folder.id : null - }); - // (ドライブの)フォルダーだったら - } else if (obj.type == 'folder') { - const folder = obj.id; - // 移動先が自分自身ならreject - if (this.folder && folder == this.folder.id) return false; - if (this.folders.some(f => f.id == folder)) return false; - this.removeFolder(folder); - this.api('drive/folders/update', { - folder_id: folder, - parent_id: this.folder ? this.folder.id : null - }).then(() => { - // something - }).catch(err => { - switch (err) { - case 'detected-circular-definition': - dialog('<i class="fa fa-exclamation-triangle"></i>%i18n:desktop.tags.mk-drive-browser.unable-to-process%', - '%i18n:desktop.tags.mk-drive-browser.circular-reference-detected%', [{ - text: '%i18n:common.ok%' - }]); - break; - default: - alert('%i18n:desktop.tags.mk-drive-browser.unhandled-error% ' + err); - } - }); - } - - return false; - }; - - this.oncontextmenu = e => { - e.preventDefault(); - e.stopImmediatePropagation(); - - const ctx = riot.mount(document.body.appendChild(document.createElement('mk-drive-browser-base-contextmenu')), { - browser: this - })[0]; - ctx.open({ - x: e.pageX - window.pageXOffset, - y: e.pageY - window.pageYOffset - }); - - return false; - }; - - this.selectLocalFile = () => { - this.refs.fileInput.click(); - }; - - this.urlUpload = () => { - inputDialog('%i18n:desktop.tags.mk-drive-browser.url-upload%', - '%i18n:desktop.tags.mk-drive-browser.url-of-file%', null, url => { - - this.api('drive/files/upload_from_url', { - url: url, - folder_id: this.folder ? this.folder.id : undefined - }); - - dialog('<i class="fa fa-check"></i>%i18n:desktop.tags.mk-drive-browser.url-upload-requested%', - '%i18n:desktop.tags.mk-drive-browser.may-take-time%', [{ - text: '%i18n:common.ok%' - }]); - }); - }; - - this.createFolder = () => { - inputDialog('%i18n:desktop.tags.mk-drive-browser.create-folder%', - '%i18n:desktop.tags.mk-drive-browser.folder-name%', null, name => { - - this.api('drive/folders/create', { - name: name, - folder_id: this.folder ? this.folder.id : undefined - }).then(folder => { - this.addFolder(folder, true); - this.update(); - }); - }); - }; - - this.changeFileInput = () => { - this.refs.fileInput.files.forEach(file => { - this.upload(file, this.folder); - }); - }; - - this.upload = (file, folder) => { - if (folder && typeof folder == 'object') folder = folder.id; - this.refs.uploader.upload(file, folder); - }; - - this.chooseFile = file => { - const isAlreadySelected = this.selectedFiles.some(f => f.id == file.id); - if (this.multiple) { - if (isAlreadySelected) { - this.selectedFiles = this.selectedFiles.filter(f => f.id != file.id); - } else { - this.selectedFiles.push(file); - } - this.update(); - this.trigger('change-selection', this.selectedFiles); - } else { - if (isAlreadySelected) { - this.trigger('selected', file); - } else { - this.selectedFiles = [file]; - this.trigger('change-selection', [file]); - } - } - }; - - this.newWindow = folderId => { - riot.mount(document.body.appendChild(document.createElement('mk-drive-browser-window')), { - folder: folderId - }); - }; - - this.move = target => { - if (target == null) { - this.goRoot(); - return; - } else if (typeof target == 'object') { - target = target.id; - } - - this.update({ - fetching: true - }); - - this.api('drive/folders/show', { - folder_id: target - }).then(folder => { - this.folder = folder; - this.hierarchyFolders = []; - - const dive = folder => { - this.hierarchyFolders.unshift(folder); - if (folder.parent) dive(folder.parent); - }; - - if (folder.parent) dive(folder.parent); - - this.update(); - this.fetch(); - }); - }; - - this.addFolder = (folder, unshift = false) => { - const current = this.folder ? this.folder.id : null; - if (current != folder.parent_id) return; - - if (this.folders.some(f => f.id == folder.id)) { - const exist = this.folders.map(f => f.id).indexOf(folder.id); - this.folders[exist] = folder; - this.update(); - return; - } - - if (unshift) { - this.folders.unshift(folder); - } else { - this.folders.push(folder); - } - - this.update(); - }; - - this.addFile = (file, unshift = false) => { - const current = this.folder ? this.folder.id : null; - if (current != file.folder_id) return; - - if (this.files.some(f => f.id == file.id)) { - const exist = this.files.map(f => f.id).indexOf(file.id); - this.files[exist] = file; - this.update(); - return; - } - - if (unshift) { - this.files.unshift(file); - } else { - this.files.push(file); - } - - this.update(); - }; - - this.removeFolder = folder => { - if (typeof folder == 'object') folder = folder.id; - this.folders = this.folders.filter(f => f.id != folder); - this.update(); - }; - - this.removeFile = file => { - if (typeof file == 'object') file = file.id; - this.files = this.files.filter(f => f.id != file); - this.update(); - }; - - this.appendFile = file => this.addFile(file); - this.appendFolder = file => this.addFolder(file); - this.prependFile = file => this.addFile(file, true); - this.prependFolder = file => this.addFolder(file, true); - - this.goRoot = () => { - // 既にrootにいるなら何もしない - if (this.folder == null) return; - - this.update({ - folder: null, - hierarchyFolders: [] - }); - this.fetch(); - }; - - this.fetch = () => { - this.update({ - folders: [], - files: [], - moreFolders: false, - moreFiles: false, - fetching: true - }); - - let fetchedFolders = null; - let fetchedFiles = null; - - const foldersMax = 30; - const filesMax = 30; - - // フォルダ一覧取得 - this.api('drive/folders', { - folder_id: this.folder ? this.folder.id : null, - limit: foldersMax + 1 - }).then(folders => { - if (folders.length == foldersMax + 1) { - this.moreFolders = true; - folders.pop(); - } - fetchedFolders = folders; - complete(); - }); - - // ファイル一覧取得 - this.api('drive/files', { - folder_id: this.folder ? this.folder.id : null, - limit: filesMax + 1 - }).then(files => { - if (files.length == filesMax + 1) { - this.moreFiles = true; - files.pop(); - } - fetchedFiles = files; - complete(); - }); - - let flag = false; - const complete = () => { - if (flag) { - fetchedFolders.forEach(this.appendFolder); - fetchedFiles.forEach(this.appendFile); - this.update({ - fetching: false - }); - } else { - flag = true; - } - }; - }; - - this.fetchMoreFiles = () => { - this.update({ - fetching: true - }); - - const max = 30; - - // ファイル一覧取得 - this.api('drive/files', { - folder_id: this.folder ? this.folder.id : null, - limit: max + 1 - }).then(files => { - if (files.length == max + 1) { - this.moreFiles = true; - files.pop(); - } else { - this.moreFiles = false; - } - files.forEach(this.appendFile); - this.update({ - fetching: false - }); - }); - }; - - </script> -</mk-drive-browser> diff --git a/src/web/app/desktop/tags/drive/file-contextmenu.tag b/src/web/app/desktop/tags/drive/file-contextmenu.tag deleted file mode 100644 index 0ea72333fb..0000000000 --- a/src/web/app/desktop/tags/drive/file-contextmenu.tag +++ /dev/null @@ -1,99 +0,0 @@ -<mk-drive-browser-file-contextmenu> - <mk-contextmenu ref="ctx"> - <ul> - <li onclick={ parent.rename }> - <p><i class="fa fa-i-cursor"></i>%i18n:desktop.tags.mk-drive-browser-file-contextmenu.rename%</p> - </li> - <li onclick={ parent.copyUrl }> - <p><i class="fa fa-link"></i>%i18n:desktop.tags.mk-drive-browser-file-contextmenu.copy-url%</p> - </li> - <li><a href={ parent.file.url + '?download' } download={ parent.file.name } onclick={ parent.download }><i class="fa fa-download"></i>%i18n:desktop.tags.mk-drive-browser-file-contextmenu.download%</a></li> - <li class="separator"></li> - <li onclick={ parent.delete }> - <p><i class="fa fa-trash-o"></i>%i18n:common.delete%</p> - </li> - <li class="separator"></li> - <li class="has-child"> - <p>%i18n:desktop.tags.mk-drive-browser-file-contextmenu.else-files%<i class="fa fa-caret-right"></i></p> - <ul> - <li onclick={ parent.setAvatar }> - <p>%i18n:desktop.tags.mk-drive-browser-file-contextmenu.set-as-avatar%</p> - </li> - <li onclick={ parent.setBanner }> - <p>%i18n:desktop.tags.mk-drive-browser-file-contextmenu.set-as-banner%</p> - </li> - </ul> - </li> - <li class="has-child"> - <p>%i18n:desktop.tags.mk-drive-browser-file-contextmenu.open-in-app%...<i class="fa fa-caret-right"></i></p> - <ul> - <li onclick={ parent.addApp }> - <p>%i18n:desktop.tags.mk-drive-browser-file-contextmenu.add-app%...</p> - </li> - </ul> - </li> - </ul> - </mk-contextmenu> - <script> - import copyToClipboard from '../../../common/scripts/copy-to-clipboard'; - import dialog from '../../scripts/dialog'; - import inputDialog from '../../scripts/input-dialog'; - import updateAvatar from '../../scripts/update-avatar'; - import NotImplementedException from '../../scripts/not-implemented-exception'; - - this.mixin('i'); - this.mixin('api'); - - this.browser = this.opts.browser; - this.file = this.opts.file; - - this.on('mount', () => { - this.refs.ctx.on('closed', () => { - this.trigger('closed'); - this.unmount(); - }); - }); - - this.open = pos => { - this.refs.ctx.open(pos); - }; - - this.rename = () => { - this.refs.ctx.close(); - - inputDialog('%i18n:desktop.tags.mk-drive-browser-file-contextmenu.rename-file%', '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.input-new-file-name%', this.file.name, name => { - this.api('drive/files/update', { - file_id: this.file.id, - name: name - }) - }); - }; - - this.copyUrl = () => { - copyToClipboard(this.file.url); - this.refs.ctx.close(); - dialog('<i class="fa fa-check"></i>%i18n:desktop.tags.mk-drive-browser-file-contextmenu.copied%', - '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.copied-url-to-clipboard%', [{ - text: '%i18n:common.ok%' - }]); - }; - - this.download = () => { - this.refs.ctx.close(); - }; - - this.setAvatar = () => { - this.refs.ctx.close(); - updateAvatar(this.I, null, this.file); - }; - - this.setBanner = () => { - this.refs.ctx.close(); - updateBanner(this.I, null, this.file); - }; - - this.addApp = () => { - NotImplementedException(); - }; - </script> -</mk-drive-browser-file-contextmenu> diff --git a/src/web/app/desktop/tags/drive/file.tag b/src/web/app/desktop/tags/drive/file.tag deleted file mode 100644 index 64838d6814..0000000000 --- a/src/web/app/desktop/tags/drive/file.tag +++ /dev/null @@ -1,208 +0,0 @@ -<mk-drive-browser-file data-is-selected={ isSelected } data-is-contextmenu-showing={ isContextmenuShowing.toString() } onclick={ onclick } oncontextmenu={ oncontextmenu } draggable="true" ondragstart={ ondragstart } ondragend={ ondragend } title={ title }> - <div class="label" if={ I.avatar_id == file.id }><img src="/assets/label.svg"/> - <p>%i18n:desktop.tags.mk-drive-browser-file.avatar%</p> - </div> - <div class="label" if={ I.banner_id == file.id }><img src="/assets/label.svg"/> - <p>%i18n:desktop.tags.mk-drive-browser-file.banner%</p> - </div> - <div class="label" if={ I.data.wallpaper == file.id }><img src="/assets/label.svg"/> - <p>%i18n:desktop.tags.mk-drive-browser-file.wallpaper%</p> - </div> - <div class="thumbnail"><img src={ file.url + '?thumbnail&size=128' } alt=""/></div> - <p class="name"><span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span><span class="ext" if={ file.name.lastIndexOf('.') != -1 }>{ file.name.substr(file.name.lastIndexOf('.')) }</span></p> - <style> - :scope - display block - margin 4px - padding 8px 0 0 0 - width 144px - height 180px - border-radius 4px - - &, * - cursor pointer - - &:hover - background rgba(0, 0, 0, 0.05) - - > .label - &:before - &:after - background #0b65a5 - - &:active - background rgba(0, 0, 0, 0.1) - - > .label - &:before - &:after - background #0b588c - - &[data-is-selected] - background $theme-color - - &:hover - background lighten($theme-color, 10%) - - &:active - background darken($theme-color, 10%) - - > .label - &:before - &:after - display none - - > .name - color $theme-color-foreground - - &[data-is-contextmenu-showing='true'] - &:after - content "" - pointer-events none - position absolute - top -4px - right -4px - bottom -4px - left -4px - border 2px dashed rgba($theme-color, 0.3) - border-radius 4px - - > .label - position absolute - top 0 - left 0 - pointer-events none - - &:before - content "" - display block - position absolute - z-index 1 - top 0 - left 57px - width 28px - height 8px - background #0c7ac9 - - &:after - content "" - display block - position absolute - z-index 1 - top 57px - left 0 - width 8px - height 28px - background #0c7ac9 - - > img - position absolute - z-index 2 - top 0 - left 0 - - > p - position absolute - z-index 3 - top 19px - left -28px - width 120px - margin 0 - text-align center - line-height 28px - color #fff - transform rotate(-45deg) - - > .thumbnail - width 128px - height 128px - left 8px - - > img - display block - position absolute - top 0 - left 0 - right 0 - bottom 0 - margin auto - max-width 128px - max-height 128px - pointer-events none - - > .name - display block - margin 4px 0 0 0 - font-size 0.8em - text-align center - word-break break-all - color #444 - overflow hidden - - > .ext - opacity 0.5 - - </style> - <script> - import bytesToSize from '../../../common/scripts/bytes-to-size'; - - this.mixin('i'); - - this.file = this.opts.file; - this.browser = this.parent; - this.title = `${this.file.name}\n${this.file.type} ${bytesToSize(this.file.datasize)}`; - this.isContextmenuShowing = false; - this.isSelected = this.browser.selectedFiles.some(f => f.id == this.file.id); - - this.browser.on('change-selection', selections => { - this.isSelected = selections.some(f => f.id == this.file.id); - this.update(); - }); - - this.onclick = () => { - this.browser.chooseFile(this.file); - }; - - this.oncontextmenu = e => { - e.preventDefault(); - e.stopImmediatePropagation(); - - this.update({ - isContextmenuShowing: true - }); - const ctx = riot.mount(document.body.appendChild(document.createElement('mk-drive-browser-file-contextmenu')), { - browser: this.browser, - file: this.file - })[0]; - ctx.open({ - x: e.pageX - window.pageXOffset, - y: e.pageY - window.pageYOffset - }); - ctx.on('closed', () => { - this.update({ - isContextmenuShowing: false - }); - }); - return false; - }; - - this.ondragstart = e => { - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('text', JSON.stringify({ - type: 'file', - id: this.file.id, - file: this.file - })); - this.isDragging = true; - - // 親ブラウザに対して、ドラッグが開始されたフラグを立てる - // (=あなたの子供が、ドラッグを開始しましたよ) - this.browser.isDragSource = true; - }; - - this.ondragend = e => { - this.isDragging = false; - this.browser.isDragSource = false; - }; - </script> -</mk-drive-browser-file> diff --git a/src/web/app/desktop/tags/drive/folder-contextmenu.tag b/src/web/app/desktop/tags/drive/folder-contextmenu.tag deleted file mode 100644 index ce95ccfb1d..0000000000 --- a/src/web/app/desktop/tags/drive/folder-contextmenu.tag +++ /dev/null @@ -1,63 +0,0 @@ -<mk-drive-browser-folder-contextmenu> - <mk-contextmenu ref="ctx"> - <ul> - <li onclick={ parent.move }> - <p><i class="fa fa-arrow-right"></i>%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.move-to-this-folder%</p> - </li> - <li onclick={ parent.newWindow }> - <p><i class="fa fa-share-square-o"></i>%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.show-in-new-window%</p> - </li> - <li class="separator"></li> - <li onclick={ parent.rename }> - <p><i class="fa fa-i-cursor"></i>%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.rename%</p> - </li> - <li class="separator"></li> - <li onclick={ parent.delete }> - <p><i class="fa fa-trash-o"></i>%i18n:common.delete%</p> - </li> - </ul> - </mk-contextmenu> - <script> - import inputDialog from '../../scripts/input-dialog'; - - this.mixin('api'); - - this.browser = this.opts.browser; - this.folder = this.opts.folder; - - this.open = pos => { - this.refs.ctx.open(pos); - - this.refs.ctx.on('closed', () => { - this.trigger('closed'); - this.unmount(); - }); - }; - - this.move = () => { - this.browser.move(this.folder.id); - this.refs.ctx.close(); - }; - - this.newWindow = () => { - this.browser.newWindow(this.folder.id); - this.refs.ctx.close(); - }; - - this.createFolder = () => { - this.browser.createFolder(); - this.refs.ctx.close(); - }; - - this.rename = () => { - this.refs.ctx.close(); - - inputDialog('%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.rename-folder%', '%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.input-new-folder-name%', this.folder.name, name => { - this.api('drive/folders/update', { - folder_id: this.folder.id, - name: name - }); - }); - }; - </script> -</mk-drive-browser-folder-contextmenu> diff --git a/src/web/app/desktop/tags/drive/folder.tag b/src/web/app/desktop/tags/drive/folder.tag deleted file mode 100644 index e03c4e3534..0000000000 --- a/src/web/app/desktop/tags/drive/folder.tag +++ /dev/null @@ -1,204 +0,0 @@ -<mk-drive-browser-folder data-is-contextmenu-showing={ isContextmenuShowing.toString() } data-draghover={ draghover.toString() } onclick={ onclick } onmouseover={ onmouseover } onmouseout={ onmouseout } ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop } oncontextmenu={ oncontextmenu } draggable="true" ondragstart={ ondragstart } ondragend={ ondragend } title={ title }> - <p class="name"><i class="fa fa-fw { fa-folder-o: !hover, fa-folder-open-o: hover }"></i>{ folder.name }</p> - <style> - :scope - display block - margin 4px - padding 8px - width 144px - height 64px - background lighten($theme-color, 95%) - border-radius 4px - - &, * - cursor pointer - - * - pointer-events none - - &:hover - background lighten($theme-color, 90%) - - &:active - background lighten($theme-color, 85%) - - &[data-is-contextmenu-showing='true'] - &[data-draghover='true'] - &:after - content "" - pointer-events none - position absolute - top -4px - right -4px - bottom -4px - left -4px - border 2px dashed rgba($theme-color, 0.3) - border-radius 4px - - &[data-draghover='true'] - background lighten($theme-color, 90%) - - > .name - margin 0 - font-size 0.9em - color darken($theme-color, 30%) - - > i - margin-right 4px - margin-left 2px - text-align left - - </style> - <script> - import dialog from '../../scripts/dialog'; - - this.mixin('api'); - - this.folder = this.opts.folder; - this.browser = this.parent; - - this.title = this.folder.name; - this.hover = false; - this.draghover = false; - this.isContextmenuShowing = false; - - this.onclick = () => { - this.browser.move(this.folder); - }; - - this.onmouseover = () => { - this.hover = true; - }; - - this.onmouseout = () => { - this.hover = false - }; - - this.ondragover = e => { - e.preventDefault(); - e.stopPropagation(); - - // 自分自身がドラッグされていない場合 - if (!this.isDragging) { - // ドラッグされてきたものがファイルだったら - if (e.dataTransfer.effectAllowed === 'all') { - e.dataTransfer.dropEffect = 'copy'; - } else { - e.dataTransfer.dropEffect = 'move'; - } - } else { - // 自分自身にはドロップさせない - e.dataTransfer.dropEffect = 'none'; - } - return false; - }; - - this.ondragenter = e => { - e.preventDefault(); - if (!this.isDragging) this.draghover = true; - }; - - this.ondragleave = () => { - this.draghover = false; - }; - - this.ondrop = e => { - e.preventDefault(); - e.stopPropagation(); - this.draghover = false; - - // ファイルだったら - if (e.dataTransfer.files.length > 0) { - e.dataTransfer.files.forEach(file => { - this.browser.upload(file, this.folder); - }); - return false; - }; - - // データ取得 - const data = e.dataTransfer.getData('text'); - if (data == null) return false; - - // パース - // TODO: Validate JSON - const obj = JSON.parse(data); - - // (ドライブの)ファイルだったら - if (obj.type == 'file') { - const file = obj.id; - this.browser.removeFile(file); - this.api('drive/files/update', { - file_id: file, - folder_id: this.folder.id - }); - // (ドライブの)フォルダーだったら - } else if (obj.type == 'folder') { - const folder = obj.id; - // 移動先が自分自身ならreject - if (folder == this.folder.id) return false; - this.browser.removeFolder(folder); - this.api('drive/folders/update', { - folder_id: folder, - parent_id: this.folder.id - }).then(() => { - // something - }).catch(err => { - switch (err) { - case 'detected-circular-definition': - dialog('<i class="fa fa-exclamation-triangle"></i>%i18n:desktop.tags.mk-drive-browser-folder.unable-to-process%', - '%i18n:desktop.tags.mk-drive-browser-folder.circular-reference-detected%', [{ - text: '%i18n:common.ok%' - }]); - break; - default: - alert('%i18n:desktop.tags.mk-drive-browser-folder.unhandled-error% ' + err); - } - }); - } - - return false; - }; - - this.ondragstart = e => { - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('text', JSON.stringify({ - type: 'folder', - id: this.folder.id - })); - this.isDragging = true; - - // 親ブラウザに対して、ドラッグが開始されたフラグを立てる - // (=あなたの子供が、ドラッグを開始しましたよ) - this.browser.isDragSource = true; - }; - - this.ondragend = e => { - this.isDragging = false; - this.browser.isDragSource = false; - }; - - this.oncontextmenu = e => { - e.preventDefault(); - e.stopImmediatePropagation(); - - this.update({ - isContextmenuShowing: true - }); - const ctx = riot.mount(document.body.appendChild(document.createElement('mk-drive-browser-folder-contextmenu')), { - browser: this.browser, - folder: this.folder - })[0]; - ctx.open({ - x: e.pageX - window.pageXOffset, - y: e.pageY - window.pageYOffset - }); - ctx.on('closed', () => { - this.update({ - isContextmenuShowing: false - }); - }); - - return false; - }; - </script> -</mk-drive-browser-folder> diff --git a/src/web/app/desktop/tags/drive/nav-folder.tag b/src/web/app/desktop/tags/drive/nav-folder.tag deleted file mode 100644 index c89d9edc1c..0000000000 --- a/src/web/app/desktop/tags/drive/nav-folder.tag +++ /dev/null @@ -1,95 +0,0 @@ -<mk-drive-browser-nav-folder data-draghover={ draghover } onclick={ onclick } ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop }><i class="fa fa-cloud" if={ folder == null }></i><span>{ folder == null ? '%i18n:desktop.tags.mk-drive-browser-nav-folder.drive%' : folder.name }</span> - <style> - :scope - &[data-draghover] - background #eee - - </style> - <script> - this.mixin('api'); - - this.folder = this.opts.folder ? this.opts.folder : null; - this.browser = this.parent; - - this.hover = false; - - this.onclick = () => { - this.browser.move(this.folder); - }; - - this.onmouseover = () => { - this.hover = true - }; - - this.onmouseout = () => { - this.hover = false - }; - - this.ondragover = e => { - e.preventDefault(); - e.stopPropagation(); - - // このフォルダがルートかつカレントディレクトリならドロップ禁止 - if (this.folder == null && this.browser.folder == null) { - e.dataTransfer.dropEffect = 'none'; - // ドラッグされてきたものがファイルだったら - } else if (e.dataTransfer.effectAllowed == 'all') { - e.dataTransfer.dropEffect = 'copy'; - } else { - e.dataTransfer.dropEffect = 'move'; - } - return false; - }; - - this.ondragenter = () => { - if (this.folder || this.browser.folder) this.draghover = true; - }; - - this.ondragleave = () => { - if (this.folder || this.browser.folder) this.draghover = false; - }; - - this.ondrop = e => { - e.stopPropagation(); - this.draghover = false; - - // ファイルだったら - if (e.dataTransfer.files.length > 0) { - e.dataTransfer.files.forEach(file => { - this.browser.upload(file, this.folder); - }); - return false; - }; - - // データ取得 - const data = e.dataTransfer.getData('text'); - if (data == null) return false; - - // パース - // TODO: Validate JSON - const obj = JSON.parse(data); - - // (ドライブの)ファイルだったら - if (obj.type == 'file') { - const file = obj.id; - this.browser.removeFile(file); - this.api('drive/files/update', { - file_id: file, - folder_id: this.folder ? this.folder.id : null - }); - // (ドライブの)フォルダーだったら - } else if (obj.type == 'folder') { - const folder = obj.id; - // 移動先が自分自身ならreject - if (this.folder && folder == this.folder.id) return false; - this.browser.removeFolder(folder); - this.api('drive/folders/update', { - folder_id: folder, - parent_id: this.folder ? this.folder.id : null - }); - } - - return false; - }; - </script> -</mk-drive-browser-nav-folder> diff --git a/src/web/app/desktop/tags/ellipsis-icon.tag b/src/web/app/desktop/tags/ellipsis-icon.tag deleted file mode 100644 index 8462bfc4af..0000000000 --- a/src/web/app/desktop/tags/ellipsis-icon.tag +++ /dev/null @@ -1,37 +0,0 @@ -<mk-ellipsis-icon> - <div></div> - <div></div> - <div></div> - <style> - :scope - display block - width 70px - margin 0 auto - text-align center - - > div - display inline-block - width 18px - height 18px - background-color rgba(0, 0, 0, 0.3) - border-radius 100% - animation bounce 1.4s infinite ease-in-out both - - &:nth-child(1) - animation-delay 0s - - &:nth-child(2) - margin 0 6px - animation-delay 0.16s - - &:nth-child(3) - animation-delay 0.32s - - @keyframes bounce - 0%, 80%, 100% - transform scale(0) - 40% - transform scale(1) - - </style> -</mk-ellipsis-icon> diff --git a/src/web/app/desktop/tags/follow-button.tag b/src/web/app/desktop/tags/follow-button.tag deleted file mode 100644 index 00ff686f69..0000000000 --- a/src/web/app/desktop/tags/follow-button.tag +++ /dev/null @@ -1,142 +0,0 @@ -<mk-follow-button> - <button class={ wait: wait, follow: !user.is_following, unfollow: user.is_following } if={ !init } onclick={ onclick } disabled={ wait } title={ user.is_following ? 'フォロー解除' : 'フォローする' }><i class="fa fa-minus" if={ !wait && user.is_following }></i><i class="fa fa-plus" if={ !wait && !user.is_following }></i><i class="fa fa-spinner fa-pulse fa-fw" if={ wait }></i></button> - <div class="init" if={ init }><i class="fa fa-spinner fa-pulse fa-fw"></i></div> - <style> - :scope - display block - - > button - > .init - display block - cursor pointer - padding 0 - margin 0 - width 32px - height 32px - font-size 1em - outline none - border-radius 4px - - * - pointer-events none - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - &.follow - color #888 - background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) - border solid 1px #e2e2e2 - - &:hover - background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) - border-color #dcdcdc - - &:active - background #ececec - border-color #dcdcdc - - &.unfollow - color $theme-color-foreground - background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) - border solid 1px lighten($theme-color, 15%) - - &:not(:disabled) - font-weight bold - - &:hover:not(:disabled) - background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) - border-color $theme-color - - &:active:not(:disabled) - background $theme-color - border-color $theme-color - - &.wait - cursor wait !important - opacity 0.7 - - </style> - <script> - import isPromise from '../../common/scripts/is-promise'; - - this.mixin('i'); - this.mixin('api'); - this.mixin('stream'); - - this.user = null; - this.userPromise = isPromise(this.opts.user) - ? this.opts.user - : Promise.resolve(this.opts.user); - this.init = true; - this.wait = false; - - this.on('mount', () => { - this.userPromise.then(user => { - this.update({ - init: false, - user: user - }); - this.stream.on('follow', this.onStreamFollow); - this.stream.on('unfollow', this.onStreamUnfollow); - }); - }); - - this.on('unmount', () => { - this.stream.off('follow', this.onStreamFollow); - this.stream.off('unfollow', this.onStreamUnfollow); - }); - - this.onStreamFollow = user => { - if (user.id == this.user.id) { - this.update({ - user: user - }); - } - }; - - this.onStreamUnfollow = user => { - if (user.id == this.user.id) { - this.update({ - user: user - }); - } - }; - - this.onclick = () => { - this.wait = true; - if (this.user.is_following) { - this.api('following/delete', { - user_id: this.user.id - }).then(() => { - this.user.is_following = false; - }).catch(err => { - console.error(err); - }).then(() => { - this.wait = false; - this.update(); - }); - } else { - this.api('following/create', { - user_id: this.user.id - }).then(() => { - this.user.is_following = true; - }).catch(err => { - console.error(err); - }).then(() => { - this.wait = false; - this.update(); - }); - } - }; - </script> -</mk-follow-button> diff --git a/src/web/app/desktop/tags/following-setuper.tag b/src/web/app/desktop/tags/following-setuper.tag deleted file mode 100644 index 3197e93616..0000000000 --- a/src/web/app/desktop/tags/following-setuper.tag +++ /dev/null @@ -1,169 +0,0 @@ -<mk-following-setuper> - <p class="title">気になるユーザーをフォロー:</p> - <div class="users" if={ !fetching && users.length > 0 }> - <div class="user" each={ users }><a class="avatar-anchor" href={ '/' + username }><img class="avatar" src={ avatar_url + '?thumbnail&size=42' } alt="" data-user-preview={ id }/></a> - <div class="body"><a class="name" href={ '/' + username } target="_blank" data-user-preview={ id }>{ name }</a> - <p class="username">@{ username }</p> - </div> - <mk-follow-button user={ this }/> - </div> - </div> - <p class="empty" if={ !fetching && users.length == 0 }>おすすめのユーザーは見つかりませんでした。</p> - <p class="fetching" if={ fetching }><i class="fa fa-spinner fa-pulse fa-fw"></i>読み込んでいます<mk-ellipsis/></p> - <a class="refresh" onclick={ refresh }>もっと見る</a> - <button class="close" onclick={ close } title="閉じる"><i class="fa fa-times"></i></button> - <style> - :scope - display block - padding 24px - - > .title - margin 0 0 12px 0 - font-size 1em - font-weight bold - color #888 - - > .users - &:after - content "" - display block - clear both - - > .user - padding 16px - width 238px - float left - - &:after - content "" - display block - clear both - - > .avatar-anchor - display block - float left - margin 0 12px 0 0 - - > .avatar - display block - width 42px - height 42px - margin 0 - border-radius 8px - vertical-align bottom - - > .body - float left - width calc(100% - 54px) - - > .name - margin 0 - font-size 16px - line-height 24px - color #555 - - > .username - margin 0 - font-size 15px - line-height 16px - color #ccc - - > mk-follow-button - position absolute - top 16px - right 16px - - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > .fetching - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - - > .refresh - display block - margin 0 8px 0 0 - text-align right - font-size 0.9em - color #999 - - > .close - cursor pointer - display block - position absolute - top 6px - right 6px - z-index 1 - margin 0 - padding 0 - font-size 1.2em - color #999 - border none - outline none - background transparent - - &:hover - color #555 - - &:active - color #222 - - > i - padding 14px - - </style> - <script> - this.mixin('api'); - this.mixin('user-preview'); - - this.users = null; - this.fetching = true; - - this.limit = 6; - this.page = 0; - - this.on('mount', () => { - this.fetch(); - }); - - this.fetch = () => { - this.update({ - fetching: true, - users: null - }); - - this.api('users/recommendation', { - limit: this.limit, - offset: this.limit * this.page - }).then(users => { - this.fetching = false - this.users = users - this.update({ - fetching: false, - users: users - }); - }); - }; - - this.refresh = () => { - if (this.users.length < this.limit) { - this.page = 0; - } else { - this.page++; - } - this.fetch(); - }; - - this.close = () => { - this.unmount(); - }; - </script> -</mk-following-setuper> diff --git a/src/web/app/desktop/tags/home-widgets/activity.tag b/src/web/app/desktop/tags/home-widgets/activity.tag deleted file mode 100644 index 8bd8bfb2aa..0000000000 --- a/src/web/app/desktop/tags/home-widgets/activity.tag +++ /dev/null @@ -1,234 +0,0 @@ -<mk-activity-home-widget> - <p class="title"><i class="fa fa-bar-chart"></i>%i18n:desktop.tags.mk-activity-home-widget.title%</p> - <button onclick={ toggle } title="%i18n:desktop.tags.mk-activity-home-widget.toggle%"><i class="fa fa-sort"></i></button> - <p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:common.loading%<mk-ellipsis/></p> - <mk-activity-home-widget-calender if={ !initializing && view == 0 } data={ [].concat(data) }/> - <mk-activity-home-widget-chart if={ !initializing && view == 1 } data={ [].concat(data) }/> - <style> - :scope - display block - background #fff - - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) - - > i - margin-right 4px - - > button - position absolute - z-index 2 - top 0 - right 0 - padding 0 - width 42px - font-size 0.9em - line-height 42px - color #ccc - - &:hover - color #aaa - - &:active - color #999 - - > .initializing - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - - </style> - <script> - this.mixin('i'); - this.mixin('api'); - - this.initializing = true; - this.view = 0; - - this.on('mount', () => { - this.api('aggregation/users/activity', { - user_id: this.I.id, - limit: 20 * 7 - }).then(data => { - this.update({ - initializing: false, - data - }); - }); - }); - - this.toggle = () => { - this.view++; - if (this.view == 2) this.view = 0; - }; - </script> -</mk-activity-home-widget> - -<mk-activity-home-widget-calender> - <svg viewBox="0 0 21 7" preserveAspectRatio="none"> - <rect each={ data } class="day" - width="1" height="1" - riot-x={ x } riot-y={ date.weekday } - rx="1" ry="1" - fill="transparent"> - <title>{ date.year }/{ date.month }/{ date.day }<br/>Post: { posts }, Reply: { replies }, Repost: { reposts }</title> - </rect> - <rect each={ data } - width="1" height="1" - riot-x={ x } riot-y={ date.weekday } - rx="1" ry="1" - fill={ color } - style="pointer-events: none; transform: scale({ v });"/> - <rect class="today" - width="1" height="1" - riot-x={ data[data.length - 1].x } riot-y={ data[data.length - 1].date.weekday } - rx="1" ry="1" - fill="none" - stroke-width="0.1" - stroke="#f73520"/> - </svg> - <style> - :scope - display block - - > svg - display block - padding 10px - width 100% - - > rect - transform-origin center - - &.day - &:hover - fill rgba(0, 0, 0, 0.05) - - </style> - <script> - this.data = this.opts.data; - this.data.forEach(d => d.total = d.posts + d.replies + d.reposts); - const peak = Math.max.apply(null, this.data.map(d => d.total)); - - let x = 0; - this.data.reverse().forEach(d => { - d.x = x; - d.date.weekday = (new Date(d.date.year, d.date.month - 1, d.date.day)).getDay(); - - d.v = d.total / (peak / 2); - if (d.v > 1) d.v = 1; - const ch = d.date.weekday == 0 || d.date.weekday == 6 ? 275 : 170; - const cs = d.v * 100; - const cl = 15 + ((1 - d.v) * 80); - d.color = `hsl(${ch}, ${cs}%, ${cl}%)`; - - if (d.date.weekday == 6) x++; - }); - </script> -</mk-activity-home-widget-calender> - -<mk-activity-home-widget-chart> - <svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none" onmousedown={ onMousedown }> - <title>Black ... Total<br/>Blue ... Posts<br/>Red ... Replies<br/>Green ... Reposts</title> - <polyline - riot-points={ pointsPost } - fill="none" - stroke-width="1" - stroke="#41ddde"/> - <polyline - riot-points={ pointsReply } - fill="none" - stroke-width="1" - stroke="#f7796c"/> - <polyline - riot-points={ pointsRepost } - fill="none" - stroke-width="1" - stroke="#a1de41"/> - <polyline - riot-points={ pointsTotal } - fill="none" - stroke-width="1" - stroke="#555" - stroke-dasharray="2 2"/> - </svg> - <style> - :scope - display block - - > svg - display block - padding 10px - width 100% - cursor all-scroll - </style> - <script> - this.viewBoxX = 140; - this.viewBoxY = 60; - this.zoom = 1; - this.pos = 0; - - this.data = this.opts.data.reverse(); - this.data.forEach(d => d.total = d.posts + d.replies + d.reposts); - const peak = Math.max.apply(null, this.data.map(d => d.total)); - - this.on('mount', () => { - this.render(); - }); - - this.render = () => { - this.update({ - pointsPost: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.posts / peak)) * this.viewBoxY}`).join(' '), - pointsReply: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' '), - pointsRepost: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.reposts / peak)) * this.viewBoxY}`).join(' '), - pointsTotal: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ') - }); - }; - - this.onMousedown = e => { - e.preventDefault(); - - const clickX = e.clientX; - const clickY = e.clientY; - const baseZoom = this.zoom; - const basePos = this.pos; - - // 動かした時 - dragListen(me => { - let moveLeft = me.clientX - clickX; - let moveTop = me.clientY - clickY; - - this.zoom = baseZoom + (-moveTop / 20); - this.pos = basePos + moveLeft; - if (this.zoom < 1) this.zoom = 1; - if (this.pos > 0) this.pos = 0; - if (this.pos < -(((this.data.length - 1) * this.zoom) - this.viewBoxX)) this.pos = -(((this.data.length - 1) * this.zoom) - this.viewBoxX); - - this.render(); - }); - }; - - function dragListen(fn) { - window.addEventListener('mousemove', fn); - window.addEventListener('mouseleave', dragClear.bind(null, fn)); - window.addEventListener('mouseup', dragClear.bind(null, fn)); - } - - function dragClear(fn) { - window.removeEventListener('mousemove', fn); - window.removeEventListener('mouseleave', dragClear); - window.removeEventListener('mouseup', dragClear); - } - </script> -</mk-activity-home-widget-chart> - diff --git a/src/web/app/desktop/tags/home-widgets/broadcast.tag b/src/web/app/desktop/tags/home-widgets/broadcast.tag deleted file mode 100644 index 1102e22c7f..0000000000 --- a/src/web/app/desktop/tags/home-widgets/broadcast.tag +++ /dev/null @@ -1,83 +0,0 @@ -<mk-broadcast-home-widget> - <div class="icon"> - <svg height="32" version="1.1" viewBox="0 0 32 32" width="32"> - <path class="tower" d="M16.04,11.24c1.79,0,3.239-1.45,3.239-3.24S17.83,4.76,16.04,4.76c-1.79,0-3.24,1.45-3.24,3.24 C12.78,9.78,14.24,11.24,16.04,11.24z M16.04,13.84c-0.82,0-1.66-0.2-2.4-0.6L7.34,29.98h2.98l1.72-2h8l1.681,2H24.7L18.42,13.24 C17.66,13.64,16.859,13.84,16.04,13.84z M16.02,14.8l2.02,7.2h-4L16.02,14.8z M12.04,25.98l2-2h4l2,2H12.04z"></path> - <path class="wave a" d="M4.66,1.04c-0.508-0.508-1.332-0.508-1.84,0c-1.86,1.92-2.8,4.44-2.8,6.94c0,2.52,0.94,5.04,2.8,6.96 c0.5,0.52,1.32,0.52,1.82,0s0.5-1.36,0-1.88C3.28,11.66,2.6,9.82,2.6,7.98S3.28,4.3,4.64,2.9C5.157,2.391,5.166,1.56,4.66,1.04z"></path> - <path class="wave b" d="M9.58,12.22c0.5-0.5,0.5-1.34,0-1.84C8.94,9.72,8.62,8.86,8.62,8s0.32-1.72,0.96-2.38c0.5-0.52,0.5-1.34,0-1.84 C9.346,3.534,9.02,3.396,8.68,3.4c-0.32,0-0.66,0.12-0.9,0.38C6.64,4.94,6.08,6.48,6.08,8s0.58,3.06,1.7,4.22 C8.28,12.72,9.1,12.72,9.58,12.22z"></path> - <path class="wave c" d="M22.42,3.78c-0.5,0.5-0.5,1.34,0,1.84c0.641,0.66,0.96,1.52,0.96,2.38s-0.319,1.72-0.96,2.38c-0.5,0.52-0.5,1.34,0,1.84 c0.487,0.497,1.285,0.505,1.781,0.018c0.007-0.006,0.013-0.012,0.02-0.018c1.139-1.16,1.699-2.7,1.699-4.22s-0.561-3.06-1.699-4.22 c-0.494-0.497-1.297-0.5-1.794-0.007C22.424,3.775,22.422,3.778,22.42,3.78z"></path> - <path class="wave d" d="M29.18,1.06c-0.479-0.502-1.273-0.522-1.775-0.044c-0.016,0.015-0.029,0.029-0.045,0.044c-0.5,0.52-0.5,1.36,0,1.88 c1.361,1.4,2.041,3.24,2.041,5.08s-0.68,3.66-2.041,5.08c-0.5,0.52-0.5,1.36,0,1.88c0.509,0.508,1.332,0.508,1.841,0 c1.86-1.92,2.8-4.44,2.8-6.96C31.99,5.424,30.98,2.931,29.18,1.06z"></path> - </svg> - </div> - <h1>開発者募集中!</h1> - <p><a href="https://github.com/syuilo/misskey" target="_blank">Misskeyはオープンソースで開発されています。リポジトリはこちら。</a></p> - <style> - :scope - display block - padding 10px 10px 10px 50px - background transparent - border-color #4078c0 !important - - &:after - content "" - display block - clear both - - > .icon - display block - float left - margin-left -40px - - > svg - fill currentColor - color #4078c0 - - > .wave - opacity 1 - - &.a - animation wave 20s ease-in-out 2.1s infinite - &.b - animation wave 20s ease-in-out 2s infinite - &.c - animation wave 20s ease-in-out 2s infinite - &.d - animation wave 20s ease-in-out 2.1s infinite - - @keyframes wave - 0% - opacity 1 - 1.5% - opacity 0 - 3.5% - opacity 0 - 5% - opacity 1 - 6.5% - opacity 0 - 8.5% - opacity 0 - 10% - opacity 1 - - > h1 - margin 0 - font-size 0.95em - font-weight normal - color #4078c0 - - > p - display block - z-index 1 - margin 0 - font-size 0.7em - color #555 - - a - color #555 - - - - - - </style> -</mk-broadcast-home-widget> diff --git a/src/web/app/desktop/tags/home-widgets/calendar.tag b/src/web/app/desktop/tags/home-widgets/calendar.tag deleted file mode 100644 index 9aa4ac6326..0000000000 --- a/src/web/app/desktop/tags/home-widgets/calendar.tag +++ /dev/null @@ -1,150 +0,0 @@ -<mk-calendar-home-widget data-special={ special }> - <div class="calendar" data-is-holiday={ isHoliday }> - <p class="month-and-year"><span class="year">{ year }年</span><span class="month">{ month }月</span></p> - <p class="day">{ day }日</p> - <p class="week-day">{ weekDay }曜日</p> - </div> - <div class="info"> - <div> - <p>今日:<b>{ dayP.toFixed(1) }%</b></p> - <div class="meter"> - <div class="val" style={ 'width:' + dayP + '%' }></div> - </div> - </div> - <div> - <p>今月:<b>{ monthP.toFixed(1) }%</b></p> - <div class="meter"> - <div class="val" style={ 'width:' + monthP + '%' }></div> - </div> - </div> - <div> - <p>今年:<b>{ yearP.toFixed(1) }%</b></p> - <div class="meter"> - <div class="val" style={ 'width:' + yearP + '%' }></div> - </div> - </div> - </div> - <style> - :scope - display block - padding 16px 0 - color #777 - background #fff - - &[data-special='on-new-years-day'] - border-color #ef95a0 !important - - &:after - content "" - display block - clear both - - > .calendar - float left - width 60% - text-align center - - &[data-is-holiday] - > .day - color #ef95a0 - - > p - margin 0 - line-height 18px - font-size 14px - - > span - margin 0 4px - - > .day - margin 10px 0 - line-height 32px - font-size 28px - - > .info - display block - float left - width 40% - padding 0 16px 0 0 - - > div - margin-bottom 8px - - &:last-child - margin-bottom 4px - - > p - margin 0 0 2px 0 - font-size 12px - line-height 18px - color #888 - - > b - margin-left 2px - - > .meter - width 100% - overflow hidden - background #eee - border-radius 8px - - > .val - height 4px - background $theme-color - - &:nth-child(1) - > .meter > .val - background #f7796c - - &:nth-child(2) - > .meter > .val - background #a1de41 - - &:nth-child(3) - > .meter > .val - background #41ddde - - </style> - <script> - this.draw = () => { - const now = new Date(); - const nd = now.getDate(); - const nm = now.getMonth(); - const ny = now.getFullYear(); - - this.year = ny; - this.month = nm + 1; - this.day = nd; - this.weekDay = ['日', '月', '火', '水', '木', '金', '土'][now.getDay()]; - - this.dayNumer = now - new Date(ny, nm, nd); - this.dayDenom = 1000/*ms*/ * 60/*s*/ * 60/*m*/ * 24/*h*/; - this.monthNumer = now - new Date(ny, nm, 1); - this.monthDenom = new Date(ny, nm + 1, 1) - new Date(ny, nm, 1); - this.yearNumer = now - new Date(ny, 0, 1); - this.yearDenom = new Date(ny + 1, 0, 1) - new Date(ny, 0, 1); - - this.dayP = this.dayNumer / this.dayDenom * 100; - this.monthP = this.monthNumer / this.monthDenom * 100; - this.yearP = this.yearNumer / this.yearDenom * 100; - - this.isHoliday = now.getDay() == 0 || now.getDay() == 6; - - this.special = - nm == 0 && nd == 1 ? 'on-new-years-day' : - false; - - this.update(); - }; - - this.draw(); - - this.on('mount', () => { - this.clock = setInterval(this.draw, 1000); - }); - - this.on('unmount', () => { - clearInterval(this.clock); - }); - </script> -</mk-calendar-home-widget> diff --git a/src/web/app/desktop/tags/home-widgets/donation.tag b/src/web/app/desktop/tags/home-widgets/donation.tag deleted file mode 100644 index d533e82831..0000000000 --- a/src/web/app/desktop/tags/home-widgets/donation.tag +++ /dev/null @@ -1,32 +0,0 @@ -<mk-donation-home-widget> - <article> - <h1><i class="fa fa-heart"></i>%i18n:desktop.tags.mk-donation-home-widget.title%</h1> - <p>{'%i18n:desktop.tags.mk-donation-home-widget.text%'.substr(0, '%i18n:desktop.tags.mk-donation-home-widget.text%'.indexOf('{'))}<a href="/syuilo" data-user-preview="@syuilo">@syuilo</a>{'%i18n:desktop.tags.mk-donation-home-widget.text%'.substr('%i18n:desktop.tags.mk-donation-home-widget.text%'.indexOf('}') + 1)}</p> - </article> - <style> - :scope - display block - background #fff - border-color #ead8bb !important - - > article - padding 20px - - > h1 - margin 0 0 5px 0 - font-size 1em - color #888 - - > i - margin-right 0.25em - - > p - display block - z-index 1 - margin 0 - font-size 0.8em - color #999 - - </style> - <script>this.mixin('user-preview');</script> -</mk-donation-home-widget> diff --git a/src/web/app/desktop/tags/home-widgets/mentions.tag b/src/web/app/desktop/tags/home-widgets/mentions.tag deleted file mode 100644 index b94e9b04c5..0000000000 --- a/src/web/app/desktop/tags/home-widgets/mentions.tag +++ /dev/null @@ -1,118 +0,0 @@ -<mk-mentions-home-widget> - <header><span data-is-active={ mode == 'all' } onclick={ setMode.bind(this, 'all') }>すべて</span><span data-is-active={ mode == 'following' } onclick={ setMode.bind(this, 'following') }>フォロー中</span></header> - <div class="loading" if={ isLoading }> - <mk-ellipsis-icon/> - </div> - <p class="empty" if={ isEmpty }><i class="fa fa-comments-o"></i><span if={ mode == 'all' }>あなた宛ての投稿はありません。</span><span if={ mode == 'following' }>あなたがフォローしているユーザーからの言及はありません。</span></p> - <mk-timeline ref="timeline"><yield to="footer"><i class="fa fa-moon-o" if={ !parent.moreLoading }></i><i class="fa fa-spinner fa-pulse fa-fw" if={ parent.moreLoading }></i></yield/> - <style> - :scope - display block - background #fff - - > header - padding 8px 16px - border-bottom solid 1px #eee - - > span - margin-right 16px - line-height 27px - font-size 18px - color #555 - - &:not([data-is-active]) - color $theme-color - cursor pointer - - &:hover - text-decoration underline - - > .loading - padding 64px 0 - - > .empty - display block - margin 0 auto - padding 32px - max-width 400px - text-align center - color #999 - - > i - display block - margin-bottom 16px - font-size 3em - color #ccc - - </style> - <script> - this.mixin('i'); - this.mixin('api'); - - this.isLoading = true; - this.isEmpty = false; - this.moreLoading = false; - this.mode = 'all'; - - this.on('mount', () => { - document.addEventListener('keydown', this.onDocumentKeydown); - window.addEventListener('scroll', this.onScroll); - - this.fetch(() => this.trigger('loaded')); - }); - - this.on('unmount', () => { - document.removeEventListener('keydown', this.onDocumentKeydown); - window.removeEventListener('scroll', this.onScroll); - }); - - this.onDocumentKeydown = e => { - if (e.target.tagName != 'INPUT' && tag != 'TEXTAREA') { - if (e.which == 84) { // t - this.refs.timeline.focus(); - } - } - }; - - this.fetch = cb => { - this.api('posts/mentions', { - following: this.mode == 'following' - }).then(posts => { - this.update({ - isLoading: false, - isEmpty: posts.length == 0 - }); - this.refs.timeline.setPosts(posts); - if (cb) cb(); - }); - }; - - this.more = () => { - if (this.moreLoading || this.isLoading || this.refs.timeline.posts.length == 0) return; - this.update({ - moreLoading: true - }); - this.api('posts/mentions', { - following: this.mode == 'following', - max_id: this.refs.timeline.tail().id - }).then(posts => { - this.update({ - moreLoading: false - }); - this.refs.timeline.prependPosts(posts); - }); - }; - - this.onScroll = () => { - const current = window.scrollY + window.innerHeight; - if (current > document.body.offsetHeight - 8) this.more(); - }; - - this.setMode = mode => { - this.update({ - mode: mode - }); - this.fetch(); - }; - </script> -</mk-mentions-home-widget> diff --git a/src/web/app/desktop/tags/home-widgets/nav.tag b/src/web/app/desktop/tags/home-widgets/nav.tag deleted file mode 100644 index 499d66014b..0000000000 --- a/src/web/app/desktop/tags/home-widgets/nav.tag +++ /dev/null @@ -1,17 +0,0 @@ -<mk-nav-home-widget><a href={ CONFIG.aboutUrl }>Misskeyについて</a><i>・</i><a href={ CONFIG.statsUrl }>統計</a><i>・</i><a href={ CONFIG.statusUrl }>ステータス</a><i>・</i><a href="http://zawazawa.jp/misskey/">Wiki</a><i>・</i><a href="https://github.com/syuilo/misskey">リポジトリ</a><i>・</i><a href={ CONFIG.devUrl }>開発者</a><i>・</i><a href="https://twitter.com/misskey_xyz" target="_blank">Follow us on <i class="fa fa-twitter"></i></a> - <style> - :scope - display block - padding 16px - font-size 12px - color #aaa - background #fff - - a - color #999 - - i - color #ccc - - </style> -</mk-nav-home-widget> diff --git a/src/web/app/desktop/tags/home-widgets/notifications.tag b/src/web/app/desktop/tags/home-widgets/notifications.tag deleted file mode 100644 index b1170855ac..0000000000 --- a/src/web/app/desktop/tags/home-widgets/notifications.tag +++ /dev/null @@ -1,51 +0,0 @@ -<mk-notifications-home-widget> - <p class="title"><i class="fa fa-bell-o"></i>%i18n:desktop.tags.mk-notifications-home-widget.title%</p> - <button onclick={ settings } title="%i18n:desktop.tags.mk-notifications-home-widget.settings%"><i class="fa fa-cog"></i></button> - <mk-notifications/> - <style> - :scope - display block - background #fff - - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) - - > i - margin-right 4px - - > button - position absolute - z-index 2 - top 0 - right 0 - padding 0 - width 42px - font-size 0.9em - line-height 42px - color #ccc - - &:hover - color #aaa - - &:active - color #999 - - > mk-notifications - max-height 300px - overflow auto - - </style> - <script> - this.settings = () => { - const w = riot.mount(document.body.appendChild(document.createElement('mk-settings-window')))[0]; - w.switch('notification'); - }; - </script> -</mk-notifications-home-widget> diff --git a/src/web/app/desktop/tags/home-widgets/photo-stream.tag b/src/web/app/desktop/tags/home-widgets/photo-stream.tag deleted file mode 100644 index d1f29589f3..0000000000 --- a/src/web/app/desktop/tags/home-widgets/photo-stream.tag +++ /dev/null @@ -1,91 +0,0 @@ -<mk-photo-stream-home-widget> - <p class="title"><i class="fa fa-camera"></i>%i18n:desktop.tags.mk-photo-stream-home-widget.title%</p> - <p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:common.loading%<mk-ellipsis/></p> - <div class="stream" if={ !initializing && images.length > 0 }> - <virtual each={ image in images }> - <div class="img" style={ 'background-image: url(' + image.url + '?thumbnail&size=256)' }></div> - </virtual> - </div> - <p class="empty" if={ !initializing && images.length == 0 }>%i18n:desktop.tags.mk-photo-stream-home-widget.no-photos%</p> - <style> - :scope - display block - background #fff - - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) - - > i - margin-right 4px - - > .stream - display -webkit-flex - display -moz-flex - display -ms-flex - display flex - justify-content center - flex-wrap wrap - padding 8px - - > .img - flex 1 1 33% - width 33% - height 80px - background-position center center - background-size cover - border solid 2px transparent - border-radius 4px - - > .initializing - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - - </style> - <script> - this.mixin('i'); - this.mixin('api'); - this.mixin('stream'); - - this.images = []; - this.initializing = true; - - this.on('mount', () => { - this.stream.on('drive_file_created', this.onStreamDriveFileCreated); - - this.api('drive/stream', { - type: 'image/*', - limit: 9 - }).then(images => { - this.update({ - initializing: false, - images: images - }); - }); - }); - - this.on('unmount', () => { - this.stream.off('drive_file_created', this.onStreamDriveFileCreated); - }); - - this.onStreamDriveFileCreated = file => { - if (/^image\/.+$/.test(file.type)) { - this.images.unshift(file); - if (this.images.length > 9) this.images.pop(); - this.update(); - } - }; - </script> -</mk-photo-stream-home-widget> diff --git a/src/web/app/desktop/tags/home-widgets/profile.tag b/src/web/app/desktop/tags/home-widgets/profile.tag deleted file mode 100644 index e6a8752113..0000000000 --- a/src/web/app/desktop/tags/home-widgets/profile.tag +++ /dev/null @@ -1,61 +0,0 @@ -<mk-profile-home-widget> - <div class="banner" style={ I.banner_url ? 'background-image: url(' + I.banner_url + '?thumbnail&size=256)' : '' } title="クリックでバナー編集" onclick={ setBanner }></div><img class="avatar" src={ I.avatar_url + '?thumbnail&size=64' } onclick={ setAvatar } alt="avatar" title="クリックでアバター編集" data-user-preview={ I.id }/><a class="name" href={ '/' + I.username }>{ I.name }</a> - <p class="username">@{ I.username }</p> - <style> - :scope - display block - overflow hidden - background #fff - - > .banner - height 100px - background-color #f5f5f5 - background-size cover - background-position center - cursor pointer - - > .avatar - display block - position absolute - top 76px - left 16px - width 58px - height 58px - margin 0 - border solid 3px #fff - border-radius 8px - vertical-align bottom - cursor pointer - - > .name - display block - margin 10px 0 0 84px - line-height 16px - font-weight bold - color #555 - - > .username - display block - margin 4px 0 8px 84px - line-height 16px - font-size 0.9em - color #999 - - </style> - <script> - import inputDialog from '../../scripts/input-dialog'; - import updateAvatar from '../../scripts/update-avatar'; - import updateBanner from '../../scripts/update-banner'; - - this.mixin('i'); - this.mixin('user-preview'); - - this.setAvatar = () => { - updateAvatar(this.I); - }; - - this.setBanner = () => { - updateBanner(this.I); - }; - </script> -</mk-profile-home-widget> diff --git a/src/web/app/desktop/tags/home-widgets/recommended-polls.tag b/src/web/app/desktop/tags/home-widgets/recommended-polls.tag deleted file mode 100644 index b724718af7..0000000000 --- a/src/web/app/desktop/tags/home-widgets/recommended-polls.tag +++ /dev/null @@ -1,106 +0,0 @@ -<mk-recommended-polls-home-widget> - <p class="title"><i class="fa fa-pie-chart"></i>%i18n:desktop.tags.mk-recommended-polls-home-widget.title%</p> - <button onclick={ fetch } title="%i18n:desktop.tags.mk-recommended-polls-home-widget.refresh%"><i class="fa fa-refresh"></i></button> - <div class="poll" if={ !loading && poll != null }> - <p if={ poll.text }><a href="/{ poll.user.username }/{ poll.id }">{ poll.text }</a></p> - <p if={ !poll.text }><a href="/{ poll.user.username }/{ poll.id }"><i class="fa fa-link"></i></a></p> - <mk-poll post={ poll }/> - </div> - <p class="empty" if={ !loading && poll == null }>%i18n:desktop.tags.mk-recommended-polls-home-widget.nothing%</p> - <p class="loading" if={ loading }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:common.loading%<mk-ellipsis/></p> - <style> - :scope - display block - background #fff - - > .title - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - border-bottom solid 1px #eee - - > i - margin-right 4px - - > button - position absolute - z-index 2 - top 0 - right 0 - padding 0 - width 42px - font-size 0.9em - line-height 42px - color #ccc - - &:hover - color #aaa - - &:active - color #999 - - > .poll - padding 16px - font-size 12px - color #555 - - > p - margin 0 0 8px 0 - - > a - color inherit - - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > .loading - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - - </style> - <script> - this.mixin('api'); - - this.poll = null; - this.loading = true; - - this.offset = 0; - - this.on('mount', () => { - this.fetch(); - }); - - this.fetch = () => { - this.update({ - loading: true, - poll: null - }); - this.api('posts/polls/recommendation', { - limit: 1, - offset: this.offset - }).then(posts => { - const poll = posts ? posts[0] : null; - if (poll == null) { - this.offset = 0; - } else { - this.offset++; - } - this.update({ - loading: false, - poll: poll - }); - }); - }; - </script> -</mk-recommended-polls-home-widget> diff --git a/src/web/app/desktop/tags/home-widgets/rss-reader.tag b/src/web/app/desktop/tags/home-widgets/rss-reader.tag deleted file mode 100644 index 550d7e76de..0000000000 --- a/src/web/app/desktop/tags/home-widgets/rss-reader.tag +++ /dev/null @@ -1,92 +0,0 @@ -<mk-rss-reader-home-widget> - <p class="title"><i class="fa fa-rss-square"></i>RSS</p> - <button onclick={ settings } title="設定"><i class="fa fa-cog"></i></button> - <div class="feed" if={ !initializing }> - <virtual each={ item in items }><a href={ item.link } target="_blank">{ item.title }</a></virtual> - </div> - <p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>読み込んでいます<mk-ellipsis/></p> - <style> - :scope - display block - background #fff - - > .title - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) - - > i - margin-right 4px - - > button - position absolute - top 0 - right 0 - padding 0 - width 42px - font-size 0.9em - line-height 42px - color #ccc - - &:hover - color #aaa - - &:active - color #999 - - > .feed - padding 12px 16px - font-size 0.9em - - > a - display block - padding 4px 0 - color #666 - border-bottom dashed 1px #eee - - &:last-child - border-bottom none - - > .initializing - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - - </style> - <script> - this.url = 'http://news.yahoo.co.jp/pickup/rss.xml'; - this.items = []; - this.initializing = true; - - this.on('mount', () => { - this.fetch(); - this.clock = setInterval(this.fetch, 60000); - }); - - this.on('unmount', () => { - clearInterval(this.clock); - }); - - this.fetch = () => { - fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.url}`).then(res => { - res.json().then(feed => { - this.update({ - initializing: false, - items: feed.items - }); - }); - }); - }; - - this.settings = () => { - }; - </script> -</mk-rss-reader-home-widget> diff --git a/src/web/app/desktop/tags/home-widgets/server.tag b/src/web/app/desktop/tags/home-widgets/server.tag deleted file mode 100644 index bc8f313d53..0000000000 --- a/src/web/app/desktop/tags/home-widgets/server.tag +++ /dev/null @@ -1,510 +0,0 @@ -<mk-server-home-widget> - <p class="title"><i class="fa fa-server"></i>%i18n:desktop.tags.mk-server-home-widget.title%</p> - <button onclick={ toggle } title="%i18n:desktop.tags.mk-server-home-widget.toggle%"><i class="fa fa-sort"></i></button> - <p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:common.loading%<mk-ellipsis/></p> - <mk-server-home-widget-cpu-and-memory-usage if={ !initializing } show={ view == 0 } connection={ connection }/> - <mk-server-home-widget-cpu if={ !initializing } show={ view == 1 } connection={ connection } meta={ meta }/> - <mk-server-home-widget-memory if={ !initializing } show={ view == 2 } connection={ connection }/> - <mk-server-home-widget-disk if={ !initializing } show={ view == 3 } connection={ connection }/> - <mk-server-home-widget-uptimes if={ !initializing } show={ view == 4 } connection={ connection }/> - <mk-server-home-widget-info if={ !initializing } show={ view == 5 } connection={ connection } meta={ meta }/> - <style> - :scope - display block - background #fff - - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) - - > i - margin-right 4px - - > button - position absolute - z-index 2 - top 0 - right 0 - padding 0 - width 42px - font-size 0.9em - line-height 42px - color #ccc - - &:hover - color #aaa - - &:active - color #999 - - > .initializing - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - - </style> - <script> - import Connection from '../../../common/scripts/server-stream'; - - this.mixin('api'); - - this.initializing = true; - this.view = 0; - this.connection = new Connection(); - - this.on('mount', () => { - this.api('meta').then(meta => { - this.update({ - initializing: false, - meta - }); - }); - }); - - this.on('unmount', () => { - this.connection.close(); - }); - - this.toggle = () => { - this.view++; - if (this.view == 6) this.view = 0; - }; - </script> -</mk-server-home-widget> - -<mk-server-home-widget-cpu-and-memory-usage> - <svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none"> - <defs> - <linearGradient id={ cpuGradientId } x1="0" x2="0" y1="1" y2="0"> - <stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop> - <stop offset="33%" stop-color="hsl(120, 80%, 70%)"></stop> - <stop offset="66%" stop-color="hsl(60, 80%, 70%)"></stop> - <stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop> - </linearGradient> - <mask id={ cpuMaskId } x="0" y="0" riot-width={ viewBoxX } riot-height={ viewBoxY }> - <polygon - riot-points={ cpuPolygonPoints } - fill="#fff" - fill-opacity="0.5"/> - <polyline - riot-points={ cpuPolylinePoints } - fill="none" - stroke="#fff" - stroke-width="1"/> - </mask> - </defs> - <rect - x="-1" y="-1" - riot-width={ viewBoxX + 2 } riot-height={ viewBoxY + 2 } - style="stroke: none; fill: url(#{ cpuGradientId }); mask: url(#{ cpuMaskId })"/> - <text x="1" y="5">CPU <tspan>{ cpuP }%</tspan></text> - </svg> - <svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none"> - <defs> - <linearGradient id={ memGradientId } x1="0" x2="0" y1="1" y2="0"> - <stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop> - <stop offset="33%" stop-color="hsl(120, 80%, 70%)"></stop> - <stop offset="66%" stop-color="hsl(60, 80%, 70%)"></stop> - <stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop> - </linearGradient> - <mask id={ memMaskId } x="0" y="0" riot-width={ viewBoxX } riot-height={ viewBoxY }> - <polygon - riot-points={ memPolygonPoints } - fill="#fff" - fill-opacity="0.5"/> - <polyline - riot-points={ memPolylinePoints } - fill="none" - stroke="#fff" - stroke-width="1"/> - </mask> - </defs> - <rect - x="-1" y="-1" - riot-width={ viewBoxX + 2 } riot-height={ viewBoxY + 2 } - style="stroke: none; fill: url(#{ memGradientId }); mask: url(#{ memMaskId })"/> - <text x="1" y="5">MEM <tspan>{ memP }%</tspan></text> - </svg> - <style> - :scope - display block - - > svg - display block - padding 10px - width 50% - float left - - &:first-child - padding-right 5px - - &:last-child - padding-left 5px - - > text - font-size 5px - fill rgba(0, 0, 0, 0.55) - - > tspan - opacity 0.5 - - &:after - content "" - display block - clear both - </style> - <script> - import uuid from '../../../common/scripts/uuid'; - - this.viewBoxX = 50; - this.viewBoxY = 30; - this.stats = []; - this.connection = this.opts.connection; - this.cpuGradientId = uuid(); - this.cpuMaskId = uuid(); - this.memGradientId = uuid(); - this.memMaskId = uuid(); - - this.on('mount', () => { - this.connection.on('stats', this.onStats); - }); - - this.on('unmount', () => { - this.connection.off('stats', this.onStats); - }); - - this.onStats = stats => { - stats.mem.used = stats.mem.total - stats.mem.free; - this.stats.push(stats); - if (this.stats.length > 50) this.stats.shift(); - - const cpuPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - s.cpu_usage) * this.viewBoxY}`).join(' '); - const memPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - (s.mem.used / s.mem.total)) * this.viewBoxY}`).join(' '); - - const cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ cpuPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`; - const memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ memPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`; - - const cpuP = (stats.cpu_usage * 100).toFixed(0); - const memP = (stats.mem.used / stats.mem.total * 100).toFixed(0); - - this.update({ - cpuPolylinePoints, - memPolylinePoints, - cpuPolygonPoints, - memPolygonPoints, - cpuP, - memP - }); - }; - </script> -</mk-server-home-widget-cpu-and-memory-usage> - -<mk-server-home-widget-cpu> - <mk-server-home-widget-pie ref="pie"/> - <div> - <p><i class="fa fa-microchip"></i>CPU</p> - <p>{ cores } Cores</p> - <p>{ model }</p> - </div> - <style> - :scope - display block - - > mk-server-home-widget-pie - padding 10px - height 100px - float left - - > div - float left - width calc(100% - 100px) - padding 10px 10px 10px 0 - - > p - margin 0 - font-size 12px - color #505050 - - &:first-child - font-weight bold - - > i - margin-right 4px - - &:after - content "" - display block - clear both - - </style> - <script> - this.cores = this.opts.meta.cpu.cores; - this.model = this.opts.meta.cpu.model; - this.connection = this.opts.connection; - - this.on('mount', () => { - this.connection.on('stats', this.onStats); - }); - - this.on('unmount', () => { - this.connection.off('stats', this.onStats); - }); - - this.onStats = stats => { - this.refs.pie.render(stats.cpu_usage); - }; - </script> -</mk-server-home-widget-cpu> - -<mk-server-home-widget-memory> - <mk-server-home-widget-pie ref="pie"/> - <div> - <p><i class="fa fa-flask"></i>Memory</p> - <p>Total: { bytesToSize(total, 1) }</p> - <p>Used: { bytesToSize(used, 1) }</p> - <p>Free: { bytesToSize(free, 1) }</p> - </div> - <style> - :scope - display block - - > mk-server-home-widget-pie - padding 10px - height 100px - float left - - > div - float left - width calc(100% - 100px) - padding 10px 10px 10px 0 - - > p - margin 0 - font-size 12px - color #505050 - - &:first-child - font-weight bold - - > i - margin-right 4px - - &:after - content "" - display block - clear both - - </style> - <script> - import bytesToSize from '../../../common/scripts/bytes-to-size'; - - this.connection = this.opts.connection; - this.bytesToSize = bytesToSize; - - this.on('mount', () => { - this.connection.on('stats', this.onStats); - }); - - this.on('unmount', () => { - this.connection.off('stats', this.onStats); - }); - - this.onStats = stats => { - stats.mem.used = stats.mem.total - stats.mem.free; - this.refs.pie.render(stats.mem.used / stats.mem.total); - - this.update({ - total: stats.mem.total, - used: stats.mem.used, - free: stats.mem.free - }); - }; - </script> -</mk-server-home-widget-memory> - -<mk-server-home-widget-disk> - <mk-server-home-widget-pie ref="pie"/> - <div> - <p><i class="fa fa-hdd-o"></i>Storage</p> - <p>Total: { bytesToSize(total, 1) }</p> - <p>Available: { bytesToSize(available, 1) }</p> - <p>Used: { bytesToSize(used, 1) }</p> - </div> - <style> - :scope - display block - - > mk-server-home-widget-pie - padding 10px - height 100px - float left - - > div - float left - width calc(100% - 100px) - padding 10px 10px 10px 0 - - > p - margin 0 - font-size 12px - color #505050 - - &:first-child - font-weight bold - - > i - margin-right 4px - - &:after - content "" - display block - clear both - - </style> - <script> - import bytesToSize from '../../../common/scripts/bytes-to-size'; - - this.connection = this.opts.connection; - this.bytesToSize = bytesToSize; - - this.on('mount', () => { - this.connection.on('stats', this.onStats); - }); - - this.on('unmount', () => { - this.connection.off('stats', this.onStats); - }); - - this.onStats = stats => { - stats.disk.used = stats.disk.total - stats.disk.free; - - this.refs.pie.render(stats.disk.used / stats.disk.total); - - this.update({ - total: stats.disk.total, - used: stats.disk.used, - available: stats.disk.available - }); - }; - </script> -</mk-server-home-widget-disk> - -<mk-server-home-widget-uptimes> - <p>Uptimes</p> - <p>Process: { process ? process.toFixed(0) : '---' }s</p> - <p>OS: { os ? os.toFixed(0) : '---' }s</p> - <style> - :scope - display block - padding 10px 14px - - > p - margin 0 - font-size 12px - color #505050 - - &:first-child - font-weight bold - - </style> - <script> - this.connection = this.opts.connection; - - this.on('mount', () => { - this.connection.on('stats', this.onStats); - }); - - this.on('unmount', () => { - this.connection.off('stats', this.onStats); - }); - - this.onStats = stats => { - this.update({ - process: stats.process_uptime, - os: stats.os_uptime - }); - }; - </script> -</mk-server-home-widget-uptimes> - -<mk-server-home-widget-info> - <p>Maintainer: <b>{ meta.maintainer }</b></p> - <p>Machine: { meta.machine }</p> - <p>Node: { meta.node }</p> - <style> - :scope - display block - padding 10px 14px - - > p - margin 0 - font-size 12px - color #505050 - - </style> - <script> - this.meta = this.opts.meta; - </script> -</mk-server-home-widget-info> - -<mk-server-home-widget-pie> - <svg viewBox="0 0 1 1" preserveAspectRatio="none"> - <circle - riot-r={ r } - cx="50%" cy="50%" - fill="none" - stroke-width="0.1" - stroke="rgba(0, 0, 0, 0.05)"/> - <circle - riot-r={ r } - cx="50%" cy="50%" - riot-stroke-dasharray={ Math.PI * (r * 2) } - riot-stroke-dashoffset={ strokeDashoffset } - fill="none" - stroke-width="0.1" - riot-stroke={ color }/> - <text x="50%" y="50%" dy="0.05" text-anchor="middle">{ (p * 100).toFixed(0) }%</text> - </svg> - <style> - :scope - display block - - > svg - display block - height 100% - - > circle - transform-origin center - transform rotate(-90deg) - transition stroke-dashoffset 0.5s ease - - > text - font-size 0.15px - fill rgba(0, 0, 0, 0.6) - - </style> - <script> - this.r = 0.4; - - this.render = p => { - const color = `hsl(${180 - (p * 180)}, 80%, 70%)`; - const strokeDashoffset = (1 - p) * (Math.PI * (this.r * 2)); - - this.update({ - p, - color, - strokeDashoffset - }); - }; - </script> -</mk-server-home-widget-pie> diff --git a/src/web/app/desktop/tags/home-widgets/timeline.tag b/src/web/app/desktop/tags/home-widgets/timeline.tag deleted file mode 100644 index 08d96ad715..0000000000 --- a/src/web/app/desktop/tags/home-widgets/timeline.tag +++ /dev/null @@ -1,118 +0,0 @@ -<mk-timeline-home-widget> - <mk-following-setuper if={ noFollowing }/> - <div class="loading" if={ isLoading }> - <mk-ellipsis-icon/> - </div> - <p class="empty" if={ isEmpty }><i class="fa fa-comments-o"></i>自分の投稿や、自分がフォローしているユーザーの投稿が表示されます。</p> - <mk-timeline ref="timeline"><yield to="footer"><i class="fa fa-moon-o" if={ !parent.moreLoading }></i><i class="fa fa-spinner fa-pulse fa-fw" if={ parent.moreLoading }></i></yield/> - <style> - :scope - display block - background #fff - - > mk-following-setuper - border-bottom solid 1px #eee - - > .loading - padding 64px 0 - - > .empty - display block - margin 0 auto - padding 32px - max-width 400px - text-align center - color #999 - - > i - display block - margin-bottom 16px - font-size 3em - color #ccc - - </style> - <script> - this.mixin('i'); - this.mixin('api'); - this.mixin('stream'); - - this.isLoading = true; - this.isEmpty = false; - this.moreLoading = false; - this.noFollowing = this.I.following_count == 0; - - this.on('mount', () => { - this.stream.on('post', this.onStreamPost); - this.stream.on('follow', this.onStreamFollow); - this.stream.on('unfollow', this.onStreamUnfollow); - - document.addEventListener('keydown', this.onDocumentKeydown); - window.addEventListener('scroll', this.onScroll); - - this.load(() => this.trigger('loaded')); - }); - - this.on('unmount', () => { - this.stream.off('post', this.onStreamPost); - this.stream.off('follow', this.onStreamFollow); - this.stream.off('unfollow', this.onStreamUnfollow); - - document.removeEventListener('keydown', this.onDocumentKeydown); - window.removeEventListener('scroll', this.onScroll); - }); - - this.onDocumentKeydown = e => { - if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { - if (e.which == 84) { // t - this.refs.timeline.focus(); - } - } - }; - - this.load = (cb) => { - this.api('posts/timeline').then(posts => { - this.update({ - isLoading: false, - isEmpty: posts.length == 0 - }); - this.refs.timeline.setPosts(posts); - if (cb) cb(); - }); - }; - - this.more = () => { - if (this.moreLoading || this.isLoading || this.refs.timeline.posts.length == 0) return; - this.update({ - moreLoading: true - }); - this.api('posts/timeline', { - max_id: this.refs.timeline.tail().id - }).then(posts => { - this.update({ - moreLoading: false - }); - this.refs.timeline.prependPosts(posts); - }); - }; - - this.onStreamPost = post => { - this.update({ - isEmpty: false - }); - this.refs.timeline.addPost(post); - }; - - this.onStreamFollow = () => { - this.load(); - }; - - this.onStreamUnfollow = () => { - this.load(); - }; - - this.onScroll = () => { - const current = window.scrollY + window.innerHeight; - if (current > document.body.offsetHeight - 8) this.more(); - }; - </script> -</mk-timeline-home-widget> diff --git a/src/web/app/desktop/tags/home-widgets/tips.tag b/src/web/app/desktop/tags/home-widgets/tips.tag deleted file mode 100644 index 5a535099ab..0000000000 --- a/src/web/app/desktop/tags/home-widgets/tips.tag +++ /dev/null @@ -1,78 +0,0 @@ -<mk-tips-home-widget> - <p ref="tip"><i class="fa fa-lightbulb-o"></i><span ref="text"></span></p> - <style> - :scope - display block - background transparent !important - border none !important - overflow visible !important - - > p - display block - margin 0 - padding 0 12px - text-align center - font-size 0.7em - color #999 - - > i - margin-right 4px - - kbd - display inline - padding 0 6px - margin 0 2px - font-size 1em - font-family inherit - border solid 1px #999 - border-radius 2px - - </style> - <script> - import anime from 'animejs'; - - this.tips = [ - '<kbd>t</kbd>でタイムラインにフォーカスできます', - '<kbd>p</kbd>または<kbd>n</kbd>で投稿フォームを開きます', - '投稿フォームにはファイルをドラッグ&ドロップできます', - '投稿フォームにクリップボードにある画像データをペーストできます', - 'ドライブにファイルをドラッグ&ドロップしてアップロードできます', - 'ドライブでファイルをドラッグしてフォルダ移動できます', - 'ドライブでフォルダをドラッグしてフォルダ移動できます', - 'ホームをカスタマイズできます(準備中)', - 'MisskeyはMIT Licenseです' - ] - - this.on('mount', () => { - this.set(); - this.clock = setInterval(this.change, 20000); - }); - - this.on('unmount', () => { - clearInterval(this.clock); - }); - - this.set = () => { - this.refs.text.innerHTML = this.tips[Math.floor(Math.random() * this.tips.length)]; - }; - - this.change = () => { - anime({ - targets: this.refs.tip, - opacity: 0, - duration: 500, - easing: 'linear', - complete: this.set - }); - - setTimeout(() => { - anime({ - targets: this.refs.tip, - opacity: 1, - duration: 500, - easing: 'linear' - }); - }, 500); - }; - </script> -</mk-tips-home-widget> diff --git a/src/web/app/desktop/tags/home-widgets/trends.tag b/src/web/app/desktop/tags/home-widgets/trends.tag deleted file mode 100644 index 021df3f728..0000000000 --- a/src/web/app/desktop/tags/home-widgets/trends.tag +++ /dev/null @@ -1,112 +0,0 @@ -<mk-trends-home-widget> - <p class="title"><i class="fa fa-fire"></i>%i18n:desktop.tags.mk-trends-home-widget.title%</p> - <button onclick={ fetch } title="%i18n:desktop.tags.mk-trends-home-widget.refresh%"><i class="fa fa-refresh"></i></button> - <div class="post" if={ !loading && post != null }> - <p class="text"><a href="/{ post.user.username }/{ post.id }">{ post.text }</a></p> - <p class="author">―<a href="/{ post.user.username }">@{ post.user.username }</a></p> - </div> - <p class="empty" if={ !loading && post == null }>%i18n:desktop.tags.mk-trends-home-widget.nothing%</p> - <p class="loading" if={ loading }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:common.loading%<mk-ellipsis/></p> - <style> - :scope - display block - background #fff - - > .title - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - border-bottom solid 1px #eee - - > i - margin-right 4px - - > button - position absolute - z-index 2 - top 0 - right 0 - padding 0 - width 42px - font-size 0.9em - line-height 42px - color #ccc - - &:hover - color #aaa - - &:active - color #999 - - > .post - padding 16px - font-size 12px - font-style oblique - color #555 - - > p - margin 0 - - > .text, - > .author - > a - color inherit - - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > .loading - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - - </style> - <script> - this.mixin('api'); - - this.post = null; - this.loading = true; - - this.offset = 0; - - this.on('mount', () => { - this.fetch(); - }); - - this.fetch = () => { - this.update({ - loading: true, - post: null - }); - this.api('posts/trend', { - limit: 1, - offset: this.offset, - repost: false, - reply: false, - media: false, - poll: false - }).then(posts => { - const post = posts ? posts[0] : null; - if (post == null) { - this.offset = 0; - } else { - this.offset++; - } - this.update({ - loading: false, - post: post - }); - }); - }; - </script> -</mk-trends-home-widget> diff --git a/src/web/app/desktop/tags/home-widgets/user-recommendation.tag b/src/web/app/desktop/tags/home-widgets/user-recommendation.tag deleted file mode 100644 index f78d7944f1..0000000000 --- a/src/web/app/desktop/tags/home-widgets/user-recommendation.tag +++ /dev/null @@ -1,152 +0,0 @@ -<mk-user-recommendation-home-widget> - <p class="title"><i class="fa fa-users"></i>%i18n:desktop.tags.mk-user-recommendation-home-widget.title%</p> - <button onclick={ refresh } title="%i18n:desktop.tags.mk-user-recommendation-home-widget.refresh%"><i class="fa fa-refresh"></i></button> - <div class="user" if={ !loading && users.length != 0 } each={ _user in users }> - <a class="avatar-anchor" href={ '/' + _user.username }> - <img class="avatar" src={ _user.avatar_url + '?thumbnail&size=42' } alt="" data-user-preview={ _user.id }/> - </a> - <div class="body"> - <a class="name" href={ '/' + _user.username } data-user-preview={ _user.id }>{ _user.name }</a> - <p class="username">@{ _user.username }</p> - </div> - <mk-follow-button user={ _user }/> - </div> - <p class="empty" if={ !loading && users.length == 0 }>%i18n:desktop.tags.mk-user-recommendation-home-widget.no-one%</p> - <p class="loading" if={ loading }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:common.loading%<mk-ellipsis/></p> - <style> - :scope - display block - background #fff - - > .title - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - border-bottom solid 1px #eee - - > i - margin-right 4px - - > button - position absolute - z-index 2 - top 0 - right 0 - padding 0 - width 42px - font-size 0.9em - line-height 42px - color #ccc - - &:hover - color #aaa - - &:active - color #999 - - > .user - padding 16px - border-bottom solid 1px #eee - - &:last-child - border-bottom none - - &:after - content "" - display block - clear both - - > .avatar-anchor - display block - float left - margin 0 12px 0 0 - - > .avatar - display block - width 42px - height 42px - margin 0 - border-radius 8px - vertical-align bottom - - > .body - float left - width calc(100% - 54px) - - > .name - margin 0 - font-size 16px - line-height 24px - color #555 - - > .username - display block - margin 0 - font-size 15px - line-height 16px - color #ccc - - > mk-follow-button - position absolute - top 16px - right 16px - - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > .loading - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - - </style> - <script> - this.mixin('api'); - this.mixin('user-preview'); - - this.users = null; - this.loading = true; - - this.limit = 3; - this.page = 0; - - this.on('mount', () => { - this.fetch(); - }); - - this.fetch = () => { - this.update({ - loading: true, - users: null - }); - this.api('users/recommendation', { - limit: this.limit, - offset: this.limit * this.page - }).then(users => { - this.update({ - loading: false, - users: users - }); - }); - }; - - this.refresh = () => { - if (this.users.length < this.limit) { - this.page = 0; - } else { - this.page++; - } - this.fetch(); - }; - </script> -</mk-user-recommendation-home-widget> diff --git a/src/web/app/desktop/tags/home-widgets/version.tag b/src/web/app/desktop/tags/home-widgets/version.tag deleted file mode 100644 index 079e4e86b8..0000000000 --- a/src/web/app/desktop/tags/home-widgets/version.tag +++ /dev/null @@ -1,22 +0,0 @@ -<mk-version-home-widget> - <p>ver{ version }</p> - <style> - :scope - display block - background transparent !important - border none !important - overflow visible !important - - > p - display block - margin 0 - padding 0 12px - text-align center - font-size 0.7em - color #aaa - - </style> - <script> - this.version = VERSION; - </script> -</mk-version-home-widget> diff --git a/src/web/app/desktop/tags/home.tag b/src/web/app/desktop/tags/home.tag deleted file mode 100644 index 37b2d3cf7e..0000000000 --- a/src/web/app/desktop/tags/home.tag +++ /dev/null @@ -1,129 +0,0 @@ -<mk-home> - <div class="main"> - <div class="left" ref="left"></div> - <main> - <mk-timeline-home-widget ref="tl" if={ mode == 'timeline' }/> - <mk-mentions-home-widget ref="tl" if={ mode == 'mentions' }/> - </main> - <div class="right" ref="right"></div> - </div> - <style> - :scope - display block - - > .main - margin 0 auto - max-width 1200px - - &:after - content "" - display block - clear both - - > * - float left - - > * - display block - //border solid 1px #eaeaea - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - //box-shadow 0px 2px 16px rgba(0, 0, 0, 0.2) - - &:not(:last-child) - margin-bottom 16px - - > main - padding 16px - width calc(100% - 275px * 2) - - > *:not(main) - width 275px - - > .left - padding 16px 0 16px 16px - - > .right - padding 16px 16px 16px 0 - - @media (max-width 1100px) - > *:not(main) - display none - - > main - float none - width 100% - max-width 700px - margin 0 auto - - </style> - <script> - this.mixin('i'); - - this.mode = this.opts.mode || 'timeline'; - - const _home = { - left: [ - 'profile', - 'calendar', - 'activity', - 'rss-reader', - 'trends', - 'photo-stream', - 'version' - ], - right: [ - 'broadcast', - 'notifications', - 'user-recommendation', - 'recommended-polls', - 'server', - 'donation', - 'nav', - 'tips' - ] - }; - - this.home = []; - - this.on('mount', () => { - this.refs.tl.on('loaded', () => { - this.trigger('loaded'); - }); -/* - this.I.data.home.forEach(widget => { - try { - const el = document.createElement(`mk-${widget.name}-home-widget`); - switch (widget.place) { - case 'left': this.refs.left.appendChild(el); break; - case 'right': this.refs.right.appendChild(el); break; - } - this.home.push(riot.mount(el, { - id: widget.id, - data: widget.data - })[0]); - } catch (e) { - // noop - } - }); -*/ - _home.left.forEach(widget => { - const el = document.createElement(`mk-${widget}-home-widget`); - this.refs.left.appendChild(el); - this.home.push(riot.mount(el)[0]); - }); - - _home.right.forEach(widget => { - const el = document.createElement(`mk-${widget}-home-widget`); - this.refs.right.appendChild(el); - this.home.push(riot.mount(el)[0]); - }); - }); - - this.on('unmount', () => { - this.home.forEach(widget => { - widget.unmount(); - }); - }); - </script> -</mk-home> diff --git a/src/web/app/desktop/tags/image-dialog.tag b/src/web/app/desktop/tags/image-dialog.tag deleted file mode 100644 index 39d16ca139..0000000000 --- a/src/web/app/desktop/tags/image-dialog.tag +++ /dev/null @@ -1,61 +0,0 @@ -<mk-image-dialog> - <div class="bg" ref="bg" onclick={ close }></div><img ref="img" src={ image.url } alt={ image.name } title={ image.name } onclick={ close }/> - <style> - :scope - display block - position fixed - z-index 2048 - top 0 - left 0 - width 100% - height 100% - opacity 0 - - > .bg - display block - position fixed - z-index 1 - top 0 - left 0 - width 100% - height 100% - background rgba(0, 0, 0, 0.7) - - > img - position fixed - z-index 2 - top 0 - right 0 - bottom 0 - left 0 - max-width 100% - max-height 100% - margin auto - cursor zoom-out - - </style> - <script> - import anime from 'animejs'; - - this.image = this.opts.image; - - this.on('mount', () => { - anime({ - targets: this.root, - opacity: 1, - duration: 100, - easing: 'linear' - }); - }); - - this.close = () => { - anime({ - targets: this.root, - opacity: 0, - duration: 100, - easing: 'linear', - complete: () => this.unmount() - }); - }; - </script> -</mk-image-dialog> diff --git a/src/web/app/desktop/tags/images-viewer.tag b/src/web/app/desktop/tags/images-viewer.tag deleted file mode 100644 index 44a61cb747..0000000000 --- a/src/web/app/desktop/tags/images-viewer.tag +++ /dev/null @@ -1,45 +0,0 @@ -<mk-images-viewer> - <div class="image" ref="view" onmousemove={ mousemove } style={ 'background-image: url(' + image.url + '?thumbnail' } onclick={ click }><img src={ image.url + '?thumbnail&size=512' } alt={ image.name } title={ image.name }/></div> - <style> - :scope - display block - overflow hidden - border-radius 4px - - > .image - cursor zoom-in - - > img - display block - max-height 256px - max-width 100% - margin 0 auto - - &:hover - > img - visibility hidden - - &:not(:hover) - background-image none !important - - </style> - <script> - this.images = this.opts.images; - this.image = this.images[0]; - - this.mousemove = e => { - const rect = this.refs.view.getBoundingClientRect(); - const mouseX = e.clientX - rect.left; - const mouseY = e.clientY - rect.top; - const xp = mouseX / this.refs.view.offsetWidth * 100; - const yp = mouseY / this.refs.view.offsetHeight * 100; - this.refs.view.style.backgroundPosition = xp + '% ' + yp + '%'; - }; - - this.click = () => { - riot.mount(document.body.appendChild(document.createElement('mk-image-dialog')), { - image: this.image - }); - }; - </script> -</mk-images-viewer> diff --git a/src/web/app/desktop/tags/index.js b/src/web/app/desktop/tags/index.js deleted file mode 100644 index 177ba41293..0000000000 --- a/src/web/app/desktop/tags/index.js +++ /dev/null @@ -1,93 +0,0 @@ -require('./contextmenu.tag'); -require('./dialog.tag'); -require('./window.tag'); -require('./input-dialog.tag'); -require('./follow-button.tag'); -require('./drive/base-contextmenu.tag'); -require('./drive/file-contextmenu.tag'); -require('./drive/folder-contextmenu.tag'); -require('./drive/file.tag'); -require('./drive/folder.tag'); -require('./drive/nav-folder.tag'); -require('./drive/browser-window.tag'); -require('./drive/browser.tag'); -require('./select-file-from-drive-window.tag'); -require('./crop-window.tag'); -require('./settings.tag'); -require('./settings-window.tag'); -require('./analog-clock.tag'); -require('./ui-header.tag'); -require('./ui-header-account.tag'); -require('./ui-header-notifications.tag'); -require('./ui-header-clock.tag'); -require('./ui-header-nav.tag'); -require('./ui-header-post-button.tag'); -require('./ui-header-search.tag'); -require('./notifications.tag'); -require('./post-form-window.tag'); -require('./post-form.tag'); -require('./timeline-post.tag'); -require('./post-preview.tag'); -require('./repost-form-window.tag'); -require('./home-widgets/user-recommendation.tag'); -require('./home-widgets/timeline.tag'); -require('./home-widgets/mentions.tag'); -require('./home-widgets/calendar.tag'); -require('./home-widgets/donation.tag'); -require('./home-widgets/tips.tag'); -require('./home-widgets/nav.tag'); -require('./home-widgets/profile.tag'); -require('./home-widgets/notifications.tag'); -require('./home-widgets/rss-reader.tag'); -require('./home-widgets/photo-stream.tag'); -require('./home-widgets/broadcast.tag'); -require('./home-widgets/version.tag'); -require('./home-widgets/recommended-polls.tag'); -require('./home-widgets/trends.tag'); -require('./home-widgets/activity.tag'); -require('./home-widgets/server.tag'); -require('./timeline.tag'); -require('./messaging/window.tag'); -require('./messaging/room-window.tag'); -require('./following-setuper.tag'); -require('./ellipsis-icon.tag'); -require('./ui.tag'); -require('./home.tag'); -require('./user-header.tag'); -require('./user-profile.tag'); -require('./user-timeline.tag'); -require('./user.tag'); -require('./user-home.tag'); -require('./user-graphs.tag'); -require('./user-photos.tag'); -require('./big-follow-button.tag'); -require('./pages/entrance.tag'); -require('./pages/entrance/signin.tag'); -require('./pages/entrance/signup.tag'); -require('./pages/home.tag'); -require('./pages/user.tag'); -require('./pages/post.tag'); -require('./pages/search.tag'); -require('./pages/not-found.tag'); -require('./autocomplete-suggestion.tag'); -require('./progress-dialog.tag'); -require('./user-preview.tag'); -require('./post-detail.tag'); -require('./post-detail-sub.tag'); -require('./search.tag'); -require('./search-posts.tag'); -require('./set-avatar-suggestion.tag'); -require('./set-banner-suggestion.tag'); -require('./repost-form.tag'); -require('./timeline-post-sub.tag'); -require('./sub-post-content.tag'); -require('./images-viewer.tag'); -require('./image-dialog.tag'); -require('./donation.tag'); -require('./users-list.tag'); -require('./user-following.tag'); -require('./user-followers.tag'); -require('./user-following-window.tag'); -require('./user-followers-window.tag'); -require('./list-user.tag'); -require('./ui-notification.tag'); diff --git a/src/web/app/desktop/tags/input-dialog.tag b/src/web/app/desktop/tags/input-dialog.tag deleted file mode 100644 index f343c4625a..0000000000 --- a/src/web/app/desktop/tags/input-dialog.tag +++ /dev/null @@ -1,167 +0,0 @@ -<mk-input-dialog> - <mk-window ref="window" is-modal={ true } width={ '500px' }> - <yield to="header"> - <i class="fa fa-i-cursor"></i>{ parent.title } - </yield> - <yield to="content"> - <div class="body"> - <input ref="text" oninput={ parent.update } onkeydown={ parent.onKeydown } placeholder={ parent.placeholder }/> - </div> - <div class="action"> - <button class="cancel" onclick={ parent.cancel }>キャンセル</button> - <button class="ok" disabled={ !parent.allowEmpty && refs.text.value.length == 0 } onclick={ parent.ok }>決定</button> - </div> - </yield> - </mk-window> - <style> - :scope - display block - - > mk-window - [data-yield='header'] - > i - margin-right 4px - - [data-yield='content'] - > .body - padding 16px - - > input - display block - padding 8px - margin 0 - width 100% - max-width 100% - min-width 100% - font-size 1em - color #333 - background #fff - outline none - border solid 1px rgba($theme-color, 0.1) - border-radius 4px - transition border-color .3s ease - - &:hover - border-color rgba($theme-color, 0.2) - transition border-color .1s ease - - &:focus - color $theme-color - border-color rgba($theme-color, 0.5) - transition border-color 0s ease - - &::-webkit-input-placeholder - color rgba($theme-color, 0.3) - - > .action - height 72px - background lighten($theme-color, 95%) - - .ok - .cancel - display block - position absolute - bottom 16px - cursor pointer - padding 0 - margin 0 - width 120px - height 40px - font-size 1em - outline none - border-radius 4px - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - &:disabled - opacity 0.7 - cursor default - - .ok - right 16px - color $theme-color-foreground - background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) - border solid 1px lighten($theme-color, 15%) - - &:not(:disabled) - font-weight bold - - &:hover:not(:disabled) - background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) - border-color $theme-color - - &:active:not(:disabled) - background $theme-color - border-color $theme-color - - .cancel - right 148px - color #888 - background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) - border solid 1px #e2e2e2 - - &:hover - background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) - border-color #dcdcdc - - &:active - background #ececec - border-color #dcdcdc - - </style> - <script> - this.done = false; - - this.title = this.opts.title; - this.placeholder = this.opts.placeholder; - this.default = this.opts.default; - this.allowEmpty = this.opts.allowEmpty != null ? this.opts.allowEmpty : true; - - this.on('mount', () => { - this.text = this.refs.window.refs.text; - if (this.default) this.text.value = this.default; - this.text.focus(); - - this.refs.window.on('closing', () => { - if (this.done) { - this.opts.onOk(this.text.value); - } else { - if (this.opts.onCancel) this.opts.onCancel(); - } - }); - - this.refs.window.on('closed', () => { - this.unmount(); - }); - }); - - this.cancel = () => { - this.done = false; - this.refs.window.close(); - }; - - this.ok = () => { - if (!this.allowEmpty && this.text.value == '') return; - this.done = true; - this.refs.window.close(); - }; - - this.onKeydown = e => { - if (e.which == 13) { // Enter - e.preventDefault(); - e.stopPropagation(); - this.ok(); - } - }; - </script> -</mk-input-dialog> diff --git a/src/web/app/desktop/tags/list-user.tag b/src/web/app/desktop/tags/list-user.tag deleted file mode 100644 index 91a6de0a0d..0000000000 --- a/src/web/app/desktop/tags/list-user.tag +++ /dev/null @@ -1,93 +0,0 @@ -<mk-list-user> - <a class="avatar-anchor" href={ '/' + user.username }> - <img class="avatar" src={ user.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - </a> - <div class="main"> - <header> - <a class="name" href={ '/' + user.username }>{ user.name }</a> - <span class="username">@{ user.username }</span> - </header> - <div class="body"> - <p class="followed" if={ user.is_followed }>フォローされています</p> - <div class="description">{ user.description }</div> - </div> - </div> - <mk-follow-button user={ user }/> - <style> - :scope - display block - margin 0 - padding 16px - font-size 16px - - &:after - content "" - display block - clear both - - > .avatar-anchor - display block - float left - margin 0 16px 0 0 - - > .avatar - display block - width 58px - height 58px - margin 0 - border-radius 8px - vertical-align bottom - - > .main - float left - width calc(100% - 74px) - - > header - margin-bottom 2px - - > .name - display inline - margin 0 - padding 0 - color #777 - font-size 1em - font-weight 700 - text-align left - text-decoration none - - &:hover - text-decoration underline - - > .username - text-align left - margin 0 0 0 8px - color #ccc - - > .body - > .followed - display inline-block - margin 0 0 4px 0 - padding 2px 8px - vertical-align top - font-size 10px - color #71afc7 - background #eefaff - border-radius 4px - - > .description - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 1.1em - color #717171 - - > mk-follow-button - position absolute - top 16px - right 16px - - </style> - <script>this.user = this.opts.user</script> -</mk-list-user> diff --git a/src/web/app/desktop/tags/messaging/room-window.tag b/src/web/app/desktop/tags/messaging/room-window.tag deleted file mode 100644 index 5d8a4303a5..0000000000 --- a/src/web/app/desktop/tags/messaging/room-window.tag +++ /dev/null @@ -1,30 +0,0 @@ -<mk-messaging-room-window> - <mk-window ref="window" is-modal={ false } width={ '500px' } height={ '560px' }> - <yield to="header"><i class="fa fa-comments"></i>メッセージ: { parent.user.name }</yield> - <yield to="content"> - <mk-messaging-room user={ parent.user }/> - </yield> - </mk-window> - <style> - :scope - > mk-window - [data-yield='header'] - > i - margin-right 4px - - [data-yield='content'] - > mk-messaging-room - height 100% - overflow auto - - </style> - <script> - this.user = this.opts.user; - - this.on('mount', () => { - this.refs.window.on('closed', () => { - this.unmount(); - }); - }); - </script> -</mk-messaging-room-window> diff --git a/src/web/app/desktop/tags/messaging/window.tag b/src/web/app/desktop/tags/messaging/window.tag deleted file mode 100644 index 5e478f0367..0000000000 --- a/src/web/app/desktop/tags/messaging/window.tag +++ /dev/null @@ -1,34 +0,0 @@ -<mk-messaging-window> - <mk-window ref="window" is-modal={ false } width={ '500px' } height={ '560px' }> - <yield to="header"><i class="fa fa-comments"></i>メッセージ</yield> - <yield to="content"> - <mk-messaging ref="index"/> - </yield> - </mk-window> - <style> - :scope - > mk-window - [data-yield='header'] - > i - margin-right 4px - - [data-yield='content'] - > mk-messaging - height 100% - overflow auto - - </style> - <script> - this.on('mount', () => { - this.refs.window.on('closed', () => { - this.unmount(); - }); - - this.refs.window.refs.index.on('navigate-user', user => { - riot.mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), { - user: user - }); - }); - }); - </script> -</mk-messaging-window> diff --git a/src/web/app/desktop/tags/notifications.tag b/src/web/app/desktop/tags/notifications.tag deleted file mode 100644 index 21e4fe7fa5..0000000000 --- a/src/web/app/desktop/tags/notifications.tag +++ /dev/null @@ -1,283 +0,0 @@ -<mk-notifications> - <div class="notifications" if={ notifications.length != 0 }> - <virtual each={ notification, i in notifications }> - <div class="notification { notification.type }"> - <mk-time time={ notification.created_at }/> - <virtual if={ notification.type == 'reaction' }> - <a class="avatar-anchor" href={ '/' + notification.user.username } data-user-preview={ notification.user.id }> - <img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/> - </a> - <div class="text"> - <p><mk-reaction-icon reaction={ notification.reaction }/><a href={ '/' + notification.user.username } data-user-preview={ notification.user.id }>{ notification.user.name }</a></p><a class="post-ref" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a> - </div> - </virtual> - <virtual if={ notification.type == 'repost' }> - <a class="avatar-anchor" href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }> - <img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/> - </a> - <div class="text"> - <p><i class="fa fa-retweet"></i><a href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>{ notification.post.user.name }</a></p><a class="post-ref" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post.repost) }</a> - </div> - </virtual> - <virtual if={ notification.type == 'quote' }> - <a class="avatar-anchor" href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }> - <img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/> - </a> - <div class="text"> - <p><i class="fa fa-quote-left"></i><a href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>{ notification.post.user.name }</a></p><a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a> - </div> - </virtual> - <virtual if={ notification.type == 'follow' }> - <a class="avatar-anchor" href={ '/' + notification.user.username } data-user-preview={ notification.user.id }> - <img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/> - </a> - <div class="text"> - <p><i class="fa fa-user-plus"></i><a href={ '/' + notification.user.username } data-user-preview={ notification.user.id }>{ notification.user.name }</a></p> - </div> - </virtual> - <virtual if={ notification.type == 'reply' }> - <a class="avatar-anchor" href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }> - <img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/> - </a> - <div class="text"> - <p><i class="fa fa-reply"></i><a href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>{ notification.post.user.name }</a></p><a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a> - </div> - </virtual> - <virtual if={ notification.type == 'mention' }> - <a class="avatar-anchor" href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }> - <img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/> - </a> - <div class="text"> - <p><i class="fa fa-at"></i><a href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>{ notification.post.user.name }</a></p><a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a> - </div> - </virtual> - <virtual if={ notification.type == 'poll_vote' }> - <a class="avatar-anchor" href={ '/' + notification.user.username } data-user-preview={ notification.user.id }> - <img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/> - </a> - <div class="text"> - <p><i class="fa fa-pie-chart"></i><a href={ '/' + notification.user.username } data-user-preview={ notification.user.id }>{ notification.user.name }</a></p><a class="post-ref" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a> - </div> - </virtual> - </div> - <p class="date" if={ i != notifications.length - 1 && notification._date != notifications[i + 1]._date }><span><i class="fa fa-angle-up"></i>{ notification._datetext }</span><span><i class="fa fa-angle-down"></i>{ notifications[i + 1]._datetext }</span></p> - </virtual> - </div> - <button class="more { fetching: fetchingMoreNotifications }" if={ moreNotifications } onclick={ fetchMoreNotifications } disabled={ fetchingMoreNotifications }> - <i class="fa fa-spinner fa-pulse fa-fw" if={ fetchingMoreNotifications }></i>{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:desktop.tags.mk-notifications.more%' } - </button> - <p class="empty" if={ notifications.length == 0 && !loading }>ありません!</p> - <p class="loading" if={ loading }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:common.loading%<mk-ellipsis/></p> - <style> - :scope - display block - - > .notifications - > .notification - margin 0 - padding 16px - overflow-wrap break-word - font-size 0.9em - border-bottom solid 1px rgba(0, 0, 0, 0.05) - - &:last-child - border-bottom none - - > mk-time - display inline - position absolute - top 16px - right 12px - vertical-align top - color rgba(0, 0, 0, 0.6) - font-size small - - &:after - content "" - display block - clear both - - > .avatar-anchor - display block - float left - position -webkit-sticky - position sticky - top 16px - - > img - display block - min-width 36px - min-height 36px - max-width 36px - max-height 36px - border-radius 6px - - > .text - float right - width calc(100% - 36px) - padding-left 8px - - p - margin 0 - - i, mk-reaction-icon - margin-right 4px - - .post-preview - color rgba(0, 0, 0, 0.7) - - .post-ref - color rgba(0, 0, 0, 0.7) - - &:before, &:after - font-family FontAwesome - font-size 1em - font-weight normal - font-style normal - display inline-block - margin-right 3px - - &:before - content "\f10d" - - &:after - content "\f10e" - - &.repost, &.quote - .text p i - color #77B255 - - &.follow - .text p i - color #53c7ce - - &.reply, &.mention - .text p i - color #555 - - > .date - display block - margin 0 - line-height 32px - text-align center - font-size 0.8em - color #aaa - background #fdfdfd - border-bottom solid 1px rgba(0, 0, 0, 0.05) - - span - margin 0 16px - - i - margin-right 8px - - > .more - display block - width 100% - padding 16px - color #555 - border-top solid 1px rgba(0, 0, 0, 0.05) - - &:hover - background rgba(0, 0, 0, 0.025) - - &:active - background rgba(0, 0, 0, 0.05) - - &.fetching - cursor wait - - > i - margin-right 4px - - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > .loading - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - - </style> - <script> - import getPostSummary from '../../common/scripts/get-post-summary'; - this.getPostSummary = getPostSummary; - - this.mixin('i'); - this.mixin('api'); - this.mixin('stream'); - this.mixin('user-preview'); - - this.notifications = []; - this.loading = true; - - this.on('mount', () => { - const max = 10; - - this.api('i/notifications', { - limit: max + 1 - }).then(notifications => { - if (notifications.length == max + 1) { - this.moreNotifications = true; - notifications.pop(); - } - - this.update({ - loading: false, - notifications: notifications - }); - }); - - this.stream.on('notification', this.onNotification); - }); - - this.on('unmount', () => { - this.stream.off('notification', this.onNotification); - }); - - this.on('update', () => { - this.notifications.forEach(notification => { - const date = new Date(notification.created_at).getDate(); - const month = new Date(notification.created_at).getMonth() + 1; - notification._date = date; - notification._datetext = `${month}月 ${date}日`; - }); - }); - - this.onNotification = notification => { - this.notifications.unshift(notification); - this.update(); - }; - - this.fetchMoreNotifications = () => { - this.update({ - fetchingMoreNotifications: true - }); - - const max = 30; - - this.api('i/notifications', { - limit: max + 1, - max_id: this.notifications[this.notifications.length - 1].id - }).then(notifications => { - if (notifications.length == max + 1) { - this.moreNotifications = true; - notifications.pop(); - } else { - this.moreNotifications = false; - } - this.update({ - notifications: this.notifications.concat(notifications), - fetchingMoreNotifications: false - }); - }); - }; - </script> -</mk-notifications> diff --git a/src/web/app/desktop/tags/pages/entrance.tag b/src/web/app/desktop/tags/pages/entrance.tag deleted file mode 100644 index 7ad19c073e..0000000000 --- a/src/web/app/desktop/tags/pages/entrance.tag +++ /dev/null @@ -1,102 +0,0 @@ -<mk-entrance> - <main> - <img src="/assets/title.svg" alt="Misskey"/> - <mk-entrance-signin if={ mode == 'signin' }/> - <mk-entrance-signup if={ mode == 'signup' }/> - <div class="introduction" if={ mode == 'introduction' }> - <mk-introduction/> - <button onclick={ signin }>わかった</button> - </div> - </main> - <mk-forkit/> - <footer> - <mk-copyright/> - </footer> - <!-- ↓ https://github.com/riot/riot/issues/2134 (将来的)--> - <style data-disable-scope="data-disable-scope"> - #wait { - right: auto; - left: 15px; - } - </style> - <style> - :scope - display block - height 100% - - > main - display block - padding-bottom 16px - - > img - display block - width 160px - height 170px - margin 0 auto - pointer-events none - user-select none - - > .introduction - max-width 360px - margin 0 auto - color #777 - - > mk-introduction - padding 32px - background #fff - box-shadow 0 4px 16px rgba(0, 0, 0, 0.2) - - > button - display block - margin 16px auto 0 auto - color #666 - - &:hover - text-decoration underline - - > .tl - padding 32px 0 - background #fff - - > h2 - display block - margin 0 - padding 0 - text-align center - font-size 20px - color #5b6b73 - - > mk-public-timeline - max-width 500px - margin 0 auto - > footer - > mk-copyright - margin 0 - text-align center - line-height 64px - font-size 10px - color rgba(#000, 0.5) - - </style> - <script> - this.mode = 'signin'; - - this.signup = () => { - this.update({ - mode: 'signup' - }); - }; - - this.signin = () => { - this.update({ - mode: 'signin' - }); - }; - - this.introduction = () => { - this.update({ - mode: 'introduction' - }); - }; - </script> -</mk-entrance> diff --git a/src/web/app/desktop/tags/pages/entrance/signin.tag b/src/web/app/desktop/tags/pages/entrance/signin.tag deleted file mode 100644 index 6caa747c1c..0000000000 --- a/src/web/app/desktop/tags/pages/entrance/signin.tag +++ /dev/null @@ -1,134 +0,0 @@ -<mk-entrance-signin><a class="help" href={ CONFIG.aboutUrl + '/help' } title="お困りですか?"><i class="fa fa-question"></i></a> - <div class="form"> - <h1><img if={ user } src={ user.avatar_url + '?thumbnail&size=32' }/> - <p>{ user ? user.name : 'アカウント' }</p> - </h1> - <mk-signin ref="signin"/> - </div> - <div class="divider"><span>or</span></div> - <button class="signup" onclick={ parent.signup }>新規登録</button><a class="introduction" onclick={ introduction }>Misskeyについて</a> - <style> - :scope - display block - width 290px - margin 0 auto - text-align center - - &:hover - > .help - opacity 1 - - > .help - cursor pointer - display block - position absolute - top 0 - right 0 - z-index 1 - margin 0 - padding 0 - font-size 1.2em - color #999 - border none - outline none - background transparent - opacity 0 - transition opacity 0.1s ease - - &:hover - color #444 - - &:active - color #222 - - > i - padding 14px - - > .form - padding 10px 28px 16px 28px - background #fff - box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2) - - > h1 - display block - margin 0 - padding 0 - height 54px - line-height 54px - text-align center - text-transform uppercase - font-size 1em - font-weight bold - color rgba(0, 0, 0, 0.5) - border-bottom solid 1px rgba(0, 0, 0, 0.1) - - > p - display inline - margin 0 - padding 0 - - > img - display inline-block - top 10px - width 32px - height 32px - margin-right 8px - border-radius 100% - - &[src=''] - display none - - > .divider - padding 16px 0 - text-align center - - &:after - content "" - display block - position absolute - top 50% - width 100% - height 1px - border-top solid 1px rgba(0, 0, 0, 0.1) - - > * - z-index 1 - padding 0 8px - color rgba(0, 0, 0, 0.5) - background #fdfdfd - - > .signup - width 100% - line-height 56px - font-size 1em - color #fff - background $theme-color - border-radius 64px - - &:hover - background lighten($theme-color, 5%) - - &:active - background darken($theme-color, 5%) - - > .introduction - display inline-block - margin-top 16px - font-size 12px - color #666 - - </style> - <script> - this.on('mount', () => { - this.refs.signin.on('user', user => { - this.update({ - user: user - }); - }); - }); - - this.introduction = () => { - this.parent.introduction(); - }; - </script> -</mk-entrance-signin> diff --git a/src/web/app/desktop/tags/pages/entrance/signup.tag b/src/web/app/desktop/tags/pages/entrance/signup.tag deleted file mode 100644 index 0722d82a65..0000000000 --- a/src/web/app/desktop/tags/pages/entrance/signup.tag +++ /dev/null @@ -1,47 +0,0 @@ -<mk-entrance-signup> - <mk-signup/> - <button class="cancel" type="button" onclick={ parent.signin } title="キャンセル"><i class="fa fa-times"></i></button> - <style> - :scope - display block - width 368px - margin 0 auto - - &:hover - > .cancel - opacity 1 - - > mk-signup - padding 18px 32px 0 32px - background #fff - box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2) - - > .cancel - cursor pointer - display block - position absolute - top 0 - right 0 - z-index 1 - margin 0 - padding 0 - font-size 1.2em - color #999 - border none - outline none - box-shadow none - background transparent - opacity 0 - transition opacity 0.1s ease - - &:hover - color #555 - - &:active - color #222 - - > i - padding 14px - - </style> -</mk-entrance-signup> diff --git a/src/web/app/desktop/tags/pages/home.tag b/src/web/app/desktop/tags/pages/home.tag deleted file mode 100644 index 124a2eefa3..0000000000 --- a/src/web/app/desktop/tags/pages/home.tag +++ /dev/null @@ -1,50 +0,0 @@ -<mk-home-page> - <mk-ui ref="ui" page={ page }> - <mk-home ref="home" mode={ parent.opts.mode }/> - </mk-ui> - <style> - :scope - display block - </style> - <script> - import Progress from '../../../common/scripts/loading'; - import getPostSummary from '../../../common/scripts/get-post-summary'; - - this.mixin('i'); - this.mixin('api'); - this.mixin('stream'); - - this.unreadCount = 0; - - this.page = this.opts.mode || 'timeline'; - - this.on('mount', () => { - this.refs.ui.refs.home.on('loaded', () => { - Progress.done(); - }); - document.title = 'Misskey'; - Progress.start(); - this.stream.on('post', this.onStreamPost); - document.addEventListener('visibilitychange', this.windowOnVisibilitychange, false); - }); - - this.on('unmount', () => { - this.stream.off('post', this.onStreamPost); - document.removeEventListener('visibilitychange', this.windowOnVisibilitychange); - }); - - this.onStreamPost = post => { - if (document.hidden && post.user_id != this.I.id) { - this.unreadCount++; - document.title = `(${this.unreadCount}) ${getPostSummary(post)}`; - } - }; - - this.windowOnVisibilitychange = () => { - if (!document.hidden) { - this.unreadCount = 0; - document.title = 'Misskey'; - } - }; - </script> -</mk-home-page> diff --git a/src/web/app/desktop/tags/pages/not-found.tag b/src/web/app/desktop/tags/pages/not-found.tag deleted file mode 100644 index e62ea11008..0000000000 --- a/src/web/app/desktop/tags/pages/not-found.tag +++ /dev/null @@ -1,11 +0,0 @@ -<mk-not-found> - <mk-ui> - <main> - <h1>Not Found</h1> - </main> - </mk-ui> - <style> - :scope - display block - </style> -</mk-not-found> diff --git a/src/web/app/desktop/tags/pages/post.tag b/src/web/app/desktop/tags/pages/post.tag deleted file mode 100644 index c91e98bbd4..0000000000 --- a/src/web/app/desktop/tags/pages/post.tag +++ /dev/null @@ -1,35 +0,0 @@ -<mk-post-page> - <mk-ui ref="ui"> - <main> - <mk-post-detail ref="detail" post={ parent.post }/> - </main> - </mk-ui> - <style> - :scope - display block - - main - padding 16px - - > mk-post-detail - margin 0 auto - - </style> - <script> - import Progress from '../../../common/scripts/loading'; - - this.post = this.opts.post; - - this.on('mount', () => { - Progress.start(); - - this.refs.ui.refs.detail.on('post-fetched', () => { - Progress.set(0.5); - }); - - this.refs.ui.refs.detail.on('loaded', () => { - Progress.done(); - }); - }); - </script> -</mk-post-page> diff --git a/src/web/app/desktop/tags/pages/search.tag b/src/web/app/desktop/tags/pages/search.tag deleted file mode 100644 index 4f5867bdb9..0000000000 --- a/src/web/app/desktop/tags/pages/search.tag +++ /dev/null @@ -1,20 +0,0 @@ -<mk-search-page> - <mk-ui ref="ui"> - <mk-search ref="search" query={ parent.opts.query }/> - </mk-ui> - <style> - :scope - display block - </style> - <script> - import Progress from '../../../common/scripts/loading'; - - this.on('mount', () => { - Progress.start(); - - this.refs.ui.refs.search.on('loaded', () => { - Progress.done(); - }); - }); - </script> -</mk-search-page> diff --git a/src/web/app/desktop/tags/pages/user.tag b/src/web/app/desktop/tags/pages/user.tag deleted file mode 100644 index 864fe22735..0000000000 --- a/src/web/app/desktop/tags/pages/user.tag +++ /dev/null @@ -1,27 +0,0 @@ -<mk-user-page> - <mk-ui ref="ui"> - <mk-user ref="user" user={ parent.user } page={ parent.opts.page }/> - </mk-ui> - <style> - :scope - display block - </style> - <script> - import Progress from '../../../common/scripts/loading'; - - this.user = this.opts.user; - - this.on('mount', () => { - Progress.start(); - - this.refs.ui.refs.user.on('user-fetched', user => { - Progress.set(0.5); - document.title = user.name + ' | Misskey' - }); - - this.refs.ui.refs.user.on('loaded', () => { - Progress.done(); - }); - }); - </script> -</mk-user-page> diff --git a/src/web/app/desktop/tags/post-detail-sub.tag b/src/web/app/desktop/tags/post-detail-sub.tag deleted file mode 100644 index 8a0ada5f2a..0000000000 --- a/src/web/app/desktop/tags/post-detail-sub.tag +++ /dev/null @@ -1,156 +0,0 @@ -<mk-post-detail-sub title={ title }> - <a class="avatar-anchor" href={ '/' + post.user.username }> - <img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=64' } alt="avatar" data-user-preview={ post.user_id }/> - </a> - <div class="main"> - <header> - <div class="left"> - <a class="name" href={ '/' + post.user.username } data-user-preview={ post.user_id }>{ post.user.name }</a> - <span class="username">@{ post.user.username }</span> - </div> - <div class="right"> - <a class="time" href={ '/' + this.post.user.username + '/' + this.post.id }> - <mk-time time={ post.created_at }/> - </a> - </div> - </header> - <div class="body"> - <div class="text" ref="text"></div> - <div class="media" if={ post.media }> - <virtual each={ file in post.media }> - <img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/> - </virtual> - </div> - </div> - </div> - <style> - :scope - display block - margin 0 - padding 20px 32px - background #fdfdfd - - &:after - content "" - display block - clear both - - &:hover - > .main > footer > button - color #888 - - > .avatar-anchor - display block - float left - margin 0 16px 0 0 - - > .avatar - display block - width 44px - height 44px - margin 0 - border-radius 4px - vertical-align bottom - - > .main - float left - width calc(100% - 60px) - - > header - margin-bottom 4px - white-space nowrap - - &:after - content "" - display block - clear both - - > .left - float left - - > .name - display inline - margin 0 - padding 0 - color #777 - font-size 1em - font-weight 700 - text-align left - text-decoration none - - &:hover - text-decoration underline - - > .username - text-align left - margin 0 0 0 8px - color #ccc - - > .right - float right - - > .time - font-size 0.9em - color #c0c0c0 - - > .body - - > .text - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 1em - color #717171 - - > mk-url-preview - margin-top 8px - - > .media - > img - display block - max-width 100% - - </style> - <script> - import compile from '../../common/scripts/text-compiler'; - import dateStringify from '../../common/scripts/date-stringify'; - - this.mixin('api'); - this.mixin('user-preview'); - - this.post = this.opts.post; - this.title = dateStringify(this.post.created_at); - - this.on('mount', () => { - if (this.post.text) { - const tokens = this.post.ast; - - this.refs.text.innerHTML = compile(tokens); - - this.refs.text.children.forEach(e => { - if (e.tagName == 'MK-URL') riot.mount(e); - }); - } - }); - - this.like = () => { - if (this.post.is_liked) { - this.api('posts/likes/delete', { - post_id: this.post.id - }).then(() => { - this.post.is_liked = false; - this.update(); - }); - } else { - this.api('posts/likes/create', { - post_id: this.post.id - }).then(() => { - this.post.is_liked = true; - this.update(); - }); - } - }; - </script> -</mk-post-detail-sub> diff --git a/src/web/app/desktop/tags/post-detail.tag b/src/web/app/desktop/tags/post-detail.tag deleted file mode 100644 index b162a4084a..0000000000 --- a/src/web/app/desktop/tags/post-detail.tag +++ /dev/null @@ -1,352 +0,0 @@ -<mk-post-detail title={ title }> - <div class="fetching" if={ fetching }> - <mk-ellipsis-icon/> - </div> - <div class="main" if={ !fetching }> - <button class="read-more" if={ p.reply_to && p.reply_to.reply_to_id && context == null } title="会話をもっと読み込む" onclick={ loadContext } disabled={ contextFetching }> - <i class="fa fa-ellipsis-v" if={ !contextFetching }></i> - <i class="fa fa-spinner fa-pulse" if={ contextFetching }></i> - </button> - <div class="context"> - <virtual each={ post in context }> - <mk-post-detail-sub post={ post }/> - </virtual> - </div> - <div class="reply-to" if={ p.reply_to }> - <mk-post-detail-sub post={ p.reply_to }/> - </div> - <div class="repost" if={ isRepost }> - <p> - <a class="avatar-anchor" href={ '/' + post.user.username } data-user-preview={ post.user_id }> - <img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=32' } alt="avatar"/> - </a> - <i class="fa fa-retweet"></i><a class="name" href={ '/' + post.user.username }> - { post.user.name } - </a> - がRepost - </p> - </div> - <article> - <a class="avatar-anchor" href={ '/' + p.user.username }> - <img class="avatar" src={ p.user.avatar_url + '?thumbnail&size=64' } alt="avatar" data-user-preview={ p.user.id }/> - </a> - <header> - <a class="name" href={ '/' + p.user.username } data-user-preview={ p.user.id }>{ p.user.name }</a> - <span class="username">@{ p.user.username }</span> - <a class="time" href={ url }> - <mk-time time={ p.created_at }/> - </a> - </header> - <div class="body"> - <div class="text" ref="text"></div> - <div class="media" if={ p.media }> - <virtual each={ file in p.media }><img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/></virtual> - </div> - <mk-poll if={ p.poll } post={ p }/> - </div> - <footer> - <mk-reactions-viewer post={ p }/> - <button onclick={ reply } title="返信"><i class="fa fa-reply"></i> - <p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p> - </button> - <button onclick={ repost } title="Repost"><i class="fa fa-retweet"></i> - <p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p> - </button> - <button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton" title="リアクション"><i class="fa fa-plus"></i> - <p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p> - </button> - <button><i class="fa fa-ellipsis-h"></i></button> - </footer> - </article> - <div class="replies"> - <virtual each={ post in replies }> - <mk-post-detail-sub post={ post }/> - </virtual> - </div> - </div> - <style> - :scope - display block - margin 0 - padding 0 - width 640px - overflow hidden - background #fff - border solid 1px rgba(0, 0, 0, 0.1) - border-radius 8px - - > .fetching - padding 64px 0 - - > .main - - > .read-more - display block - margin 0 - padding 10px 0 - width 100% - font-size 1em - text-align center - color #999 - cursor pointer - background #fafafa - outline none - border none - border-bottom solid 1px #eef0f2 - border-radius 6px 6px 0 0 - - &:hover - background #f6f6f6 - - &:active - background #f0f0f0 - - &:disabled - color #ccc - - > .context - > * - border-bottom 1px solid #eef0f2 - - > .repost - color #9dbb00 - background linear-gradient(to bottom, #edfde2 0%, #fff 100%) - - > p - margin 0 - padding 16px 32px - - .avatar-anchor - display inline-block - - .avatar - vertical-align bottom - min-width 28px - min-height 28px - max-width 28px - max-height 28px - margin 0 8px 0 0 - border-radius 6px - - i - margin-right 4px - - .name - font-weight bold - - & + article - padding-top 8px - - > .reply-to - border-bottom 1px solid #eef0f2 - - > article - padding 28px 32px 18px 32px - - &:after - content "" - display block - clear both - - &:hover - > .main > footer > button - color #888 - - > .avatar-anchor - display block - width 60px - height 60px - - > .avatar - display block - width 60px - height 60px - margin 0 - border-radius 8px - vertical-align bottom - - > header - position absolute - top 28px - left 108px - width calc(100% - 108px) - - > .name - display inline-block - margin 0 - line-height 24px - color #777 - font-size 18px - font-weight 700 - text-align left - text-decoration none - - &:hover - text-decoration underline - - > .username - display block - text-align left - margin 0 - color #ccc - - > .time - position absolute - top 0 - right 32px - font-size 1em - color #c0c0c0 - - > .body - padding 8px 0 - - > .text - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 1.5em - color #717171 - - .link - &:after - content "\f14c" - display inline-block - padding-left 2px - font-family FontAwesome - font-size .9em - font-weight 400 - font-style normal - - > mk-url-preview - margin-top 8px - - > .media - > img - display block - max-width 100% - - > footer - font-size 1.2em - - > button - margin 0 28px 0 0 - padding 8px - background transparent - border none - font-size 1em - color #ddd - cursor pointer - - &:hover - color #666 - - > .count - display inline - margin 0 0 0 8px - color #999 - - &.reacted - color $theme-color - - > .replies - > * - border-top 1px solid #eef0f2 - - </style> - <script> - import compile from '../../common/scripts/text-compiler'; - import dateStringify from '../../common/scripts/date-stringify'; - - this.mixin('api'); - this.mixin('user-preview'); - - this.fetching = true; - this.contextFetching = false; - this.context = null; - this.post = null; - - this.on('mount', () => { - this.api('posts/show', { - post_id: this.opts.post - }).then(post => { - const isRepost = post.repost != null; - const p = isRepost ? post.repost : post; - p.reactions_count = p.reaction_counts ? Object.keys(p.reaction_counts).map(key => p.reaction_counts[key]).reduce((a, b) => a + b) : 0; - - this.update({ - fetching: false, - post: post, - isRepost: isRepost, - p: p, - title: dateStringify(p.created_at) - }); - - this.trigger('loaded'); - - if (this.p.text) { - const tokens = this.p.ast; - - this.refs.text.innerHTML = compile(tokens); - - this.refs.text.children.forEach(e => { - if (e.tagName == 'MK-URL') riot.mount(e); - }); - - // URLをプレビュー - tokens - .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) - .map(t => { - riot.mount(this.refs.text.appendChild(document.createElement('mk-url-preview')), { - url: t.url - }); - }); - } - - // Get replies - this.api('posts/replies', { - post_id: this.p.id, - limit: 8 - }).then(replies => { - this.update({ - replies: replies - }); - }); - }); - }); - - this.reply = () => { - riot.mount(document.body.appendChild(document.createElement('mk-post-form-window')), { - reply: this.p - }); - }; - - this.repost = () => { - riot.mount(document.body.appendChild(document.createElement('mk-repost-form-window')), { - post: this.p - }); - }; - - this.react = () => { - riot.mount(document.body.appendChild(document.createElement('mk-reaction-picker')), { - source: this.refs.reactButton, - post: this.p - }); - }; - - this.loadContext = () => { - this.contextFetching = true; - - // Fetch context - this.api('posts/context', { - post_id: this.p.reply_to_id - }).then(context => { - this.update({ - contextFetching: false, - context: context.reverse() - }); - }); - }; - </script> -</mk-post-detail> diff --git a/src/web/app/desktop/tags/post-form-window.tag b/src/web/app/desktop/tags/post-form-window.tag deleted file mode 100644 index 05a09b7803..0000000000 --- a/src/web/app/desktop/tags/post-form-window.tag +++ /dev/null @@ -1,68 +0,0 @@ -<mk-post-form-window> - <mk-window ref="window" is-modal={ true }> - <yield to="header"> - <span if={ !parent.opts.reply }>%i18n:desktop.tags.mk-post-form-window.post%</span> - <span if={ parent.opts.reply }>%i18n:desktop.tags.mk-post-form-window.reply%</span> - <span class="files" if={ parent.files.length != 0 }>{ '%i18n:desktop.tags.mk-post-form-window.attaches%'.replace('{}', parent.files.length) }</span> - <span class="uploading-files" if={ parent.uploadingFiles.length != 0 }>{ '%i18n:desktop.tags.mk-post-form-window.uploading-media%'.replace('{}', parent.uploadingFiles.length) }<mk-ellipsis/></span> - </yield> - <yield to="content"> - <div class="ref" if={ parent.opts.reply }> - <mk-post-preview post={ parent.opts.reply }/> - </div> - <div class="body"> - <mk-post-form ref="form" reply={ parent.opts.reply }/> - </div> - </yield> - </mk-window> - <style> - :scope - > mk-window - - [data-yield='header'] - > .files - > .uploading-files - margin-left 8px - opacity 0.8 - - &:before - content '(' - - &:after - content ')' - - [data-yield='content'] - > .ref - > mk-post-preview - margin 16px 22px - - </style> - <script> - this.uploadingFiles = []; - this.files = []; - - this.on('mount', () => { - this.refs.window.refs.form.focus(); - - this.refs.window.on('closed', () => { - this.unmount(); - }); - - this.refs.window.refs.form.on('post', () => { - this.refs.window.close(); - }); - - this.refs.window.refs.form.on('change-uploading-files', files => { - this.update({ - uploadingFiles: files || [] - }); - }); - - this.refs.window.refs.form.on('change-files', files => { - this.update({ - files: files || [] - }); - }); - }); - </script> -</mk-post-form-window> diff --git a/src/web/app/desktop/tags/post-form.tag b/src/web/app/desktop/tags/post-form.tag deleted file mode 100644 index 6a363d67cd..0000000000 --- a/src/web/app/desktop/tags/post-form.tag +++ /dev/null @@ -1,534 +0,0 @@ -<mk-post-form ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop }> - <div class="content"> - <textarea class={ with: (files.length != 0 || poll) } ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder={ placeholder }></textarea> - <div class="medias { with: poll }" if={ files.length != 0 }> - <ul> - <li each={ files }> - <div class="img" style="background-image: url({ url + '?thumbnail&size=64' })" title={ name }></div> - <img class="remove" onclick={ removeFile } src="/assets/desktop/remove.png" title="%i18n:desktop.tags.mk-post-form.attach-cancel%" alt=""/> - </li> - <li class="add" if={ files.length < 4 } title="%i18n:desktop.tags.mk-post-form.attach-media-from-local%" onclick={ selectFile }><i class="fa fa-plus"></i></li> - </ul> - <p class="remain">{ 4 - files.length }/4</p> - </div> - <mk-poll-editor if={ poll } ref="poll" ondestroy={ onPollDestroyed }/> - </div> - <mk-uploader ref="uploader"/> - <button ref="upload" title="%i18n:desktop.tags.mk-post-form.attach-media-from-local%" onclick={ selectFile }><i class="fa fa-upload"></i></button> - <button ref="drive" title="%i18n:desktop.tags.mk-post-form.attach-media-from-drive%" onclick={ selectFileFromDrive }><i class="fa fa-cloud"></i></button> - <button class="kao" title="%i18n:desktop.tags.mk-post-form.insert-a-kao%" onclick={ kao }><i class="fa fa-smile-o"></i></button> - <button class="poll" title="%i18n:desktop.tags.mk-post-form.create-poll%" onclick={ addPoll }><i class="fa fa-pie-chart"></i></button> - <p class="text-count { over: refs.text.value.length > 1000 }">{ '%i18n:desktop.tags.mk-post-form.text-remain%'.replace('{}', 1000 - refs.text.value.length) }</p> - <button class={ wait: wait } ref="submit" disabled={ wait || (refs.text.value.length == 0 && files.length == 0 && !poll && !repost) } onclick={ post }> - { wait ? '%i18n:desktop.tags.mk-post-form.posting%' : submitText }<mk-ellipsis if={ wait }/> - </button> - <input ref="file" type="file" accept="image/*" multiple="multiple" tabindex="-1" onchange={ changeFile }/> - <div class="dropzone" if={ draghover }></div> - <style> - :scope - display block - padding 16px - background lighten($theme-color, 95%) - - &:after - content "" - display block - clear both - - > .content - - [ref='text'] - display block - padding 12px - margin 0 - width 100% - max-width 100% - min-width 100% - min-height calc(16px + 12px + 12px) - font-size 16px - color #333 - background #fff - outline none - border solid 1px rgba($theme-color, 0.1) - border-radius 4px - transition border-color .3s ease - - &:hover - border-color rgba($theme-color, 0.2) - transition border-color .1s ease - - & + * - & + * + * - border-color rgba($theme-color, 0.2) - transition border-color .1s ease - - &:focus - color $theme-color - border-color rgba($theme-color, 0.5) - transition border-color 0s ease - - & + * - & + * + * - border-color rgba($theme-color, 0.5) - transition border-color 0s ease - - &:disabled - opacity 0.5 - - &::-webkit-input-placeholder - color rgba($theme-color, 0.3) - - &.with - border-bottom solid 1px rgba($theme-color, 0.1) !important - border-radius 4px 4px 0 0 - - > .medias - margin 0 - padding 0 - background lighten($theme-color, 98%) - border solid 1px rgba($theme-color, 0.1) - border-top none - border-radius 0 0 4px 4px - transition border-color .3s ease - - &.with - border-bottom solid 1px rgba($theme-color, 0.1) !important - border-radius 0 - - > .remain - display block - position absolute - top 8px - right 8px - margin 0 - padding 0 - color rgba($theme-color, 0.4) - - > ul - display block - margin 0 - padding 4px - list-style none - - &:after - content "" - display block - clear both - - > li - display block - float left - margin 4px - padding 0 - cursor move - - &:hover > .remove - display block - - > .img - width 64px - height 64px - background-size cover - background-position center center - - > .remove - display none - position absolute - top -6px - right -6px - width 16px - height 16px - cursor pointer - - > .add - display block - float left - margin 4px - padding 0 - border dashed 2px rgba($theme-color, 0.2) - cursor pointer - - &:hover - border-color rgba($theme-color, 0.3) - - > i - color rgba($theme-color, 0.4) - - > i - display block - width 60px - height 60px - line-height 60px - text-align center - font-size 1.2em - color rgba($theme-color, 0.2) - - > mk-poll-editor - background lighten($theme-color, 98%) - border solid 1px rgba($theme-color, 0.1) - border-top none - border-radius 0 0 4px 4px - transition border-color .3s ease - - > mk-uploader - margin 8px 0 0 0 - padding 8px - border solid 1px rgba($theme-color, 0.2) - border-radius 4px - - [ref='file'] - display none - - .text-count - pointer-events none - display block - position absolute - bottom 16px - right 138px - margin 0 - line-height 40px - color rgba($theme-color, 0.5) - - &.over - color #ec3828 - - [ref='submit'] - display block - position absolute - bottom 16px - right 16px - cursor pointer - padding 0 - margin 0 - width 110px - height 40px - font-size 1em - color $theme-color-foreground - background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) - outline none - border solid 1px lighten($theme-color, 15%) - border-radius 4px - - &:not(:disabled) - font-weight bold - - &:hover:not(:disabled) - background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) - border-color $theme-color - - &:active:not(:disabled) - background $theme-color - border-color $theme-color - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - &:disabled - opacity 0.7 - cursor default - - &.wait - background linear-gradient( - 45deg, - darken($theme-color, 10%) 25%, - $theme-color 25%, - $theme-color 50%, - darken($theme-color, 10%) 50%, - darken($theme-color, 10%) 75%, - $theme-color 75%, - $theme-color - ) - background-size 32px 32px - animation stripe-bg 1.5s linear infinite - opacity 0.7 - cursor wait - - @keyframes stripe-bg - from {background-position: 0 0;} - to {background-position: -64px 32px;} - - [ref='upload'] - [ref='drive'] - .kao - .poll - display inline-block - cursor pointer - padding 0 - margin 8px 4px 0 0 - width 40px - height 40px - font-size 1em - color rgba($theme-color, 0.5) - background transparent - outline none - border solid 1px transparent - border-radius 4px - - &:hover - background transparent - border-color rgba($theme-color, 0.3) - - &:active - color rgba($theme-color, 0.6) - background linear-gradient(to bottom, lighten($theme-color, 80%) 0%, lighten($theme-color, 90%) 100%) - border-color rgba($theme-color, 0.5) - box-shadow 0 2px 4px rgba(0, 0, 0, 0.15) inset - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - > .dropzone - position absolute - left 0 - top 0 - width 100% - height 100% - border dashed 2px rgba($theme-color, 0.5) - pointer-events none - - </style> - <script> - import getKao from '../../common/scripts/get-kao'; - import notify from '../scripts/notify'; - import Autocomplete from '../scripts/autocomplete'; - - this.mixin('api'); - - this.wait = false; - this.uploadings = []; - this.files = []; - this.autocomplete = null; - this.poll = false; - - this.inReplyToPost = this.opts.reply; - - this.repost = this.opts.repost; - - this.placeholder = this.repost - ? '%i18n:desktop.tags.mk-post-form.quote-placeholder%' - : this.inReplyToPost - ? '%i18n:desktop.tags.mk-post-form.reply-placeholder%' - : '%i18n:desktop.tags.mk-post-form.post-placeholder%'; - - this.submitText = this.repost - ? '%i18n:desktop.tags.mk-post-form.repost%' - : this.inReplyToPost - ? '%i18n:desktop.tags.mk-post-form.reply%' - : '%i18n:desktop.tags.mk-post-form.post%'; - - this.draftId = this.repost - ? 'repost:' + this.repost.id - : this.inReplyToPost - ? 'reply:' + this.inReplyToPost.id - : 'post'; - - this.on('mount', () => { - this.refs.uploader.on('uploaded', file => { - this.addFile(file); - }); - - this.refs.uploader.on('change-uploads', uploads => { - this.trigger('change-uploading-files', uploads); - }); - - this.autocomplete = new Autocomplete(this.refs.text); - this.autocomplete.attach(); - - // 書きかけの投稿を復元 - const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftId]; - if (draft) { - this.refs.text.value = draft.data.text; - this.files = draft.data.files; - if (draft.data.poll) { - this.poll = true; - this.update(); - this.refs.poll.set(draft.data.poll); - } - this.trigger('change-files', this.files); - this.update(); - } - }); - - this.on('unmount', () => { - this.autocomplete.detach(); - }); - - this.focus = () => { - this.refs.text.focus(); - }; - - this.clear = () => { - this.refs.text.value = ''; - this.files = []; - this.poll = false; - this.trigger('change-files'); - this.update(); - }; - - this.ondragover = e => { - e.preventDefault(); - e.stopPropagation(); - this.draghover = true; - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; - }; - - this.ondragenter = e => { - this.draghover = true; - }; - - this.ondragleave = e => { - this.draghover = false; - }; - - this.ondrop = e => { - e.preventDefault(); - e.stopPropagation(); - this.draghover = false; - - // ファイルだったら - if (e.dataTransfer.files.length > 0) { - e.dataTransfer.files.forEach(this.upload); - } - }; - - this.onkeydown = e => { - if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post(); - }; - - this.onpaste = e => { - e.clipboardData.items.forEach(item => { - if (item.kind == 'file') { - this.upload(item.getAsFile()); - } - }); - }; - - this.selectFile = () => { - this.refs.file.click(); - }; - - this.selectFileFromDrive = () => { - const i = riot.mount(document.body.appendChild(document.createElement('mk-select-file-from-drive-window')), { - multiple: true - })[0]; - i.one('selected', files => { - files.forEach(this.addFile); - }); - }; - - this.changeFile = () => { - this.refs.file.files.forEach(this.upload); - }; - - this.upload = file => { - this.refs.uploader.upload(file); - }; - - this.addFile = file => { - this.files.push(file); - this.trigger('change-files', this.files); - this.update(); - }; - - this.removeFile = e => { - const file = e.item; - this.files = this.files.filter(x => x.id != file.id); - this.trigger('change-files', this.files); - this.update(); - }; - - this.addPoll = () => { - this.poll = true; - }; - - this.onPollDestroyed = () => { - this.update({ - poll: false - }); - }; - - this.post = e => { - this.wait = true; - - const files = this.files && this.files.length > 0 - ? this.files.map(f => f.id) - : undefined; - - this.api('posts/create', { - text: this.refs.text.value == '' ? undefined : this.refs.text.value, - media_ids: files, - reply_to_id: this.inReplyToPost ? this.inReplyToPost.id : undefined, - repost_id: this.repost ? this.repost.id : undefined, - poll: this.poll ? this.refs.poll.get() : undefined - }).then(data => { - this.clear(); - this.removeDraft(); - this.trigger('post'); - notify(this.repost - ? '%i18n:desktop.tags.mk-post-form.reposted%' - : this.inReplyToPost - ? '%i18n:desktop.tags.mk-post-form.replied%' - : '%i18n:desktop.tags.mk-post-form.posted%'); - }).catch(err => { - notify(this.repost - ? '%i18n:desktop.tags.mk-post-form.repost-failed%' - : this.inReplyToPost - ? '%i18n:desktop.tags.mk-post-form.reply-failed%' - : '%i18n:desktop.tags.mk-post-form.post-failed%'); - }).then(() => { - this.update({ - wait: false - }); - }); - }; - - this.kao = () => { - this.refs.text.value += getKao(); - }; - - this.on('update', () => { - this.saveDraft(); - }); - - this.saveDraft = () => { - const data = JSON.parse(localStorage.getItem('drafts') || '{}'); - - data[this.draftId] = { - updated_at: new Date(), - data: { - text: this.refs.text.value, - files: this.files, - poll: this.poll && this.refs.poll ? this.refs.poll.get() : undefined - } - } - - localStorage.setItem('drafts', JSON.stringify(data)); - }; - - this.removeDraft = () => { - const data = JSON.parse(localStorage.getItem('drafts') || '{}'); - - delete data[this.draftId]; - - localStorage.setItem('drafts', JSON.stringify(data)); - }; - </script> -</mk-post-form> diff --git a/src/web/app/desktop/tags/post-preview.tag b/src/web/app/desktop/tags/post-preview.tag deleted file mode 100644 index 9a7db5ffa3..0000000000 --- a/src/web/app/desktop/tags/post-preview.tag +++ /dev/null @@ -1,94 +0,0 @@ -<mk-post-preview title={ title }> - <article><a class="avatar-anchor" href={ '/' + post.user.username }><img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=64' } alt="avatar" data-user-preview={ post.user_id }/></a> - <div class="main"> - <header><a class="name" href={ '/' + post.user.username } data-user-preview={ post.user_id }>{ post.user.name }</a><span class="username">@{ post.user.username }</span><a class="time" href={ '/' + post.user.username + '/' + post.id }> - <mk-time time={ post.created_at }/></a></header> - <div class="body"> - <mk-sub-post-content class="text" post={ post }/> - </div> - </div> - </article> - <style> - :scope - display block - margin 0 - padding 0 - font-size 0.9em - background #fff - - > article - - &:after - content "" - display block - clear both - - &:hover - > .main > footer > button - color #888 - - > .avatar-anchor - display block - float left - margin 0 16px 0 0 - - > .avatar - display block - width 52px - height 52px - margin 0 - border-radius 8px - vertical-align bottom - - > .main - float left - width calc(100% - 68px) - - > header - display flex - margin 4px 0 - white-space nowrap - - > .name - margin 0 .5em 0 0 - padding 0 - color #607073 - font-size 1em - line-height 1.1em - font-weight 700 - text-align left - text-decoration none - white-space normal - - &:hover - text-decoration underline - - > .username - text-align left - margin 0 .5em 0 0 - color #d1d8da - - > .time - margin-left auto - color #b2b8bb - - > .body - - > .text - cursor default - margin 0 - padding 0 - font-size 1.1em - color #717171 - - </style> - <script> - import dateStringify from '../../common/scripts/date-stringify'; - - this.mixin('user-preview'); - - this.post = this.opts.post; - - this.title = dateStringify(this.post.created_at); - </script> -</mk-post-preview> diff --git a/src/web/app/desktop/tags/progress-dialog.tag b/src/web/app/desktop/tags/progress-dialog.tag deleted file mode 100644 index a0ac51b2f4..0000000000 --- a/src/web/app/desktop/tags/progress-dialog.tag +++ /dev/null @@ -1,97 +0,0 @@ -<mk-progress-dialog> - <mk-window ref="window" is-modal={ false } can-close={ false } width={ '500px' }> - <yield to="header">{ parent.title }<mk-ellipsis/></yield> - <yield to="content"> - <div class="body"> - <p class="init" if={ isNaN(parent.value) }>待機中<mk-ellipsis/></p> - <p class="percentage" if={ !isNaN(parent.value) }>{ Math.floor((parent.value / parent.max) * 100) }</p> - <progress if={ !isNaN(parent.value) && parent.value < parent.max } value={ isNaN(parent.value) ? 0 : parent.value } max={ parent.max }></progress> - <div class="progress waiting" if={ parent.value >= parent.max }></div> - </div> - </yield> - </mk-window> - <style> - :scope - display block - - > mk-window - [data-yield='content'] - - > .body - padding 18px 24px 24px 24px - - > .init - display block - margin 0 - text-align center - color rgba(#000, 0.7) - - > .percentage - display block - margin 0 0 4px 0 - text-align center - line-height 16px - color rgba($theme-color, 0.7) - - &:after - content '%' - - > progress - > .progress - display block - margin 0 - width 100% - height 10px - background transparent - border none - border-radius 4px - overflow hidden - - &::-webkit-progress-value - background $theme-color - - &::-webkit-progress-bar - background rgba($theme-color, 0.1) - - > .progress - background linear-gradient( - 45deg, - lighten($theme-color, 30%) 25%, - $theme-color 25%, - $theme-color 50%, - lighten($theme-color, 30%) 50%, - lighten($theme-color, 30%) 75%, - $theme-color 75%, - $theme-color - ) - background-size 32px 32px - animation progress-dialog-tag-progress-waiting 1.5s linear infinite - - @keyframes progress-dialog-tag-progress-waiting - from {background-position: 0 0;} - to {background-position: -64px 32px;} - - </style> - <script> - this.title = this.opts.title; - this.value = parseInt(this.opts.value, 10); - this.max = parseInt(this.opts.max, 10); - - this.on('mount', () => { - this.refs.window.on('closed', () => { - this.unmount(); - }); - }); - - this.updateProgress = (value, max) => { - this.update({ - value: parseInt(value, 10), - max: parseInt(max, 10) - }); - }; - - this.close = () => { - this.refs.window.close(); - }; - </script> -</mk-progress-dialog> diff --git a/src/web/app/desktop/tags/repost-form-window.tag b/src/web/app/desktop/tags/repost-form-window.tag deleted file mode 100644 index 58ececf0ec..0000000000 --- a/src/web/app/desktop/tags/repost-form-window.tag +++ /dev/null @@ -1,47 +0,0 @@ -<mk-repost-form-window> - <mk-window ref="window" is-modal={ true }> - <yield to="header"> - <i class="fa fa-retweet"></i>%i18n:desktop.tags.mk-repost-form-window.title% - </yield> - <yield to="content"> - <mk-repost-form ref="form" post={ parent.opts.post }/> - </yield> - </mk-window> - <style> - :scope - > mk-window - [data-yield='header'] - > i - margin-right 4px - - </style> - <script> - this.onDocumentKeydown = e => { - if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { - if (e.which == 27) { // Esc - this.refs.window.close(); - } - } - }; - - this.on('mount', () => { - this.refs.window.refs.form.on('cancel', () => { - this.refs.window.close(); - }); - - this.refs.window.refs.form.on('posted', () => { - this.refs.window.close(); - }); - - document.addEventListener('keydown', this.onDocumentKeydown); - - this.refs.window.on('closed', () => { - this.unmount(); - }); - }); - - this.on('unmount', () => { - document.removeEventListener('keydown', this.onDocumentKeydown); - }); - </script> -</mk-repost-form-window> diff --git a/src/web/app/desktop/tags/repost-form.tag b/src/web/app/desktop/tags/repost-form.tag deleted file mode 100644 index c3cf6c1fb3..0000000000 --- a/src/web/app/desktop/tags/repost-form.tag +++ /dev/null @@ -1,127 +0,0 @@ -<mk-repost-form> - <mk-post-preview post={ opts.post }/> - <virtual if={ !quote }> - <footer> - <a class="quote" if={ !quote } onclick={ onquote }>%i18n:desktop.tags.mk-repost-form.quote%</a> - <button class="cancel" onclick={ cancel }>%i18n:desktop.tags.mk-repost-form.cancel%</button> - <button class="ok" onclick={ ok } disabled={ wait }>{ wait ? '%i18n:desktop.tags.mk-repost-form.reposting%' : '%i18n:desktop.tags.mk-repost-form.repost%' }</button> - </footer> - </virtual> - <virtual if={ quote }> - <mk-post-form ref="form" repost={ opts.post }/> - </virtual> - <style> - :scope - - > mk-post-preview - margin 16px 22px - - > div - padding 16px - - > footer - height 72px - background lighten($theme-color, 95%) - - > .quote - position absolute - bottom 16px - left 28px - line-height 40px - - button - display block - position absolute - bottom 16px - cursor pointer - padding 0 - margin 0 - width 120px - height 40px - font-size 1em - outline none - border-radius 4px - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - > .cancel - right 148px - color #888 - background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) - border solid 1px #e2e2e2 - - &:hover - background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) - border-color #dcdcdc - - &:active - background #ececec - border-color #dcdcdc - - > .ok - right 16px - font-weight bold - color $theme-color-foreground - background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) - border solid 1px lighten($theme-color, 15%) - - &:hover - background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) - border-color $theme-color - - &:active - background $theme-color - border-color $theme-color - - </style> - <script> - import notify from '../scripts/notify'; - - this.mixin('api'); - - this.wait = false; - this.quote = false; - - this.cancel = () => { - this.trigger('cancel'); - }; - - this.ok = () => { - this.wait = true; - this.api('posts/create', { - repost_id: this.opts.post.id - }).then(data => { - this.trigger('posted'); - notify('%i18n:desktop.tags.mk-repost-form.success%'); - }).catch(err => { - notify('%i18n:desktop.tags.mk-repost-form.failure%'); - }).then(() => { - this.update({ - wait: false - }); - }); - }; - - this.onquote = () => { - this.update({ - quote: true - }); - - this.refs.form.on('post', () => { - this.trigger('posted'); - }); - - this.refs.form.focus(); - }; - </script> -</mk-repost-form> diff --git a/src/web/app/desktop/tags/search-posts.tag b/src/web/app/desktop/tags/search-posts.tag deleted file mode 100644 index 4025f87332..0000000000 --- a/src/web/app/desktop/tags/search-posts.tag +++ /dev/null @@ -1,90 +0,0 @@ -<mk-search-posts> - <div class="loading" if={ isLoading }> - <mk-ellipsis-icon/> - </div> - <p class="empty" if={ isEmpty }><i class="fa fa-search"></i>「{ query }」に関する投稿は見つかりませんでした。</p> - <mk-timeline ref="timeline"><yield to="footer"><i class="fa fa-moon-o" if={ !parent.moreLoading }></i><i class="fa fa-spinner fa-pulse fa-fw" if={ parent.moreLoading }></i></yield/> - <style> - :scope - display block - background #fff - - > .loading - padding 64px 0 - - > .empty - display block - margin 0 auto - padding 32px - max-width 400px - text-align center - color #999 - - > i - display block - margin-bottom 16px - font-size 3em - color #ccc - - </style> - <script> - this.mixin('api'); - - this.query = this.opts.query; - this.isLoading = true; - this.isEmpty = false; - this.moreLoading = false; - this.page = 0; - - this.on('mount', () => { - document.addEventListener('keydown', this.onDocumentKeydown); - window.addEventListener('scroll', this.onScroll); - - this.api('posts/search', { - query: this.query - }).then(posts => { - this.update({ - isLoading: false, - isEmpty: posts.length == 0 - }); - this.refs.timeline.setPosts(posts); - this.trigger('loaded'); - }); - }); - - this.on('unmount', () => { - document.removeEventListener('keydown', this.onDocumentKeydown); - window.removeEventListener('scroll', this.onScroll); - }); - - this.onDocumentKeydown = e => { - if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { - if (e.which == 84) { // t - this.refs.timeline.focus(); - } - } - }; - - this.more = () => { - if (this.moreLoading || this.isLoading || this.timeline.posts.length == 0) return; - this.update({ - moreLoading: true - }); - this.api('posts/search', { - query: this.query, - page: this.page + 1 - }).then(posts => { - this.update({ - moreLoading: false, - page: page + 1 - }); - this.refs.timeline.prependPosts(posts); - }); - }; - - this.onScroll = () => { - const current = window.scrollY + window.innerHeight; - if (current > document.body.offsetHeight - 16) this.more(); - }; - </script> -</mk-search-posts> diff --git a/src/web/app/desktop/tags/search.tag b/src/web/app/desktop/tags/search.tag deleted file mode 100644 index d5159fe4e9..0000000000 --- a/src/web/app/desktop/tags/search.tag +++ /dev/null @@ -1,34 +0,0 @@ -<mk-search> - <header> - <h1>{ query }</h1> - </header> - <mk-search-posts ref="posts" query={ query }/> - <style> - :scope - display block - padding-bottom 32px - - > header - width 100% - max-width 600px - margin 0 auto - color #555 - - > mk-search-posts - max-width 600px - margin 0 auto - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - overflow hidden - - </style> - <script> - this.query = this.opts.query; - - this.on('mount', () => { - this.refs.posts.on('loaded', () => { - this.trigger('loaded'); - }); - }); - </script> -</mk-search> diff --git a/src/web/app/desktop/tags/select-file-from-drive-window.tag b/src/web/app/desktop/tags/select-file-from-drive-window.tag deleted file mode 100644 index 8a7e725b71..0000000000 --- a/src/web/app/desktop/tags/select-file-from-drive-window.tag +++ /dev/null @@ -1,173 +0,0 @@ -<mk-select-file-from-drive-window> - <mk-window ref="window" is-modal={ true } width={ '800px' } height={ '500px' }> - <yield to="header"> - <mk-raw content={ parent.title }/> - <span class="count" if={ parent.multiple && parent.files.length > 0 }>({ parent.files.length }ファイル選択中)</span> - </yield> - <yield to="content"> - <mk-drive-browser ref="browser" multiple={ parent.multiple }/> - <div> - <button class="upload" title="PCからドライブにファイルをアップロード" onclick={ parent.upload }><i class="fa fa-upload"></i></button> - <button class="cancel" onclick={ parent.close }>キャンセル</button> - <button class="ok" disabled={ parent.multiple && parent.files.length == 0 } onclick={ parent.ok }>決定</button> - </div> - </yield> - </mk-window> - <style> - :scope - > mk-window - [data-yield='header'] - > mk-raw - > i - margin-right 4px - - .count - margin-left 8px - opacity 0.7 - - [data-yield='content'] - > mk-drive-browser - height calc(100% - 72px) - - > div - height 72px - background lighten($theme-color, 95%) - - .upload - display inline-block - position absolute - top 8px - left 16px - cursor pointer - padding 0 - margin 8px 4px 0 0 - width 40px - height 40px - font-size 1em - color rgba($theme-color, 0.5) - background transparent - outline none - border solid 1px transparent - border-radius 4px - - &:hover - background transparent - border-color rgba($theme-color, 0.3) - - &:active - color rgba($theme-color, 0.6) - background transparent - border-color rgba($theme-color, 0.5) - box-shadow 0 2px 4px rgba(darken($theme-color, 50%), 0.15) inset - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - .ok - .cancel - display block - position absolute - bottom 16px - cursor pointer - padding 0 - margin 0 - width 120px - height 40px - font-size 1em - outline none - border-radius 4px - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - &:disabled - opacity 0.7 - cursor default - - .ok - right 16px - color $theme-color-foreground - background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) - border solid 1px lighten($theme-color, 15%) - - &:not(:disabled) - font-weight bold - - &:hover:not(:disabled) - background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) - border-color $theme-color - - &:active:not(:disabled) - background $theme-color - border-color $theme-color - - .cancel - right 148px - color #888 - background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) - border solid 1px #e2e2e2 - - &:hover - background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) - border-color #dcdcdc - - &:active - background #ececec - border-color #dcdcdc - - </style> - <script> - this.files = []; - - this.multiple = this.opts.multiple != null ? this.opts.multiple : false; - this.title = this.opts.title || '<i class="fa fa-file-o"></i>ファイルを選択'; - - this.on('mount', () => { - this.refs.window.refs.browser.on('selected', file => { - this.files = [file]; - this.ok(); - }); - - this.refs.window.refs.browser.on('change-selection', files => { - this.update({ - files: files - }); - }); - - this.refs.window.on('closed', () => { - this.unmount(); - }); - }); - - this.close = () => { - this.refs.window.close(); - }; - - this.upload = () => { - this.refs.window.refs.browser.selectLocalFile(); - }; - - this.ok = () => { - this.trigger('selected', this.multiple ? this.files : this.files[0]); - this.refs.window.close(); - }; - </script> -</mk-select-file-from-drive-window> diff --git a/src/web/app/desktop/tags/set-avatar-suggestion.tag b/src/web/app/desktop/tags/set-avatar-suggestion.tag deleted file mode 100644 index 2fe7f963fe..0000000000 --- a/src/web/app/desktop/tags/set-avatar-suggestion.tag +++ /dev/null @@ -1,48 +0,0 @@ -<mk-set-avatar-suggestion onclick={ set }> - <p><b>アバターを設定</b>してみませんか? - <button onclick={ close }><i class="fa fa-times"></i></button> - </p> - <style> - :scope - display block - cursor pointer - color #fff - background #a8cad0 - - &:hover - background #70abb5 - - > p - display block - margin 0 auto - padding 8px - max-width 1024px - - > a - font-weight bold - color #fff - - > button - position absolute - top 0 - right 0 - padding 8px - color #fff - - </style> - <script> - import updateAvatar from '../scripts/update-avatar'; - - this.mixin('i'); - - this.set = () => { - updateAvatar(this.I); - }; - - this.close = e => { - e.preventDefault(); - e.stopPropagation(); - this.unmount(); - }; - </script> -</mk-set-avatar-suggestion> diff --git a/src/web/app/desktop/tags/set-banner-suggestion.tag b/src/web/app/desktop/tags/set-banner-suggestion.tag deleted file mode 100644 index d3c50362d9..0000000000 --- a/src/web/app/desktop/tags/set-banner-suggestion.tag +++ /dev/null @@ -1,48 +0,0 @@ -<mk-set-banner-suggestion onclick={ set }> - <p><b>バナーを設定</b>してみませんか? - <button onclick={ close }><i class="fa fa-times"></i></button> - </p> - <style> - :scope - display block - cursor pointer - color #fff - background #a8cad0 - - &:hover - background #70abb5 - - > p - display block - margin 0 auto - padding 8px - max-width 1024px - - > a - font-weight bold - color #fff - - > button - position absolute - top 0 - right 0 - padding 8px - color #fff - - </style> - <script> - import updateBanner from '../scripts/update-banner'; - - this.mixin('i'); - - this.set = () => { - updateBanner(this.I); - }; - - this.close = e => { - e.preventDefault(); - e.stopPropagation(); - this.unmount(); - }; - </script> -</mk-set-banner-suggestion> diff --git a/src/web/app/desktop/tags/settings-window.tag b/src/web/app/desktop/tags/settings-window.tag deleted file mode 100644 index 28065e0a0d..0000000000 --- a/src/web/app/desktop/tags/settings-window.tag +++ /dev/null @@ -1,30 +0,0 @@ -<mk-settings-window> - <mk-window ref="window" is-modal={ true } width={ '700px' } height={ '550px' }> - <yield to="header"><i class="fa fa-cog"></i>設定</yield> - <yield to="content"> - <mk-settings/> - </yield> - </mk-window> - <style> - :scope - > mk-window - [data-yield='header'] - > i - margin-right 4px - - [data-yield='content'] - overflow hidden - - </style> - <script> - this.on('mount', () => { - this.refs.window.on('closed', () => { - this.unmount(); - }); - }); - - this.close = () => { - this.refs.window.close(); - }; - </script> -</mk-settings-window> diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag deleted file mode 100644 index a89cfda0e4..0000000000 --- a/src/web/app/desktop/tags/settings.tag +++ /dev/null @@ -1,213 +0,0 @@ -<mk-settings> - <div class="nav"> - <p class={ active: page == 'account' } onmousedown={ setPage.bind(null, 'account') }><i class="fa fa-fw fa-user"></i>アカウント</p> - <p class={ active: page == 'web' } onmousedown={ setPage.bind(null, 'web') }><i class="fa fa-fw fa-desktop"></i>Web</p> - <p class={ active: page == 'notification' } onmousedown={ setPage.bind(null, 'notification') }><i class="fa fa-fw fa-bell-o"></i>通知</p> - <p class={ active: page == 'drive' } onmousedown={ setPage.bind(null, 'drive') }><i class="fa fa-fw fa-cloud"></i>ドライブ</p> - <p class={ active: page == 'apps' } onmousedown={ setPage.bind(null, 'apps') }><i class="fa fa-fw fa-puzzle-piece"></i>アプリ</p> - <p class={ active: page == 'twitter' } onmousedown={ setPage.bind(null, 'twitter') }><i class="fa fa-fw fa-twitter"></i>Twitter</p> - <p class={ active: page == 'signin' } onmousedown={ setPage.bind(null, 'signin') }><i class="fa fa-fw fa-sign-in"></i>ログイン履歴</p> - <p class={ active: page == 'password' } onmousedown={ setPage.bind(null, 'password') }><i class="fa fa-fw fa-unlock-alt"></i>パスワード</p> - <p class={ active: page == 'api' } onmousedown={ setPage.bind(null, 'api') }><i class="fa fa-fw fa-key"></i>API</p> - </div> - <div class="pages"> - <section class="account" show={ page == 'account' }> - <h1>アカウント</h1> - <label class="avatar"> - <p>アバター</p><img class="avatar" src={ I.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - <button class="style-normal" onclick={ avatar }>画像を選択</button> - </label> - <label> - <p>名前</p> - <input ref="accountName" type="text" value={ I.name }/> - </label> - <label> - <p>場所</p> - <input ref="accountLocation" type="text" value={ I.profile.location }/> - </label> - <label> - <p>自己紹介</p> - <textarea ref="accountDescription">{ I.description }</textarea> - </label> - <label> - <p>誕生日</p> - <input ref="accountBirthday" type="date" value={ I.profile.birthday }/> - </label> - <button class="style-primary" onclick={ updateAccount }>保存</button> - </section> - - <section class="web" show={ page == 'web' }> - <h1>デザイン</h1> - </section> - - <section class="web" show={ page == 'web' }> - </section> - - <section class="apps" show={ page == 'apps' }> - <h1>アプリケーション</h1> - <mk-authorized-apps/> - </section> - - <section class="twitter" show={ page == 'twitter' }> - <h1>Twitter</h1> - <mk-twitter-setting/> - </section> - - <section class="signin" show={ page == 'signin' }> - <h1>ログイン履歴</h1> - <mk-signin-history/> - </section> - - <section class="api" show={ page == 'api' }> - <h1>API</h1> - <mk-api-info/> - </section> - </div> - <style> - :scope - display flex - width 100% - height 100% - - input:not([type]) - input[type='text'] - input[type='password'] - input[type='email'] - input[type='date'] - textarea - padding 8px - width 100% - font-size 16px - color #55595c - border solid 1px #dadada - border-radius 4px - - &:hover - border-color #aeaeae - - &:focus - border-color #aeaeae - - > .nav - flex 0 0 200px - width 100% - height 100% - padding 16px 0 0 0 - overflow auto - border-right solid 1px #ddd - - > p - display block - padding 10px 16px - margin 0 - color #666 - cursor pointer - user-select none - transition margin-left 0.2s ease - - > i - margin-right 4px - - &:hover - color #555 - - &.active - margin-left 8px - color $theme-color !important - - > .pages - width 100% - height 100% - flex auto - overflow auto - - > section - padding 32px - - // & + section - // margin-top 16px - - h1 - display block - margin 0 - padding 0 0 8px 0 - font-size 1em - color #555 - border-bottom solid 1px #eee - - label - display block - margin 16px 0 - - &:after - content "" - display block - clear both - - > p - margin 0 0 8px 0 - font-weight bold - color #373a3c - - &.checkbox - > input - position absolute - top 0 - left 0 - - &:checked + p - color $theme-color - - > p - width calc(100% - 32px) - margin 0 0 0 32px - font-weight bold - - &:last-child - font-weight normal - color #999 - - &.account - > .general - > .avatar - > img - display block - float left - width 64px - height 64px - border-radius 4px - - > button - float left - margin-left 8px - - </style> - <script> - import updateAvatar from '../scripts/update-avatar'; - import notify from '../scripts/notify'; - - this.mixin('i'); - this.mixin('api'); - - this.page = 'account'; - - this.setPage = page => { - this.page = page; - }; - - this.avatar = () => { - updateAvatar(this.I); - }; - - this.updateAccount = () => { - this.api('i/update', { - name: this.refs.accountName.value, - location: this.refs.accountLocation.value || null, - description: this.refs.accountDescription.value || null, - birthday: this.refs.accountBirthday.value || null - }).then(() => { - notify('プロフィールを更新しました'); - }); - }; - </script> -</mk-settings> diff --git a/src/web/app/desktop/tags/sub-post-content.tag b/src/web/app/desktop/tags/sub-post-content.tag deleted file mode 100644 index 02cb5251b2..0000000000 --- a/src/web/app/desktop/tags/sub-post-content.tag +++ /dev/null @@ -1,54 +0,0 @@ -<mk-sub-post-content> - <div class="body"> - <a class="reply" if={ post.reply_to_id }> - <i class="fa fa-reply"></i> - </a> - <span ref="text"></span> - <a class="quote" if={ post.repost_id } href={ '/post:' + post.repost_id }>RP: ...</a> - </div> - <details if={ post.media }> - <summary>({ post.media.length }つのメディア)</summary> - <mk-images-viewer images={ post.media }/> - </details> - <details if={ post.poll }> - <summary>投票</summary> - <mk-poll post={ post }/> - </details> - <style> - :scope - display block - overflow-wrap break-word - - > .body - > .reply - margin-right 6px - color #717171 - - > .quote - margin-left 4px - font-style oblique - color #a0bf46 - - mk-poll - font-size 80% - - </style> - <script> - import compile from '../../common/scripts/text-compiler'; - - this.mixin('user-preview'); - - this.post = this.opts.post; - - this.on('mount', () => { - if (this.post.text) { - const tokens = this.post.ast; - this.refs.text.innerHTML = compile(tokens, false); - - this.refs.text.children.forEach(e => { - if (e.tagName == 'MK-URL') riot.mount(e); - }); - } - }); - </script> -</mk-sub-post-content> diff --git a/src/web/app/desktop/tags/timeline-post-sub.tag b/src/web/app/desktop/tags/timeline-post-sub.tag deleted file mode 100644 index ab1e26721b..0000000000 --- a/src/web/app/desktop/tags/timeline-post-sub.tag +++ /dev/null @@ -1,107 +0,0 @@ -<mk-timeline-post-sub title={ title }> - <article> - <a class="avatar-anchor" href={ '/' + post.user.username }> - <img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=64' } alt="avatar" data-user-preview={ post.user_id }/> - </a> - <div class="main"> - <header> - <a class="name" href={ '/' + post.user.username } data-user-preview={ post.user_id }>{ post.user.name }</a> - <span class="username">@{ post.user.username }</span> - <a class="created-at" href={ '/' + post.user.username + '/' + post.id }> - <mk-time time={ post.created_at }/> - </a> - </header> - <div class="body"> - <mk-sub-post-content class="text" post={ post }/> - </div> - </div> - </article> - <style> - :scope - display block - margin 0 - padding 0 - font-size 0.9em - - > article - padding 16px - - &:after - content "" - display block - clear both - - &:hover - > .main > footer > button - color #888 - - > .avatar-anchor - display block - float left - margin 0 14px 0 0 - - > .avatar - display block - width 52px - height 52px - margin 0 - border-radius 8px - vertical-align bottom - - > .main - float left - width calc(100% - 66px) - - > header - display flex - margin-bottom 2px - white-space nowrap - line-height 21px - - > .name - display block - margin 0 .5em 0 0 - padding 0 - overflow hidden - color #607073 - font-size 1em - font-weight 700 - text-align left - text-decoration none - text-overflow ellipsis - - &:hover - text-decoration underline - - > .username - text-align left - margin 0 .5em 0 0 - color #d1d8da - - > .created-at - margin-left auto - color #b2b8bb - - > .body - - > .text - cursor default - margin 0 - padding 0 - font-size 1.1em - color #717171 - - pre - max-height 120px - font-size 80% - - </style> - <script> - import dateStringify from '../../common/scripts/date-stringify'; - - this.mixin('user-preview'); - - this.post = this.opts.post; - this.title = dateStringify(this.post.created_at); - </script> -</mk-timeline-post-sub> diff --git a/src/web/app/desktop/tags/timeline-post.tag b/src/web/app/desktop/tags/timeline-post.tag deleted file mode 100644 index 150b928dfd..0000000000 --- a/src/web/app/desktop/tags/timeline-post.tag +++ /dev/null @@ -1,487 +0,0 @@ -<mk-timeline-post tabindex="-1" title={ title } onkeydown={ onKeyDown }> - <div class="reply-to" if={ p.reply_to }> - <mk-timeline-post-sub post={ p.reply_to }/> - </div> - <div class="repost" if={ isRepost }> - <p> - <a class="avatar-anchor" href={ '/' + post.user.username } data-user-preview={ post.user_id }> - <img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=32' } alt="avatar"/> - </a> - <i class="fa fa-retweet"></i>{'%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('{'))}<a class="name" href={ '/' + post.user.username } data-user-preview={ post.user_id }>{ post.user.name }</a>{'%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1)} - </p> - <mk-time time={ post.created_at }/> - </div> - <article> - <a class="avatar-anchor" href={ '/' + p.user.username }> - <img class="avatar" src={ p.user.avatar_url + '?thumbnail&size=64' } alt="avatar" data-user-preview={ p.user.id }/> - </a> - <div class="main"> - <header> - <a class="name" href={ '/' + p.user.username } data-user-preview={ p.user.id }>{ p.user.name }</a> - <span class="is-bot" if={ p.user.is_bot }>bot</span> - <span class="username">@{ p.user.username }</span> - <div class="info"> - <span class="app" if={ p.app }>via <b>{ p.app.name }</b></span> - <a class="created-at" href={ url }> - <mk-time time={ p.created_at }/> - </a> - </div> - </header> - <div class="body"> - <div class="text" ref="text"> - <a class="reply" if={ p.reply_to }> - <i class="fa fa-reply"></i> - </a> - <p class="dummy"></p> - <a class="quote" if={ p.repost != null }>RP:</a> - </div> - <div class="media" if={ p.media }> - <mk-images-viewer images={ p.media }/> - </div> - <mk-poll if={ p.poll } post={ p } ref="pollViewer"/> - <div class="repost" if={ p.repost }><i class="fa fa-quote-right fa-flip-horizontal"></i> - <mk-post-preview class="repost" post={ p.repost }/> - </div> - </div> - <footer> - <mk-reactions-viewer post={ p } ref="reactionsViewer"/> - <button onclick={ reply } title="%i18n:desktop.tags.mk-timeline-post.reply%"><i class="fa fa-reply"></i> - <p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p> - </button> - <button onclick={ repost } title="%i18n:desktop.tags.mk-timeline-post.repost%"><i class="fa fa-retweet"></i> - <p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p> - </button> - <button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton" title="%i18n:desktop.tags.mk-timeline-post.add-reaction%"><i class="fa fa-plus"></i> - <p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p> - </button> - <button> - <i class="fa fa-ellipsis-h"></i> - </button> - <button onclick={ toggleDetail } title="%i18n:desktop.tags.mk-timeline-post.detail"> - <i class="fa fa-caret-down" if={ !isDetailOpened }></i> - <i class="fa fa-caret-up" if={ isDetailOpened }></i> - </button> - </footer> - </div> - </article> - <div class="detail" if={ isDetailOpened }> - <mk-post-status-graph width="462" height="130" post={ p }/> - </div> - <style> - :scope - display block - margin 0 - padding 0 - background #fff - - &:focus - z-index 1 - - &:after - content "" - pointer-events none - position absolute - top 2px - right 2px - bottom 2px - left 2px - border 2px solid rgba($theme-color, 0.3) - border-radius 4px - - > .repost - color #9dbb00 - background linear-gradient(to bottom, #edfde2 0%, #fff 100%) - - > p - margin 0 - padding 16px 32px - line-height 28px - - .avatar-anchor - display inline-block - - .avatar - vertical-align bottom - width 28px - height 28px - margin 0 8px 0 0 - border-radius 6px - - i - margin-right 4px - - .name - font-weight bold - - > mk-time - position absolute - top 16px - right 32px - font-size 0.9em - line-height 28px - - & + article - padding-top 8px - - > .reply-to - padding 0 16px - background rgba(0, 0, 0, 0.0125) - - > mk-post-preview - background transparent - - > article - padding 28px 32px 18px 32px - - &:after - content "" - display block - clear both - - &:hover - > .main > footer > button - color #888 - - > .avatar-anchor - display block - float left - margin 0 16px 10px 0 - position -webkit-sticky - position sticky - top 74px - - > .avatar - display block - width 58px - height 58px - margin 0 - border-radius 8px - vertical-align bottom - - > .main - float left - width calc(100% - 74px) - - > header - display flex - margin-bottom 4px - white-space nowrap - line-height 1.4 - - > .name - display block - margin 0 .5em 0 0 - padding 0 - overflow hidden - color #777 - font-size 1em - font-weight 700 - text-align left - text-decoration none - text-overflow ellipsis - - &:hover - text-decoration underline - - > .is-bot - text-align left - margin 0 .5em 0 0 - padding 1px 6px - font-size 12px - color #aaa - border solid 1px #ddd - border-radius 3px - - > .username - text-align left - margin 0 .5em 0 0 - color #ccc - - > .info - margin-left auto - text-align right - font-size 0.9em - - > .app - margin-right 8px - padding-right 8px - color #ccc - border-right solid 1px #eaeaea - - > .created-at - color #c0c0c0 - - > .body - - > .text - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 1.1em - color #717171 - - > .dummy - display none - - mk-url-preview - margin-top 8px - - .link - &:after - content "\f14c" - display inline-block - padding-left 2px - font-family FontAwesome - font-size .9em - font-weight 400 - font-style normal - - > .reply - margin-right 8px - color #717171 - - > .quote - margin-left 4px - font-style oblique - color #a0bf46 - - code - padding 4px 8px - margin 0 0.5em - font-size 80% - color #525252 - background #f8f8f8 - border-radius 2px - - pre > code - padding 16px - margin 0 - - [data-is-me]:after - content "you" - padding 0 4px - margin-left 4px - font-size 80% - color $theme-color-foreground - background $theme-color - border-radius 4px - - > .media - > img - display block - max-width 100% - - > mk-poll - font-size 80% - - > .repost - margin 8px 0 - - > i:first-child - position absolute - top -8px - left -8px - z-index 1 - color #c0dac6 - font-size 28px - background #fff - - > mk-post-preview - padding 16px - border dashed 1px #c0dac6 - border-radius 8px - - > footer - > button - margin 0 28px 0 0 - padding 0 8px - line-height 32px - font-size 1em - color #ddd - background transparent - border none - cursor pointer - - &:hover - color #666 - - > .count - display inline - margin 0 0 0 8px - color #999 - - &.reacted - color $theme-color - - &:last-child - position absolute - right 0 - margin 0 - - > .detail - padding-top 4px - background rgba(0, 0, 0, 0.0125) - - </style> - <script> - import compile from '../../common/scripts/text-compiler'; - import dateStringify from '../../common/scripts/date-stringify'; - - this.mixin('api'); - this.mixin('stream'); - this.mixin('user-preview'); - - this.isDetailOpened = false; - - this.set = post => { - this.post = post; - this.isRepost = this.post.repost && this.post.text == null && this.post.media_ids == null && this.post.poll == null; - this.p = this.isRepost ? this.post.repost : this.post; - this.p.reactions_count = this.p.reaction_counts ? Object.keys(this.p.reaction_counts).map(key => this.p.reaction_counts[key]).reduce((a, b) => a + b) : 0; - this.title = dateStringify(this.p.created_at); - this.url = `/${this.p.user.username}/${this.p.id}`; - }; - - this.set(this.opts.post); - - this.refresh = post => { - this.set(post); - this.update(); - if (this.refs.reactionsViewer) this.refs.reactionsViewer.update({ - post - }); - if (this.refs.pollViewer) this.refs.pollViewer.init(post); - }; - - this.onStreamPostUpdated = data => { - const post = data.post; - if (post.id == this.post.id) { - this.refresh(post); - } - }; - - this.onStreamConnected = () => { - this.capture(); - }; - - this.capture = withHandler => { - this.stream.send({ - type: 'capture', - id: this.post.id - }); - if (withHandler) this.stream.on('post-updated', this.onStreamPostUpdated); - }; - - this.decapture = withHandler => { - this.stream.send({ - type: 'decapture', - id: this.post.id - }); - if (withHandler) this.stream.off('post-updated', this.onStreamPostUpdated); - }; - - this.on('mount', () => { - this.capture(true); - this.stream.on('_connected_', this.onStreamConnected); - - if (this.p.text) { - const tokens = this.p.ast; - - this.refs.text.innerHTML = this.refs.text.innerHTML.replace('<p class="dummy"></p>', compile(tokens)); - - this.refs.text.children.forEach(e => { - if (e.tagName == 'MK-URL') riot.mount(e); - }); - - // URLをプレビュー - tokens - .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) - .map(t => { - riot.mount(this.refs.text.appendChild(document.createElement('mk-url-preview')), { - url: t.url - }); - }); - } - }); - - this.on('unmount', () => { - this.decapture(true); - this.stream.off('_connected_', this.onStreamConnected); - }); - - this.reply = () => { - riot.mount(document.body.appendChild(document.createElement('mk-post-form-window')), { - reply: this.p - }); - }; - - this.repost = () => { - riot.mount(document.body.appendChild(document.createElement('mk-repost-form-window')), { - post: this.p - }); - }; - - this.react = () => { - riot.mount(document.body.appendChild(document.createElement('mk-reaction-picker')), { - source: this.refs.reactButton, - post: this.p - }); - }; - - this.toggleDetail = () => { - this.update({ - isDetailOpened: !this.isDetailOpened - }); - }; - - this.onKeyDown = e => { - let shouldBeCancel = true; - - switch (true) { - case e.which == 38: // [↑] - case e.which == 74: // [j] - case e.which == 9 && e.shiftKey: // [Shift] + [Tab] - focus(this.root, e => e.previousElementSibling); - break; - - case e.which == 40: // [↓] - case e.which == 75: // [k] - case e.which == 9: // [Tab] - focus(this.root, e => e.nextElementSibling); - break; - - case e.which == 81: // [q] - case e.which == 69: // [e] - this.repost(); - break; - - case e.which == 70: // [f] - case e.which == 76: // [l] - this.like(); - break; - - case e.which == 82: // [r] - this.reply(); - break; - - default: - shouldBeCancel = false; - } - - if (shouldBeCancel) e.preventDefault(); - }; - - function focus(el, fn) { - const target = fn(el); - if (target) { - if (target.hasAttribute('tabindex')) { - target.focus(); - } else { - focus(target, fn); - } - } - } - </script> -</mk-timeline-post> diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag deleted file mode 100644 index d4cd50455c..0000000000 --- a/src/web/app/desktop/tags/timeline.tag +++ /dev/null @@ -1,92 +0,0 @@ -<mk-timeline> - <virtual each={ post, i in posts }> - <mk-timeline-post post={ post }/> - <p class="date" if={ i != posts.length - 1 && post._date != posts[i + 1]._date }><span><i class="fa fa-angle-up"></i>{ post._datetext }</span><span><i class="fa fa-angle-down"></i>{ posts[i + 1]._datetext }</span></p> - </virtual> - <footer data-yield="footer"> - <yield from="footer"/> - </footer> - <style> - :scope - display block - - > mk-timeline-post - border-bottom solid 1px #eaeaea - - &:first-child - border-top-left-radius 6px - border-top-right-radius 6px - - &:last-of-type - border-bottom none - - > .date - display block - margin 0 - line-height 32px - font-size 14px - text-align center - color #aaa - background #fdfdfd - border-bottom solid 1px #eaeaea - - span - margin 0 16px - - i - margin-right 8px - - > footer - padding 16px - text-align center - color #ccc - border-top solid 1px #eaeaea - border-bottom-left-radius 4px - border-bottom-right-radius 4px - - </style> - <script> - this.posts = []; - - this.on('update', () => { - this.posts.forEach(post => { - const date = new Date(post.created_at).getDate(); - const month = new Date(post.created_at).getMonth() + 1; - post._date = date; - post._datetext = `${month}月 ${date}日`; - }); - }); - - this.setPosts = posts => { - this.update({ - posts: posts - }); - }; - - this.prependPosts = posts => { - posts.forEach(post => { - this.posts.push(post); - this.update(); - }); - } - - this.addPost = post => { - this.posts.unshift(post); - this.update(); - }; - - this.tail = () => { - return this.posts[this.posts.length - 1]; - }; - - this.clear = () => { - this.posts = []; - this.update(); - }; - - this.focus = () => { - this.root.children[0].focus(); - }; - - </script> -</mk-timeline> diff --git a/src/web/app/desktop/tags/ui-header-account.tag b/src/web/app/desktop/tags/ui-header-account.tag deleted file mode 100644 index 23c4fdbbf9..0000000000 --- a/src/web/app/desktop/tags/ui-header-account.tag +++ /dev/null @@ -1,214 +0,0 @@ -<mk-ui-header-account> - <button class="header" data-active={ isOpen.toString() } onclick={ toggle }> - <span class="username">{ I.username }<i class="fa fa-angle-down" if={ !isOpen }></i><i class="fa fa-angle-up" if={ isOpen }></i></span> - <img class="avatar" src={ I.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - </button> - <div class="menu" if={ isOpen }> - <ul> - <li> - <a href={ '/' + I.username }><i class="fa fa-user"></i>%i18n:desktop.tags.mk-ui-header-account.profile%<i class="fa fa-angle-right"></i></a> - </li> - <li onclick={ drive }> - <p><i class="fa fa-cloud"></i>%i18n:desktop.tags.mk-ui-header-account.drive%<i class="fa fa-angle-right"></i></p> - </li> - <li> - <a href="/i>mentions"><i class="fa fa-at"></i>%i18n:desktop.tags.mk-ui-header-account.mentions%<i class="fa fa-angle-right"></i></a> - </li> - </ul> - <ul> - <li onclick={ settings }> - <p><i class="fa fa-cog"></i>%i18n:desktop.tags.mk-ui-header-account.settings%<i class="fa fa-angle-right"></i></p> - </li> - </ul> - <ul> - <li onclick={ signout }> - <p><i class="fa fa-power-off"></i>%i18n:desktop.tags.mk-ui-header-account.signout%<i class="fa fa-angle-right"></i></p> - </li> - </ul> - </div> - <style> - :scope - display block - float left - - > .header - display block - margin 0 - padding 0 - color #9eaba8 - border none - background transparent - cursor pointer - - * - pointer-events none - - &:hover - &[data-active='true'] - color darken(#9eaba8, 20%) - - > .avatar - filter saturate(150%) - - &:active - color darken(#9eaba8, 30%) - - > .username - display block - float left - margin 0 12px 0 16px - max-width 16em - line-height 48px - font-weight bold - font-family Meiryo, sans-serif - text-decoration none - - i - margin-left 8px - - > .avatar - display block - float left - min-width 32px - max-width 32px - min-height 32px - max-height 32px - margin 8px 8px 8px 0 - border-radius 4px - transition filter 100ms ease - - > .menu - display block - position absolute - top 56px - right -2px - width 230px - font-size 0.8em - background #fff - border-radius 4px - box-shadow 0 1px 4px rgba(0, 0, 0, 0.25) - - &:before - content "" - pointer-events none - display block - position absolute - top -28px - right 12px - border-top solid 14px transparent - border-right solid 14px transparent - border-bottom solid 14px rgba(0, 0, 0, 0.1) - border-left solid 14px transparent - - &:after - content "" - pointer-events none - display block - position absolute - top -27px - right 12px - border-top solid 14px transparent - border-right solid 14px transparent - border-bottom solid 14px #fff - border-left solid 14px transparent - - ul - display block - margin 10px 0 - padding 0 - list-style none - - & + ul - padding-top 10px - border-top solid 1px #eee - - > li - display block - margin 0 - padding 0 - - > a - > p - display block - z-index 1 - padding 0 28px - margin 0 - line-height 40px - color #868C8C - cursor pointer - - * - pointer-events none - - > i:first-of-type - margin-right 6px - - > i:last-of-type - display block - position absolute - top 0 - right 8px - z-index 1 - padding 0 20px - font-size 1.2em - line-height 40px - - &:hover, &:active - text-decoration none - background $theme-color - color $theme-color-foreground - - </style> - <script> - import contains from '../../common/scripts/contains'; - import signout from '../../common/scripts/signout'; - this.signout = signout; - - this.mixin('i'); - - this.isOpen = false; - - this.on('before-unmount', () => { - this.close(); - }); - - this.toggle = () => { - this.isOpen ? this.close() : this.open(); - }; - - this.open = () => { - this.update({ - isOpen: true - }); - document.querySelectorAll('body *').forEach(el => { - el.addEventListener('mousedown', this.mousedown); - }); - }; - - this.close = () => { - this.update({ - isOpen: false - }); - document.querySelectorAll('body *').forEach(el => { - el.removeEventListener('mousedown', this.mousedown); - }); - }; - - this.mousedown = e => { - e.preventDefault(); - if (!contains(this.root, e.target) && this.root != e.target) this.close(); - return false; - }; - - this.drive = () => { - this.close(); - riot.mount(document.body.appendChild(document.createElement('mk-drive-browser-window'))); - }; - - this.settings = () => { - this.close(); - riot.mount(document.body.appendChild(document.createElement('mk-settings-window'))); - }; - - </script> -</mk-ui-header-account> diff --git a/src/web/app/desktop/tags/ui-header-clock.tag b/src/web/app/desktop/tags/ui-header-clock.tag deleted file mode 100644 index b8cb078497..0000000000 --- a/src/web/app/desktop/tags/ui-header-clock.tag +++ /dev/null @@ -1,86 +0,0 @@ -<mk-ui-header-clock> - <div class="header"> - <time ref="time"> - <span class="yyyymmdd">{ yyyy }/{ mm }/{ dd }</span> - <br> - <span class="hhnn">{ hh }<span style="visibility:{ now.getSeconds() % 2 == 0 ? 'visible' : 'hidden' }">:</span>{ nn }</span> - </time> - </div> - <div class="content"> - <mk-analog-clock/> - </div> - <style> - :scope - display inline-block - overflow visible - - > .header - padding 0 12px - text-align center - font-size 10px - - &, * - cursor: default - - &:hover - background #899492 - - & + .content - visibility visible - - > time - color #fff !important - - * - color #fff !important - - &:after - content "" - display block - clear both - - > time - display table-cell - vertical-align middle - height 48px - color #9eaba8 - - > .yyyymmdd - opacity 0.7 - - > .content - visibility hidden - display block - position absolute - top auto - right 0 - z-index 3 - margin 0 - padding 0 - width 256px - background #899492 - - </style> - <script> - this.now = new Date(); - - this.draw = () => { - const now = this.now = new Date(); - this.yyyy = now.getFullYear(); - this.mm = ('0' + (now.getMonth() + 1)).slice(-2); - this.dd = ('0' + now.getDate()).slice(-2); - this.hh = ('0' + now.getHours()).slice(-2); - this.nn = ('0' + now.getMinutes()).slice(-2); - this.update(); - }; - - this.on('mount', () => { - this.draw(); - this.clock = setInterval(this.draw, 1000); - }); - - this.on('unmount', () => { - clearInterval(this.clock); - }); - </script> -</mk-ui-header-clock> diff --git a/src/web/app/desktop/tags/ui-header-nav.tag b/src/web/app/desktop/tags/ui-header-nav.tag deleted file mode 100644 index c36ce65798..0000000000 --- a/src/web/app/desktop/tags/ui-header-nav.tag +++ /dev/null @@ -1,133 +0,0 @@ -<mk-ui-header-nav> - <ul if={ SIGNIN }> - <li class="home { active: page == 'home' }"> - <a href={ CONFIG.url }> - <i class="fa fa-home"></i> - <p>%i18n:desktop.tags.mk-ui-header-nav.home%</p> - </a> - </li> - <li class="messaging"> - <a onclick={ messaging }> - <i class="fa fa-comments"></i> - <p>%i18n:desktop.tags.mk-ui-header-nav.messaging%</p> - <i class="fa fa-circle" if={ hasUnreadMessagingMessages }></i> - </a> - </li> - <li class="info"> - <a href="https://twitter.com/misskey_xyz" target="_blank"> - <i class="fa fa-info"></i> - <p>%i18n:desktop.tags.mk-ui-header-nav.info%</p> - </a> - </li> - </ul> - <style> - :scope - display inline-block - margin 0 - padding 0 - line-height 3rem - vertical-align top - - > ul - display inline-block - margin 0 - padding 0 - vertical-align top - line-height 3rem - list-style none - - > li - display inline-block - vertical-align top - height 48px - line-height 48px - - &.active - > a - border-bottom solid 3px $theme-color - - > a - display inline-block - z-index 1 - height 100% - padding 0 24px - font-size 13px - font-variant small-caps - color #9eaba8 - text-decoration none - transition none - cursor pointer - - * - pointer-events none - - &:hover - color darken(#9eaba8, 20%) - text-decoration none - - > i:first-child - margin-right 8px - - > i:last-child - margin-left 5px - vertical-align super - font-size 10px - color $theme-color - - @media (max-width 1100px) - margin-left -5px - - > p - display inline - margin 0 - - @media (max-width 1100px) - display none - - @media (max-width 700px) - padding 0 12px - - </style> - <script> - this.mixin('i'); - this.mixin('api'); - this.mixin('stream'); - - this.page = this.opts.page; - - this.on('mount', () => { - this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages); - this.stream.on('unread_messaging_message', this.onUnreadMessagingMessage); - - // Fetch count of unread messaging messages - this.api('messaging/unread').then(res => { - if (res.count > 0) { - this.update({ - hasUnreadMessagingMessages: true - }); - } - }); - }); - - this.on('unmount', () => { - this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages); - this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage); - }); - - this.onReadAllMessagingMessages = () => { - this.update({ - hasUnreadMessagingMessages: false - }); - }; - - this.onUnreadMessagingMessage = () => { - this.update({ - hasUnreadMessagingMessages: true - }); - }; - - this.messaging = () => { - riot.mount(document.body.appendChild(document.createElement('mk-messaging-window'))); - }; - </script> -</mk-ui-header-nav> diff --git a/src/web/app/desktop/tags/ui-header-notifications.tag b/src/web/app/desktop/tags/ui-header-notifications.tag deleted file mode 100644 index 3cd8d1e3df..0000000000 --- a/src/web/app/desktop/tags/ui-header-notifications.tag +++ /dev/null @@ -1,108 +0,0 @@ -<mk-ui-header-notifications> - <button class="header" data-active={ isOpen } onclick={ toggle }><i class="fa fa-bell-o"></i></button> - <div class="notifications" if={ isOpen }> - <mk-notifications/> - </div> - <style> - :scope - display block - float left - - > .header - display block - margin 0 - padding 0 - width 32px - color #9eaba8 - border none - background transparent - cursor pointer - - * - pointer-events none - - &:hover - &[data-active='true'] - color darken(#9eaba8, 20%) - - &:active - color darken(#9eaba8, 30%) - - > i - font-size 1.2em - line-height 48px - - > .notifications - display block - position absolute - top 56px - right -72px - width 300px - background #fff - border-radius 4px - box-shadow 0 1px 4px rgba(0, 0, 0, 0.25) - - &:before - content "" - pointer-events none - display block - position absolute - top -28px - right 74px - border-top solid 14px transparent - border-right solid 14px transparent - border-bottom solid 14px rgba(0, 0, 0, 0.1) - border-left solid 14px transparent - - &:after - content "" - pointer-events none - display block - position absolute - top -27px - right 74px - border-top solid 14px transparent - border-right solid 14px transparent - border-bottom solid 14px #fff - border-left solid 14px transparent - - > mk-notifications - max-height 350px - font-size 1rem - overflow auto - - </style> - <script> - import contains from '../../common/scripts/contains'; - - this.isOpen = false; - - this.toggle = () => { - this.isOpen ? this.close() : this.open(); - }; - - this.open = () => { - this.update({ - isOpen: true - }); - document.querySelectorAll('body *').forEach(el => { - el.addEventListener('mousedown', this.mousedown); - }); - }; - - this.close = () => { - this.update({ - isOpen: false - }); - document.querySelectorAll('body *').forEach(el => { - el.removeEventListener('mousedown', this.mousedown); - }); - }; - - this.mousedown = e => { - e.preventDefault(); - if (!contains(this.root, e.target) && this.root != e.target) this.close(); - return false; - }; - </script> -</mk-ui-header-notifications> diff --git a/src/web/app/desktop/tags/ui-header-post-button.tag b/src/web/app/desktop/tags/ui-header-post-button.tag deleted file mode 100644 index ca380b06ea..0000000000 --- a/src/web/app/desktop/tags/ui-header-post-button.tag +++ /dev/null @@ -1,42 +0,0 @@ -<mk-ui-header-post-button> - <button onclick={ post } title="新規投稿"><i class="fa fa-pencil-square-o"></i></button> - <style> - :scope - display inline-block - padding 8px - height 100% - vertical-align top - - > button - display inline-block - margin 0 - padding 0 10px - height 100% - font-size 1.2em - font-weight normal - text-decoration none - color $theme-color-foreground - background $theme-color !important - outline none - border none - border-radius 2px - transition background 0.1s ease - cursor pointer - - * - pointer-events none - - &:hover - background lighten($theme-color, 10%) !important - - &:active - background darken($theme-color, 10%) !important - transition background 0s ease - - </style> - <script> - this.post = e => { - this.parent.parent.openPostForm(); - }; - </script> -</mk-ui-header-post-button> diff --git a/src/web/app/desktop/tags/ui-header-search.tag b/src/web/app/desktop/tags/ui-header-search.tag deleted file mode 100644 index 616476f42c..0000000000 --- a/src/web/app/desktop/tags/ui-header-search.tag +++ /dev/null @@ -1,42 +0,0 @@ -<mk-ui-header-search> - <form class="search" onsubmit={ onsubmit }> - <input ref="q" type="search" placeholder=" %i18n:desktop.tags.mk-ui-header-search.placeholder%"/> - <div class="result"></div> - </form> - <style> - :scope - - > form - display block - float left - - > input - user-select text - cursor auto - margin 0 - padding 6px 18px - width 14em - height 48px - font-size 1em - line-height calc(48px - 12px) - background transparent - outline none - //border solid 1px #ddd - border none - border-radius 0 - transition color 0.5s ease, border 0.5s ease - font-family FontAwesome, sans-serif - - &::-webkit-input-placeholder - color #9eaba8 - - </style> - <script> - this.mixin('page'); - - this.onsubmit = e => { - e.preventDefault(); - this.page('/search:' + this.refs.q.value); - }; - </script> -</mk-ui-header-search> diff --git a/src/web/app/desktop/tags/ui-header.tag b/src/web/app/desktop/tags/ui-header.tag deleted file mode 100644 index fa7f2cb2ac..0000000000 --- a/src/web/app/desktop/tags/ui-header.tag +++ /dev/null @@ -1,86 +0,0 @@ -<mk-ui-header> - <mk-donation if={ SIGNIN && I.data.no_donation != 'true' }/> - <mk-special-message/> - <div class="main"> - <div class="backdrop"></div> - <div class="main"> - <div class="container"> - <div class="left"> - <mk-ui-header-nav page={ opts.page }/> - </div> - <div class="right"> - <mk-ui-header-search/> - <mk-ui-header-account if={ SIGNIN }/> - <mk-ui-header-notifications if={ SIGNIN }/> - <mk-ui-header-post-button if={ SIGNIN }/> - <mk-ui-header-clock/> - </div> - </div> - </div> - </div> - <style> - :scope - display block - position -webkit-sticky - position sticky - top 0 - z-index 1024 - width 100% - box-shadow 0 1px 1px rgba(0, 0, 0, 0.075) - - > .main - - > .backdrop - position absolute - top 0 - z-index 1023 - width 100% - height 48px - backdrop-filter blur(12px) - //background-color rgba(255, 255, 255, 0.75) - background #fff - - &:after - content "" - display block - width 100% - height 48px - background-image url(/assets/desktop/header-logo.svg) - background-size 46px - background-position center - background-repeat no-repeat - opacity 0.3 - - > .main - z-index 1024 - margin 0 - padding 0 - background-clip content-box - font-size 0.9rem - user-select none - - > .container - width 100% - max-width 1300px - margin 0 auto - - &:after - content "" - display block - clear both - - > .left - float left - height 3rem - - > .right - float right - height 48px - - @media (max-width 1100px) - > mk-ui-header-search - display none - - </style> - <script>this.mixin('i');</script> -</mk-ui-header> diff --git a/src/web/app/desktop/tags/ui-notification.tag b/src/web/app/desktop/tags/ui-notification.tag deleted file mode 100644 index f39d766d8c..0000000000 --- a/src/web/app/desktop/tags/ui-notification.tag +++ /dev/null @@ -1,51 +0,0 @@ -<mk-ui-notification> - <p>{ opts.message }</p> - <style> - :scope - display block - position fixed - z-index 10000 - top -128px - left 0 - right 0 - margin 0 auto - padding 128px 0 0 0 - width 500px - color rgba(#000, 0.6) - background rgba(#fff, 0.9) - border-radius 0 0 8px 8px - box-shadow 0 2px 4px rgba(#000, 0.2) - transform translateY(-64px) - opacity 0 - - > p - margin 0 - line-height 64px - text-align center - - </style> - <script> - import anime from 'animejs'; - - this.on('mount', () => { - anime({ - targets: this.root, - opacity: 1, - translateY: [-64, 0], - easing: 'easeOutElastic', - duration: 500 - }); - - setTimeout(() => { - anime({ - targets: this.root, - opacity: 0, - translateY: -64, - duration: 500, - easing: 'easeInElastic', - complete: () => this.unmount() - }); - }, 6000); - }); - </script> -</mk-ui-notification> diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag deleted file mode 100644 index 788fb56131..0000000000 --- a/src/web/app/desktop/tags/ui.tag +++ /dev/null @@ -1,37 +0,0 @@ -<mk-ui> - <mk-ui-header page={ opts.page }/> - <mk-set-avatar-suggestion if={ SIGNIN && I.avatar_id == null }/> - <mk-set-banner-suggestion if={ SIGNIN && I.banner_id == null }/> - <div class="content"> - <yield /> - </div> - <mk-stream-indicator/> - <style> - :scope - display block - </style> - <script> - this.mixin('i'); - - this.openPostForm = () => { - riot.mount(document.body.appendChild(document.createElement('mk-post-form-window'))); - }; - - this.on('mount', () => { - document.addEventListener('keydown', this.onkeydown); - }); - - this.on('unmount', () => { - document.removeEventListener('keydown', this.onkeydown); - }); - - this.onkeydown = e => { - if (e.target.tagName == 'INPUT' || e.target.tagName == 'TEXTAREA') return; - - if (e.which == 80 || e.which == 78) { // p or n - e.preventDefault(); - this.openPostForm(); - } - }; - </script> -</mk-ui> diff --git a/src/web/app/desktop/tags/user-followers-window.tag b/src/web/app/desktop/tags/user-followers-window.tag deleted file mode 100644 index 43127a68a8..0000000000 --- a/src/web/app/desktop/tags/user-followers-window.tag +++ /dev/null @@ -1,19 +0,0 @@ -<mk-user-followers-window> - <mk-window is-modal={ false } width={ '400px' } height={ '550px' }><yield to="header"><img src={ parent.user.avatar_url + '?thumbnail&size=64' } alt=""/>{ parent.user.name }のフォロワー</yield> -<yield to="content"> - <mk-user-followers user={ parent.user }/></yield> - </mk-window> - <style> - :scope - > mk-window - [data-yield='header'] - > img - display inline-block - vertical-align bottom - height calc(100% - 10px) - margin 5px - border-radius 4px - - </style> - <script>this.user = this.opts.user</script> -</mk-user-followers-window> diff --git a/src/web/app/desktop/tags/user-followers.tag b/src/web/app/desktop/tags/user-followers.tag deleted file mode 100644 index ea670e2729..0000000000 --- a/src/web/app/desktop/tags/user-followers.tag +++ /dev/null @@ -1,23 +0,0 @@ -<mk-user-followers> - <mk-users-list fetch={ fetch } count={ user.followers_count } you-know-count={ user.followers_you_know_count } no-users={ 'フォロワーはいないようです。' }/> - <style> - :scope - display block - height 100% - - </style> - <script> - this.mixin('api'); - - this.user = this.opts.user; - - this.fetch = (iknow, limit, cursor, cb) => { - this.api('users/followers', { - user_id: this.user.id, - iknow: iknow, - limit: limit, - cursor: cursor ? cursor : undefined - }).then(cb); - }; - </script> -</mk-user-followers> diff --git a/src/web/app/desktop/tags/user-following-window.tag b/src/web/app/desktop/tags/user-following-window.tag deleted file mode 100644 index 10a84db315..0000000000 --- a/src/web/app/desktop/tags/user-following-window.tag +++ /dev/null @@ -1,19 +0,0 @@ -<mk-user-following-window> - <mk-window is-modal={ false } width={ '400px' } height={ '550px' }><yield to="header"><img src={ parent.user.avatar_url + '?thumbnail&size=64' } alt=""/>{ parent.user.name }のフォロー</yield> -<yield to="content"> - <mk-user-following user={ parent.user }/></yield> - </mk-window> - <style> - :scope - > mk-window - [data-yield='header'] - > img - display inline-block - vertical-align bottom - height calc(100% - 10px) - margin 5px - border-radius 4px - - </style> - <script>this.user = this.opts.user</script> -</mk-user-following-window> diff --git a/src/web/app/desktop/tags/user-following.tag b/src/web/app/desktop/tags/user-following.tag deleted file mode 100644 index 4523beac2c..0000000000 --- a/src/web/app/desktop/tags/user-following.tag +++ /dev/null @@ -1,23 +0,0 @@ -<mk-user-following> - <mk-users-list fetch={ fetch } count={ user.following_count } you-know-count={ user.following_you_know_count } no-users={ 'フォロー中のユーザーはいないようです。' }/> - <style> - :scope - display block - height 100% - - </style> - <script> - this.mixin('api'); - - this.user = this.opts.user; - - this.fetch = (iknow, limit, cursor, cb) => { - this.api('users/following', { - user_id: this.user.id, - iknow: iknow, - limit: limit, - cursor: cursor ? cursor : undefined - }).then(cb); - }; - </script> -</mk-user-following> diff --git a/src/web/app/desktop/tags/user-graphs.tag b/src/web/app/desktop/tags/user-graphs.tag deleted file mode 100644 index 0677d8c187..0000000000 --- a/src/web/app/desktop/tags/user-graphs.tag +++ /dev/null @@ -1,41 +0,0 @@ -<mk-user-graphs> - <section> - <h1>投稿</h1> - <mk-user-posts-graph user={ opts.user }/> - </section> - <section> - <h1>フォロー/フォロワー</h1> - <mk-user-friends-graph user={ opts.user }/> - </section> - <section> - <h1>いいね</h1> - <mk-user-likes-graph user={ opts.user }/> - </section> - <style> - :scope - display block - - > section - margin 16px 0 - background #fff - border solid 1px rgba(0, 0, 0, 0.1) - border-radius 4px - - > h1 - margin 0 0 8px 0 - padding 0 16px - line-height 40px - font-size 1em - color #666 - border-bottom solid 1px #eee - - > *:not(h1) - margin 0 auto 16px auto - - </style> - <script> - this.on('mount', () => { - this.trigger('loaded'); - }); - </script> -</mk-user-graphs> diff --git a/src/web/app/desktop/tags/user-header.tag b/src/web/app/desktop/tags/user-header.tag deleted file mode 100644 index ea7ea6bb37..0000000000 --- a/src/web/app/desktop/tags/user-header.tag +++ /dev/null @@ -1,147 +0,0 @@ -<mk-user-header data-is-dark-background={ user.banner_url != null }> - <div class="banner" ref="banner" style={ user.banner_url ? 'background-image: url(' + user.banner_url + '?thumbnail&size=1024)' : '' } onclick={ onUpdateBanner }></div><img class="avatar" src={ user.avatar_url + '?thumbnail&size=150' } alt="avatar"/> - <div class="title"> - <p class="name" href={ '/' + user.username }>{ user.name }</p> - <p class="username">@{ user.username }</p> - <p class="location" if={ user.profile.location }><i class="fa fa-map-marker"></i>{ user.profile.location }</p> - </div> - <footer> - <a href={ '/' + user.username }>投稿</a> - <a href={ '/' + user.username + '/media' }>メディア</a> - <a href={ '/' + user.username + '/graphs' }>グラフ</a> - </footer> - <style> - :scope - $footer-height = 58px - - display block - background #fff - - &[data-is-dark-background] - > .banner - background-color #383838 - - > .title - color #fff - background linear-gradient(transparent, rgba(0, 0, 0, 0.7)) - - > .name - text-shadow 0 0 8px #000 - - > .banner - height 280px - background-color #f5f5f5 - background-size cover - background-position center - - > .avatar - display block - position absolute - bottom 16px - left 16px - z-index 2 - width 150px - height 150px - margin 0 - border solid 3px #fff - border-radius 8px - box-shadow 1px 1px 3px rgba(0, 0, 0, 0.2) - - > .title - position absolute - bottom $footer-height - left 0 - width 100% - padding 0 0 8px 195px - color #656565 - font-family '游ゴシック', 'YuGothic', 'ヒラギノ角ゴ ProN W3', 'Hiragino Kaku Gothic ProN', 'Meiryo', 'メイリオ', sans-serif - - > .name - display block - margin 0 - line-height 40px - font-weight bold - font-size 2em - - > .username - > .location - display inline-block - margin 0 16px 0 0 - line-height 20px - opacity 0.8 - - > i - margin-right 4px - - > footer - z-index 1 - height $footer-height - padding-left 195px - background #fff - - > a - display inline-block - margin 0 - width 100px - line-height $footer-height - color #555 - - > button - display block - position absolute - top 0 - right 0 - margin 8px - padding 0 - width $footer-height - 16px - line-height $footer-height - 16px - 2px - font-size 1.2em - color #777 - border solid 1px #eee - border-radius 4px - - &:hover - color #555 - border solid 1px #ddd - - </style> - <script> - import updateBanner from '../scripts/update-banner'; - - this.mixin('i'); - - this.user = this.opts.user; - - this.on('mount', () => { - window.addEventListener('load', this.scroll); - window.addEventListener('scroll', this.scroll); - window.addEventListener('resize', this.scroll); - }); - - this.on('unmount', () => { - window.removeEventListener('load', this.scroll); - window.removeEventListener('scroll', this.scroll); - window.removeEventListener('resize', this.scroll); - }); - - this.scroll = () => { - const top = window.scrollY; - const height = 280/*px*/; - - const pos = 50 - ((top / height) * 50); - this.refs.banner.style.backgroundPosition = `center ${pos}%`; - - const blur = top / 32 - if (blur <= 10) this.refs.banner.style.filter = `blur(${blur}px)`; - }; - - this.onUpdateBanner = () => { - if (!this.SIGNIN || this.I.id != this.user.id) return; - - updateBanner(this.I, i => { - this.user.banner_url = i.banner_url; - this.update(); - }); - }; - </script> -</mk-user-header> diff --git a/src/web/app/desktop/tags/user-home.tag b/src/web/app/desktop/tags/user-home.tag deleted file mode 100644 index a879db5bb6..0000000000 --- a/src/web/app/desktop/tags/user-home.tag +++ /dev/null @@ -1,46 +0,0 @@ -<mk-user-home> - <div class="side"> - <mk-user-profile user={ user }/> - <mk-user-photos user={ user }/> - </div> - <main> - <mk-user-timeline ref="tl" user={ user }/> - </main> - <style> - :scope - display flex - justify-content center - - > * - > * - display block - //border solid 1px #eaeaea - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - &:not(:last-child) - margin-bottom 16px - - > main - flex 1 1 560px - max-width 560px - margin 0 - padding 16px 0 16px 16px - - > .side - flex 1 1 270px - max-width 270px - margin 0 - padding 16px 0 16px 0 - - </style> - <script> - this.user = this.opts.user; - - this.on('mount', () => { - this.refs.tl.on('loaded', () => { - this.trigger('loaded'); - }); - }); - </script> -</mk-user-home> diff --git a/src/web/app/desktop/tags/user-photos.tag b/src/web/app/desktop/tags/user-photos.tag deleted file mode 100644 index dce1e50add..0000000000 --- a/src/web/app/desktop/tags/user-photos.tag +++ /dev/null @@ -1,91 +0,0 @@ -<mk-user-photos> - <p class="title"><i class="fa fa-camera"></i>フォト</p> - <p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>読み込んでいます<mk-ellipsis/></p> - <div class="stream" if={ !initializing && images.length > 0 }> - <virtual each={ image in images }> - <div class="img" style={ 'background-image: url(' + image.url + '?thumbnail&size=256)' }></div> - </virtual> - </div> - <p class="empty" if={ !initializing && images.length == 0 }>写真はありません</p> - <style> - :scope - display block - background #fff - - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) - - > i - margin-right 4px - - > .stream - display -webkit-flex - display -moz-flex - display -ms-flex - display flex - justify-content center - flex-wrap wrap - padding 8px - - > .img - flex 1 1 33% - width 33% - height 80px - background-position center center - background-size cover - background-clip content-box - border solid 2px transparent - - > .initializing - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - - </style> - <script> - import isPromise from '../../common/scripts/is-promise'; - - this.mixin('api'); - - this.images = []; - this.initializing = true; - this.user = null; - this.userPromise = isPromise(this.opts.user) - ? this.opts.user - : Promise.resolve(this.opts.user); - - this.on('mount', () => { - this.userPromise.then(user => { - this.update({ - user: user - }); - - this.api('users/posts', { - user_id: this.user.id, - with_media: true, - limit: 9 - }).then(posts => { - this.initializing = false; - posts.forEach(post => { - post.media.forEach(media => { - if (this.images.length < 9) this.images.push(media); - }); - }); - this.update(); - }); - }); - }); - </script> -</mk-user-photos> diff --git a/src/web/app/desktop/tags/user-preview.tag b/src/web/app/desktop/tags/user-preview.tag deleted file mode 100644 index b836ff1e78..0000000000 --- a/src/web/app/desktop/tags/user-preview.tag +++ /dev/null @@ -1,149 +0,0 @@ -<mk-user-preview> - <virtual if={ user != null }> - <div class="banner" style={ user.banner_url ? 'background-image: url(' + user.banner_url + '?thumbnail&size=512)' : '' }></div><a class="avatar" href={ '/' + user.username } target="_blank"><img src={ user.avatar_url + '?thumbnail&size=64' } alt="avatar"/></a> - <div class="title"> - <p class="name">{ user.name }</p> - <p class="username">@{ user.username }</p> - </div> - <div class="description">{ user.description }</div> - <div class="status"> - <div> - <p>投稿</p><a>{ user.posts_count }</a> - </div> - <div> - <p>フォロー</p><a>{ user.following_count }</a> - </div> - <div> - <p>フォロワー</p><a>{ user.followers_count }</a> - </div> - </div> - <mk-follow-button if={ SIGNIN && user.id != I.id } user={ userPromise }/> - </virtual> - <style> - :scope - display block - position absolute - z-index 2048 - margin-top -8px - width 250px - background #fff - background-clip content-box - border solid 1px rgba(0, 0, 0, 0.1) - border-radius 4px - overflow hidden - opacity 0 - - > .banner - height 84px - background-color #f5f5f5 - background-size cover - background-position center - - > .avatar - display block - position absolute - top 62px - left 13px - - > img - display block - width 58px - height 58px - margin 0 - border solid 3px #fff - border-radius 8px - - > .title - display block - padding 8px 0 8px 82px - - > .name - display block - margin 0 - font-weight bold - line-height 16px - color #656565 - - > .username - display block - margin 0 - line-height 16px - font-size 0.8em - color #999 - - > .description - padding 0 16px - font-size 0.7em - color #555 - - > .status - padding 8px 16px - - > div - display inline-block - width 33% - - > p - margin 0 - font-size 0.7em - color #aaa - - > a - font-size 1em - color $theme-color - - > mk-follow-button - position absolute - top 92px - right 8px - - </style> - <script> - import anime from 'animejs'; - - this.mixin('i'); - this.mixin('api'); - - this.u = this.opts.user; - this.user = null; - this.userPromise = - typeof this.u == 'string' ? - new Promise((resolve, reject) => { - this.api('users/show', { - user_id: this.u[0] == '@' ? undefined : this.u, - username: this.u[0] == '@' ? this.u.substr(1) : undefined - }).then(resolve); - }) - : Promise.resolve(this.u); - - this.on('mount', () => { - this.userPromise.then(user => { - this.update({ - user: user - }); - this.open(); - }); - }); - - this.open = () => { - anime({ - targets: this.root, - opacity: 1, - 'margin-top': 0, - duration: 200, - easing: 'easeOutQuad' - }); - }; - - this.close = () => { - anime({ - targets: this.root, - opacity: 0, - 'margin-top': '-8px', - duration: 200, - easing: 'easeOutQuad', - complete: () => this.unmount() - }); - }; - </script> -</mk-user-preview> diff --git a/src/web/app/desktop/tags/user-profile.tag b/src/web/app/desktop/tags/user-profile.tag deleted file mode 100644 index 7472a47801..0000000000 --- a/src/web/app/desktop/tags/user-profile.tag +++ /dev/null @@ -1,102 +0,0 @@ -<mk-user-profile> - <div class="friend-form" if={ SIGNIN && I.id != user.id }> - <mk-big-follow-button user={ user }/> - <p class="followed" if={ user.is_followed }>フォローされています</p> - </div> - <div class="description" if={ user.description }>{ user.description }</div> - <div class="birthday" if={ user.profile.birthday }> - <p><i class="fa fa-birthday-cake"></i>{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' } ({ age(user.profile.birthday) }歳)</p> - </div> - <div class="twitter" if={ user.twitter }> - <p><i class="fa fa-twitter"></i><a href={ 'https://twitter.com/' + user.twitter.screen_name } target="_blank">@{ user.twitter.screen_name }</a></p> - </div> - <div class="status"> - <p class="posts-count"><i class="fa fa-angle-right"></i><a>{ user.posts_count }</a><b>ポスト</b></p> - <p class="following"><i class="fa fa-angle-right"></i><a onclick={ showFollowing }>{ user.following_count }</a>人を<b>フォロー</b></p> - <p class="followers"><i class="fa fa-angle-right"></i><a onclick={ showFollowers }>{ user.followers_count }</a>人の<b>フォロワー</b></p> - </div> - <style> - :scope - display block - background #fff - - > *:first-child - border-top none !important - - > .friend-form - padding 16px - border-top solid 1px #eee - - > mk-big-follow-button - width 100% - - > .followed - margin 12px 0 0 0 - padding 0 - text-align center - line-height 24px - font-size 0.8em - color #71afc7 - background #eefaff - border-radius 4px - - > .description - padding 16px - color #555 - border-top solid 1px #eee - - > .birthday - padding 16px - color #555 - border-top solid 1px #eee - - > p - margin 0 - - > i - margin-right 8px - - > .twitter - padding 16px - color #555 - border-top solid 1px #eee - - > p - margin 0 - - > i - margin-right 8px - - > .status - padding 16px - color #555 - border-top solid 1px #eee - - > p - margin 8px 0 - - > i - margin-left 8px - margin-right 8px - - </style> - <script> - this.age = require('s-age'); - - this.mixin('i'); - - this.user = this.opts.user; - - this.showFollowing = () => { - riot.mount(document.body.appendChild(document.createElement('mk-user-following-window')), { - user: this.user - }); - }; - - this.showFollowers = () => { - riot.mount(document.body.appendChild(document.createElement('mk-user-followers-window')), { - user: this.user - }); - }; - </script> -</mk-user-profile> diff --git a/src/web/app/desktop/tags/user-timeline.tag b/src/web/app/desktop/tags/user-timeline.tag deleted file mode 100644 index 08ab47b160..0000000000 --- a/src/web/app/desktop/tags/user-timeline.tag +++ /dev/null @@ -1,136 +0,0 @@ -<mk-user-timeline> - <header> - <span data-is-active={ mode == 'default' } onclick={ setMode.bind(this, 'default') }>投稿</span><span data-is-active={ mode == 'with-replies' } onclick={ setMode.bind(this, 'with-replies') }>投稿と返信</span> - </header> - <div class="loading" if={ isLoading }> - <mk-ellipsis-icon/> - </div> - <p class="empty" if={ isEmpty }><i class="fa fa-comments-o"></i>このユーザーはまだ何も投稿していないようです。</p> - <mk-timeline ref="timeline"><yield to="footer"><i class="fa fa-moon-o" if={ !parent.moreLoading }></i><i class="fa fa-spinner fa-pulse fa-fw" if={ parent.moreLoading }></i></yield/> - <style> - :scope - display block - background #fff - - > header - padding 8px 16px - border-bottom solid 1px #eee - - > span - margin-right 16px - line-height 27px - font-size 18px - color #555 - - &:not([data-is-active]) - color $theme-color - cursor pointer - - &:hover - text-decoration underline - - > .loading - padding 64px 0 - - > .empty - display block - margin 0 auto - padding 32px - max-width 400px - text-align center - color #999 - - > i - display block - margin-bottom 16px - font-size 3em - color #ccc - - </style> - <script> - import isPromise from '../../common/scripts/is-promise'; - - this.mixin('api'); - - this.user = null; - this.userPromise = isPromise(this.opts.user) - ? this.opts.user - : Promise.resolve(this.opts.user); - this.isLoading = true; - this.isEmpty = false; - this.moreLoading = false; - this.unreadCount = 0; - this.mode = 'default'; - - this.on('mount', () => { - document.addEventListener('keydown', this.onDocumentKeydown); - window.addEventListener('scroll', this.onScroll); - - this.userPromise.then(user => { - this.update({ - user: user - }); - - this.fetch(() => this.trigger('loaded')); - }); - }); - - this.on('unmount', () => { - document.removeEventListener('keydown', this.onDocumentKeydown); - window.removeEventListener('scroll', this.onScroll); - }); - - this.onDocumentKeydown = e => { - if (e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') { - if (e.which == 84) { // [t] - this.refs.timeline.focus(); - } - } - }; - - this.fetch = cb => { - this.api('users/posts', { - user_id: this.user.id, - with_replies: this.mode == 'with-replies' - }).then(posts => { - this.update({ - isLoading: false, - isEmpty: posts.length == 0 - }); - this.refs.timeline.setPosts(posts); - if (cb) cb(); - }); - }; - - this.more = () => { - if (this.moreLoading || this.isLoading || this.refs.timeline.posts.length == 0) return; - this.update({ - moreLoading: true - }); - this.api('users/posts', { - user_id: this.user.id, - with_replies: this.mode == 'with-replies', - max_id: this.refs.timeline.tail().id - }).then(posts => { - this.update({ - moreLoading: false - }); - this.refs.timeline.prependPosts(posts); - }); - }; - - this.onScroll = () => { - const current = window.scrollY + window.innerHeight; - if (current > document.body.offsetHeight - 16/*遊び*/) { - this.more(); - } - }; - - this.setMode = mode => { - this.update({ - mode: mode - }); - this.fetch(); - }; - </script> -</mk-user-timeline> diff --git a/src/web/app/desktop/tags/user.tag b/src/web/app/desktop/tags/user.tag deleted file mode 100644 index db4fd7cc73..0000000000 --- a/src/web/app/desktop/tags/user.tag +++ /dev/null @@ -1,54 +0,0 @@ -<mk-user> - <div class="user" if={ !fetching }> - <header> - <mk-user-header user={ user }/> - </header> - <div class="body"> - <mk-user-home if={ page == 'home' } user={ user }/> - <mk-user-graphs if={ page == 'graphs' } user={ user }/> - </div> - </div> - <style> - :scope - display block - background #fff - - > .user - > header - max-width 560px + 270px - margin 0 auto - padding 0 16px - - > mk-user-header - border solid 1px rgba(0, 0, 0, 0.075) - border-top none - border-radius 0 0 6px 6px - overflow hidden - - > .body - max-width 560px + 270px - margin 0 auto - padding 0 16px - - </style> - <script> - this.mixin('api'); - - this.username = this.opts.user; - this.page = this.opts.page ? this.opts.page : 'home'; - this.fetching = true; - this.user = null; - - this.on('mount', () => { - this.api('users/show', { - username: this.username - }).then(user => { - this.update({ - fetching: false, - user: user - }); - this.trigger('loaded'); - }); - }); - </script> -</mk-user> diff --git a/src/web/app/desktop/tags/users-list.tag b/src/web/app/desktop/tags/users-list.tag deleted file mode 100644 index eb34b15a29..0000000000 --- a/src/web/app/desktop/tags/users-list.tag +++ /dev/null @@ -1,138 +0,0 @@ -<mk-users-list> - <nav> - <div> - <span data-is-active={ mode == 'all' } onclick={ setMode.bind(this, 'all') }>すべて<span>{ opts.count }</span></span> - <span if={ SIGNIN && opts.youKnowCount } data-is-active={ mode == 'iknow' } onclick={ setMode.bind(this, 'iknow') }>知り合い<span>{ opts.youKnowCount }</span></span> - </div> - </nav> - <div class="users" if={ !fetching && users.length != 0 }> - <div each={ users }> - <mk-list-user user={ this }/> - </div> - </div> - <button class="more" if={ !fetching && next != null } onclick={ more } disabled={ moreFetching }> - <span if={ !moreFetching }>もっと</span> - <span if={ moreFetching }>読み込み中<mk-ellipsis/></span> - </button> - <p class="no" if={ !fetching && users.length == 0 }>{ opts.noUsers }</p> - <p class="fetching" if={ fetching }><i class="fa fa-spinner fa-pulse fa-fw"></i>読み込んでいます<mk-ellipsis/></p> - <style> - :scope - display block - height 100% - background #fff - - > nav - z-index 1 - box-shadow 0 1px 0 rgba(#000, 0.1) - - > div - display flex - justify-content center - margin 0 auto - max-width 600px - - > span - display block - flex 1 1 - text-align center - line-height 52px - font-size 14px - color #657786 - border-bottom solid 2px transparent - cursor pointer - - * - pointer-events none - - &[data-is-active] - font-weight bold - color $theme-color - border-color $theme-color - cursor default - - > span - display inline-block - margin-left 4px - padding 2px 5px - font-size 12px - line-height 1 - color #888 - background #eee - border-radius 20px - - > .users - height calc(100% - 54px) - overflow auto - - > * - border-bottom solid 1px rgba(0, 0, 0, 0.05) - - > * - max-width 600px - margin 0 auto - - > .no - margin 0 - padding 16px - text-align center - color #aaa - - > .fetching - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - - </style> - <script> - this.mixin('i'); - - this.limit = 30; - this.mode = 'all'; - - this.fetching = true; - this.moreFetching = false; - - this.on('mount', () => { - this.fetch(() => this.trigger('loaded')); - }); - - this.fetch = cb => { - this.update({ - fetching: true - }); - this.opts.fetch(this.mode == 'iknow', this.limit, null, obj => { - this.update({ - fetching: false, - users: obj.users, - next: obj.next - }); - if (cb) cb(); - }); - }; - - this.more = () => { - this.update({ - moreFetching: true - }); - this.opts.fetch(this.mode == 'iknow', this.limit, this.cursor, obj => { - this.update({ - moreFetching: false, - users: this.users.concat(obj.users), - next: obj.next - }); - }); - }; - - this.setMode = mode => { - this.update({ - mode: mode - }); - this.fetch(); - }; - </script> -</mk-users-list> diff --git a/src/web/app/desktop/tags/window.tag b/src/web/app/desktop/tags/window.tag deleted file mode 100644 index aefb6499b7..0000000000 --- a/src/web/app/desktop/tags/window.tag +++ /dev/null @@ -1,519 +0,0 @@ -<mk-window data-flexible={ isFlexible } ondragover={ ondragover }> - <div class="bg" ref="bg" show={ isModal } onclick={ bgClick }></div> - <div class="main" ref="main" tabindex="-1" data-is-modal={ isModal } onmousedown={ onBodyMousedown } onkeydown={ onKeydown }> - <div class="body"> - <header ref="header" onmousedown={ onHeaderMousedown }> - <h1 data-yield="header"><yield from="header"/></h1> - <button class="close" if={ canClose } onmousedown={ repelMove } onclick={ close } title="閉じる"><i class="fa fa-times"></i></button> - </header> - <div class="content" data-yield="content"><yield from="content"/></div> - </div> - <div class="handle top" if={ canResize } onmousedown={ onTopHandleMousedown }></div> - <div class="handle right" if={ canResize } onmousedown={ onRightHandleMousedown }></div> - <div class="handle bottom" if={ canResize } onmousedown={ onBottomHandleMousedown }></div> - <div class="handle left" if={ canResize } onmousedown={ onLeftHandleMousedown }></div> - <div class="handle top-left" if={ canResize } onmousedown={ onTopLeftHandleMousedown }></div> - <div class="handle top-right" if={ canResize } onmousedown={ onTopRightHandleMousedown }></div> - <div class="handle bottom-right" if={ canResize } onmousedown={ onBottomRightHandleMousedown }></div> - <div class="handle bottom-left" if={ canResize } onmousedown={ onBottomLeftHandleMousedown }></div> - </div> - <style> - :scope - display block - - > .bg - display block - position fixed - z-index 2048 - top 0 - left 0 - width 100% - height 100% - background rgba(0, 0, 0, 0.7) - opacity 0 - pointer-events none - - > .main - display block - position fixed - z-index 2048 - top 15% - left 0 - margin 0 - opacity 0 - pointer-events none - - &:focus - &:not([data-is-modal]) - > .body - box-shadow 0 0 0px 1px rgba($theme-color, 0.5), 0 2px 6px 0 rgba(0, 0, 0, 0.2) - - > .handle - $size = 8px - - position absolute - - &.top - top -($size) - left 0 - width 100% - height $size - cursor ns-resize - - &.right - top 0 - right -($size) - width $size - height 100% - cursor ew-resize - - &.bottom - bottom -($size) - left 0 - width 100% - height $size - cursor ns-resize - - &.left - top 0 - left -($size) - width $size - height 100% - cursor ew-resize - - &.top-left - top -($size) - left -($size) - width $size * 2 - height $size * 2 - cursor nwse-resize - - &.top-right - top -($size) - right -($size) - width $size * 2 - height $size * 2 - cursor nesw-resize - - &.bottom-right - bottom -($size) - right -($size) - width $size * 2 - height $size * 2 - cursor nwse-resize - - &.bottom-left - bottom -($size) - left -($size) - width $size * 2 - height $size * 2 - cursor nesw-resize - - > .body - height 100% - overflow hidden - background #fff - border-radius 6px - box-shadow 0 2px 6px 0 rgba(0, 0, 0, 0.2) - - > header - z-index 128 - overflow hidden - cursor move - background #fff - border-radius 6px 6px 0 0 - box-shadow 0 1px 0 rgba(#000, 0.1) - - &, * - user-select none - - > h1 - pointer-events none - display block - margin 0 - height 40px - text-align center - font-size 1em - line-height 40px - font-weight normal - color #666 - - > .close - cursor pointer - display block - position absolute - top 0 - right 0 - z-index 1 - margin 0 - padding 0 - font-size 1.2em - color rgba(#000, 0.4) - border none - outline none - background transparent - - &:hover - color rgba(#000, 0.6) - - &:active - color darken(#000, 30%) - - > i - padding 0 - width 40px - line-height 40px - - > .content - height 100% - - &:not([flexible]) - > .main > .body > .content - height calc(100% - 40px) - - </style> - <script> - import anime from 'animejs'; - import contains from '../../common/scripts/contains'; - - this.minHeight = 40; - this.minWidth = 200; - - this.isModal = this.opts.isModal != null ? this.opts.isModal : false; - this.canClose = this.opts.canClose != null ? this.opts.canClose : true; - this.isFlexible = this.opts.height == null; - this.canResize = !this.isFlexible; - - this.on('mount', () => { - this.refs.main.style.width = this.opts.width || '530px'; - this.refs.main.style.height = this.opts.height || 'auto'; - - this.refs.main.style.top = '15%'; - this.refs.main.style.left = (window.innerWidth / 2) - (this.refs.main.offsetWidth / 2) + 'px'; - - this.refs.header.addEventListener('contextmenu', e => { - e.preventDefault(); - }); - - window.addEventListener('resize', this.onBrowserResize); - - this.open(); - }); - - this.on('unmount', () => { - window.removeEventListener('resize', this.onBrowserResize); - }); - - this.onBrowserResize = () => { - const position = this.refs.main.getBoundingClientRect(); - const browserWidth = window.innerWidth; - const browserHeight = window.innerHeight; - const windowWidth = this.refs.main.offsetWidth; - const windowHeight = this.refs.main.offsetHeight; - if (position.left < 0) this.refs.main.style.left = 0; - if (position.top < 0) this.refs.main.style.top = 0; - if (position.left + windowWidth > browserWidth) this.refs.main.style.left = browserWidth - windowWidth + 'px'; - if (position.top + windowHeight > browserHeight) this.refs.main.style.top = browserHeight - windowHeight + 'px'; - }; - - this.open = () => { - this.trigger('opening'); - - this.top(); - - if (this.isModal) { - this.refs.bg.style.pointerEvents = 'auto'; - anime({ - targets: this.refs.bg, - opacity: 1, - duration: 100, - easing: 'linear' - }); - } - - this.refs.main.style.pointerEvents = 'auto'; - anime({ - targets: this.refs.main, - opacity: 1, - scale: [1.1, 1], - duration: 200, - easing: 'easeOutQuad' - }); - - //this.refs.main.focus(); - - setTimeout(() => { - this.trigger('opened'); - }, 300); - }; - - this.close = () => { - this.trigger('closing'); - - if (this.isModal) { - this.refs.bg.style.pointerEvents = 'none'; - anime({ - targets: this.refs.bg, - opacity: 0, - duration: 300, - easing: 'linear' - }); - } - - this.refs.main.style.pointerEvents = 'none'; - - anime({ - targets: this.refs.main, - opacity: 0, - scale: 0.8, - duration: 300, - easing: [0.5, -0.5, 1, 0.5] - }); - - setTimeout(() => { - this.trigger('closed'); - }, 300); - }; - - // 最前面へ移動します - this.top = () => { - let z = 0; - - const ws = document.querySelectorAll('mk-window'); - ws.forEach(w => { - if (w == this.root) return; - const m = w.querySelector(':scope > .main'); - const mz = Number(document.defaultView.getComputedStyle(m, null).zIndex); - if (mz > z) z = mz; - }); - - if (z > 0) { - this.refs.main.style.zIndex = z + 1; - if (this.isModal) this.refs.bg.style.zIndex = z + 1; - } - }; - - this.repelMove = e => { - e.stopPropagation(); - return true; - }; - - this.bgClick = () => { - if (this.canClose) this.close(); - }; - - this.onBodyMousedown = () => { - this.top(); - }; - - // ヘッダー掴み時 - this.onHeaderMousedown = e => { - e.preventDefault(); - - if (!contains(this.refs.main, document.activeElement)) this.refs.main.focus(); - - const position = this.refs.main.getBoundingClientRect(); - - const clickX = e.clientX; - const clickY = e.clientY; - const moveBaseX = clickX - position.left; - const moveBaseY = clickY - position.top; - const browserWidth = window.innerWidth; - const browserHeight = window.innerHeight; - const windowWidth = this.refs.main.offsetWidth; - const windowHeight = this.refs.main.offsetHeight; - - // 動かした時 - dragListen(me => { - let moveLeft = me.clientX - moveBaseX; - let moveTop = me.clientY - moveBaseY; - - // 上はみ出し - if (moveTop < 0) moveTop = 0; - - // 左はみ出し - if (moveLeft < 0) moveLeft = 0; - - // 下はみ出し - if (moveTop + windowHeight > browserHeight) moveTop = browserHeight - windowHeight; - - // 右はみ出し - if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth; - - this.refs.main.style.left = moveLeft + 'px'; - this.refs.main.style.top = moveTop + 'px'; - }); - }; - - // 上ハンドル掴み時 - this.onTopHandleMousedown = e => { - e.preventDefault(); - - const base = e.clientY; - const height = parseInt(getComputedStyle(this.refs.main, '').height, 10); - const top = parseInt(getComputedStyle(this.refs.main, '').top, 10); - - // 動かした時 - dragListen(me => { - const move = me.clientY - base; - if (top + move > 0) { - if (height + -move > this.minHeight) { - this.applyTransformHeight(height + -move); - this.applyTransformTop(top + move); - } else { // 最小の高さより小さくなろうとした時 - this.applyTransformHeight(this.minHeight); - this.applyTransformTop(top + (height - this.minHeight)); - } - } else { // 上のはみ出し時 - this.applyTransformHeight(top + height); - this.applyTransformTop(0); - } - }); - }; - - // 右ハンドル掴み時 - this.onRightHandleMousedown = e => { - e.preventDefault(); - - const base = e.clientX; - const width = parseInt(getComputedStyle(this.refs.main, '').width, 10); - const left = parseInt(getComputedStyle(this.refs.main, '').left, 10); - const browserWidth = window.innerWidth; - - // 動かした時 - dragListen(me => { - const move = me.clientX - base; - if (left + width + move < browserWidth) { - if (width + move > this.minWidth) { - this.applyTransformWidth(width + move); - } else { // 最小の幅より小さくなろうとした時 - this.applyTransformWidth(this.minWidth); - } - } else { // 右のはみ出し時 - this.applyTransformWidth(browserWidth - left); - } - }); - }; - - // 下ハンドル掴み時 - this.onBottomHandleMousedown = e => { - e.preventDefault(); - - const base = e.clientY; - const height = parseInt(getComputedStyle(this.refs.main, '').height, 10); - const top = parseInt(getComputedStyle(this.refs.main, '').top, 10); - const browserHeight = window.innerHeight; - - // 動かした時 - dragListen(me => { - const move = me.clientY - base; - if (top + height + move < browserHeight) { - if (height + move > this.minHeight) { - this.applyTransformHeight(height + move); - } else { // 最小の高さより小さくなろうとした時 - this.applyTransformHeight(this.minHeight); - } - } else { // 下のはみ出し時 - this.applyTransformHeight(browserHeight - top); - } - }); - }; - - // 左ハンドル掴み時 - this.onLeftHandleMousedown = e => { - e.preventDefault(); - - const base = e.clientX; - const width = parseInt(getComputedStyle(this.refs.main, '').width, 10); - const left = parseInt(getComputedStyle(this.refs.main, '').left, 10); - - // 動かした時 - dragListen(me => { - const move = me.clientX - base; - if (left + move > 0) { - if (width + -move > this.minWidth) { - this.applyTransformWidth(width + -move); - this.applyTransformLeft(left + move); - } else { // 最小の幅より小さくなろうとした時 - this.applyTransformWidth(this.minWidth); - this.applyTransformLeft(left + (width - this.minWidth)); - } - } else { // 左のはみ出し時 - this.applyTransformWidth(left + width); - this.applyTransformLeft(0); - } - }); - }; - - // 左上ハンドル掴み時 - this.onTopLeftHandleMousedown = e => { - this.onTopHandleMousedown(e); - this.onLeftHandleMousedown(e); - }; - - // 右上ハンドル掴み時 - this.onTopRightHandleMousedown = e => { - this.onTopHandleMousedown(e); - this.onRightHandleMousedown(e); - }; - - // 右下ハンドル掴み時 - this.onBottomRightHandleMousedown = e => { - this.onBottomHandleMousedown(e); - this.onRightHandleMousedown(e); - }; - - // 左下ハンドル掴み時 - this.onBottomLeftHandleMousedown = e => { - this.onBottomHandleMousedown(e); - this.onLeftHandleMousedown(e); - }; - - // 高さを適用 - this.applyTransformHeight = height => { - this.refs.main.style.height = height + 'px'; - }; - - // 幅を適用 - this.applyTransformWidth = width => { - this.refs.main.style.width = width + 'px'; - }; - - // Y座標を適用 - this.applyTransformTop = top => { - this.refs.main.style.top = top + 'px'; - }; - - // X座標を適用 - this.applyTransformLeft = left => { - this.refs.main.style.left = left + 'px'; - }; - - function dragListen(fn) { - window.addEventListener('mousemove', fn); - window.addEventListener('mouseleave', dragClear.bind(null, fn)); - window.addEventListener('mouseup', dragClear.bind(null, fn)); - } - - function dragClear(fn) { - window.removeEventListener('mousemove', fn); - window.removeEventListener('mouseleave', dragClear); - window.removeEventListener('mouseup', dragClear); - } - - this.ondragover = e => { - e.dataTransfer.dropEffect = 'none'; - }; - - this.onKeydown = e => { - if (e.which == 27) { // Esc - if (this.canClose) { - e.preventDefault(); - e.stopPropagation(); - this.close(); - } - } - }; - - </script> -</mk-window> diff --git a/src/web/app/dev/router.js b/src/web/app/dev/router.js deleted file mode 100644 index 7fde30fa5c..0000000000 --- a/src/web/app/dev/router.js +++ /dev/null @@ -1,42 +0,0 @@ -import * as riot from 'riot'; -const route = require('page'); -let page = null; - -export default me => { - route('/', index); - route('/apps', apps); - route('/app/new', newApp); - route('/app/:app', app); - route('*', notFound); - - function index() { - mount(document.createElement('mk-index')); - } - - function apps() { - mount(document.createElement('mk-apps-page')); - } - - function newApp() { - mount(document.createElement('mk-new-app-page')); - } - - function app(ctx) { - const el = document.createElement('mk-app-page'); - el.setAttribute('app', ctx.params.app); - mount(el); - } - - function notFound() { - mount(document.createElement('mk-not-found')); - } - - // EXEC - route(); -}; - -function mount(content) { - if (page) page.unmount(); - const body = document.getElementById('app'); - page = riot.mount(body.appendChild(content))[0]; -} diff --git a/src/web/app/dev/style.styl b/src/web/app/dev/style.styl deleted file mode 100644 index 4fd537709d..0000000000 --- a/src/web/app/dev/style.styl +++ /dev/null @@ -1,4 +0,0 @@ -@import "../base" - -html - background-color #fff diff --git a/src/web/app/dev/tags/index.js b/src/web/app/dev/tags/index.js deleted file mode 100644 index 1e0c73697e..0000000000 --- a/src/web/app/dev/tags/index.js +++ /dev/null @@ -1,5 +0,0 @@ -require('./pages/index.tag'); -require('./pages/apps.tag'); -require('./pages/app.tag'); -require('./pages/new-app.tag'); -require('./new-app-form.tag'); diff --git a/src/web/app/dev/tags/new-app-form.tag b/src/web/app/dev/tags/new-app-form.tag deleted file mode 100644 index dc63145d71..0000000000 --- a/src/web/app/dev/tags/new-app-form.tag +++ /dev/null @@ -1,252 +0,0 @@ -<mk-new-app-form> - <form onsubmit={ onsubmit } autocomplete="off"> - <section class="name"> - <label> - <p class="caption">アプリケーション名</p> - <input ref="name" type="text" placeholder="ex) Misskey for iOS" autocomplete="off" required="required"/> - </label> - </section> - <section class="nid"> - <label> - <p class="caption">Named ID</p> - <input ref="nid" type="text" pattern="^[a-zA-Z0-9-]{3,30}$" placeholder="ex) misskey-for-ios" autocomplete="off" required="required" onkeyup={ onChangeNid }/> - <p class="info" if={ nidState == 'wait' } style="color:#999"><i class="fa fa-fw fa-spinner fa-pulse"></i>確認しています...</p> - <p class="info" if={ nidState == 'ok' } style="color:#3CB7B5"><i class="fa fa-fw fa-check"></i>利用できます</p> - <p class="info" if={ nidState == 'unavailable' } style="color:#FF1161"><i class="fa fa-fw fa-exclamation-triangle"></i>既に利用されています</p> - <p class="info" if={ nidState == 'error' } style="color:#FF1161"><i class="fa fa-fw fa-exclamation-triangle"></i>通信エラー</p> - <p class="info" if={ nidState == 'invalid-format' } style="color:#FF1161"><i class="fa fa-fw fa-exclamation-triangle"></i>a~z、A~Z、0~9、-(ハイフン)が使えます</p> - <p class="info" if={ nidState == 'min-range' } style="color:#FF1161"><i class="fa fa-fw fa-exclamation-triangle"></i>3文字以上でお願いします!</p> - <p class="info" if={ nidState == 'max-range' } style="color:#FF1161"><i class="fa fa-fw fa-exclamation-triangle"></i>30文字以内でお願いします</p> - </label> - </section> - <section class="description"> - <label> - <p class="caption">アプリの概要</p> - <textarea ref="description" placeholder="ex) Misskey iOSクライアント。" autocomplete="off" required="required"></textarea> - </label> - </section> - <section class="callback"> - <label> - <p class="caption">コールバックURL (オプション)</p> - <input ref="cb" type="url" placeholder="ex) https://your.app.example.com/callback.php" autocomplete="off"/> - </label> - </section> - <section class="permission"> - <p class="caption">権限</p> - <div ref="permission"> - <label> - <input type="checkbox" value="account-read"/> - <p>アカウントの情報を見る。</p> - </label> - <label> - <input type="checkbox" value="account-write"/> - <p>アカウントの情報を操作する。</p> - </label> - <label> - <input type="checkbox" value="post-write"/> - <p>投稿する。</p> - </label> - <label> - <input type="checkbox" value="reaction-write"/> - <p>リアクションしたりリアクションをキャンセルする。</p> - </label> - <label> - <input type="checkbox" value="following-write"/> - <p>フォローしたりフォロー解除する。</p> - </label> - <label> - <input type="checkbox" value="drive-read"/> - <p>ドライブを見る。</p> - </label> - <label> - <input type="checkbox" value="drive-write"/> - <p>ドライブを操作する。</p> - </label> - <label> - <input type="checkbox" value="notification-read"/> - <p>通知を見る。</p> - </label> - <label> - <input type="checkbox" value="notification-write"/> - <p>通知を操作する。</p> - </label> - </div> - <p><i class="fa fa-exclamation-triangle"></i>アプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーキーはすべて無効になります。</p> - </section> - <button onclick={ onsubmit }>アプリ作成</button> - </form> - <style> - :scope - display block - overflow hidden - - > form - - section - display block - margin 16px 0 - - .caption - margin 0 0 4px 0 - color #616161 - font-size 0.95em - - > i - margin-right 0.25em - color #96adac - - .info - display block - margin 4px 0 - font-size 0.8em - - > i - margin-right 0.3em - - section.permission - div - padding 8px 0 - max-height 160px - overflow auto - background #fff - border solid 1px #cecece - border-radius 4px - - label - display block - padding 0 12px - line-height 32px - cursor pointer - - &:hover - > p - color #999 - - [type='checkbox']:checked + p - color #000 - - [type='checkbox'] - margin-right 4px - - [type='checkbox']:checked + p - color #111 - - > p - display inline - color #aaa - user-select none - - > p:last-child - margin 6px - font-size 0.8em - color #999 - - > i - margin-right 4px - - [type=text] - [type=url] - textarea - user-select text - display inline-block - cursor auto - padding 8px 12px - margin 0 - width 100% - font-size 1em - color #333 - background #fff - outline none - border solid 1px #cecece - border-radius 4px - - &:hover - border-color #bbb - - &:focus - border-color $theme-color - - &:disabled - opacity 0.5 - - > button - margin 20px 0 32px 0 - width 100% - font-size 1em - color #111 - border-radius 3px - - </style> - <script> - this.mixin('api'); - - this.nidState = null; - - this.onChangeNid = () => { - const nid = this.refs.nid.value; - - if (nid == '') { - this.update({ - nidState: null - }); - return; - } - - const err = - !nid.match(/^[a-zA-Z0-9\-]+$/) ? 'invalid-format' : - nid.length < 3 ? 'min-range' : - nid.length > 30 ? 'max-range' : - null; - - if (err) { - this.update({ - nidState: err - }); - return; - } - - this.update({ - nidState: 'wait' - }); - - this.api('app/name_id/available', { - name_id: nid - }).then(result => { - this.update({ - nidState: result.available ? 'ok' : 'unavailable' - }); - }).catch(err => { - this.update({ - nidState: 'error' - }); - }); - }; - - this.onsubmit = () => { - const name = this.refs.name.value; - const nid = this.refs.nid.value; - const description = this.refs.description.value; - const cb = this.refs.cb.value; - const permission = []; - - this.refs.permission.querySelectorAll('input').forEach(el => { - if (el.checked) permission.push(el.value); - }); - - const locker = document.body.appendChild(document.createElement('mk-locker')); - - this.api('app/create', { - name: name, - name_id: nid, - description: description, - callback_url: cb, - permission: permission - }).then(() => { - location.href = '/apps'; - }).catch(() => { - alert('アプリの作成に失敗しました。再度お試しください。'); - locker.parentNode.removeChild(locker); - }); - }; - </script> -</mk-new-app-form> diff --git a/src/web/app/dev/tags/pages/app.tag b/src/web/app/dev/tags/pages/app.tag deleted file mode 100644 index b25e0d8595..0000000000 --- a/src/web/app/dev/tags/pages/app.tag +++ /dev/null @@ -1,32 +0,0 @@ -<mk-app-page> - <p if={ fetching }>読み込み中</p> - <main if={ !fetching }> - <header> - <h1>{ app.name }</h1> - </header> - <div class="body"> - <p>App Secret</p> - <input value={ app.secret } readonly="readonly"/> - </div> - </main> - <style> - :scope - display block - </style> - <script> - this.mixin('api'); - - this.fetching = true; - - this.on('mount', () => { - this.api('app/show', { - app_id: this.opts.app - }).then(app => { - this.update({ - fetching: false, - app: app - }); - }); - }); - </script> -</mk-app-page> diff --git a/src/web/app/dev/tags/pages/apps.tag b/src/web/app/dev/tags/pages/apps.tag deleted file mode 100644 index 43db70fcf2..0000000000 --- a/src/web/app/dev/tags/pages/apps.tag +++ /dev/null @@ -1,33 +0,0 @@ -<mk-apps-page> - <h1>アプリを管理</h1><a href="/app/new">アプリ作成</a> - <div class="apps"> - <p if={ fetching }>読み込み中</p> - <virtual if={ !fetching }> - <p if={ apps.length == 0 }>アプリなし</p> - <ul if={ apps.length > 0 }> - <li each={ app in apps }><a href={ '/app/' + app.id }> - <p class="name">{ app.name }</p></a></li> - </ul> - </virtual> - </div> - <style> - :scope - display block - </style> - <script> - this.mixin('api'); - - this.fetching = true; - - this.on('mount', () => { - this.api('my/apps').then(apps => { - this.fetching = false - this.apps = apps - this.update({ - fetching: false, - apps: apps - }); - }); - }); - </script> -</mk-apps-page> diff --git a/src/web/app/dev/tags/pages/index.tag b/src/web/app/dev/tags/pages/index.tag deleted file mode 100644 index f863876fa7..0000000000 --- a/src/web/app/dev/tags/pages/index.tag +++ /dev/null @@ -1,6 +0,0 @@ -<mk-index><a href="/apps">アプリ</a> - <style> - :scope - display block - </style> -</mk-index> diff --git a/src/web/app/dev/tags/pages/new-app.tag b/src/web/app/dev/tags/pages/new-app.tag deleted file mode 100644 index 238b6865e1..0000000000 --- a/src/web/app/dev/tags/pages/new-app.tag +++ /dev/null @@ -1,42 +0,0 @@ -<mk-new-app-page> - <main> - <header> - <h1>新しいアプリを作成</h1> - <p>MisskeyのAPIを利用したアプリケーションを作成できます。</p> - </header> - <mk-new-app-form/> - </main> - <style> - :scope - display block - padding 64px 0 - - > main - width 100% - max-width 700px - margin 0 auto - - > header - margin 0 0 16px 0 - padding 0 0 16px 0 - border-bottom solid 1px #282827 - - > h1 - margin 0 0 12px 0 - padding 0 - line-height 32px - font-size 32px - font-weight normal - color #000 - - > p - margin 0 - line-height 16px - color #9a9894 - - - - - - </style> -</mk-new-app-page> diff --git a/src/web/app/init.js b/src/web/app/init.js deleted file mode 100644 index 44391b8fcb..0000000000 --- a/src/web/app/init.js +++ /dev/null @@ -1,193 +0,0 @@ -/** - * App initializer - */ - -"use strict"; - -import * as riot from 'riot'; -import api from './common/scripts/api'; -import signout from './common/scripts/signout'; -import checkForUpdate from './common/scripts/check-for-update'; -import Connection from './common/scripts/home-stream'; -import Progress from './common/scripts/loading'; -import mixin from './common/mixins'; -import generateDefaultUserdata from './common/scripts/generate-default-userdata'; -import CONFIG from './common/scripts/config'; -require('./common/tags'); - -/** - * APP ENTRY POINT! - */ - -console.info(`Misskey v${VERSION}`); - -document.domain = CONFIG.host; - -// Set global configuration -riot.mixin({ CONFIG }); - -// ↓ NodeList、HTMLCollection、FileList、DataTransferItemListで forEach を使えるようにする -if (NodeList.prototype.forEach === undefined) { - NodeList.prototype.forEach = Array.prototype.forEach; -} -if (HTMLCollection.prototype.forEach === undefined) { - HTMLCollection.prototype.forEach = Array.prototype.forEach; -} -if (FileList.prototype.forEach === undefined) { - FileList.prototype.forEach = Array.prototype.forEach; -} -if (window.DataTransferItemList && DataTransferItemList.prototype.forEach === undefined) { - DataTransferItemList.prototype.forEach = Array.prototype.forEach; -} - -// iOSでプライベートモードだとlocalStorageが使えないので既存のメソッドを上書きする -try { - localStorage.setItem('kyoppie', 'yuppie'); -} catch (e) { - Storage.prototype.setItem = () => { }; // noop -} - -// クライアントを更新すべきならする -if (localStorage.getItem('should-refresh') == 'true') { - localStorage.removeItem('should-refresh'); - location.reload(true); -} - -// 更新チェック -setTimeout(checkForUpdate, 3000); - -// ユーザーをフェッチしてコールバックする -export default callback => { - // Get cached account data - let cachedMe = JSON.parse(localStorage.getItem('me')); - - if (cachedMe) { - fetched(cachedMe); - - // 後から新鮮なデータをフェッチ - fetchme(cachedMe.token, freshData => { - Object.assign(cachedMe, freshData); - cachedMe.trigger('updated'); - }); - } else { - // Get token from cookie - const i = (document.cookie.match(/i=(!\w+)/) || [null, null])[1]; - - fetchme(i, fetched); - } - - // フェッチが完了したとき - function fetched(me) { - if (me) { - riot.observable(me); - - // この me オブジェクトを更新するメソッド - me.update = data => { - if (data) Object.assign(me, data); - me.trigger('updated'); - }; - - // ローカルストレージにキャッシュ - localStorage.setItem('me', JSON.stringify(me)); - - me.on('updated', () => { - // キャッシュ更新 - localStorage.setItem('me', JSON.stringify(me)); - }); - } - - // Init home stream connection - const stream = me ? new Connection(me) : null; - - // ミックスイン初期化 - mixin(me, stream); - - // ローディング画面クリア - const ini = document.getElementById('ini'); - ini.parentNode.removeChild(ini); - - // アプリ基底要素マウント - const app = document.createElement('div'); - app.setAttribute('id', 'app'); - document.body.appendChild(app); - - try { - callback(me, stream); - } catch (e) { - panic(e); - } - } -}; - -// ユーザーをフェッチしてコールバックする -function fetchme(token, cb) { - let me = null; - - // Return when not signed in - if (token == null) { - return done(); - } - - // Fetch user - fetch(`${CONFIG.apiUrl}/i`, { - method: 'POST', - body: JSON.stringify({ - i: token - }) - }).then(res => { // When success - // When failed to authenticate user - if (res.status !== 200) { - return signout(); - } - - res.json().then(i => { - me = i; - me.token = token; - - // initialize it if user data is empty - me.data ? done() : init(); - }); - }, () => { // When failure - // Render the error screen - document.body.innerHTML = '<mk-error />'; - riot.mount('*'); - Progress.done(); - }); - - function done() { - if (cb) cb(me); - } - - // Initialize user data - function init() { - const data = generateDefaultUserdata(); - api(token, 'i/appdata/set', { - data - }).then(() => { - me.data = data; - done(); - }); - } -} - -// BSoD -function panic(e) { - console.error(e); - - // Display blue screen - document.documentElement.style.background = '#1269e2'; - document.body.innerHTML = - '<div id="error">' - + '<h1>:( 致命的な問題が発生しました。</h1>' - + '<p>お使いのブラウザ(またはOS)のバージョンを更新すると解決する可能性があります。</p>' - + '<hr>' - + `<p>エラーコード: ${e.toString()}</p>` - + `<p>ブラウザ バージョン: ${navigator.userAgent}</p>` - + `<p>クライアント バージョン: ${VERSION}</p>` - + '<hr>' - + '<p>問題が解決しない場合は、上記の情報をお書き添えの上 syuilotan@yahoo.co.jp までご連絡ください。</p>' - + '<p>Thank you for using Misskey.</p>' - + '</div>'; - - // TODO: Report the bug -} diff --git a/src/web/app/mobile/router.js b/src/web/app/mobile/router.js deleted file mode 100644 index d0b45d9614..0000000000 --- a/src/web/app/mobile/router.js +++ /dev/null @@ -1,136 +0,0 @@ -/** - * Mobile App Router - */ - -import * as riot from 'riot'; -const route = require('page'); -let page = null; - -export default me => { - route('/', index); - route('/i/notifications', notifications); - route('/i/messaging', messaging); - route('/i/messaging/:username', messaging); - route('/i/drive', drive); - route('/i/drive/folder/:folder', drive); - route('/i/drive/file/:file', drive); - route('/i/settings', settings); - route('/i/settings/signin-history', settingsSignin); - route('/i/settings/api', settingsApi); - route('/i/settings/twitter', settingsTwitter); - route('/i/settings/authorized-apps', settingsAuthorizedApps); - route('/post/new', newPost); - route('/post::post', post); - route('/search::query', search); - route('/:user', user.bind(null, 'posts')); - route('/:user/graphs', user.bind(null, 'graphs')); - route('/:user/followers', userFollowers); - route('/:user/following', userFollowing); - route('/:user/:post', post); - route('*', notFound); - - function index() { - me ? home() : entrance(); - } - - function home() { - mount(document.createElement('mk-home-page')); - } - - function entrance() { - mount(document.createElement('mk-entrance')); - } - - function notifications() { - mount(document.createElement('mk-notifications-page')); - } - - function messaging(ctx) { - if (ctx.params.username) { - const el = document.createElement('mk-messaging-room-page'); - el.setAttribute('username', ctx.params.username); - mount(el); - } else { - mount(document.createElement('mk-messaging-page')); - } - } - - function newPost() { - mount(document.createElement('mk-new-post-page')); - } - - function settings() { - mount(document.createElement('mk-settings-page')); - } - - function settingsSignin() { - mount(document.createElement('mk-signin-history-page')); - } - - function settingsApi() { - mount(document.createElement('mk-api-info-page')); - } - - function settingsTwitter() { - mount(document.createElement('mk-twitter-setting-page')); - } - - function settingsAuthorizedApps() { - mount(document.createElement('mk-authorized-apps-page')); - } - - function search(ctx) { - const el = document.createElement('mk-search-page'); - el.setAttribute('query', ctx.params.query); - mount(el); - } - - function user(page, ctx) { - const el = document.createElement('mk-user-page'); - el.setAttribute('user', ctx.params.user); - el.setAttribute('page', page); - mount(el); - } - - function userFollowing(ctx) { - const el = document.createElement('mk-user-following-page'); - el.setAttribute('user', ctx.params.user); - mount(el); - } - - function userFollowers(ctx) { - const el = document.createElement('mk-user-followers-page'); - el.setAttribute('user', ctx.params.user); - mount(el); - } - - function post(ctx) { - const el = document.createElement('mk-post-page'); - el.setAttribute('post', ctx.params.post); - mount(el); - } - - function drive(ctx) { - const el = document.createElement('mk-drive-page'); - if (ctx.params.folder) el.setAttribute('folder', ctx.params.folder); - if (ctx.params.file) el.setAttribute('file', ctx.params.file); - mount(el); - } - - function notFound() { - mount(document.createElement('mk-not-found')); - } - - riot.mixin('page', { - page: route - }); - - // EXEC - route(); -}; - -function mount(content) { - if (page) page.unmount(); - const body = document.getElementById('app'); - page = riot.mount(body.appendChild(content))[0]; -} diff --git a/src/web/app/mobile/script.js b/src/web/app/mobile/script.js deleted file mode 100644 index 503e0fd673..0000000000 --- a/src/web/app/mobile/script.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Mobile Client - */ - -// Style -import './style.styl'; - -require('./tags'); -import init from '../init'; -import route from './router'; - -/** - * init - */ -init(me => { - // http://qiita.com/junya/items/3ff380878f26ca447f85 - document.body.setAttribute('ontouchstart', ''); - - // Start routing - route(me); -}); diff --git a/src/web/app/mobile/scripts/open-post-form.js b/src/web/app/mobile/scripts/open-post-form.js deleted file mode 100644 index e0fae4d8ca..0000000000 --- a/src/web/app/mobile/scripts/open-post-form.js +++ /dev/null @@ -1,15 +0,0 @@ -import * as riot from 'riot'; - -export default opts => { - const app = document.getElementById('app'); - app.style.display = 'none'; - - function recover() { - app.style.display = 'block'; - } - - const form = riot.mount(document.body.appendChild(document.createElement('mk-post-form')), opts)[0]; - form - .on('cancel', recover) - .on('post', recover); -}; diff --git a/src/web/app/mobile/scripts/ui-event.js b/src/web/app/mobile/scripts/ui-event.js deleted file mode 100644 index 2e406549a4..0000000000 --- a/src/web/app/mobile/scripts/ui-event.js +++ /dev/null @@ -1,5 +0,0 @@ -import * as riot from 'riot'; - -const ev = riot.observable(); - -export default ev; diff --git a/src/web/app/mobile/style.styl b/src/web/app/mobile/style.styl deleted file mode 100644 index bd6965e402..0000000000 --- a/src/web/app/mobile/style.styl +++ /dev/null @@ -1,6 +0,0 @@ -@import "../base" - -#wait - top auto - bottom 15px - left 15px diff --git a/src/web/app/mobile/tags/drive-folder-selector.tag b/src/web/app/mobile/tags/drive-folder-selector.tag deleted file mode 100644 index eebd62df6c..0000000000 --- a/src/web/app/mobile/tags/drive-folder-selector.tag +++ /dev/null @@ -1,69 +0,0 @@ -<mk-drive-folder-selector> - <div class="body"> - <header> - <h1>%i18n:mobile.tags.mk-drive-folder-selector.select-folder%</h1> - <button class="close" onclick={ cancel }><i class="fa fa-times"></i></button> - <button class="ok" onclick={ ok }><i class="fa fa-check"></i></button> - </header> - <mk-drive ref="browser" select-folder={ true }/> - </div> - <style> - :scope - display block - position fixed - z-index 2048 - top 0 - left 0 - width 100% - height 100% - padding 8px - background rgba(0, 0, 0, 0.2) - - > .body - width 100% - height 100% - background #fff - - > header - border-bottom solid 1px #eee - - > h1 - margin 0 - padding 0 - text-align center - line-height 42px - font-size 1em - font-weight normal - - > .close - position absolute - top 0 - left 0 - line-height 42px - width 42px - - > .ok - position absolute - top 0 - right 0 - line-height 42px - width 42px - - > mk-drive - height calc(100% - 42px) - overflow scroll - -webkit-overflow-scrolling touch - - </style> - <script> - this.cancel = () => { - this.trigger('canceled'); - this.unmount(); - }; - - this.ok = () => { - this.trigger('selected', this.refs.browser.folder); - this.unmount(); - }; - </script> -</mk-drive-folder-selector> diff --git a/src/web/app/mobile/tags/drive-selector.tag b/src/web/app/mobile/tags/drive-selector.tag deleted file mode 100644 index 32845432f2..0000000000 --- a/src/web/app/mobile/tags/drive-selector.tag +++ /dev/null @@ -1,83 +0,0 @@ -<mk-drive-selector> - <div class="body"> - <header> - <h1>%i18n:mobile.tags.mk-drive-selector.select-file%<span class="count" if={ files.length > 0 }>({ files.length })</span></h1> - <button class="close" onclick={ cancel }><i class="fa fa-times"></i></button> - <button class="ok" onclick={ ok }><i class="fa fa-check"></i></button> - </header> - <mk-drive ref="browser" select-file={ true } multiple={ opts.multiple }/> - </div> - <style> - :scope - display block - position fixed - z-index 2048 - top 0 - left 0 - width 100% - height 100% - padding 8px - background rgba(0, 0, 0, 0.2) - - > .body - width 100% - height 100% - background #fff - - > header - border-bottom solid 1px #eee - - > h1 - margin 0 - padding 0 - text-align center - line-height 42px - font-size 1em - font-weight normal - - > .count - margin-left 4px - opacity 0.5 - - > .close - position absolute - top 0 - left 0 - line-height 42px - width 42px - - > .ok - position absolute - top 0 - right 0 - line-height 42px - width 42px - - > mk-drive - height calc(100% - 42px) - overflow scroll - -webkit-overflow-scrolling touch - - </style> - <script> - this.files = []; - - this.on('mount', () => { - this.refs.browser.on('change-selection', files => { - this.update({ - files: files - }); - }); - }); - - this.cancel = () => { - this.trigger('canceled'); - this.unmount(); - }; - - this.ok = () => { - this.trigger('selected', this.files); - this.unmount(); - }; - </script> -</mk-drive-selector> diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag deleted file mode 100644 index e19325091d..0000000000 --- a/src/web/app/mobile/tags/drive.tag +++ /dev/null @@ -1,559 +0,0 @@ -<mk-drive> - <nav> - <p onclick={ goRoot }><i class="fa fa-cloud"></i>%i18n:mobile.tags.mk-drive.drive%</p> - <virtual each={ folder in hierarchyFolders }> - <span><i class="fa fa-angle-right"></i></span> - <p onclick={ move }>{ folder.name }</p> - </virtual> - <virtual if={ folder != null }> - <span><i class="fa fa-angle-right"></i></span> - <p>{ folder.name }</p> - </virtual> - <virtual if={ file != null }> - <span><i class="fa fa-angle-right"></i></span> - <p>{ file.name }</p> - </virtual> - </nav> - <mk-uploader ref="uploader"/> - <div class="browser { fetching: fetching }" if={ file == null }> - <div class="info" if={ info }> - <p if={ folder == null }>{ (info.usage / info.capacity * 100).toFixed(1) }% %i18n:mobile.tags.mk-drive.used%</p> - <p if={ folder != null && (folder.folders_count > 0 || folder.files_count > 0) }> - <virtual if={ folder.folders_count > 0 }>{ folder.folders_count } %i18n:mobile.tags.mk-drive.folder-count%</virtual> - <virtual if={ folder.folders_count > 0 && folder.files_count > 0 }>%i18n:mobile.tags.mk-drive.count-separator%</virtual> - <virtual if={ folder.files_count > 0 }>{ folder.files_count } %i18n:mobile.tags.mk-drive.file-count%</virtual> - </p> - </div> - <div class="folders" if={ folders.length > 0 }> - <virtual each={ folder in folders }> - <mk-drive-folder folder={ folder }/> - </virtual> - <p if={ moreFolders }>%i18n:mobile.tags.mk-drive.load-more%</p> - </div> - <div class="files" if={ files.length > 0 }> - <virtual each={ file in files }> - <mk-drive-file file={ file }/> - </virtual> - <button class="more" if={ moreFiles } onclick={ fetchMoreFiles }> - { fetchingMoreFiles ? '%i18n:common.loading%' : '%i18n:mobile.tags.mk-drive.load-more%' } - </button> - </div> - <div class="empty" if={ files.length == 0 && folders.length == 0 && !fetching }> - <p if={ folder == null }>%i18n:mobile.tags.mk-drive.nothing-in-drive%</p> - <p if={ folder != null }>%i18n:mobile.tags.mk-drive.folder-is-empty%</p> - </div> - </div> - <div class="fetching" if={ fetching && file == null && files.length == 0 && folders.length == 0 }> - <div class="spinner"> - <div class="dot1"></div> - <div class="dot2"></div> - </div> - </div> - <input ref="file" type="file" multiple="multiple" onchange={ changeLocalFile }/> - <mk-drive-file-viewer if={ file != null } file={ file }/> - <style> - :scope - display block - background #fff - - &[data-is-naked] - > nav - top 48px - - > nav - display block - position sticky - position -webkit-sticky - top 0 - z-index 1 - width 100% - padding 10px 12px - overflow auto - white-space nowrap - font-size 0.9em - color rgba(0, 0, 0, 0.67) - -webkit-backdrop-filter blur(12px) - backdrop-filter blur(12px) - background-color rgba(#fff, 0.75) - border-bottom solid 1px rgba(0, 0, 0, 0.13) - - > p - display inline - margin 0 - padding 0 - - &:last-child - font-weight bold - - > i - margin-right 4px - - > span - margin 0 8px - opacity 0.5 - - > .browser - &.fetching - opacity 0.5 - - > .info - border-bottom solid 1px #eee - - &:empty - display none - - > p - display block - max-width 500px - margin 0 auto - padding 4px 16px - font-size 10px - color #777 - - > .folders - > mk-drive-folder - border-bottom solid 1px #eee - - > .files - > mk-drive-file - border-bottom solid 1px #eee - - > .more - display block - width 100% - padding 16px - font-size 16px - color #555 - - > .empty - padding 16px - text-align center - color #999 - pointer-events none - - > p - margin 0 - - > .fetching - .spinner - margin 100px auto - width 40px - height 40px - text-align center - - animation sk-rotate 2.0s infinite linear - - .dot1, .dot2 - width 60% - height 60% - display inline-block - position absolute - top 0 - background rgba(0, 0, 0, 0.2) - border-radius 100% - - animation sk-bounce 2.0s infinite ease-in-out - - .dot2 - top auto - bottom 0 - animation-delay -1.0s - - @keyframes sk-rotate { 100% { transform: rotate(360deg); }} - - @keyframes sk-bounce { - 0%, 100% { - transform: scale(0.0); - } 50% { - transform: scale(1.0); - } - } - - > [ref='file'] - display none - - </style> - <script> - this.mixin('i'); - this.mixin('api'); - this.mixin('stream'); - - this.files = []; - this.folders = []; - this.hierarchyFolders = []; - this.selectedFiles = []; - - // 現在の階層(フォルダ) - // * null でルートを表す - this.folder = null; - - this.file = null; - - this.isFileSelectMode = this.opts.selectFile; - this.multiple =this.opts.multiple; - - this.on('mount', () => { - this.stream.on('drive_file_created', this.onStreamDriveFileCreated); - this.stream.on('drive_file_updated', this.onStreamDriveFileUpdated); - this.stream.on('drive_folder_created', this.onStreamDriveFolderCreated); - this.stream.on('drive_folder_updated', this.onStreamDriveFolderUpdated); - - if (this.opts.folder) { - this.cd(this.opts.folder, true); - } else if (this.opts.file) { - this.cf(this.opts.file, true); - } else { - this.fetch(); - } - }); - - this.on('unmount', () => { - this.stream.off('drive_file_created', this.onStreamDriveFileCreated); - this.stream.off('drive_file_updated', this.onStreamDriveFileUpdated); - this.stream.off('drive_folder_created', this.onStreamDriveFolderCreated); - this.stream.off('drive_folder_updated', this.onStreamDriveFolderUpdated); - }); - - this.onStreamDriveFileCreated = file => { - this.addFile(file, true); - }; - - this.onStreamDriveFileUpdated = file => { - const current = this.folder ? this.folder.id : null; - if (current != file.folder_id) { - this.removeFile(file); - } else { - this.addFile(file, true); - } - }; - - this.onStreamDriveFolderCreated = folder => { - this.addFolder(folder, true); - }; - - this.onStreamDriveFolderUpdated = folder => { - const current = this.folder ? this.folder.id : null; - if (current != folder.parent_id) { - this.removeFolder(folder); - } else { - this.addFolder(folder, true); - } - }; - - this.move = ev => { - this.cd(ev.item.folder); - }; - - this.cd = (target, silent = false) => { - this.file = null; - - if (target == null) { - this.goRoot(); - return; - } else if (typeof target == 'object') target = target.id; - - this.update({ - fetching: true - }); - - this.api('drive/folders/show', { - folder_id: target - }).then(folder => { - this.folder = folder; - this.hierarchyFolders = []; - - if (folder.parent) dive(folder.parent); - - this.update(); - this.trigger('open-folder', this.folder, silent); - this.fetch(); - }); - }; - - this.addFolder = (folder, unshift = false) => { - const current = this.folder ? this.folder.id : null; - // 追加しようとしているフォルダが、今居る階層とは違う階層のものだったら中断 - if (current != folder.parent_id) return; - - // 追加しようとしているフォルダを既に所有してたら中断 - if (this.folders.some(f => f.id == folder.id)) return; - - if (unshift) { - this.folders.unshift(folder); - } else { - this.folders.push(folder); - } - - this.update(); - }; - - this.addFile = (file, unshift = false) => { - const current = this.folder ? this.folder.id : null; - // 追加しようとしているファイルが、今居る階層とは違う階層のものだったら中断 - if (current != file.folder_id) return; - - if (this.files.some(f => f.id == file.id)) { - const exist = this.files.map(f => f.id).indexOf(file.id); - this.files[exist] = file; - this.update(); - return; - } - - if (unshift) { - this.files.unshift(file); - } else { - this.files.push(file); - } - - this.update(); - }; - - this.removeFolder = folder => { - if (typeof folder == 'object') folder = folder.id; - this.folders = this.folders.filter(f => f.id != folder); - this.update(); - }; - - this.removeFile = file => { - if (typeof file == 'object') file = file.id; - this.files = this.files.filter(f => f.id != file); - this.update(); - }; - - this.appendFile = file => this.addFile(file); - this.appendFolder = file => this.addFolder(file); - this.prependFile = file => this.addFile(file, true); - this.prependFolder = file => this.addFolder(file, true); - - this.goRoot = () => { - if (this.folder || this.file) { - this.update({ - file: null, - folder: null, - hierarchyFolders: [] - }); - this.trigger('move-root'); - this.fetch(); - } - }; - - this.fetch = () => { - this.update({ - folders: [], - files: [], - moreFolders: false, - moreFiles: false, - fetching: true - }); - - this.trigger('begin-fetch'); - - let fetchedFolders = null; - let fetchedFiles = null; - - const foldersMax = 20; - const filesMax = 20; - - // フォルダ一覧取得 - this.api('drive/folders', { - folder_id: this.folder ? this.folder.id : null, - limit: foldersMax + 1 - }).then(folders => { - if (folders.length == foldersMax + 1) { - this.moreFolders = true; - folders.pop(); - } - fetchedFolders = folders; - complete(); - }); - - // ファイル一覧取得 - this.api('drive/files', { - folder_id: this.folder ? this.folder.id : null, - limit: filesMax + 1 - }).then(files => { - if (files.length == filesMax + 1) { - this.moreFiles = true; - files.pop(); - } - fetchedFiles = files; - complete(); - }); - - let flag = false; - const complete = () => { - if (flag) { - fetchedFolders.forEach(this.appendFolder); - fetchedFiles.forEach(this.appendFile); - this.update({ - fetching: false - }); - // 一連の読み込みが完了したイベントを発行 - this.trigger('fetched'); - } else { - flag = true; - // 一連の読み込みが半分完了したイベントを発行 - this.trigger('fetch-mid'); - } - }; - - if (this.folder == null) { - // Fetch addtional drive info - this.api('drive').then(info => { - this.update({ info }); - }); - } - }; - - this.fetchMoreFiles = () => { - this.update({ - fetching: true, - fetchingMoreFiles: true - }); - - const max = 30; - - // ファイル一覧取得 - this.api('drive/files', { - folder_id: this.folder ? this.folder.id : null, - limit: max + 1, - max_id: this.files[this.files.length - 1].id - }).then(files => { - if (files.length == max + 1) { - this.moreFiles = true; - files.pop(); - } else { - this.moreFiles = false; - } - files.forEach(this.appendFile); - this.update({ - fetching: false, - fetchingMoreFiles: false - }); - }); - }; - - this.chooseFile = file => { - if (this.isFileSelectMode) { - if (this.selectedFiles.some(f => f.id == file.id)) { - this.selectedFiles = this.selectedFiles.filter(f => f.id != file.id); - } else { - this.selectedFiles.push(file); - } - this.update(); - this.trigger('change-selection', this.selectedFiles); - } else { - this.cf(file); - } - }; - - this.cf = (file, silent = false) => { - if (typeof file == 'object') file = file.id; - - this.update({ - fetching: true - }); - - this.api('drive/files/show', { - file_id: file - }).then(file => { - this.fetching = false; - this.file = file; - this.folder = null; - this.hierarchyFolders = []; - - if (file.folder) dive(file.folder); - - this.update(); - this.trigger('open-file', this.file, silent); - }); - }; - - const dive = folder => { - this.hierarchyFolders.unshift(folder); - if (folder.parent) dive(folder.parent); - }; - - this.openContextMenu = () => { - const fn = window.prompt('何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>'); - if (fn == null || fn == '') return; - switch (fn) { - case '1': - this.refs.file.click(); - break; - case '2': - this.urlUpload(); - break; - case '3': - this.createFolder(); - break; - case '4': - this.renameFolder(); - break; - case '5': - this.moveFolder(); - break; - case '6': - alert('ごめんなさい!フォルダの削除は未実装です...。'); - break; - } - }; - - this.createFolder = () => { - const name = window.prompt('フォルダー名'); - if (name == null || name == '') return; - this.api('drive/folders/create', { - name: name, - parent_id: this.folder ? this.folder.id : undefined - }).then(folder => { - this.addFolder(folder, true); - this.update(); - }); - }; - - this.renameFolder = () => { - if (this.folder == null) { - alert('現在いる場所はルートで、フォルダではないため名前の変更はできません。名前を変更したいフォルダに移動してからやってください。'); - return; - } - const name = window.prompt('フォルダー名', this.folder.name); - if (name == null || name == '') return; - this.api('drive/folders/update', { - name: name, - folder_id: this.folder.id - }).then(folder => { - this.cd(folder); - }); - }; - - this.moveFolder = () => { - if (this.folder == null) { - alert('現在いる場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。'); - return; - } - const dialog = riot.mount(document.body.appendChild(document.createElement('mk-drive-folder-selector')))[0]; - dialog.one('selected', folder => { - this.api('drive/folders/update', { - parent_id: folder ? folder.id : null, - folder_id: this.folder.id - }).then(folder => { - this.cd(folder); - }); - }); - }; - - this.urlUpload = () => { - const url = window.prompt('アップロードしたいファイルのURL'); - if (url == null || url == '') return; - this.api('drive/files/upload_from_url', { - url: url, - folder_id: this.folder ? this.folder.id : undefined - }); - alert('アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。'); - }; - - this.changeLocalFile = () => { - this.refs.file.files.forEach(f => this.refs.uploader.upload(f, this.folder)); - }; - </script> -</mk-drive> diff --git a/src/web/app/mobile/tags/drive/file-viewer.tag b/src/web/app/mobile/tags/drive/file-viewer.tag deleted file mode 100644 index e6129652b0..0000000000 --- a/src/web/app/mobile/tags/drive/file-viewer.tag +++ /dev/null @@ -1,225 +0,0 @@ -<mk-drive-file-viewer> - <div class="preview"> - <img if={ kind == 'image' } src={ file.url } alt={ file.name } title={ file.name }> - <i if={ kind != 'image' } class="fa fa-file"></i> - <footer if={ kind == 'image' }> - <span class="size"> - <span class="width">{ file.properties.width }</span> - <span class="time">×</span> - <span class="height">{ file.properties.height }</span> - <span class="px">px</span> - </span> - <span class="separator"></span> - <span class="aspect-ratio"> - <span class="width">{ file.properties.width / gcd(file.properties.width, file.properties.height) }</span> - <span class="colon">:</span> - <span class="height">{ file.properties.height / gcd(file.properties.width, file.properties.height) }</span> - </span> - </footer> - </div> - <div class="info"> - <div> - <span class="type"><mk-file-type-icon type={ file.type }/>{ file.type }</span> - <span class="separator"></span> - <span class="data-size">{ bytesToSize(file.datasize) }</span> - <span class="separator"></span> - <span class="created-at" onclick={ showCreatedAt }><i class="fa fa-clock-o"></i><mk-time time={ file.created_at }/></span> - </div> - </div> - <div class="menu"> - <div> - <a href={ file.url + '?download' } download={ file.name }> - <i class="fa fa-download"></i>%i18n:mobile.tags.mk-drive-file-viewer.download% - </a> - <button onclick={ rename }> - <i class="fa fa-pencil"></i>%i18n:mobile.tags.mk-drive-file-viewer.rename% - </button> - <button onclick={ move }> - <i class="fa fa-folder-open"></i>%i18n:mobile.tags.mk-drive-file-viewer.move% - </button> - </div> - </div> - <div class="hash"> - <div> - <p> - <i class="fa fa-hashtag"></i>%i18n:mobile.tags.mk-drive-file-viewer.hash% - </p> - <code>{ file.hash }</code> - </div> - </div> - <style> - :scope - display block - - > .preview - padding 8px - background #f0f0f0 - - > img - display block - max-width 100% - max-height 300px - margin 0 auto - box-shadow 1px 1px 4px rgba(0, 0, 0, 0.2) - - > footer - padding 8px 8px 0 8px - font-size 0.8em - color #888 - text-align center - - > .separator - display inline - padding 0 4px - - > .size - display inline - - .time - margin 0 2px - - .px - margin-left 4px - - > .aspect-ratio - display inline - opacity 0.7 - - &:before - content "(" - - &:after - content ")" - - > .info - padding 14px - font-size 0.8em - border-top solid 1px #dfdfdf - - > div - max-width 500px - margin 0 auto - - > .separator - padding 0 4px - color #cdcdcd - - > .type - > .data-size - color #9d9d9d - - > mk-file-type-icon - margin-right 4px - - > .created-at - color #bdbdbd - - > i - margin-right 2px - - > .menu - padding 14px - border-top solid 1px #dfdfdf - - > div - max-width 500px - margin 0 auto - - > * - display block - width 100% - padding 10px 16px - margin 0 0 12px 0 - color #333 - font-size 0.9em - text-align center - text-decoration none - text-shadow 0 1px 0 rgba(255, 255, 255, 0.9) - background-image linear-gradient(#fafafa, #eaeaea) - border 1px solid #ddd - border-bottom-color #cecece - border-radius 3px - - &:last-child - margin-bottom 0 - - &:active - background-color #767676 - background-image none - border-color #444 - box-shadow 0 1px 3px rgba(0, 0, 0, 0.075), inset 0 0 5px rgba(0, 0, 0, 0.2) - - > i - margin-right 4px - - > .hash - padding 14px - border-top solid 1px #dfdfdf - - > div - max-width 500px - margin 0 auto - - > p - display block - margin 0 - padding 0 - color #555 - font-size 0.9em - - > i - margin-right 4px - - > code - display block - width 100% - margin 6px 0 0 0 - padding 8px - white-space nowrap - overflow auto - font-size 0.8em - border solid 1px #dfdfdf - border-radius 2px - background #f5f5f5 - - </style> - <script> - import bytesToSize from '../../../common/scripts/bytes-to-size'; - import gcd from '../../../common/scripts/gcd'; - - this.bytesToSize = bytesToSize; - this.gcd = gcd; - - this.mixin('api'); - - this.file = this.opts.file; - this.kind = this.file.type.split('/')[0]; - - this.rename = () => { - const name = window.prompt('名前を変更', this.file.name); - if (name == null || name == '' || name == this.file.name) return; - this.api('drive/files/update', { - file_id: this.file.id, - name: name - }).then(() => { - this.parent.cf(this.file, true); - }); - }; - - this.move = () => { - const dialog = riot.mount(document.body.appendChild(document.createElement('mk-drive-folder-selector')))[0]; - dialog.one('selected', folder => { - this.api('drive/files/update', { - file_id: this.file.id, - folder_id: folder == null ? null : folder.id - }).then(() => { - this.parent.cf(this.file, true); - }); - }); - }; - - this.showCreatedAt = () => { - alert(new Date(this.file.created_at).toLocaleString()); - }; - </script> -</mk-drive-file-viewer> diff --git a/src/web/app/mobile/tags/drive/file.tag b/src/web/app/mobile/tags/drive/file.tag deleted file mode 100644 index bf51f79a5d..0000000000 --- a/src/web/app/mobile/tags/drive/file.tag +++ /dev/null @@ -1,138 +0,0 @@ -<mk-drive-file onclick={ onclick } data-is-selected={ isSelected }> - <div class="container"> - <div class="thumbnail" style={ 'background-image: url(' + file.url + '?thumbnail&size=128)' }></div> - <div class="body"> - <p class="name">{ file.name }</p> - <!-- - if file.tags.length > 0 - ul.tags - each tag in file.tags - li.tag(style={background: tag.color, color: contrast(tag.color)})= tag.name - --> - <footer> - <p class="type"><mk-file-type-icon type={ file.type }/>{ file.type }</p> - <p class="separator"></p> - <p class="data-size">{ bytesToSize(file.datasize) }</p> - <p class="separator"></p> - <p class="created-at"> - <i class="fa fa-clock-o"></i><mk-time time={ file.created_at }/> - </p> - </footer> - </div> - </div> - <style> - :scope - display block - - &, * - user-select none - - * - pointer-events none - - > .container - max-width 500px - margin 0 auto - padding 16px - - &:after - content "" - display block - clear both - - > .thumbnail - display block - float left - width 64px - height 64px - background-size cover - background-position center center - - > .body - display block - float left - width calc(100% - 74px) - margin-left 10px - - > .name - display block - margin 0 - padding 0 - font-size 0.9em - font-weight bold - color #555 - text-overflow ellipsis - overflow-wrap break-word - - > .tags - display block - margin 4px 0 0 0 - padding 0 - list-style none - font-size 0.5em - - > .tag - display inline-block - margin 0 5px 0 0 - padding 1px 5px - border-radius 2px - - > footer - display block - margin 4px 0 0 0 - font-size 0.7em - - > .separator - display inline - margin 0 - padding 0 4px - color #CDCDCD - - > .type - display inline - margin 0 - padding 0 - color #9D9D9D - - > mk-file-type-icon - margin-right 4px - - > .data-size - display inline - margin 0 - padding 0 - color #9D9D9D - - > .created-at - display inline - margin 0 - padding 0 - color #BDBDBD - - > i - margin-right 2px - - &[data-is-selected] - background $theme-color - - &, * - color #fff !important - - </style> - <script> - import bytesToSize from '../../../common/scripts/bytes-to-size'; - this.bytesToSize = bytesToSize; - - this.browser = this.parent; - this.file = this.opts.file; - this.isSelected = this.browser.selectedFiles.some(f => f.id == this.file.id); - - this.browser.on('change-selection', selections => { - this.isSelected = selections.some(f => f.id == this.file.id); - }); - - this.onclick = () => { - this.browser.chooseFile(this.file); - }; - </script> -</mk-drive-file> diff --git a/src/web/app/mobile/tags/drive/folder.tag b/src/web/app/mobile/tags/drive/folder.tag deleted file mode 100644 index 27e86662c7..0000000000 --- a/src/web/app/mobile/tags/drive/folder.tag +++ /dev/null @@ -1,47 +0,0 @@ -<mk-drive-folder onclick={ onclick }> - <div class="container"> - <p class="name"><i class="fa fa-folder"></i>{ folder.name }</p><i class="fa fa-angle-right"></i> - </div> - <style> - :scope - display block - color #777 - - &, * - user-select none - - * - pointer-events none - - > .container - max-width 500px - margin 0 auto - padding 16px - - > .name - display block - margin 0 - padding 0 - - > i - margin-right 6px - - > i - position absolute - top 0 - bottom 0 - right 8px - margin auto 0 auto 0 - width 1em - height 1em - - </style> - <script> - this.browser = this.parent; - this.folder = this.opts.folder; - - this.onclick = () => { - this.browser.cd(this.folder); - }; - </script> -</mk-drive-folder> diff --git a/src/web/app/mobile/tags/follow-button.tag b/src/web/app/mobile/tags/follow-button.tag deleted file mode 100644 index 67d580eb99..0000000000 --- a/src/web/app/mobile/tags/follow-button.tag +++ /dev/null @@ -1,123 +0,0 @@ -<mk-follow-button> - <button class={ wait: wait, follow: !user.is_following, unfollow: user.is_following } if={ !init } onclick={ onclick } disabled={ wait }><i class="fa fa-minus" if={ !wait && user.is_following }></i><i class="fa fa-plus" if={ !wait && !user.is_following }></i><i class="fa fa-spinner fa-pulse fa-fw" if={ wait }></i>{ user.is_following ? '%i18n:mobile.tags.mk-follow-button.unfollow%' : '%i18n:mobile.tags.mk-follow-button.follow%' }</button> - <div class="init" if={ init }><i class="fa fa-spinner fa-pulse fa-fw"></i></div> - <style> - :scope - display block - - > button - > .init - display block - user-select none - cursor pointer - padding 0 16px - margin 0 - height inherit - font-size 16px - outline none - border solid 1px $theme-color - border-radius 4px - - * - pointer-events none - - &.follow - color $theme-color - background transparent - - &:hover - background rgba($theme-color, 0.1) - - &:active - background rgba($theme-color, 0.2) - - &.unfollow - color $theme-color-foreground - background $theme-color - - &.wait - cursor wait !important - opacity 0.7 - - &.init - cursor wait !important - opacity 0.7 - - > i - margin-right 4px - - </style> - <script> - import isPromise from '../../common/scripts/is-promise'; - - this.mixin('i'); - this.mixin('api'); - this.mixin('stream'); - - this.user = null; - this.userPromise = isPromise(this.opts.user) - ? this.opts.user - : Promise.resolve(this.opts.user); - this.init = true; - this.wait = false; - - this.on('mount', () => { - this.userPromise.then(user => { - this.update({ - init: false, - user: user - }); - this.stream.on('follow', this.onStreamFollow); - this.stream.on('unfollow', this.onStreamUnfollow); - }); - }); - - this.on('unmount', () => { - this.stream.off('follow', this.onStreamFollow); - this.stream.off('unfollow', this.onStreamUnfollow); - }); - - this.onStreamFollow = user => { - if (user.id == this.user.id) { - this.update({ - user: user - }); - } - }; - - this.onStreamUnfollow = user => { - if (user.id == this.user.id) { - this.update({ - user: user - }); - } - }; - - this.onclick = () => { - this.wait = true; - if (this.user.is_following) { - this.api('following/delete', { - user_id: this.user.id - }).then(() => { - this.user.is_following = false; - }).catch(err => { - console.error(err); - }).then(() => { - this.wait = false; - this.update(); - }); - } else { - this.api('following/create', { - user_id: this.user.id - }).then(() => { - this.user.is_following = true; - }).catch(err => { - console.error(err); - }).then(() => { - this.wait = false; - this.update(); - }); - } - }; - </script> -</mk-follow-button> diff --git a/src/web/app/mobile/tags/home-timeline.tag b/src/web/app/mobile/tags/home-timeline.tag deleted file mode 100644 index 5d5399f322..0000000000 --- a/src/web/app/mobile/tags/home-timeline.tag +++ /dev/null @@ -1,59 +0,0 @@ -<mk-home-timeline> - <mk-init-following if={ noFollowing } /> - <mk-timeline ref="timeline" init={ init } more={ more } empty={ '%i18n:mobile.tags.mk-home-timeline.empty-timeline%' }/> - <style> - :scope - display block - - > mk-init-following - border-bottom solid 1px #eee - - </style> - <script> - this.mixin('i'); - this.mixin('api'); - this.mixin('stream'); - - this.noFollowing = this.I.following_count == 0; - - this.init = new Promise((res, rej) => { - this.api('posts/timeline').then(posts => { - res(posts); - this.trigger('loaded'); - }); - }); - - this.on('mount', () => { - this.stream.on('post', this.onStreamPost); - this.stream.on('follow', this.onStreamFollow); - this.stream.on('unfollow', this.onStreamUnfollow); - }); - - this.on('unmount', () => { - this.stream.off('post', this.onStreamPost); - this.stream.off('follow', this.onStreamFollow); - this.stream.off('unfollow', this.onStreamUnfollow); - }); - - this.more = () => { - return this.api('posts/timeline', { - max_id: this.refs.timeline.tail().id - }); - }; - - this.onStreamPost = post => { - this.update({ - isEmpty: false - }); - this.refs.timeline.addPost(post); - }; - - this.onStreamFollow = () => { - this.fetch(); - }; - - this.onStreamUnfollow = () => { - this.fetch(); - }; - </script> -</mk-home-timeline> diff --git a/src/web/app/mobile/tags/home.tag b/src/web/app/mobile/tags/home.tag deleted file mode 100644 index 48b5a67c38..0000000000 --- a/src/web/app/mobile/tags/home.tag +++ /dev/null @@ -1,22 +0,0 @@ -<mk-home> - <mk-home-timeline ref="tl"/> - <style> - :scope - display block - - > mk-home-timeline - max-width 600px - margin 0 auto - - @media (min-width 500px) - padding 16px - - </style> - <script> - this.on('mount', () => { - this.refs.tl.on('loaded', () => { - this.trigger('loaded'); - }); - }); - </script> -</mk-home> diff --git a/src/web/app/mobile/tags/images-viewer.tag b/src/web/app/mobile/tags/images-viewer.tag deleted file mode 100644 index 8ef4a50be0..0000000000 --- a/src/web/app/mobile/tags/images-viewer.tag +++ /dev/null @@ -1,26 +0,0 @@ -<mk-images-viewer> - <div class="image" ref="view" onclick={ click }><img ref="img" src={ image.url + '?thumbnail&size=512' } alt={ image.name } title={ image.name }/></div> - <style> - :scope - display block - overflow hidden - border-radius 4px - - > .image - - > img - display block - max-height 256px - max-width 100% - margin 0 auto - - </style> - <script> - this.images = this.opts.images; - this.image = this.images[0]; - - this.click = () => { - window.open(this.image.url); - }; - </script> -</mk-images-viewer> diff --git a/src/web/app/mobile/tags/index.js b/src/web/app/mobile/tags/index.js deleted file mode 100644 index 02d1541fcd..0000000000 --- a/src/web/app/mobile/tags/index.js +++ /dev/null @@ -1,52 +0,0 @@ -require('./ui.tag'); -require('./ui-header.tag'); -require('./ui-nav.tag'); -require('./page/entrance.tag'); -require('./page/entrance/signin.tag'); -require('./page/entrance/signup.tag'); -require('./page/home.tag'); -require('./page/drive.tag'); -require('./page/notifications.tag'); -require('./page/user.tag'); -require('./page/user-followers.tag'); -require('./page/user-following.tag'); -require('./page/post.tag'); -require('./page/new-post.tag'); -require('./page/search.tag'); -require('./page/settings.tag'); -require('./page/settings/signin.tag'); -require('./page/settings/api.tag'); -require('./page/settings/authorized-apps.tag'); -require('./page/settings/twitter.tag'); -require('./page/messaging.tag'); -require('./page/messaging-room.tag'); -require('./home.tag'); -require('./home-timeline.tag'); -require('./timeline.tag'); -require('./timeline-post.tag'); -require('./timeline-post-sub.tag'); -require('./post-preview.tag'); -require('./sub-post-content.tag'); -require('./images-viewer.tag'); -require('./drive.tag'); -require('./drive-selector.tag'); -require('./drive-folder-selector.tag'); -require('./drive/file.tag'); -require('./drive/folder.tag'); -require('./drive/file-viewer.tag'); -require('./post-form.tag'); -require('./notification.tag'); -require('./notifications.tag'); -require('./notify.tag'); -require('./notification-preview.tag'); -require('./search.tag'); -require('./search-posts.tag'); -require('./post-detail.tag'); -require('./user.tag'); -require('./user-timeline.tag'); -require('./follow-button.tag'); -require('./user-preview.tag'); -require('./users-list.tag'); -require('./user-following.tag'); -require('./user-followers.tag'); -require('./init-following.tag'); diff --git a/src/web/app/mobile/tags/init-following.tag b/src/web/app/mobile/tags/init-following.tag deleted file mode 100644 index 0c54d3a6a1..0000000000 --- a/src/web/app/mobile/tags/init-following.tag +++ /dev/null @@ -1,168 +0,0 @@ -<mk-init-following> - <p class="title">気になるユーザーをフォロー:</p> - <div class="users" if={ !fetching && users.length > 0 }> - <div class="user" each={ users }><a class="avatar-anchor" href={ '/' + username }><img class="avatar" src={ avatar_url + '?thumbnail&size=42' } alt=""/></a> - <div class="body"><a class="name" href={ '/' + username } target="_blank">{ name }</a> - <p class="username">@{ username }</p> - </div> - <mk-follow-button user={ this }/> - </div> - </div> - <p class="empty" if={ !fetching && users.length == 0 }>おすすめのユーザーは見つかりませんでした。</p> - <p class="fetching" if={ fetching }><i class="fa fa-spinner fa-pulse fa-fw"></i>読み込んでいます<mk-ellipsis/></p> - <a class="refresh" onclick={ refresh }>もっと見る</a> - <button class="close" onclick={ close } title="閉じる"><i class="fa fa-times"></i></button> - <style> - :scope - display block - padding 16px - - > .title - margin 0 0 12px 0 - font-size 1em - font-weight bold - color #888 - - > .users - &:after - content "" - display block - clear both - - > .user - padding 16px - width 238px - float left - - &:after - content "" - display block - clear both - - > .avatar-anchor - display block - float left - margin 0 12px 0 0 - - > .avatar - display block - width 42px - height 42px - margin 0 - border-radius 8px - vertical-align bottom - - > .body - float left - width calc(100% - 54px) - - > .name - margin 0 - font-size 16px - line-height 24px - color #555 - - > .username - margin 0 - font-size 15px - line-height 16px - color #ccc - - > mk-follow-button - position absolute - top 16px - right 16px - - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > .fetching - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - - > .refresh - display block - margin 0 8px 0 0 - text-align right - font-size 0.9em - color #999 - - > .close - cursor pointer - display block - position absolute - top 0 - right 0 - z-index 1 - margin 0 - padding 0 - font-size 1.2em - color #999 - border none - outline none - background transparent - - &:hover - color #555 - - &:active - color #222 - - > i - padding 14px - - </style> - <script> - this.mixin('api'); - - this.users = null; - this.fetching = true; - - this.limit = 6; - this.page = 0; - - this.on('mount', () => { - this.fetch(); - }); - - this.fetch = () => { - this.update({ - fetching: true, - users: null - }); - - this.api('users/recommendation', { - limit: this.limit, - offset: this.limit * this.page - }).then(users => { - this.fetching = false - this.users = users - this.update({ - fetching: false, - users: users - }); - }); - }; - - this.refresh = () => { - if (this.users.length < this.limit) { - this.page = 0; - } else { - this.page++; - } - this.fetch(); - }; - - this.close = () => { - this.unmount(); - }; - </script> -</mk-init-following> diff --git a/src/web/app/mobile/tags/notification-preview.tag b/src/web/app/mobile/tags/notification-preview.tag deleted file mode 100644 index 077ae78463..0000000000 --- a/src/web/app/mobile/tags/notification-preview.tag +++ /dev/null @@ -1,117 +0,0 @@ -<mk-notification-preview class={ notification.type }> - <virtual if={ notification.type == 'reaction' }> - <img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - <div class="text"> - <p><mk-reaction-icon reaction={ notification.reaction }/>{ notification.user.name }</p> - <p class="post-ref">{ getPostSummary(notification.post) }</p> - </div> - </virtual> - <virtual if={ notification.type == 'repost' }> - <img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - <div class="text"> - <p><i class="fa fa-retweet"></i>{ notification.post.user.name }</p> - <p class="post-ref">{ getPostSummary(notification.post.repost) }</p> - </div> - </virtual> - <virtual if={ notification.type == 'quote' }> - <img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - <div class="text"> - <p><i class="fa fa-quote-left"></i>{ notification.post.user.name }</p> - <p class="post-preview">{ getPostSummary(notification.post) }</p> - </div> - </virtual> - <virtual if={ notification.type == 'follow' }> - <img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - <div class="text"> - <p><i class="fa fa-user-plus"></i>{ notification.user.name }</p> - </div> - </virtual> - <virtual if={ notification.type == 'reply' }> - <img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - <div class="text"> - <p><i class="fa fa-reply"></i>{ notification.post.user.name }</p> - <p class="post-preview">{ getPostSummary(notification.post) }</p> - </div> - </virtual> - <virtual if={ notification.type == 'mention' }> - <img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - <div class="text"> - <p><i class="fa fa-at"></i>{ notification.post.user.name }</p> - <p class="post-preview">{ getPostSummary(notification.post) }</p> - </div> - </virtual> - <virtual if={ notification.type == 'poll_vote' }> - <img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - <div class="text"> - <p><i class="fa fa-pie-chart"></i>{ notification.user.name }</p> - <p class="post-ref">{ getPostSummary(notification.post) }</p> - </div> - </virtual> - <style> - :scope - display block - margin 0 - padding 8px - color #fff - overflow-wrap break-word - - &:after - content "" - display block - clear both - - img - display block - float left - min-width 36px - min-height 36px - max-width 36px - max-height 36px - border-radius 6px - - .text - float right - width calc(100% - 36px) - padding-left 8px - - p - margin 0 - - i, mk-reaction-icon - margin-right 4px - - .post-ref - - &:before, &:after - font-family FontAwesome - font-size 1em - font-weight normal - font-style normal - display inline-block - margin-right 3px - - &:before - content "\f10d" - - &:after - content "\f10e" - - &.repost, &.quote - .text p i - color #77B255 - - &.follow - .text p i - color #53c7ce - - &.reply, &.mention - .text p i - color #fff - - </style> - <script> - import getPostSummary from '../../common/scripts/get-post-summary'; - this.getPostSummary = getPostSummary; - this.notification = this.opts.notification; - </script> -</mk-notification-preview> diff --git a/src/web/app/mobile/tags/notification.tag b/src/web/app/mobile/tags/notification.tag deleted file mode 100644 index 3663709525..0000000000 --- a/src/web/app/mobile/tags/notification.tag +++ /dev/null @@ -1,170 +0,0 @@ -<mk-notification class={ notification.type }> - <mk-time time={ notification.created_at }/> - <virtual if={ notification.type == 'reaction' }> - <a class="avatar-anchor" href={ '/' + notification.user.username }> - <img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - </a> - <div class="text"> - <p> - <mk-reaction-icon reaction={ notification.reaction }/> - <a href={ '/' + notification.user.username }>{ notification.user.name }</a> - </p> - <a class="post-ref" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a> - </div> - </virtual> - <virtual if={ notification.type == 'repost' }> - <a class="avatar-anchor" href={ '/' + notification.post.user.username }> - <img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - </a> - <div class="text"> - <p> - <i class="fa fa-retweet"></i> - <a href={ '/' + notification.post.user.username }>{ notification.post.user.name }</a> - </p> - <a class="post-ref" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post.repost) }</a> - </div> - </virtual> - <virtual if={ notification.type == 'quote' }> - <a class="avatar-anchor" href={ '/' + notification.post.user.username }> - <img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - </a> - <div class="text"> - <p> - <i class="fa fa-quote-left"></i> - <a href={ '/' + notification.post.user.username }>{ notification.post.user.name }</a> - </p> - <a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a> - </div> - </virtual> - <virtual if={ notification.type == 'follow' }> - <a class="avatar-anchor" href={ '/' + notification.user.username }> - <img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - </a> - <div class="text"> - <p> - <i class="fa fa-user-plus"></i> - <a href={ '/' + notification.user.username }>{ notification.user.name }</a> - </p> - </div> - </virtual> - <virtual if={ notification.type == 'reply' }> - <a class="avatar-anchor" href={ '/' + notification.post.user.username }> - <img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - </a> - <div class="text"> - <p> - <i class="fa fa-reply"></i> - <a href={ '/' + notification.post.user.username }>{ notification.post.user.name }</a> - </p> - <a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a> - </div> - </virtual> - <virtual if={ notification.type == 'mention' }> - <a class="avatar-anchor" href={ '/' + notification.post.user.username }> - <img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - </a> - <div class="text"> - <p> - <i class="fa fa-at"></i> - <a href={ '/' + notification.post.user.username }>{ notification.post.user.name }</a> - </p> - <a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a> - </div> - </virtual> - <virtual if={ notification.type == 'poll_vote' }> - <a class="avatar-anchor" href={ '/' + notification.user.username }> - <img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - </a> - <div class="text"> - <p> - <i class="fa fa-pie-chart"></i> - <a href={ '/' + notification.user.username }>{ notification.user.name }</a> - </p> - <a class="post-ref" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a> - </div> - </virtual> - <style> - :scope - display block - margin 0 - padding 16px - overflow-wrap break-word - - > mk-time - display inline - position absolute - top 16px - right 12px - vertical-align top - color rgba(0, 0, 0, 0.6) - font-size 12px - - &:after - content "" - display block - clear both - - .avatar-anchor - display block - float left - - img - min-width 36px - min-height 36px - max-width 36px - max-height 36px - border-radius 6px - - .text - float right - width calc(100% - 36px) - padding-left 8px - - p - margin 0 - - i, mk-reaction-icon - margin-right 4px - - .post-preview - color rgba(0, 0, 0, 0.7) - - .post-ref - color rgba(0, 0, 0, 0.7) - - &:before, &:after - font-family FontAwesome - font-size 1em - font-weight normal - font-style normal - display inline-block - margin-right 3px - - &:before - content "\f10d" - - &:after - content "\f10e" - - &.repost, &.quote - .text p i - color #77B255 - - &.follow - .text p i - color #53c7ce - - &.reply, &.mention - .text p i - color #555 - - .post-preview - color rgba(0, 0, 0, 0.7) - - </style> - <script> - import getPostSummary from '../../common/scripts/get-post-summary'; - this.getPostSummary = getPostSummary; - this.notification = this.opts.notification; - </script> -</mk-notification> diff --git a/src/web/app/mobile/tags/notifications.tag b/src/web/app/mobile/tags/notifications.tag deleted file mode 100644 index 21a941e630..0000000000 --- a/src/web/app/mobile/tags/notifications.tag +++ /dev/null @@ -1,148 +0,0 @@ -<mk-notifications> - <div class="notifications" if={ notifications.length != 0 }> - <virtual each={ notification, i in notifications }> - <div> - <mk-notification notification={ notification }/> - </div> - <p class="date" if={ i != notifications.length - 1 && notification._date != notifications[i + 1]._date }><span><i class="fa fa-angle-up"></i>{ notification._datetext }</span><span><i class="fa fa-angle-down"></i>{ notifications[i + 1]._datetext }</span></p> - </virtual> - </div> - <button class="more" if={ moreNotifications } onclick={ fetchMoreNotifications } disabled={ fetchingMoreNotifications }> - <i class="fa fa-spinner fa-pulse fa-fw" if={ fetchingMoreNotifications }></i>{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:mobile.tags.mk-notifications.more%' } - </button> - <p class="empty" if={ notifications.length == 0 && !loading }>%i18n:mobile.tags.mk-notifications.empty%</p> - <p class="loading" if={ loading }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:common.loading%<mk-ellipsis/></p> - <style> - :scope - display block - background #fff - - > .notifications - - > div - border-bottom solid 1px rgba(0, 0, 0, 0.05) - - &:last-child - border-bottom none - - > mk-notification - margin 0 auto - max-width 500px - - > .date - display block - margin 0 - line-height 32px - text-align center - font-size 0.8em - color #aaa - background #fdfdfd - border-bottom solid 1px rgba(0, 0, 0, 0.05) - - span - margin 0 16px - - i - margin-right 8px - - > .more - display block - width 100% - padding 16px - color #555 - border-top solid 1px rgba(0, 0, 0, 0.05) - - > i - margin-right 4px - - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > .loading - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - - </style> - <script> - import getPostSummary from '../../common/scripts/get-post-summary'; - this.getPostSummary = getPostSummary; - - this.mixin('api'); - this.mixin('stream'); - - this.notifications = []; - this.loading = true; - - this.on('mount', () => { - const max = 10; - - this.api('i/notifications', { - limit: max + 1 - }).then(notifications => { - if (notifications.length == max + 1) { - this.moreNotifications = true; - notifications.pop(); - } - - this.update({ - loading: false, - notifications: notifications - }); - - this.trigger('fetched'); - }); - - this.stream.on('notification', this.onNotification); - }); - - this.on('unmount', () => { - this.stream.off('notification', this.onNotification); - }); - - this.on('update', () => { - this.notifications.forEach(notification => { - const date = new Date(notification.created_at).getDate(); - const month = new Date(notification.created_at).getMonth() + 1; - notification._date = date; - notification._datetext = `${month}月 ${date}日`; - }); - }); - - this.onNotification = notification => { - this.notifications.unshift(notification); - this.update(); - }; - - this.fetchMoreNotifications = () => { - this.update({ - fetchingMoreNotifications: true - }); - - const max = 30; - - this.api('i/notifications', { - limit: max + 1, - max_id: this.notifications[this.notifications.length - 1].id - }).then(notifications => { - if (notifications.length == max + 1) { - this.moreNotifications = true; - notifications.pop(); - } else { - this.moreNotifications = false; - } - this.update({ - notifications: this.notifications.concat(notifications), - fetchingMoreNotifications: false - }); - }); - }; - </script> -</mk-notifications> diff --git a/src/web/app/mobile/tags/notify.tag b/src/web/app/mobile/tags/notify.tag deleted file mode 100644 index 2dfc2dddb8..0000000000 --- a/src/web/app/mobile/tags/notify.tag +++ /dev/null @@ -1,40 +0,0 @@ -<mk-notify> - <mk-notification-preview notification={ opts.notification }/> - <style> - :scope - display block - position fixed - z-index 1024 - bottom -64px - left 0 - width 100% - height 64px - pointer-events none - -webkit-backdrop-filter blur(2px) - backdrop-filter blur(2px) - background-color rgba(#000, 0.5) - - </style> - <script> - import anime from 'animejs'; - - this.on('mount', () => { - anime({ - targets: this.root, - bottom: '0px', - duration: 500, - easing: 'easeOutQuad' - }); - - setTimeout(() => { - anime({ - targets: this.root, - bottom: '-64px', - duration: 500, - easing: 'easeOutQuad', - complete: () => this.unmount() - }); - }, 6000); - }); - </script> -</mk-notify> diff --git a/src/web/app/mobile/tags/page/drive.tag b/src/web/app/mobile/tags/page/drive.tag deleted file mode 100644 index 1169e3b9eb..0000000000 --- a/src/web/app/mobile/tags/page/drive.tag +++ /dev/null @@ -1,73 +0,0 @@ -<mk-drive-page> - <mk-ui ref="ui"> - <mk-drive ref="browser" folder={ parent.opts.folder } file={ parent.opts.file } data-is-naked="true"/> - </mk-ui> - <style> - :scope - display block - </style> - <script> - import ui from '../../scripts/ui-event'; - import Progress from '../../../common/scripts/loading'; - - this.on('mount', () => { - document.title = 'Misskey Drive'; - ui.trigger('title', '<i class="fa fa-cloud"></i>%i18n:mobile.tags.mk-drive-page.drive%'); - - ui.trigger('func', () => { - this.refs.ui.refs.browser.openContextMenu(); - }, 'ellipsis-h'); - - this.refs.ui.refs.browser.on('begin-fetch', () => { - Progress.start(); - }); - - this.refs.ui.refs.browser.on('fetched-mid', () => { - Progress.set(0.5); - }); - - this.refs.ui.refs.browser.on('fetched', () => { - Progress.done(); - }); - - this.refs.ui.refs.browser.on('move-root', () => { - const title = 'Misskey Drive'; - - // Rewrite URL - history.pushState(null, title, '/i/drive'); - - document.title = title; - ui.trigger('title', '<i class="fa fa-cloud"></i>%i18n:mobile.tags.mk-drive-page.drive%'); - }); - - this.refs.ui.refs.browser.on('open-folder', (folder, silent) => { - const title = folder.name + ' | Misskey Drive'; - - if (!silent) { - // Rewrite URL - history.pushState(null, title, '/i/drive/folder/' + folder.id); - } - - document.title = title; - // TODO: escape html characters in folder.name - ui.trigger('title', '<i class="fa fa-folder-open"></i>' + folder.name); - }); - - this.refs.ui.refs.browser.on('open-file', (file, silent) => { - const title = file.name + ' | Misskey Drive'; - - if (!silent) { - // Rewrite URL - history.pushState(null, title, '/i/drive/file/' + file.id); - } - - document.title = title; - // TODO: escape html characters in file.name - ui.trigger('title', '<mk-file-type-icon class="icon"></mk-file-type-icon>' + file.name); - riot.mount('mk-file-type-icon', { - type: file.type - }); - }); - }); - </script> -</mk-drive-page> diff --git a/src/web/app/mobile/tags/page/entrance.tag b/src/web/app/mobile/tags/page/entrance.tag deleted file mode 100644 index 380fb780bc..0000000000 --- a/src/web/app/mobile/tags/page/entrance.tag +++ /dev/null @@ -1,66 +0,0 @@ -<mk-entrance> - <main><img src="/assets/title.svg" alt="Misskey"/> - <mk-entrance-signin if={ mode == 'signin' }/> - <mk-entrance-signup if={ mode == 'signup' }/> - <div class="introduction" if={ mode == 'introduction' }> - <mk-introduction/> - <button onclick={ signin }>%i18n:common.ok%</button> - </div> - </main> - <footer> - <mk-copyright/> - </footer> - <style> - :scope - display block - height 100% - - > main - display block - - > img - display block - width 130px - height 120px - margin 0 auto - - > .introduction - max-width 300px - margin 0 auto - color #666 - - > button - display block - margin 16px auto 0 auto - - > footer - > mk-copyright - margin 0 - text-align center - line-height 64px - font-size 10px - color rgba(#000, 0.5) - - </style> - <script> - this.mode = 'signin'; - - this.signup = () => { - this.update({ - mode: 'signup' - }); - }; - - this.signin = () => { - this.update({ - mode: 'signin' - }); - }; - - this.introduction = () => { - this.update({ - mode: 'introduction' - }); - }; - </script> -</mk-entrance> diff --git a/src/web/app/mobile/tags/page/entrance/signin.tag b/src/web/app/mobile/tags/page/entrance/signin.tag deleted file mode 100644 index 827fbccb94..0000000000 --- a/src/web/app/mobile/tags/page/entrance/signin.tag +++ /dev/null @@ -1,51 +0,0 @@ -<mk-entrance-signin> - <mk-signin/> - <div class="divider"><span>or</span></div> - <button class="signup" onclick={ parent.signup }>%i18n:mobile.tags.mk-entrance-signin.signup%</button><a class="introduction" onclick={ parent.introduction }>%i18n:mobile.tags.mk-entrance-signin.about%</a> - <style> - :scope - display block - margin 0 auto - padding 0 8px - max-width 350px - text-align center - - > .signup - padding 16px - width 100% - font-size 1em - color #fff - background $theme-color - border-radius 3px - - > .divider - padding 16px 0 - text-align center - - &:after - content "" - display block - position absolute - top 50% - width 100% - height 1px - border-top solid 1px rgba(0, 0, 0, 0.1) - - > * - z-index 1 - padding 0 8px - color rgba(0, 0, 0, 0.5) - background #fdfdfd - - > .introduction - display inline-block - margin-top 16px - font-size 12px - color #666 - - - - - - </style> -</mk-entrance-signin> diff --git a/src/web/app/mobile/tags/page/entrance/signup.tag b/src/web/app/mobile/tags/page/entrance/signup.tag deleted file mode 100644 index 3798c94349..0000000000 --- a/src/web/app/mobile/tags/page/entrance/signup.tag +++ /dev/null @@ -1,38 +0,0 @@ -<mk-entrance-signup> - <mk-signup/> - <button class="cancel" type="button" onclick={ parent.signin } title="%i18n:mobile.tags.mk-entrance-signup.cancel%"><i class="fa fa-times"></i></button> - <style> - :scope - display block - margin 0 auto - padding 0 8px - max-width 350px - - > .cancel - cursor pointer - display block - position absolute - top 0 - right 0 - z-index 1 - margin 0 - padding 0 - font-size 1.2em - color #999 - border none - outline none - box-shadow none - background transparent - transition opacity 0.1s ease - - &:hover - color #555 - - &:active - color #222 - - > i - padding 14px - - </style> -</mk-entrance-signup> diff --git a/src/web/app/mobile/tags/page/home.tag b/src/web/app/mobile/tags/page/home.tag deleted file mode 100644 index 32c80fd20e..0000000000 --- a/src/web/app/mobile/tags/page/home.tag +++ /dev/null @@ -1,57 +0,0 @@ -<mk-home-page> - <mk-ui ref="ui"> - <mk-home ref="home"/> - </mk-ui> - <style> - :scope - display block - </style> - <script> - import ui from '../../scripts/ui-event'; - import Progress from '../../../common/scripts/loading'; - import getPostSummary from '../../../common/scripts/get-post-summary'; - import openPostForm from '../../scripts/open-post-form'; - - this.mixin('i'); - this.mixin('stream'); - - this.unreadCount = 0; - - this.on('mount', () => { - document.title = 'Misskey' - ui.trigger('title', '<i class="fa fa-home"></i>%i18n:mobile.tags.mk-home.home%'); - - ui.trigger('func', () => { - openPostForm(); - }, 'pencil'); - - Progress.start(); - - this.stream.on('post', this.onStreamPost); - document.addEventListener('visibilitychange', this.onVisibilitychange, false); - - this.refs.ui.refs.home.on('loaded', () => { - Progress.done(); - }); - }); - - this.on('unmount', () => { - this.stream.off('post', this.onStreamPost); - document.removeEventListener('visibilitychange', this.onVisibilitychange); - }); - - this.onStreamPost = post => { - if (document.hidden && post.user_id !== this.I.id) { - this.unreadCount++; - document.title = `(${this.unreadCount}) ${getPostSummary(post)}`; - } - }; - - this.onVisibilitychange = () => { - if (!document.hidden) { - this.unreadCount = 0; - document.title = 'Misskey'; - } - }; - </script> -</mk-home-page> diff --git a/src/web/app/mobile/tags/page/messaging-room.tag b/src/web/app/mobile/tags/page/messaging-room.tag deleted file mode 100644 index e66e03177f..0000000000 --- a/src/web/app/mobile/tags/page/messaging-room.tag +++ /dev/null @@ -1,31 +0,0 @@ -<mk-messaging-room-page> - <mk-ui ref="ui"> - <mk-messaging-room if={ !parent.fetching } user={ parent.user } is-naked={ true }/> - </mk-ui> - <style> - :scope - display block - </style> - <script> - import ui from '../../scripts/ui-event'; - - this.mixin('api'); - - this.fetching = true; - - this.on('mount', () => { - this.api('users/show', { - username: this.opts.username - }).then(user => { - this.update({ - fetching: false, - user: user - }); - - document.title = `%i18n:mobile.tags.mk-messaging-room-page.message%: ${user.name} | Misskey`; - // TODO: ユーザー名をエスケープ - ui.trigger('title', '<i class="fa fa-comments-o"></i>' + user.name); - }); - }); - </script> -</mk-messaging-room-page> diff --git a/src/web/app/mobile/tags/page/messaging.tag b/src/web/app/mobile/tags/page/messaging.tag deleted file mode 100644 index 11e8f8cb48..0000000000 --- a/src/web/app/mobile/tags/page/messaging.tag +++ /dev/null @@ -1,23 +0,0 @@ -<mk-messaging-page> - <mk-ui ref="ui"> - <mk-messaging ref="index"/> - </mk-ui> - <style> - :scope - display block - </style> - <script> - import ui from '../../scripts/ui-event'; - - this.mixin('page'); - - this.on('mount', () => { - document.title = 'Misskey | %i18n:mobile.tags.mk-messaging-page.message%'; - ui.trigger('title', '<i class="fa fa-comments-o"></i>%i18n:mobile.tags.mk-messaging-page.message%'); - - this.refs.ui.refs.index.on('navigate-user', user => { - this.page('/i/messaging/' + user.username); - }); - }); - </script> -</mk-messaging-page> diff --git a/src/web/app/mobile/tags/page/new-post.tag b/src/web/app/mobile/tags/page/new-post.tag deleted file mode 100644 index 7adde3b329..0000000000 --- a/src/web/app/mobile/tags/page/new-post.tag +++ /dev/null @@ -1,7 +0,0 @@ -<mk-new-post-page> - <mk-post-form ref="form"/> - <style> - :scope - display block - </style> -</mk-new-post-page> diff --git a/src/web/app/mobile/tags/page/notifications.tag b/src/web/app/mobile/tags/page/notifications.tag deleted file mode 100644 index f90cd1628d..0000000000 --- a/src/web/app/mobile/tags/page/notifications.tag +++ /dev/null @@ -1,24 +0,0 @@ -<mk-notifications-page> - <mk-ui ref="ui"> - <mk-notifications ref="notifications"/> - </mk-ui> - <style> - :scope - display block - </style> - <script> - import ui from '../../scripts/ui-event'; - import Progress from '../../../common/scripts/loading'; - - this.on('mount', () => { - document.title = 'Misskey | %i18n:mobile.tags.mk-notifications-page.notifications%'; - ui.trigger('title', '<i class="fa fa-bell-o"></i>%i18n:mobile.tags.mk-notifications-page.notifications%'); - - Progress.start(); - - this.refs.ui.refs.notifications.on('fetched', () => { - Progress.done(); - }); - }); - </script> -</mk-notifications-page> diff --git a/src/web/app/mobile/tags/page/post.tag b/src/web/app/mobile/tags/page/post.tag deleted file mode 100644 index 7ab4ea2714..0000000000 --- a/src/web/app/mobile/tags/page/post.tag +++ /dev/null @@ -1,41 +0,0 @@ -<mk-post-page> - <mk-ui ref="ui"> - <main> - <mk-post-detail ref="post" post={ parent.post }/> - </main> - </mk-ui> - <style> - :scope - display block - - main - background #fff - - > mk-post-detail - width 100% - max-width 500px - margin 0 auto - - </style> - <script> - import ui from '../../scripts/ui-event'; - import Progress from '../../../common/scripts/loading'; - - this.post = this.opts.post; - - this.on('mount', () => { - document.title = 'Misskey'; - ui.trigger('title', '<i class="fa fa-sticky-note-o"></i>%i18n:mobile.tags.mk-post-page.submit%'); - - Progress.start(); - - this.refs.ui.refs.post.on('post-fetched', () => { - Progress.set(0.5); - }); - - this.refs.ui.refs.post.on('loaded', () => { - Progress.done(); - }); - }); - </script> -</mk-post-page> diff --git a/src/web/app/mobile/tags/page/search.tag b/src/web/app/mobile/tags/page/search.tag deleted file mode 100644 index 869d5c8533..0000000000 --- a/src/web/app/mobile/tags/page/search.tag +++ /dev/null @@ -1,25 +0,0 @@ -<mk-search-page> - <mk-ui ref="ui"> - <mk-search ref="search" query={ parent.opts.query }/> - </mk-ui> - <style> - :scope - display block - </style> - <script> - import ui from '../../scripts/ui-event'; - import Progress from '../../../common/scripts/loading'; - - this.on('mount', () => { - document.title = `%i18n:mobile.tags.mk-search-page.search%: ${this.opts.query} | Misskey` - // TODO: クエリをHTMLエスケープ - ui.trigger('title', '<i class="fa fa-search"></i>' + this.opts.query); - - Progress.start(); - - this.refs.ui.refs.search.on('loaded', () => { - Progress.done(); - }); - }); - </script> -</mk-search-page> diff --git a/src/web/app/mobile/tags/page/settings.tag b/src/web/app/mobile/tags/page/settings.tag deleted file mode 100644 index 58094a876a..0000000000 --- a/src/web/app/mobile/tags/page/settings.tag +++ /dev/null @@ -1,23 +0,0 @@ -<mk-settings-page> - <mk-ui ref="ui"> - <ul> - <li><a><i class="fa fa-user"></i>%i18n:mobile.tags.mk-settings-page.profile%</a></li> - <li><a href="./settings/authorized-apps"><i class="fa fa-puzzle-piece"></i>%i18n:mobile.tags.mk-settings-page.applications%</a></li> - <li><a href="./settings/twitter"><i class="fa fa-twitter"></i>%i18n:mobile.tags.mk-settings-page.twitter-integration%</a></li> - <li><a href="./settings/signin-history"><i class="fa fa-sign-in"></i>%i18n:mobile.tags.mk-settings-page.signin-history%</a></li> - <li><a href="./settings/api"><i class="fa fa-key"></i>API</a></li> - </ul> - </mk-ui> - <style> - :scope - display block - </style> - <script> - import ui from '../../scripts/ui-event'; - - this.on('mount', () => { - document.title = 'Misskey | %i18n:mobile.tags.mk-settings-page.settings%'; - ui.trigger('title', '<i class="fa fa-cog"></i>%i18n:mobile.tags.mk-settings-page.settings%'); - }); - </script> -</mk-settings-page> diff --git a/src/web/app/mobile/tags/page/settings/api.tag b/src/web/app/mobile/tags/page/settings/api.tag deleted file mode 100644 index cfffeacb5a..0000000000 --- a/src/web/app/mobile/tags/page/settings/api.tag +++ /dev/null @@ -1,17 +0,0 @@ -<mk-api-info-page> - <mk-ui ref="ui"> - <mk-api-info/> - </mk-ui> - <style> - :scope - display block - </style> - <script> - const ui = require('../../../scripts/ui-event'); - - this.on('mount', () => { - document.title = 'Misskey | API'; - ui.trigger('title', '<i class="fa fa-key"></i>API'); - }); - </script> -</mk-api-info-page> diff --git a/src/web/app/mobile/tags/page/settings/authorized-apps.tag b/src/web/app/mobile/tags/page/settings/authorized-apps.tag deleted file mode 100644 index e962871ec7..0000000000 --- a/src/web/app/mobile/tags/page/settings/authorized-apps.tag +++ /dev/null @@ -1,17 +0,0 @@ -<mk-authorized-apps-page> - <mk-ui ref="ui"> - <mk-authorized-apps/> - </mk-ui> - <style> - :scope - display block - </style> - <script> - const ui = require('../../../scripts/ui-event'); - - this.on('mount', () => { - document.title = 'Misskey | %i18n:mobile.tags.mk-authorized-apps-page.application%'; - ui.trigger('title', '<i class="fa fa-puzzle-piece"></i>%i18n:mobile.tags.mk-authorized-apps-page.application%'); - }); - </script> -</mk-authorized-apps-page> diff --git a/src/web/app/mobile/tags/page/settings/signin.tag b/src/web/app/mobile/tags/page/settings/signin.tag deleted file mode 100644 index 2305ea9fb4..0000000000 --- a/src/web/app/mobile/tags/page/settings/signin.tag +++ /dev/null @@ -1,17 +0,0 @@ -<mk-signin-history-page> - <mk-ui ref="ui"> - <mk-signin-history/> - </mk-ui> - <style> - :scope - display block - </style> - <script> - const ui = require('../../../scripts/ui-event'); - - this.on('mount', () => { - document.title = 'Misskey | %i18n:mobile.tags.mk-signin-history-page.signin-history%'; - ui.trigger('title', '<i class="fa fa-sign-in"></i>%i18n:mobile.tags.mk-signin-history-page.signin-history%'); - }); - </script> -</mk-signin-history-page> diff --git a/src/web/app/mobile/tags/page/settings/twitter.tag b/src/web/app/mobile/tags/page/settings/twitter.tag deleted file mode 100644 index f4e9f7628b..0000000000 --- a/src/web/app/mobile/tags/page/settings/twitter.tag +++ /dev/null @@ -1,17 +0,0 @@ -<mk-twitter-setting-page> - <mk-ui ref="ui"> - <mk-twitter-setting/> - </mk-ui> - <style> - :scope - display block - </style> - <script> - const ui = require('../../../scripts/ui-event'); - - this.on('mount', () => { - document.title = 'Misskey | %i18n:mobile.tags.mk-twitter-setting-page.twitter-integration%'; - ui.trigger('title', '<i class="fa fa-twitter"></i>%i18n:mobile.tags.mk-twitter-setting-page.twitter-integration%'); - }); - </script> -</mk-twitter-setting-page> diff --git a/src/web/app/mobile/tags/page/user-followers.tag b/src/web/app/mobile/tags/page/user-followers.tag deleted file mode 100644 index f6fcffebe2..0000000000 --- a/src/web/app/mobile/tags/page/user-followers.tag +++ /dev/null @@ -1,39 +0,0 @@ -<mk-user-followers-page> - <mk-ui ref="ui"> - <mk-user-followers ref="list" if={ !parent.fetching } user={ parent.user }/> - </mk-ui> - <style> - :scope - display block - </style> - <script> - import ui from '../../scripts/ui-event'; - import Progress from '../../../common/scripts/loading'; - - this.mixin('api'); - - this.fetching = true; - this.user = null; - - this.on('mount', () => { - Progress.start(); - - this.api('users/show', { - username: this.opts.user - }).then(user => { - this.update({ - fetching: false, - user: user - }); - - document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) + ' | Misskey'; - // TODO: ユーザー名をエスケープ - ui.trigger('title', '<img src="' + user.avatar_url + '?thumbnail&size=64">' + '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name)); - - this.refs.ui.refs.list.on('loaded', () => { - Progress.done(); - }); - }); - }); - </script> -</mk-user-followers-page> diff --git a/src/web/app/mobile/tags/page/user-following.tag b/src/web/app/mobile/tags/page/user-following.tag deleted file mode 100644 index 4b289b6aa3..0000000000 --- a/src/web/app/mobile/tags/page/user-following.tag +++ /dev/null @@ -1,39 +0,0 @@ -<mk-user-following-page> - <mk-ui ref="ui"> - <mk-user-following ref="list" if={ !parent.fetching } user={ parent.user }/> - </mk-ui> - <style> - :scope - display block - </style> - <script> - import ui from '../../scripts/ui-event'; - import Progress from '../../../common/scripts/loading'; - - this.mixin('api'); - - this.fetching = true; - this.user = null; - - this.on('mount', () => { - Progress.start(); - - this.api('users/show', { - username: this.opts.user - }).then(user => { - this.update({ - fetching: false, - user: user - }); - - document.title = '%i18n:mobile.tags.mk-user-following-page.following-of%'.replace('{}', user.name) + ' | Misskey'; - // TODO: ユーザー名をエスケープ - ui.trigger('title', '<img src="' + user.avatar_url + '?thumbnail&size=64">' + '%i18n:mobile.tags.mk-user-following-page.following-of%'.replace('{}', user.name)); - - this.refs.ui.refs.list.on('loaded', () => { - Progress.done(); - }); - }); - }); - </script> -</mk-user-following-page> diff --git a/src/web/app/mobile/tags/page/user.tag b/src/web/app/mobile/tags/page/user.tag deleted file mode 100644 index 05ccef3113..0000000000 --- a/src/web/app/mobile/tags/page/user.tag +++ /dev/null @@ -1,26 +0,0 @@ -<mk-user-page> - <mk-ui ref="ui"> - <mk-user ref="user" user={ parent.user } page={ parent.opts.page }/> - </mk-ui> - <style> - :scope - display block - </style> - <script> - import ui from '../../scripts/ui-event'; - import Progress from '../../../common/scripts/loading'; - - this.user = this.opts.user; - - this.on('mount', () => { - Progress.start(); - - this.refs.ui.refs.user.on('loaded', user => { - Progress.done(); - document.title = user.name + ' | Misskey'; - // TODO: ユーザー名をエスケープ - ui.trigger('title', '<i class="fa fa-user"></i>' + user.name); - }); - }); - </script> -</mk-user-page> diff --git a/src/web/app/mobile/tags/post-detail.tag b/src/web/app/mobile/tags/post-detail.tag deleted file mode 100644 index 9d62a2b591..0000000000 --- a/src/web/app/mobile/tags/post-detail.tag +++ /dev/null @@ -1,359 +0,0 @@ -<mk-post-detail> - <div class="fetching" if={ fetching }> - <mk-ellipsis-icon/> - </div> - <div class="main" if={ !fetching }> - <button class="read-more" if={ p.reply_to && p.reply_to.reply_to_id && context == null } onclick={ loadContext } disabled={ loadingContext }> - <i class="fa fa-ellipsis-v" if={ !contextFetching }></i> - <i class="fa fa-spinner fa-pulse" if={ contextFetching }></i> - </button> - <div class="context"> - <virtual each={ post in context }> - <mk-post-preview post={ post }/> - </virtual> - </div> - <div class="reply-to" if={ p.reply_to }> - <mk-post-preview post={ p.reply_to }/> - </div> - <div class="repost" if={ isRepost }> - <p> - <a class="avatar-anchor" href={ '/' + post.user.username }> - <img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=32' } alt="avatar"/></a> - <i class="fa fa-retweet"></i><a class="name" href={ '/' + post.user.username }> - { post.user.name } - </a> - がRepost - </p> - </div> - <article> - <header> - <a class="avatar-anchor" href={ '/' + p.user.username }> - <img class="avatar" src={ p.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - </a> - <div> - <a class="name" href={ '/' + p.user.username }>{ p.user.name }</a> - <span class="username">@{ p.user.username }</span> - </div> - </header> - <div class="body"> - <div class="text" ref="text"></div> - <div class="media" if={ p.media }> - <virtual each={ file in p.media }><img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/></virtual> - </div> - <mk-poll if={ p.poll } post={ p }/> - </div> - <a class="time" href={ url }> - <mk-time time={ p.created_at } mode="detail"/> - </a> - <footer> - <mk-reactions-viewer post={ p }/> - <button onclick={ reply } title="%i18n:mobile.tags.mk-post-detail.reply%"><i class="fa fa-reply"></i> - <p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p> - </button> - <button onclick={ repost } title="Repost"><i class="fa fa-retweet"></i> - <p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p> - </button> - <button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton" title="%i18n:mobile.tags.mk-post-detail.reaction%"><i class="fa fa-plus"></i> - <p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p> - </button> - <button><i class="fa fa-ellipsis-h"></i></button> - </footer> - </article> - <div class="replies"> - <virtual each={ post in replies }> - <mk-post-preview post={ post }/> - </virtual> - </div> - </div> - <style> - :scope - display block - margin 0 - padding 0 - - > .fetching - padding 64px 0 - - > .main - - > .read-more - display block - margin 0 - padding 10px 0 - width 100% - font-size 1em - text-align center - color #999 - cursor pointer - background #fafafa - outline none - border none - border-bottom solid 1px #eef0f2 - border-radius 6px 6px 0 0 - box-shadow none - - &:hover - background #f6f6f6 - - &:active - background #f0f0f0 - - &:disabled - color #ccc - - > .context - > * - border-bottom 1px solid #eef0f2 - - > .repost - color #9dbb00 - background linear-gradient(to bottom, #edfde2 0%, #fff 100%) - - > p - margin 0 - padding 16px 32px - - .avatar-anchor - display inline-block - - .avatar - vertical-align bottom - min-width 28px - min-height 28px - max-width 28px - max-height 28px - margin 0 8px 0 0 - border-radius 6px - - i - margin-right 4px - - .name - font-weight bold - - & + article - padding-top 8px - - > .reply-to - border-bottom 1px solid #eef0f2 - - > article - padding 14px 16px 9px 16px - - @media (min-width 500px) - padding 28px 32px 18px 32px - - &:after - content "" - display block - clear both - - &:hover - > .main > footer > button - color #888 - - > header - display flex - line-height 1.1 - - > .avatar-anchor - display block - padding 0 .5em 0 0 - - > .avatar - display block - width 54px - height 54px - margin 0 - border-radius 8px - vertical-align bottom - - @media (min-width 500px) - width 60px - height 60px - - > div - - > .name - display inline-block - margin .4em 0 - color #777 - font-size 16px - font-weight bold - text-align left - text-decoration none - - &:hover - text-decoration underline - - > .username - display block - text-align left - margin 0 - color #ccc - - > .body - padding 8px 0 - - > .text - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 16px - color #717171 - - @media (min-width 500px) - font-size 24px - - .link - &:after - content "\f14c" - display inline-block - padding-left 2px - font-family FontAwesome - font-size .9em - font-weight 400 - font-style normal - - > mk-url-preview - margin-top 8px - - > .media - > img - display block - max-width 100% - - > .time - font-size 16px - color #c0c0c0 - - > footer - font-size 1.2em - - > button - margin 0 28px 0 0 - padding 8px - background transparent - border none - box-shadow none - font-size 1em - color #ddd - cursor pointer - - &:hover - color #666 - - > .count - display inline - margin 0 0 0 8px - color #999 - - &.reacted - color $theme-color - - > .replies - > * - border-top 1px solid #eef0f2 - - </style> - <script> - import compile from '../../common/scripts/text-compiler'; - import getPostSummary from '../../common/scripts/get-post-summary'; - import openPostForm from '../scripts/open-post-form'; - - this.mixin('api'); - - this.fetching = true; - this.loadingContext = false; - this.context = null; - this.post = null; - - this.on('mount', () => { - this.api('posts/show', { - post_id: this.opts.post - }).then(post => { - const isRepost = post.repost != null; - const p = isRepost ? post.repost : post; - p.reactions_count = p.reaction_counts ? Object.keys(p.reaction_counts).map(key => p.reaction_counts[key]).reduce((a, b) => a + b) : 0; - - this.update({ - fetching: false, - post: post, - isRepost: isRepost, - p: p, - summary: getPostSummary(p) - }); - - this.trigger('loaded'); - - if (this.p.text) { - const tokens = this.p.ast; - - this.refs.text.innerHTML = compile(tokens); - - this.refs.text.children.forEach(e => { - if (e.tagName == 'MK-URL') riot.mount(e); - }); - - // URLをプレビュー - tokens - .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) - .map(t => { - riot.mount(this.refs.text.appendChild(document.createElement('mk-url-preview')), { - url: t.url - }); - }); - } - - // Get replies - this.api('posts/replies', { - post_id: this.p.id, - limit: 8 - }).then(replies => { - this.update({ - replies: replies - }); - }); - }); - }); - - this.reply = () => { - openPostForm({ - reply: this.p - }); - }; - - this.repost = () => { - const text = window.prompt(`「${this.summary}」をRepost`); - if (text == null) return; - this.api('posts/create', { - repost_id: this.p.id, - text: text == '' ? undefined : text - }); - }; - - this.react = () => { - riot.mount(document.body.appendChild(document.createElement('mk-reaction-picker')), { - source: this.refs.reactButton, - post: this.p, - compact: true - }); - }; - - this.loadContext = () => { - this.contextFetching = true; - - // Fetch context - this.api('posts/context', { - post_id: this.p.reply_to_id - }).then(context => { - this.update({ - contextFetching: false, - context: context.reverse() - }); - }); - }; - </script> -</mk-post-detail> diff --git a/src/web/app/mobile/tags/post-form.tag b/src/web/app/mobile/tags/post-form.tag deleted file mode 100644 index 28c7796840..0000000000 --- a/src/web/app/mobile/tags/post-form.tag +++ /dev/null @@ -1,292 +0,0 @@ -<mk-post-form> - <header> - <div> - <button class="cancel" onclick={ cancel }><i class="fa fa-times"></i></button> - <div> - <span if={ refs.text } class="text-count { over: refs.text.value.length > 1000 }">{ 1000 - refs.text.value.length }</span> - <button class="submit" onclick={ post }>%i18n:mobile.tags.mk-post-form.submit%</button> - </div> - </div> - </header> - <div class="form"> - <mk-post-preview if={ opts.reply } post={ opts.reply }/> - <textarea ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder={ opts.reply ? '%i18n:mobile.tags.mk-post-form.reply-placeholder%' : '%i18n:mobile.tags.mk-post-form.post-placeholder%' }></textarea> - <div class="attaches" if={ files.length != 0 }> - <ul class="files" ref="attaches"> - <li class="file" each={ files }> - <div class="img" style="background-image: url({ url + '?thumbnail&size=64' })" title={ name }></div> - </li> - <li class="add" if={ files.length < 4 } title="%i18n:mobile.tags.mk-post-form.attach-media-from-local%" onclick={ selectFile }><i class="fa fa-plus"></i></li> - </ul> - </div> - <mk-poll-editor if={ poll } ref="poll" ondestroy={ onPollDestroyed }/> - <mk-uploader ref="uploader"/> - <button ref="upload" onclick={ selectFile }><i class="fa fa-upload"></i></button> - <button ref="drive" onclick={ selectFileFromDrive }><i class="fa fa-cloud"></i></button> - <button class="kao" onclick={ kao }><i class="fa fa-smile-o"></i></button> - <button class="poll" onclick={ addPoll }><i class="fa fa-pie-chart"></i></button> - <input ref="file" type="file" accept="image/*" multiple="multiple" onchange={ changeFile }/> - </div> - <style> - :scope - display block - padding-top 50px - - > header - position fixed - z-index 1000 - top 0 - left 0 - width 100% - height 50px - background #fff - - > div - max-width 500px - margin 0 auto - - > .cancel - width 50px - line-height 50px - font-size 24px - color #555 - - > div - position absolute - top 0 - right 0 - - > .text-count - line-height 50px - color #657786 - - > .submit - margin 8px - padding 0 16px - line-height 34px - color $theme-color-foreground - background $theme-color - border-radius 4px - - &:disabled - opacity 0.7 - - > .form - max-width 500px - margin 0 auto - - > mk-post-preview - padding 16px - - > .attaches - - > .files - display block - margin 0 - padding 4px - list-style none - - &:after - content "" - display block - clear both - - > .file - display block - float left - margin 4px - padding 0 - cursor move - - &:hover > .remove - display block - - > .img - width 64px - height 64px - background-size cover - background-position center center - - > .remove - display none - position absolute - top -6px - right -6px - width 16px - height 16px - cursor pointer - - > .add - display block - float left - margin 4px - padding 0 - border dashed 2px rgba($theme-color, 0.2) - cursor pointer - - &:hover - border-color rgba($theme-color, 0.3) - - > i - color rgba($theme-color, 0.4) - - > i - display block - width 60px - height 60px - line-height 60px - text-align center - font-size 1.2em - color rgba($theme-color, 0.2) - - > mk-uploader - margin 8px 0 0 0 - padding 8px - - > [ref='file'] - display none - - > [ref='text'] - display block - padding 12px - margin 0 - width 100% - max-width 100% - min-width 100% - min-height 80px - font-size 16px - color #333 - border none - border-bottom solid 1px #ddd - border-radius 0 - - &:disabled - opacity 0.5 - - > [ref='upload'] - > [ref='drive'] - .kao - .poll - display inline-block - padding 0 - margin 0 - width 48px - height 48px - font-size 20px - color #657786 - background transparent - outline none - border none - border-radius 0 - box-shadow none - - </style> - <script> - import getKao from '../../common/scripts/get-kao'; - - this.mixin('api'); - - this.wait = false; - this.uploadings = []; - this.files = []; - this.poll = false; - - this.on('mount', () => { - this.refs.uploader.on('uploaded', file => { - this.addFile(file); - }); - - this.refs.uploader.on('change-uploads', uploads => { - this.trigger('change-uploading-files', uploads); - }); - - this.refs.text.focus(); - }); - - this.onkeydown = e => { - if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post(); - }; - - this.onpaste = e => { - e.clipboardData.items.forEach(item => { - if (item.kind == 'file') { - this.upload(item.getAsFile()); - } - }); - }; - - this.selectFile = () => { - this.refs.file.click(); - }; - - this.selectFileFromDrive = () => { - const i = riot.mount(document.body.appendChild(document.createElement('mk-drive-selector')), { - multiple: true - })[0]; - i.one('selected', files => { - files.forEach(this.addFile); - }); - }; - - this.changeFile = () => { - this.refs.file.files.forEach(this.upload); - }; - - this.upload = file => { - this.refs.uploader.upload(file); - }; - - this.addFile = file => { - file._remove = () => { - this.files = this.files.filter(x => x.id != file.id); - this.trigger('change-files', this.files); - this.update(); - }; - - this.files.push(file); - this.trigger('change-files', this.files); - this.update(); - }; - - this.addPoll = () => { - this.poll = true; - }; - - this.onPollDestroyed = () => { - this.update({ - poll: false - }); - }; - - this.post = () => { - this.wait = true; - - const files = this.files && this.files.length > 0 - ? this.files.map(f => f.id) - : undefined; - - this.api('posts/create', { - text: this.refs.text.value == '' ? undefined : this.refs.text.value, - media_ids: files, - reply_to_id: opts.reply ? opts.reply.id : undefined, - poll: this.poll ? this.refs.poll.get() : undefined - }).then(data => { - this.trigger('post'); - this.unmount(); - }).catch(err => { - this.update({ - wait: false - }); - }); - }; - - this.cancel = () => { - this.trigger('cancel'); - this.unmount(); - }; - - this.kao = () => { - this.refs.text.value += getKao(); - }; - </script> -</mk-post-form> diff --git a/src/web/app/mobile/tags/post-preview.tag b/src/web/app/mobile/tags/post-preview.tag deleted file mode 100644 index aaf8467039..0000000000 --- a/src/web/app/mobile/tags/post-preview.tag +++ /dev/null @@ -1,94 +0,0 @@ -<mk-post-preview> - <article> - <a class="avatar-anchor" href={ '/' + post.user.username }> - <img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - </a> - <div class="main"> - <header> - <a class="name" href={ '/' + post.user.username }>{ post.user.name }</a> - <span class="username">@{ post.user.username }</span> - <a class="time" href={ '/' + post.user.username + '/' + post.id }> - <mk-time time={ post.created_at }/> - </a> - </header> - <div class="body"> - <mk-sub-post-content class="text" post={ post }/> - </div> - </div> - </article> - <style> - :scope - display block - margin 0 - padding 0 - font-size 0.9em - background #fff - - > article - &:after - content "" - display block - clear both - - &:hover - > .main > footer > button - color #888 - - > .avatar-anchor - display block - float left - margin 0 12px 0 0 - - > .avatar - display block - width 48px - height 48px - margin 0 - border-radius 8px - vertical-align bottom - - > .main - float left - width calc(100% - 60px) - - > header - display flex - margin-bottom 4px - white-space nowrap - - > .name - display block - margin 0 .5em 0 0 - padding 0 - overflow hidden - color #607073 - font-size 1em - font-weight 700 - text-align left - text-decoration none - text-overflow ellipsis - - &:hover - text-decoration underline - - > .username - text-align left - margin 0 .5em 0 0 - color #d1d8da - - > .time - margin-left auto - color #b2b8bb - - > .body - - > .text - cursor default - margin 0 - padding 0 - font-size 1.1em - color #717171 - - </style> - <script>this.post = this.opts.post</script> -</mk-post-preview> diff --git a/src/web/app/mobile/tags/search-posts.tag b/src/web/app/mobile/tags/search-posts.tag deleted file mode 100644 index 3e6caa1df2..0000000000 --- a/src/web/app/mobile/tags/search-posts.tag +++ /dev/null @@ -1,36 +0,0 @@ -<mk-search-posts> - <mk-timeline init={ init } more={ more } empty={ '%i18n:mobile.tags.mk-search-posts.empty%'.replace('{}', query) }/> - <style> - :scope - display block - background #fff - - </style> - <script> - this.mixin('api'); - - this.max = 30; - this.offset = 0; - - this.query = this.opts.query; - this.withMedia = this.opts.withMedia; - - this.init = new Promise((res, rej) => { - this.api('posts/search', { - query: this.query - }).then(posts => { - res(posts); - this.trigger('loaded'); - }); - }); - - this.more = () => { - this.offset += this.max; - return this.api('posts/search', { - query: this.query, - max: this.max, - offset: this.offset - }); - }; - </script> -</mk-search-posts> diff --git a/src/web/app/mobile/tags/search.tag b/src/web/app/mobile/tags/search.tag deleted file mode 100644 index 2d299e0a77..0000000000 --- a/src/web/app/mobile/tags/search.tag +++ /dev/null @@ -1,16 +0,0 @@ -<mk-search> - <mk-search-posts ref="posts" query={ query }/> - <style> - :scope - display block - </style> - <script> - this.query = this.opts.query; - - this.on('mount', () => { - this.refs.posts.on('loaded', () => { - this.trigger('loaded'); - }); - }); - </script> -</mk-search> diff --git a/src/web/app/mobile/tags/sub-post-content.tag b/src/web/app/mobile/tags/sub-post-content.tag deleted file mode 100644 index 97e0ecec03..0000000000 --- a/src/web/app/mobile/tags/sub-post-content.tag +++ /dev/null @@ -1,46 +0,0 @@ -<mk-sub-post-content> - <div class="body"><a class="reply" if={ post.reply_to_id }><i class="fa fa-reply"></i></a><span ref="text"></span><a class="quote" if={ post.repost_id } href={ '/post:' + post.repost_id }>RP: ...</a></div> - <details if={ post.media }> - <summary>({ post.media.length }個のメディア)</summary> - <mk-images-viewer images={ post.media }/> - </details> - <details if={ post.poll }> - <summary>%i18n:mobile.tags.mk-sub-post-content.poll%</summary> - <mk-poll post={ post }/> - </details> - <style> - :scope - display block - overflow-wrap break-word - - > .body - > .reply - margin-right 6px - color #717171 - - > .quote - margin-left 4px - font-style oblique - color #a0bf46 - - mk-poll - font-size 80% - - </style> - <script> - import compile from '../../common/scripts/text-compiler'; - - this.post = this.opts.post; - - this.on('mount', () => { - if (this.post.text) { - const tokens = this.post.ast; - this.refs.text.innerHTML = compile(tokens, false); - - this.refs.text.children.forEach(e => { - if (e.tagName == 'MK-URL') riot.mount(e); - }); - } - }); - </script> -</mk-sub-post-content> diff --git a/src/web/app/mobile/tags/timeline-post-sub.tag b/src/web/app/mobile/tags/timeline-post-sub.tag deleted file mode 100644 index 3fff552e8f..0000000000 --- a/src/web/app/mobile/tags/timeline-post-sub.tag +++ /dev/null @@ -1,101 +0,0 @@ -<mk-timeline-post-sub> - <article><a class="avatar-anchor" href={ '/' + post.user.username }><img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=96' } alt="avatar"/></a> - <div class="main"> - <header><a class="name" href={ '/' + post.user.username }>{ post.user.name }</a><span class="username">@{ post.user.username }</span><a class="created-at" href={ '/' + post.user.username + '/' + post.id }> - <mk-time time={ post.created_at }/></a></header> - <div class="body"> - <mk-sub-post-content class="text" post={ post }/> - </div> - </div> - </article> - <style> - :scope - display block - margin 0 - padding 0 - font-size 0.9em - - > article - padding 16px - - &:after - content "" - display block - clear both - - &:hover - > .main > footer > button - color #888 - - > .avatar-anchor - display block - float left - margin 0 10px 0 0 - - @media (min-width 500px) - margin-right 16px - - > .avatar - display block - width 44px - height 44px - margin 0 - border-radius 8px - vertical-align bottom - - @media (min-width 500px) - width 52px - height 52px - - > .main - float left - width calc(100% - 54px) - - @media (min-width 500px) - width calc(100% - 68px) - - > header - display flex - margin-bottom 2px - white-space nowrap - - > .name - display block - margin 0 0.5em 0 0 - padding 0 - overflow hidden - color #607073 - font-size 1em - font-weight 700 - text-align left - text-decoration none - text-overflow ellipsis - - &:hover - text-decoration underline - - > .username - text-align left - margin 0 - color #d1d8da - - > .created-at - margin-left auto - color #b2b8bb - - > .body - - > .text - cursor default - margin 0 - padding 0 - font-size 1.1em - color #717171 - - pre - max-height 120px - font-size 80% - - </style> - <script>this.post = this.opts.post</script> -</mk-timeline-post-sub> diff --git a/src/web/app/mobile/tags/timeline-post.tag b/src/web/app/mobile/tags/timeline-post.tag deleted file mode 100644 index 2395e9fb79..0000000000 --- a/src/web/app/mobile/tags/timeline-post.tag +++ /dev/null @@ -1,414 +0,0 @@ -<mk-timeline-post class={ repost: isRepost }> - <div class="reply-to" if={ p.reply_to }> - <mk-timeline-post-sub post={ p.reply_to }/> - </div> - <div class="repost" if={ isRepost }> - <p> - <a class="avatar-anchor" href={ '/' + post.user.username }> - <img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - </a> - <i class="fa fa-retweet"></i>{'%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('{'))}<a class="name" href={ '/' + post.user.username }>{ post.user.name }</a>{'%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1)} - </p> - <mk-time time={ post.created_at }/> - </div> - <article> - <a class="avatar-anchor" href={ '/' + p.user.username }> - <img class="avatar" src={ p.user.avatar_url + '?thumbnail&size=96' } alt="avatar"/> - </a> - <div class="main"> - <header> - <a class="name" href={ '/' + p.user.username }>{ p.user.name }</a> - <span class="is-bot" if={ p.user.is_bot }>bot</span> - <span class="username">@{ p.user.username }</span> - <a class="created-at" href={ url }> - <mk-time time={ p.created_at }/> - </a> - </header> - <div class="body"> - <div class="text" ref="text"> - <a class="reply" if={ p.reply_to }> - <i class="fa fa-reply"></i> - </a> - <p class="dummy"></p> - <a class="quote" if={ p.repost != null }>RP:</a> - </div> - <div class="media" if={ p.media }> - <mk-images-viewer images={ p.media }/> - </div> - <mk-poll if={ p.poll } post={ p } ref="pollViewer"/> - <span class="app" if={ p.app }>via <b>{ p.app.name }</b></span> - <div class="repost" if={ p.repost }><i class="fa fa-quote-right fa-flip-horizontal"></i> - <mk-post-preview class="repost" post={ p.repost }/> - </div> - </div> - <footer> - <mk-reactions-viewer post={ p } ref="reactionsViewer"/> - <button onclick={ reply }><i class="fa fa-reply"></i> - <p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p> - </button> - <button onclick={ repost } title="Repost"><i class="fa fa-retweet"></i> - <p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p> - </button> - <button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton"><i class="fa fa-plus"></i> - <p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p> - </button> - </footer> - </div> - </article> - <style> - :scope - display block - margin 0 - padding 0 - font-size 12px - - @media (min-width 350px) - font-size 14px - - @media (min-width 500px) - font-size 16px - - > .repost - color #9dbb00 - background linear-gradient(to bottom, #edfde2 0%, #fff 100%) - - > p - margin 0 - padding 8px 16px - line-height 28px - - @media (min-width 500px) - padding 16px - - .avatar-anchor - display inline-block - - .avatar - vertical-align bottom - width 28px - height 28px - margin 0 8px 0 0 - border-radius 6px - - i - margin-right 4px - - .name - font-weight bold - - > mk-time - position absolute - top 8px - right 16px - font-size 0.9em - line-height 28px - - @media (min-width 500px) - top 16px - - & + article - padding-top 8px - - > .reply-to - background rgba(0, 0, 0, 0.0125) - - > mk-post-preview - background transparent - - > article - padding 14px 16px 9px 16px - - &:after - content "" - display block - clear both - - > .avatar-anchor - display block - float left - margin 0 10px 8px 0 - position -webkit-sticky - position sticky - top 62px - - @media (min-width 500px) - margin-right 16px - - > .avatar - display block - width 48px - height 48px - margin 0 - border-radius 6px - vertical-align bottom - - @media (min-width 500px) - width 58px - height 58px - border-radius 8px - - > .main - float left - width calc(100% - 58px) - - @media (min-width 500px) - width calc(100% - 74px) - - > header - display flex - white-space nowrap - - @media (min-width 500px) - margin-bottom 2px - - > .name - display block - margin 0 0.5em 0 0 - padding 0 - overflow hidden - color #777 - font-size 1em - font-weight 700 - text-align left - text-decoration none - text-overflow ellipsis - - &:hover - text-decoration underline - - > .is-bot - text-align left - margin 0 0.5em 0 0 - padding 1px 6px - font-size 12px - color #aaa - border solid 1px #ddd - border-radius 3px - - > .username - text-align left - margin 0 0.5em 0 0 - color #ccc - - > .created-at - margin-left auto - font-size 0.9em - color #c0c0c0 - - > .body - - > .text - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 1.1em - color #717171 - - > .dummy - display none - - .link - &:after - content "\f14c" - display inline-block - padding-left 2px - font-family FontAwesome - font-size .9em - font-weight 400 - font-style normal - - mk-url-preview - margin-top 8px - - > .reply - margin-right 8px - color #717171 - - > .quote - margin-left 4px - font-style oblique - color #a0bf46 - - code - padding 4px 8px - margin 0 0.5em - font-size 80% - color #525252 - background #f8f8f8 - border-radius 2px - - pre > code - padding 16px - margin 0 - - [data-is-me]:after - content "you" - padding 0 4px - margin-left 4px - font-size 80% - color $theme-color-foreground - background $theme-color - border-radius 4px - - > .media - > img - display block - max-width 100% - - > .app - font-size 12px - color #ccc - - > mk-poll - font-size 80% - - > .repost - margin 8px 0 - - > i:first-child - position absolute - top -8px - left -8px - z-index 1 - color #c0dac6 - font-size 28px - background #fff - - > mk-post-preview - padding 16px - border dashed 1px #c0dac6 - border-radius 8px - - > footer - > button - margin 0 28px 0 0 - padding 8px - background transparent - border none - box-shadow none - font-size 1em - color #ddd - cursor pointer - - &:hover - color #666 - - > .count - display inline - margin 0 0 0 8px - color #999 - - &.reacted - color $theme-color - - </style> - <script> - import compile from '../../common/scripts/text-compiler'; - import getPostSummary from '../../common/scripts/get-post-summary'; - import openPostForm from '../scripts/open-post-form'; - - this.mixin('api'); - this.mixin('stream'); - - this.set = post => { - this.post = post; - this.isRepost = this.post.repost != null && this.post.text == null; - this.p = this.isRepost ? this.post.repost : this.post; - this.p.reactions_count = this.p.reaction_counts ? Object.keys(this.p.reaction_counts).map(key => this.p.reaction_counts[key]).reduce((a, b) => a + b) : 0; - this.summary = getPostSummary(this.p); - this.url = `/${this.p.user.username}/${this.p.id}`; - }; - - this.set(this.opts.post); - - this.refresh = post => { - this.set(post); - this.update(); - if (this.refs.reactionsViewer) this.refs.reactionsViewer.update({ - post - }); - if (this.refs.pollViewer) this.refs.pollViewer.init(post); - }; - - this.onStreamPostUpdated = data => { - const post = data.post; - if (post.id == this.post.id) { - this.refresh(post); - } - }; - - this.onStreamConnected = () => { - this.capture(); - }; - - this.capture = withHandler => { - this.stream.send({ - type: 'capture', - id: this.post.id - }); - if (withHandler) this.stream.on('post-updated', this.onStreamPostUpdated); - }; - - this.decapture = withHandler => { - this.stream.send({ - type: 'decapture', - id: this.post.id - }); - if (withHandler) this.stream.off('post-updated', this.onStreamPostUpdated); - }; - - this.on('mount', () => { - this.capture(true); - this.stream.on('_connected_', this.onStreamConnected); - - if (this.p.text) { - const tokens = this.p.ast; - - this.refs.text.innerHTML = this.refs.text.innerHTML.replace('<p class="dummy"></p>', compile(tokens)); - - this.refs.text.children.forEach(e => { - if (e.tagName == 'MK-URL') riot.mount(e); - }); - - // URLをプレビュー - tokens - .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) - .map(t => { - riot.mount(this.refs.text.appendChild(document.createElement('mk-url-preview')), { - url: t.url - }); - }); - } - }); - - this.on('unmount', () => { - this.decapture(true); - this.stream.off('_connected_', this.onStreamConnected); - }); - - this.reply = () => { - openPostForm({ - reply: this.p - }); - }; - - this.repost = () => { - const text = window.prompt(`「${this.summary}」をRepost`); - if (text == null) return; - this.api('posts/create', { - repost_id: this.p.id, - text: text == '' ? undefined : text - }); - }; - - this.react = () => { - riot.mount(document.body.appendChild(document.createElement('mk-reaction-picker')), { - source: this.refs.reactButton, - post: this.p, - compact: true - }); - }; - </script> -</mk-timeline-post> diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag deleted file mode 100644 index 11f4e0740b..0000000000 --- a/src/web/app/mobile/tags/timeline.tag +++ /dev/null @@ -1,140 +0,0 @@ -<mk-timeline> - <div class="init" if={ init }> - <i class="fa fa-spinner fa-pulse"></i>%i18n:common.loading% - </div> - <div class="empty" if={ !init && posts.length == 0 }> - <i class="fa fa-comments-o"></i>{ opts.empty || '%i18n:mobile.tags.mk-timeline.empty%' } - </div> - <virtual each={ post, i in posts }> - <mk-timeline-post post={ post }/> - <p class="date" if={ i != posts.length - 1 && post._date != posts[i + 1]._date }> - <span><i class="fa fa-angle-up"></i>{ post._datetext }</span> - <span><i class="fa fa-angle-down"></i>{ posts[i + 1]._datetext }</span> - </p> - </virtual> - <footer if={ !init }> - <button if={ canFetchMore } onclick={ more } disabled={ fetching }> - <span if={ !fetching }>%i18n:mobile.tags.mk-timeline.load-more%</span> - <span if={ fetching }>%i18n:common.loading%<mk-ellipsis/></span> - </button> - </footer> - <style> - :scope - display block - background #fff - - > .init - padding 64px 0 - text-align center - color #999 - - > i - margin-right 4px - - > .empty - margin 0 auto - padding 32px - max-width 400px - text-align center - color #999 - - > i - display block - margin-bottom 16px - font-size 3em - color #ccc - - > mk-timeline-post - border-bottom solid 1px #eaeaea - - &:last-of-type - border-bottom none - - > .date - display block - margin 0 - line-height 32px - text-align center - font-size 0.9em - color #aaa - background #fdfdfd - border-bottom solid 1px #eaeaea - - span - margin 0 16px - - i - margin-right 8px - - > footer - text-align center - border-top solid 1px #eaeaea - border-bottom-left-radius 4px - border-bottom-right-radius 4px - - > button - margin 0 - padding 16px - width 100% - color $theme-color - - &:disabled - opacity 0.7 - - </style> - <script> - this.posts = []; - this.init = true; - this.fetching = false; - this.canFetchMore = true; - - this.on('mount', () => { - this.opts.init.then(posts => { - this.init = false; - this.setPosts(posts); - }); - }); - - this.on('update', () => { - this.posts.forEach(post => { - const date = new Date(post.created_at).getDate(); - const month = new Date(post.created_at).getMonth() + 1; - post._date = date; - post._datetext = `${month}月 ${date}日`; - }); - }); - - this.more = () => { - if (this.init || this.fetching || this.posts.length == 0) return; - this.update({ - fetching: true - }); - this.opts.more().then(posts => { - this.fetching = false; - this.prependPosts(posts); - }); - }; - - this.setPosts = posts => { - this.update({ - posts: posts - }); - }; - - this.prependPosts = posts => { - posts.forEach(post => { - this.posts.push(post); - this.update(); - }); - } - - this.addPost = post => { - this.posts.unshift(post); - this.update(); - }; - - this.tail = () => { - return this.posts[this.posts.length - 1]; - }; - </script> -</mk-timeline> diff --git a/src/web/app/mobile/tags/ui-header.tag b/src/web/app/mobile/tags/ui-header.tag deleted file mode 100644 index 10b44b2153..0000000000 --- a/src/web/app/mobile/tags/ui-header.tag +++ /dev/null @@ -1,156 +0,0 @@ -<mk-ui-header> - <mk-special-message/> - <div class="main"> - <div class="backdrop"></div> - <div class="content"> - <button class="nav" onclick={ parent.toggleDrawer }><i class="fa fa-bars"></i></button> - <i class="fa fa-circle" if={ hasUnreadMessagingMessages }></i> - <h1 ref="title">Misskey</h1> - <button if={ func } onclick={ func }><i class="fa fa-{ funcIcon }"></i></button> - </div> - </div> - <style> - :scope - $height = 48px - - display block - position fixed - top 0 - z-index 1024 - width 100% - box-shadow 0 1px 0 rgba(#000, 0.075) - - > .main - color rgba(#fff, 0.9) - - > .backdrop - position absolute - top 0 - z-index 1023 - width 100% - height $height - -webkit-backdrop-filter blur(12px) - backdrop-filter blur(12px) - background-color rgba(#1b2023, 0.75) - - > .content - z-index 1024 - - > h1 - display block - margin 0 auto - padding 0 - width 100% - max-width calc(100% - 112px) - text-align center - font-size 1.1em - font-weight normal - line-height $height - white-space nowrap - overflow hidden - text-overflow ellipsis - - > i - > .icon - margin-right 8px - - > img - display inline-block - vertical-align bottom - width ($height - 16px) - height ($height - 16px) - margin 8px - border-radius 6px - - > .nav - display block - position absolute - top 0 - left 0 - width $height - font-size 1.4em - line-height $height - border-right solid 1px rgba(#000, 0.1) - - > i - transition all 0.2s ease - - > i - position absolute - top 8px - left 8px - pointer-events none - font-size 10px - color $theme-color - - > button:last-child - display block - position absolute - top 0 - right 0 - width $height - text-align center - font-size 1.4em - color inherit - line-height $height - border-left solid 1px rgba(#000, 0.1) - - </style> - <script> - import ui from '../scripts/ui-event'; - - this.mixin('api'); - this.mixin('stream'); - - this.func = null; - this.funcIcon = null; - - this.on('mount', () => { - this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages); - this.stream.on('unread_messaging_message', this.onUnreadMessagingMessage); - - // Fetch count of unread messaging messages - this.api('messaging/unread').then(res => { - if (res.count > 0) { - this.update({ - hasUnreadMessagingMessages: true - }); - } - }); - }); - - this.on('unmount', () => { - this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages); - this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage); - - ui.off('title', this.setTitle); - ui.off('func', this.setFunc); - }); - - this.onReadAllMessagingMessages = () => { - this.update({ - hasUnreadMessagingMessages: false - }); - }; - - this.onUnreadMessagingMessage = () => { - this.update({ - hasUnreadMessagingMessages: true - }); - }; - - this.setTitle = title => { - this.refs.title.innerHTML = title; - }; - - this.setFunc = (fn, icon) => { - this.update({ - func: fn, - funcIcon: icon - }); - }; - - ui.on('title', this.setTitle); - ui.on('func', this.setFunc); - </script> -</mk-ui-header> diff --git a/src/web/app/mobile/tags/ui-nav.tag b/src/web/app/mobile/tags/ui-nav.tag deleted file mode 100644 index 76c43ade66..0000000000 --- a/src/web/app/mobile/tags/ui-nav.tag +++ /dev/null @@ -1,169 +0,0 @@ -<mk-ui-nav> - <div class="backdrop" onclick={ parent.toggleDrawer }></div> - <div class="body"> - <a class="me" if={ SIGNIN } href={ '/' + I.username }> - <img class="avatar" src={ I.avatar_url + '?thumbnail&size=128' } alt="avatar"/> - <p class="name">{ I.name }</p> - </a> - <div class="links"> - <ul> - <li><a href="/"><i class="fa fa-home"></i>%i18n:mobile.tags.mk-ui-nav.home%<i class="fa fa-angle-right"></i></a></li> - <li><a href="/i/notifications"><i class="fa fa-bell-o"></i>%i18n:mobile.tags.mk-ui-nav.notifications%<i class="fa fa-angle-right"></i></a></li> - <li><a href="/i/messaging"><i class="fa fa-comments-o"></i>%i18n:mobile.tags.mk-ui-nav.messaging%<i class="i fa fa-circle" if={ hasUnreadMessagingMessages }></i><i class="fa fa-angle-right"></i></a></li> - </ul> - <ul> - <li><a onclick={ search }><i class="fa fa-search"></i>%i18n:mobile.tags.mk-ui-nav.search%<i class="fa fa-angle-right"></i></a></li> - </ul> - <ul> - <li><a href="/i/drive"><i class="fa fa-cloud"></i>%i18n:mobile.tags.mk-ui-nav.drive%<i class="fa fa-angle-right"></i></a></li> - </ul> - <ul> - <li><a href="/i/settings"><i class="fa fa-cog"></i>%i18n:mobile.tags.mk-ui-nav.settings%<i class="fa fa-angle-right"></i></a></li> - </ul> - </div> - <a href={ CONFIG.aboutUrl }><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a> - </div> - <style> - :scope - display none - - .backdrop - position fixed - top 0 - left 0 - z-index 1025 - width 100% - height 100% - background rgba(0, 0, 0, 0.2) - - .body - position fixed - top 0 - left 0 - z-index 1026 - width 240px - height 100% - overflow auto - color #777 - background #fff - - .me - display block - margin 0 - padding 16px - - .avatar - display inline - max-width 64px - border-radius 32px - vertical-align middle - - .name - display block - margin 0 16px - position absolute - top 0 - left 80px - padding 0 - width calc(100% - 112px) - color #777 - line-height 96px - overflow hidden - text-overflow ellipsis - white-space nowrap - - ul - display block - margin 16px 0 - padding 0 - list-style none - - &:first-child - margin-top 0 - - li - display block - font-size 1em - line-height 1em - - a - display block - padding 0 20px - line-height 3rem - line-height calc(1rem + 30px) - color #777 - text-decoration none - - > i:first-child - margin-right 0.5em - - > .i - margin-left 6px - vertical-align super - font-size 10px - color $theme-color - - > i:last-child - position absolute - top 0 - right 0 - padding 0 20px - font-size 1.2em - line-height calc(1rem + 30px) - color #ccc - - .about - margin 0 - padding 1em 0 - text-align center - font-size 0.8em - opacity 0.5 - - a - color #777 - - </style> - <script> - this.mixin('i'); - this.mixin('page'); - this.mixin('api'); - this.mixin('stream'); - - this.on('mount', () => { - this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages); - this.stream.on('unread_messaging_message', this.onUnreadMessagingMessage); - - // Fetch count of unread messaging messages - this.api('messaging/unread').then(res => { - if (res.count > 0) { - this.update({ - hasUnreadMessagingMessages: true - }); - } - }); - }); - - this.on('unmount', () => { - this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages); - this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage); - }); - - this.onReadAllMessagingMessages = () => { - this.update({ - hasUnreadMessagingMessages: false - }); - }; - - this.onUnreadMessagingMessage = () => { - this.update({ - hasUnreadMessagingMessages: true - }); - }; - - this.search = () => { - const query = window.prompt('%i18n:mobile.tags.mk-ui-nav.search%'); - if (query == null || query == '') return; - this.page('/search:' + query); - }; - </script> -</mk-ui-nav> diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag deleted file mode 100644 index b2f738dc2e..0000000000 --- a/src/web/app/mobile/tags/ui.tag +++ /dev/null @@ -1,38 +0,0 @@ -<mk-ui> - <mk-ui-header/> - <mk-ui-nav ref="nav"/> - <div class="content"> - <yield /> - </div> - <mk-stream-indicator/> - <style> - :scope - display block - padding-top 48px - </style> - <script> - this.mixin('i'); - this.mixin('stream'); - - this.isDrawerOpening = false; - - this.on('mount', () => { - this.stream.on('notification', this.onStreamNotification); - }); - - this.on('unmount', () => { - this.stream.off('notification', this.onStreamNotification); - }); - - this.toggleDrawer = () => { - this.isDrawerOpening = !this.isDrawerOpening; - this.refs.nav.root.style.display = this.isDrawerOpening ? 'block' : 'none'; - }; - - this.onStreamNotification = notification => { - riot.mount(document.body.appendChild(document.createElement('mk-notify')), { - notification: notification - }); - }; - </script> -</mk-ui> diff --git a/src/web/app/mobile/tags/user-followers.tag b/src/web/app/mobile/tags/user-followers.tag deleted file mode 100644 index b710e376c6..0000000000 --- a/src/web/app/mobile/tags/user-followers.tag +++ /dev/null @@ -1,28 +0,0 @@ -<mk-user-followers> - <mk-users-list ref="list" fetch={ fetch } count={ user.followers_count } you-know-count={ user.followers_you_know_count } no-users={ '%i18n:mobile.tags.mk-user-followers.no-users%' }/> - <style> - :scope - display block - - </style> - <script> - this.mixin('api'); - - this.user = this.opts.user; - - this.fetch = (iknow, limit, cursor, cb) => { - this.api('users/followers', { - user_id: this.user.id, - iknow: iknow, - limit: limit, - cursor: cursor ? cursor : undefined - }).then(cb); - }; - - this.on('mount', () => { - this.refs.list.on('loaded', () => { - this.trigger('loaded'); - }); - }); - </script> -</mk-user-followers> diff --git a/src/web/app/mobile/tags/user-following.tag b/src/web/app/mobile/tags/user-following.tag deleted file mode 100644 index 62ca091812..0000000000 --- a/src/web/app/mobile/tags/user-following.tag +++ /dev/null @@ -1,28 +0,0 @@ -<mk-user-following> - <mk-users-list ref="list" fetch={ fetch } count={ user.following_count } you-know-count={ user.following_you_know_count } no-users={ '%i18n:mobile.tags.mk-user-following.no-users%' }/> - <style> - :scope - display block - - </style> - <script> - this.mixin('api'); - - this.user = this.opts.user; - - this.fetch = (iknow, limit, cursor, cb) => { - this.api('users/following', { - user_id: this.user.id, - iknow: iknow, - limit: limit, - cursor: cursor ? cursor : undefined - }).then(cb); - }; - - this.on('mount', () => { - this.refs.list.on('loaded', () => { - this.trigger('loaded'); - }); - }); - </script> -</mk-user-following> diff --git a/src/web/app/mobile/tags/user-preview.tag b/src/web/app/mobile/tags/user-preview.tag deleted file mode 100644 index 48bf88a892..0000000000 --- a/src/web/app/mobile/tags/user-preview.tag +++ /dev/null @@ -1,95 +0,0 @@ -<mk-user-preview> - <a class="avatar-anchor" href={ '/' + user.username }> - <img class="avatar" src={ user.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - </a> - <div class="main"> - <header> - <a class="name" href={ '/' + user.username }>{ user.name }</a> - <span class="username">@{ user.username }</span> - </header> - <div class="body"> - <div class="description">{ user.description }</div> - </div> - </div> - <style> - :scope - display block - margin 0 - padding 16px - font-size 12px - - @media (min-width 350px) - font-size 14px - - @media (min-width 500px) - font-size 16px - - &:after - content "" - display block - clear both - - > .avatar-anchor - display block - float left - margin 0 10px 0 0 - - @media (min-width 500px) - margin-right 16px - - > .avatar - display block - width 48px - height 48px - margin 0 - border-radius 6px - vertical-align bottom - - @media (min-width 500px) - width 58px - height 58px - border-radius 8px - - > .main - float left - width calc(100% - 58px) - - @media (min-width 500px) - width calc(100% - 74px) - - > header - @media (min-width 500px) - margin-bottom 2px - - > .name - display inline - margin 0 - padding 0 - color #777 - font-size 1em - font-weight 700 - text-align left - text-decoration none - - &:hover - text-decoration underline - - > .username - text-align left - margin 0 0 0 8px - color #ccc - - > .body - - > .description - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 1.1em - color #717171 - - </style> - <script>this.user = this.opts.user</script> -</mk-user-preview> diff --git a/src/web/app/mobile/tags/user-timeline.tag b/src/web/app/mobile/tags/user-timeline.tag deleted file mode 100644 index f7b2b36da0..0000000000 --- a/src/web/app/mobile/tags/user-timeline.tag +++ /dev/null @@ -1,35 +0,0 @@ -<mk-user-timeline> - <mk-timeline ref="timeline" init={ init } more={ more } empty={ withMedia ? '%i18n:mobile.tags.mk-user-timeline.no-posts-with-media%' : '%i18n:mobile.tags.mk-user-timeline.no-posts%' }/> - <style> - :scope - display block - max-width 600px - margin 0 auto - background #fff - - </style> - <script> - this.mixin('api'); - - this.user = this.opts.user; - this.withMedia = this.opts.withMedia; - - this.init = new Promise((res, rej) => { - this.api('users/posts', { - user_id: this.user.id, - with_media: this.withMedia - }).then(posts => { - res(posts); - this.trigger('loaded'); - }); - }); - - this.more = () => { - return this.api('users/posts', { - user_id: this.user.id, - with_media: this.withMedia, - max_id: this.refs.timeline.tail().id - }); - }; - </script> -</mk-user-timeline> diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag deleted file mode 100644 index 81eb6ba2e4..0000000000 --- a/src/web/app/mobile/tags/user.tag +++ /dev/null @@ -1,211 +0,0 @@ -<mk-user> - <div class="user" if={ !fetching }> - <header> - <div class="banner" style={ user.banner_url ? 'background-image: url(' + user.banner_url + '?thumbnail&size=1024)' : '' }></div> - <div class="body"> - <div class="top"> - <a class="avatar"> - <img src={ user.avatar_url + '?thumbnail&size=200' } alt="avatar"/> - </a> - <mk-follow-button if={ SIGNIN && I.id != user.id } user={ user }/> - </div> - <div class="title"> - <h1>{ user.name }</h1> - <span class="username">@{ user.username }</span> - <span class="followed" if={ user.is_followed }>%i18n:mobile.tags.mk-user.is-followed%</span> - </div> - <div class="description">{ user.description }</div> - <div class="info"> - <p class="location" if={ user.profile.location }> - <i class="fa fa-map-marker"></i>{ user.profile.location } - </p> - <p class="birthday" if={ user.profile.birthday }> - <i class="fa fa-birthday-cake"></i>{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' } ({ age(user.profile.birthday) }歳) - </p> - </div> - <div class="status"> - <a> - <b>{ user.posts_count }</b> - <i>%i18n:mobile.tags.mk-user.posts-count%</i> - </a> - <a href="{ user.username }/following"> - <b>{ user.following_count }</b> - <i>%i18n:mobile.tags.mk-user.following%</i> - </a> - <a href="{ user.username }/followers"> - <b>{ user.followers_count }</b> - <i>%i18n:mobile.tags.mk-user.followers%</i> - </a> - </div> - <mk-activity-table user={ user }/> - </div> - <nav> - <a data-is-active={ page == 'posts' } onclick={ go.bind(null, 'posts') }>%i18n:mobile.tags.mk-user.posts%</a> - <a data-is-active={ page == 'media' } onclick={ go.bind(null, 'media') }>%i18n:mobile.tags.mk-user.media%</a> - </nav> - </header> - <div class="body"> - <mk-user-timeline if={ page == 'posts' } user={ user }/> - <mk-user-timeline if={ page == 'media' } user={ user } with-media={ true }/> - </div> - </div> - <style> - :scope - display block - - > .user - > header - > .banner - padding-bottom 33.3% - background-color #f5f5f5 - background-size cover - background-position center - - > .body - padding 12px - margin 0 auto - max-width 600px - - > .top - &:after - content '' - display block - clear both - - > .avatar - display block - float left - width 25% - height 40px - - > img - display block - position absolute - left -2px - bottom -2px - width 100% - border 2px solid #fff - border-radius 6px - - @media (min-width 500px) - left -4px - bottom -4px - border 4px solid #fff - border-radius 12px - - > mk-follow-button - float right - height 40px - - > .title - margin 8px 0 - - > h1 - margin 0 - line-height 22px - font-size 20px - color #222 - - > .username - display inline-block - line-height 20px - font-size 16px - font-weight bold - color #657786 - - > .followed - margin-left 8px - padding 2px 4px - font-size 12px - color #657786 - background #f8f8f8 - border-radius 4px - - > .description - margin 8px 0 - color #333 - - > .info - margin 8px 0 - - > p - display inline - margin 0 16px 0 0 - color #555 - - > i - margin-right 4px - - > .status - > a - color #657786 - - &:first-child - margin-right 16px - - > b - margin-right 4px - font-size 16px - color #14171a - - > i - font-size 14px - - > mk-activity-table - margin 12px 0 0 0 - - > nav - display flex - justify-content center - margin 0 auto - max-width 600px - border-bottom solid 1px #ddd - - > a - display block - flex 1 1 - text-align center - line-height 52px - font-size 14px - text-decoration none - color #657786 - border-bottom solid 2px transparent - - &[data-is-active] - font-weight bold - color $theme-color - border-color $theme-color - - > .body - @media (min-width 500px) - padding 16px 0 0 0 - - </style> - <script> - this.age = require('s-age'); - - this.mixin('i'); - this.mixin('api'); - - this.username = this.opts.user; - this.page = this.opts.page ? this.opts.page : 'posts'; - this.fetching = true; - - this.on('mount', () => { - this.api('users/show', { - username: this.username - }).then(user => { - this.fetching = false; - this.user = user; - this.trigger('loaded', user); - this.update(); - }); - }); - - this.go = page => { - this.update({ - page: page - }); - }; - </script> -</mk-user> diff --git a/src/web/app/mobile/tags/users-list.tag b/src/web/app/mobile/tags/users-list.tag deleted file mode 100644 index fb70f184d5..0000000000 --- a/src/web/app/mobile/tags/users-list.tag +++ /dev/null @@ -1,119 +0,0 @@ -<mk-users-list> - <nav> - <span data-is-active={ mode == 'all' } onclick={ setMode.bind(this, 'all') }>%i18n:mobile.tags.mk-users-list.all%<span>{ opts.count }</span></span> - <span if={ SIGNIN && opts.youKnowCount } data-is-active={ mode == 'iknow' } onclick={ setMode.bind(this, 'iknow') }>%i18n:mobile.tags.mk-users-list.known%<span>{ opts.youKnowCount }</span></span> - </nav> - <div class="users" if={ !fetching && users.length != 0 }> - <mk-user-preview each={ users } user={ this }/> - </div> - <button class="more" if={ !fetching && next != null } onclick={ more } disabled={ moreFetching }> - <span if={ !moreFetching }>%i18n:mobile.tags.mk-users-list.load-more%</span> - <span if={ moreFetching }>%i18n:common.loading%<mk-ellipsis/></span></button> - <p class="no" if={ !fetching && users.length == 0 }>{ opts.noUsers }</p> - <p class="fetching" if={ fetching }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:common.loading%<mk-ellipsis/></p> - <style> - :scope - display block - background #fff - - > nav - display flex - justify-content center - margin 0 auto - max-width 600px - border-bottom solid 1px #ddd - - > span - display block - flex 1 1 - text-align center - line-height 52px - font-size 14px - color #657786 - border-bottom solid 2px transparent - - &[data-is-active] - font-weight bold - color $theme-color - border-color $theme-color - - > span - display inline-block - margin-left 4px - padding 2px 5px - font-size 12px - line-height 1 - color #888 - background #eee - border-radius 20px - - > .users - > * - max-width 600px - margin 0 auto - border-bottom solid 1px rgba(0, 0, 0, 0.05) - - > .no - margin 0 - padding 16px - text-align center - color #aaa - - > .fetching - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - - </style> - <script> - this.mixin('i'); - - this.limit = 30; - this.mode = 'all'; - - this.fetching = true; - this.moreFetching = false; - - this.on('mount', () => { - this.fetch(() => this.trigger('loaded')); - }); - - this.fetch = cb => { - this.update({ - fetching: true - }); - this.opts.fetch(this.mode == 'iknow', this.limit, null, obj => { - this.update({ - fetching: false, - users: obj.users, - next: obj.next - }); - if (cb) cb(); - }); - }; - - this.more = () => { - this.update({ - moreFetching: true - }); - this.opts.fetch(this.mode == 'iknow', this.limit, this.next, obj => { - this.update({ - moreFetching: false, - users: this.users.concat(obj.users), - next: obj.next - }); - }); - }; - - this.setMode = mode => { - this.update({ - mode: mode - }); - this.fetch(); - }; - </script> -</mk-users-list> diff --git a/src/web/app/safe.js b/src/web/app/safe.js deleted file mode 100644 index c5fbb83a92..0000000000 --- a/src/web/app/safe.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * 古いブラウザの検知を行う - * ブートローダーとは隔離されているため互いに影響を及ぼすことはない - */ - -// Detect an old browser -if (!('fetch' in window)) { - alert( - 'お使いのブラウザが古いためMisskeyを動作させることができません。' + - 'バージョンを最新のものに更新するか、別のブラウザをお試しください。'); -} diff --git a/src/web/app/stats/script.js b/src/web/app/stats/script.js deleted file mode 100644 index 75063501bb..0000000000 --- a/src/web/app/stats/script.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Stats - */ - -// Style -import './style.styl'; - -import * as riot from 'riot'; -require('./tags'); -import init from '../init'; - -document.title = 'Misskey Statistics'; - -/** - * init - */ -init(me => { - mount(document.createElement('mk-index')); -}); - -function mount(content) { - riot.mount(document.getElementById('app').appendChild(content)); -} diff --git a/src/web/app/status/script.js b/src/web/app/status/script.js deleted file mode 100644 index 06d4d9a7a4..0000000000 --- a/src/web/app/status/script.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Status - */ - -// Style -import './style.styl'; - -import * as riot from 'riot'; -require('./tags'); -import init from '../init'; - -document.title = 'Misskey System Status'; - -/** - * init - */ -init(me => { - mount(document.createElement('mk-index')); -}); - -function mount(content) { - riot.mount(document.getElementById('app').appendChild(content)); -} diff --git a/src/web/assets/favicon.ico b/src/web/assets/favicon.ico deleted file mode 100644 index ed9820d5f4..0000000000 Binary files a/src/web/assets/favicon.ico and /dev/null differ diff --git a/src/web/assets/manifest.json b/src/web/assets/manifest.json deleted file mode 100644 index 0967ef424b..0000000000 --- a/src/web/assets/manifest.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/src/web/server.ts b/src/web/server.ts deleted file mode 100644 index dde4eca5ec..0000000000 --- a/src/web/server.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Web Server - */ - -import * as path from 'path'; -import ms = require('ms'); - -// express modules -import * as express from 'express'; -import * as bodyParser from 'body-parser'; -import * as favicon from 'serve-favicon'; -import * as compression from 'compression'; - -import config from '../conf'; - -/** - * Init app - */ -const app = express(); -app.disable('x-powered-by'); - -app.use(bodyParser.urlencoded({ extended: true })); -app.use(bodyParser.json({ - type: ['application/json', 'text/plain'] -})); -app.use(compression()); - -/** - * Initialize requests - */ -app.use((req, res, next) => { - res.header('X-Frame-Options', 'DENY'); - next(); -}); - -/** - * Static assets - */ -app.use(favicon(`${__dirname}/assets/favicon.ico`)); -app.get('/manifest.json', (req, res) => res.sendFile(`${__dirname}/assets/manifest.json`)); -app.get('/apple-touch-icon.png', (req, res) => res.sendFile(`${__dirname}/assets/apple-touch-icon.png`)); -app.use('/assets', express.static(`${__dirname}/assets`, { - maxAge: ms('7 days') -})); - -/** - * Common API - */ -app.get(/\/api:url/, require('./service/url-preview')); - -/** - * Serve config - */ -app.get('/config.json', (req, res) => { - res.send({ - recaptcha: { - siteKey: config.recaptcha.siteKey - } - }); -}); - -/** - * Routing - */ -app.get('*', (req, res) => { - res.sendFile(path.resolve(`${__dirname}/app/base.html`), { - maxAge: ms('7 days') - }); -}); - -module.exports = app; diff --git a/swagger.js b/swagger.js index 0cfd2fff08..ebd7a356e9 100644 --- a/swagger.js +++ b/swagger.js @@ -23,7 +23,7 @@ const defaultSwagger = { "swagger": "2.0", "info": { "title": "Misskey API", - "version": "aoi" + "version": "nighthike" }, "host": "api.misskey.xyz", "schemes": [ @@ -83,7 +83,7 @@ const defaultSwagger = { "type": "string", "description": "アバターに設定しているドライブのファイルのID" }, - "avatar_url": { + "avatarUrl": { "type": "string", "description": "アバターURL" }, @@ -91,7 +91,7 @@ const defaultSwagger = { "type": "string", "description": "バナーに設定しているドライブのファイルのID" }, - "banner_url": { + "bannerUrl": { "type": "string", "description": "バナーURL" }, @@ -218,8 +218,8 @@ options.apis = files.map(c => {return `${apiRoot}/${c}`;}); if(fs.existsSync('.config/config.yml')){ var config = yaml.safeLoad(fs.readFileSync('./.config/config.yml', 'utf8')); options.swaggerDefinition.host = `api.${config.url.match(/\:\/\/(.+)$/)[1]}`; - options.swaggerDefinition.schemes = config.https.enable ? - ['https'] : + options.swaggerDefinition.schemes = config.https.enable ? + ['https'] : ['http']; } diff --git a/test/api.js b/test/api.ts similarity index 72% rename from test/api.js rename to test/api.ts index 9e1d4ff61b..87bbb8ee16 100644 --- a/test/api.js +++ b/test/api.ts @@ -2,6 +2,8 @@ * API TESTS */ +import * as merge from 'object-assign-deep'; + Error.stackTraceLimit = Infinity; // During the test the env variable is set to test @@ -17,7 +19,7 @@ const should = _chai.should(); _chai.use(chaiHttp); -const server = require('../built/api/server'); +const server = require('../built/server/api'); const db = require('../built/db/mongodb').default; const async = fn => (done) => { @@ -28,7 +30,7 @@ const async = fn => (done) => { }); }; -const request = (endpoint, params, me) => new Promise((ok, ng) => { +const request = (endpoint, params, me?) => new Promise<any>((ok, ng) => { const auth = me ? { i: me.token } : {}; @@ -46,15 +48,14 @@ describe('API', () => { beforeEach(() => Promise.all([ db.get('users').drop(), db.get('posts').drop(), - db.get('drive_files').drop(), - db.get('drive_folders').drop(), + db.get('driveFiles.files').drop(), + db.get('driveFiles.chunks').drop(), + db.get('driveFolders').drop(), db.get('apps').drop(), - db.get('access_tokens').drop(), - db.get('auth_sessions').drop() + db.get('accessTokens').drop(), + db.get('authSessions').drop() ])); - afterEach(cb => setTimeout(cb, 100)); - it('greet server', done => { _chai.request(server) .get('/') @@ -137,8 +138,10 @@ describe('API', () => { describe('i/update', () => { it('アカウント設定を更新できる', async(async () => { const me = await insertSakurako({ - profile: { - gender: 'female' + account: { + profile: { + gender: 'female' + } } }); @@ -154,7 +157,7 @@ describe('API', () => { res.should.have.status(200); res.body.should.be.a('object'); res.body.should.have.property('name').eql(myName); - res.body.should.have.property('profile').a('object'); + res.body.should.have.nested.property('profile').a('object'); res.body.should.have.nested.property('profile.location').eql(myLocation); res.body.should.have.nested.property('profile.birthday').eql(myBirthday); res.body.should.have.nested.property('profile.gender').eql('female'); @@ -177,7 +180,7 @@ describe('API', () => { }, me); res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.property('profile').a('object'); + res.body.should.have.nested.property('profile').a('object'); res.body.should.have.nested.property('profile.birthday').eql(null); })); @@ -194,7 +197,7 @@ describe('API', () => { it('ユーザーが取得できる', async(async () => { const me = await insertSakurako(); const res = await request('/users/show', { - user_id: me._id.toString() + userId: me._id.toString() }, me); res.should.have.status(200); res.body.should.be.a('object'); @@ -203,14 +206,14 @@ describe('API', () => { it('ユーザーが存在しなかったら怒る', async(async () => { const res = await request('/users/show', { - user_id: '000000000000000000000000' + userId: '000000000000000000000000' }); res.should.have.status(400); })); it('間違ったIDで怒られる', async(async () => { const res = await request('/users/show', { - user_id: 'kyoppie' + userId: 'kyoppie' }); res.should.have.status(400); })); @@ -225,30 +228,32 @@ describe('API', () => { const res = await request('/posts/create', post, me); res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.property('text').eql(post.text); + res.body.should.have.property('createdPost'); + res.body.createdPost.should.have.property('text').eql(post.text); })); it('ファイルを添付できる', async(async () => { const me = await insertSakurako(); const file = await insertDriveFile({ - user_id: me._id + userId: me._id }); const res = await request('/posts/create', { - media_ids: [file._id.toString()] + mediaIds: [file._id.toString()] }, me); res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.property('media_ids').eql([file._id.toString()]); + res.body.should.have.property('createdPost'); + res.body.createdPost.should.have.property('mediaIds').eql([file._id.toString()]); })); it('他人のファイルは添付できない', async(async () => { const me = await insertSakurako(); const hima = await insertHimawari(); const file = await insertDriveFile({ - user_id: hima._id + userId: hima._id }); const res = await request('/posts/create', { - media_ids: [file._id.toString()] + mediaIds: [file._id.toString()] }, me); res.should.have.status(400); })); @@ -256,7 +261,7 @@ describe('API', () => { it('存在しないファイルは添付できない', async(async () => { const me = await insertSakurako(); const res = await request('/posts/create', { - media_ids: ['000000000000000000000000'] + mediaIds: ['000000000000000000000000'] }, me); res.should.have.status(400); })); @@ -264,7 +269,7 @@ describe('API', () => { it('不正なファイルIDで怒られる', async(async () => { const me = await insertSakurako(); const res = await request('/posts/create', { - media_ids: ['kyoppie'] + mediaIds: ['kyoppie'] }, me); res.should.have.status(400); })); @@ -272,62 +277,65 @@ describe('API', () => { it('返信できる', async(async () => { const hima = await insertHimawari(); const himaPost = await db.get('posts').insert({ - user_id: hima._id, + userId: hima._id, text: 'ひま' }); const me = await insertSakurako(); const post = { text: 'さく', - reply_to_id: himaPost._id.toString() + replyId: himaPost._id.toString() }; const res = await request('/posts/create', post, me); res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.property('text').eql(post.text); - res.body.should.have.property('reply_to_id').eql(post.reply_to_id); - res.body.should.have.property('reply_to'); - res.body.reply_to.should.have.property('text').eql(himaPost.text); + res.body.should.have.property('createdPost'); + res.body.createdPost.should.have.property('text').eql(post.text); + res.body.createdPost.should.have.property('replyId').eql(post.replyId); + res.body.createdPost.should.have.property('reply'); + res.body.createdPost.reply.should.have.property('text').eql(himaPost.text); })); it('repostできる', async(async () => { const hima = await insertHimawari(); const himaPost = await db.get('posts').insert({ - user_id: hima._id, + userId: hima._id, text: 'こらっさくらこ!' }); const me = await insertSakurako(); const post = { - repost_id: himaPost._id.toString() + repostId: himaPost._id.toString() }; const res = await request('/posts/create', post, me); res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.property('repost_id').eql(post.repost_id); - res.body.should.have.property('repost'); - res.body.repost.should.have.property('text').eql(himaPost.text); + res.body.should.have.property('createdPost'); + res.body.createdPost.should.have.property('repostId').eql(post.repostId); + res.body.createdPost.should.have.property('repost'); + res.body.createdPost.repost.should.have.property('text').eql(himaPost.text); })); it('引用repostできる', async(async () => { const hima = await insertHimawari(); const himaPost = await db.get('posts').insert({ - user_id: hima._id, + userId: hima._id, text: 'こらっさくらこ!' }); const me = await insertSakurako(); const post = { text: 'さく', - repost_id: himaPost._id.toString() + repostId: himaPost._id.toString() }; const res = await request('/posts/create', post, me); res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.property('text').eql(post.text); - res.body.should.have.property('repost_id').eql(post.repost_id); - res.body.should.have.property('repost'); - res.body.repost.should.have.property('text').eql(himaPost.text); + res.body.should.have.property('createdPost'); + res.body.createdPost.should.have.property('text').eql(post.text); + res.body.createdPost.should.have.property('repostId').eql(post.repostId); + res.body.createdPost.should.have.property('repost'); + res.body.createdPost.repost.should.have.property('text').eql(himaPost.text); })); it('文字数ぎりぎりで怒られない', async(async () => { @@ -352,7 +360,7 @@ describe('API', () => { const me = await insertSakurako(); const post = { text: 'さく', - reply_to_id: '000000000000000000000000' + replyId: '000000000000000000000000' }; const res = await request('/posts/create', post, me); res.should.have.status(400); @@ -361,7 +369,7 @@ describe('API', () => { it('存在しないrepost対象で怒られる', async(async () => { const me = await insertSakurako(); const post = { - repost_id: '000000000000000000000000' + repostId: '000000000000000000000000' }; const res = await request('/posts/create', post, me); res.should.have.status(400); @@ -371,7 +379,7 @@ describe('API', () => { const me = await insertSakurako(); const post = { text: 'さく', - reply_to_id: 'kyoppie' + replyId: 'kyoppie' }; const res = await request('/posts/create', post, me); res.should.have.status(400); @@ -380,7 +388,7 @@ describe('API', () => { it('不正なrepost対象IDで怒られる', async(async () => { const me = await insertSakurako(); const post = { - repost_id: 'kyoppie' + repostId: 'kyoppie' }; const res = await request('/posts/create', post, me); res.should.have.status(400); @@ -396,7 +404,8 @@ describe('API', () => { }, me); res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.property('poll'); + res.body.should.have.property('createdPost'); + res.body.createdPost.should.have.property('poll'); })); it('投票の選択肢が無くて怒られる', async(async () => { @@ -432,11 +441,11 @@ describe('API', () => { it('投稿が取得できる', async(async () => { const me = await insertSakurako(); const myPost = await db.get('posts').insert({ - user_id: me._id, + userId: me._id, text: 'お腹ペコい' }); const res = await request('/posts/show', { - post_id: myPost._id.toString() + postId: myPost._id.toString() }, me); res.should.have.status(200); res.body.should.be.a('object'); @@ -445,14 +454,14 @@ describe('API', () => { it('投稿が存在しなかったら怒る', async(async () => { const res = await request('/posts/show', { - post_id: '000000000000000000000000' + postId: '000000000000000000000000' }); res.should.have.status(400); })); it('間違ったIDで怒られる', async(async () => { const res = await request('/posts/show', { - post_id: 'kyoppie' + postId: 'kyoppie' }); res.should.have.status(400); })); @@ -462,13 +471,13 @@ describe('API', () => { it('リアクションできる', async(async () => { const hima = await insertHimawari(); const himaPost = await db.get('posts').insert({ - user_id: hima._id, + userId: hima._id, text: 'ひま' }); const me = await insertSakurako(); const res = await request('/posts/reactions/create', { - post_id: himaPost._id.toString(), + postId: himaPost._id.toString(), reaction: 'like' }, me); res.should.have.status(204); @@ -477,12 +486,12 @@ describe('API', () => { it('自分の投稿にはリアクションできない', async(async () => { const me = await insertSakurako(); const myPost = await db.get('posts').insert({ - user_id: me._id, + userId: me._id, text: 'お腹ペコい' }); const res = await request('/posts/reactions/create', { - post_id: myPost._id.toString(), + postId: myPost._id.toString(), reaction: 'like' }, me); res.should.have.status(400); @@ -491,19 +500,19 @@ describe('API', () => { it('二重にリアクションできない', async(async () => { const hima = await insertHimawari(); const himaPost = await db.get('posts').insert({ - user_id: hima._id, + userId: hima._id, text: 'ひま' }); const me = await insertSakurako(); - await db.get('post_reactions').insert({ - user_id: me._id, - post_id: himaPost._id, + await db.get('postReactions').insert({ + userId: me._id, + postId: himaPost._id, reaction: 'like' }); const res = await request('/posts/reactions/create', { - post_id: himaPost._id.toString(), + postId: himaPost._id.toString(), reaction: 'like' }, me); res.should.have.status(400); @@ -512,7 +521,7 @@ describe('API', () => { it('存在しない投稿にはリアクションできない', async(async () => { const me = await insertSakurako(); const res = await request('/posts/reactions/create', { - post_id: '000000000000000000000000', + postId: '000000000000000000000000', reaction: 'like' }, me); res.should.have.status(400); @@ -527,7 +536,7 @@ describe('API', () => { it('間違ったIDで怒られる', async(async () => { const me = await insertSakurako(); const res = await request('/posts/reactions/create', { - post_id: 'kyoppie', + postId: 'kyoppie', reaction: 'like' }, me); res.should.have.status(400); @@ -538,19 +547,19 @@ describe('API', () => { it('リアクションをキャンセルできる', async(async () => { const hima = await insertHimawari(); const himaPost = await db.get('posts').insert({ - user_id: hima._id, + userId: hima._id, text: 'ひま' }); const me = await insertSakurako(); - await db.get('post_reactions').insert({ - user_id: me._id, - post_id: himaPost._id, + await db.get('postReactions').insert({ + userId: me._id, + postId: himaPost._id, reaction: 'like' }); const res = await request('/posts/reactions/delete', { - post_id: himaPost._id.toString() + postId: himaPost._id.toString() }, me); res.should.have.status(204); })); @@ -558,13 +567,13 @@ describe('API', () => { it('リアクションしていない投稿はリアクションをキャンセルできない', async(async () => { const hima = await insertHimawari(); const himaPost = await db.get('posts').insert({ - user_id: hima._id, + userId: hima._id, text: 'ひま' }); const me = await insertSakurako(); const res = await request('/posts/reactions/delete', { - post_id: himaPost._id.toString() + postId: himaPost._id.toString() }, me); res.should.have.status(400); })); @@ -572,7 +581,7 @@ describe('API', () => { it('存在しない投稿はリアクションをキャンセルできない', async(async () => { const me = await insertSakurako(); const res = await request('/posts/reactions/delete', { - post_id: '000000000000000000000000' + postId: '000000000000000000000000' }, me); res.should.have.status(400); })); @@ -586,7 +595,7 @@ describe('API', () => { it('間違ったIDで怒られる', async(async () => { const me = await insertSakurako(); const res = await request('/posts/reactions/delete', { - post_id: 'kyoppie' + postId: 'kyoppie' }, me); res.should.have.status(400); })); @@ -597,21 +606,7 @@ describe('API', () => { const hima = await insertHimawari(); const me = await insertSakurako(); const res = await request('/following/create', { - user_id: hima._id.toString() - }, me); - res.should.have.status(204); - })); - - it('過去にフォロー歴があった状態でフォローできる', async(async () => { - const hima = await insertHimawari(); - const me = await insertSakurako(); - await db.get('following').insert({ - followee_id: hima._id, - follower_id: me._id, - deleted_at: new Date() - }); - const res = await request('/following/create', { - user_id: hima._id.toString() + userId: hima._id.toString() }, me); res.should.have.status(204); })); @@ -620,11 +615,11 @@ describe('API', () => { const hima = await insertHimawari(); const me = await insertSakurako(); await db.get('following').insert({ - followee_id: hima._id, - follower_id: me._id + followeeId: hima._id, + followerId: me._id }); const res = await request('/following/create', { - user_id: hima._id.toString() + userId: hima._id.toString() }, me); res.should.have.status(400); })); @@ -632,7 +627,7 @@ describe('API', () => { it('存在しないユーザーはフォローできない', async(async () => { const me = await insertSakurako(); const res = await request('/following/create', { - user_id: '000000000000000000000000' + userId: '000000000000000000000000' }, me); res.should.have.status(400); })); @@ -640,7 +635,7 @@ describe('API', () => { it('自分自身はフォローできない', async(async () => { const me = await insertSakurako(); const res = await request('/following/create', { - user_id: me._id.toString() + userId: me._id.toString() }, me); res.should.have.status(400); })); @@ -654,7 +649,7 @@ describe('API', () => { it('間違ったIDで怒られる', async(async () => { const me = await insertSakurako(); const res = await request('/following/create', { - user_id: 'kyoppie' + userId: 'kyoppie' }, me); res.should.have.status(400); })); @@ -665,29 +660,11 @@ describe('API', () => { const hima = await insertHimawari(); const me = await insertSakurako(); await db.get('following').insert({ - followee_id: hima._id, - follower_id: me._id + followeeId: hima._id, + followerId: me._id }); const res = await request('/following/delete', { - user_id: hima._id.toString() - }, me); - res.should.have.status(204); - })); - - it('過去にフォロー歴があった状態でフォロー解除できる', async(async () => { - const hima = await insertHimawari(); - const me = await insertSakurako(); - await db.get('following').insert({ - followee_id: hima._id, - follower_id: me._id, - deleted_at: new Date() - }); - await db.get('following').insert({ - followee_id: hima._id, - follower_id: me._id - }); - const res = await request('/following/delete', { - user_id: hima._id.toString() + userId: hima._id.toString() }, me); res.should.have.status(204); })); @@ -696,7 +673,7 @@ describe('API', () => { const hima = await insertHimawari(); const me = await insertSakurako(); const res = await request('/following/delete', { - user_id: hima._id.toString() + userId: hima._id.toString() }, me); res.should.have.status(400); })); @@ -704,7 +681,7 @@ describe('API', () => { it('存在しないユーザーはフォロー解除できない', async(async () => { const me = await insertSakurako(); const res = await request('/following/delete', { - user_id: '000000000000000000000000' + userId: '000000000000000000000000' }, me); res.should.have.status(400); })); @@ -712,7 +689,7 @@ describe('API', () => { it('自分自身はフォロー解除できない', async(async () => { const me = await insertSakurako(); const res = await request('/following/delete', { - user_id: me._id.toString() + userId: me._id.toString() }, me); res.should.have.status(400); })); @@ -726,7 +703,7 @@ describe('API', () => { it('間違ったIDで怒られる', async(async () => { const me = await insertSakurako(); const res = await request('/following/delete', { - user_id: 'kyoppie' + userId: 'kyoppie' }, me); res.should.have.status(400); })); @@ -736,15 +713,15 @@ describe('API', () => { it('ドライブ情報を取得できる', async(async () => { const me = await insertSakurako(); await insertDriveFile({ - user_id: me._id, + userId: me._id, datasize: 256 }); await insertDriveFile({ - user_id: me._id, + userId: me._id, datasize: 512 }); await insertDriveFile({ - user_id: me._id, + userId: me._id, datasize: 1024 }); const res = await request('/drive', {}, me); @@ -777,11 +754,11 @@ describe('API', () => { it('名前を更新できる', async(async () => { const me = await insertSakurako(); const file = await insertDriveFile({ - user_id: me._id + userId: me._id }); const newName = 'いちごパスタ.png'; const res = await request('/drive/files/update', { - file_id: file._id.toString(), + fileId: file._id.toString(), name: newName }, me); res.should.have.status(200); @@ -793,10 +770,10 @@ describe('API', () => { const me = await insertSakurako(); const hima = await insertHimawari(); const file = await insertDriveFile({ - user_id: hima._id + userId: hima._id }); const res = await request('/drive/files/update', { - file_id: file._id.toString(), + fileId: file._id.toString(), name: 'いちごパスタ.png' }, me); res.should.have.status(400); @@ -805,47 +782,47 @@ describe('API', () => { it('親フォルダを更新できる', async(async () => { const me = await insertSakurako(); const file = await insertDriveFile({ - user_id: me._id + userId: me._id }); const folder = await insertDriveFolder({ - user_id: me._id + userId: me._id }); const res = await request('/drive/files/update', { - file_id: file._id.toString(), - folder_id: folder._id.toString() + fileId: file._id.toString(), + folderId: folder._id.toString() }, me); res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.property('folder_id').eql(folder._id.toString()); + res.body.should.have.property('folderId').eql(folder._id.toString()); })); it('親フォルダを無しにできる', async(async () => { const me = await insertSakurako(); const file = await insertDriveFile({ - user_id: me._id, - folder_id: '000000000000000000000000' + userId: me._id, + folderId: '000000000000000000000000' }); const res = await request('/drive/files/update', { - file_id: file._id.toString(), - folder_id: null + fileId: file._id.toString(), + folderId: null }, me); res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.property('folder_id').eql(null); + res.body.should.have.property('folderId').eql(null); })); it('他人のフォルダには入れられない', async(async () => { const me = await insertSakurako(); const hima = await insertHimawari(); const file = await insertDriveFile({ - user_id: me._id + userId: me._id }); const folder = await insertDriveFolder({ - user_id: hima._id + userId: hima._id }); const res = await request('/drive/files/update', { - file_id: file._id.toString(), - folder_id: folder._id.toString() + fileId: file._id.toString(), + folderId: folder._id.toString() }, me); res.should.have.status(400); })); @@ -853,11 +830,11 @@ describe('API', () => { it('存在しないフォルダで怒られる', async(async () => { const me = await insertSakurako(); const file = await insertDriveFile({ - user_id: me._id + userId: me._id }); const res = await request('/drive/files/update', { - file_id: file._id.toString(), - folder_id: '000000000000000000000000' + fileId: file._id.toString(), + folderId: '000000000000000000000000' }, me); res.should.have.status(400); })); @@ -865,11 +842,11 @@ describe('API', () => { it('不正なフォルダIDで怒られる', async(async () => { const me = await insertSakurako(); const file = await insertDriveFile({ - user_id: me._id + userId: me._id }); const res = await request('/drive/files/update', { - file_id: file._id.toString(), - folder_id: 'kyoppie' + fileId: file._id.toString(), + folderId: 'kyoppie' }, me); res.should.have.status(400); })); @@ -877,7 +854,7 @@ describe('API', () => { it('ファイルが存在しなかったら怒る', async(async () => { const me = await insertSakurako(); const res = await request('/drive/files/update', { - file_id: '000000000000000000000000', + fileId: '000000000000000000000000', name: 'いちごパスタ.png' }, me); res.should.have.status(400); @@ -886,7 +863,7 @@ describe('API', () => { it('間違ったIDで怒られる', async(async () => { const me = await insertSakurako(); const res = await request('/drive/files/update', { - file_id: 'kyoppie', + fileId: 'kyoppie', name: 'いちごパスタ.png' }, me); res.should.have.status(400); @@ -909,10 +886,10 @@ describe('API', () => { it('名前を更新できる', async(async () => { const me = await insertSakurako(); const folder = await insertDriveFolder({ - user_id: me._id + userId: me._id }); const res = await request('/drive/folders/update', { - folder_id: folder._id.toString(), + folderId: folder._id.toString(), name: 'new name' }, me); res.should.have.status(200); @@ -924,10 +901,10 @@ describe('API', () => { const me = await insertSakurako(); const hima = await insertHimawari(); const folder = await insertDriveFolder({ - user_id: hima._id + userId: hima._id }); const res = await request('/drive/folders/update', { - folder_id: folder._id.toString(), + folderId: folder._id.toString(), name: 'new name' }, me); res.should.have.status(400); @@ -936,47 +913,47 @@ describe('API', () => { it('親フォルダを更新できる', async(async () => { const me = await insertSakurako(); const folder = await insertDriveFolder({ - user_id: me._id + userId: me._id }); const parentFolder = await insertDriveFolder({ - user_id: me._id + userId: me._id }); const res = await request('/drive/folders/update', { - folder_id: folder._id.toString(), - parent_id: parentFolder._id.toString() + folderId: folder._id.toString(), + parentId: parentFolder._id.toString() }, me); res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.property('parent_id').eql(parentFolder._id.toString()); + res.body.should.have.property('parentId').eql(parentFolder._id.toString()); })); it('親フォルダを無しに更新できる', async(async () => { const me = await insertSakurako(); const folder = await insertDriveFolder({ - user_id: me._id, - parent_id: '000000000000000000000000' + userId: me._id, + parentId: '000000000000000000000000' }); const res = await request('/drive/folders/update', { - folder_id: folder._id.toString(), - parent_id: null + folderId: folder._id.toString(), + parentId: null }, me); res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.property('parent_id').eql(null); + res.body.should.have.property('parentId').eql(null); })); it('他人のフォルダを親フォルダに設定できない', async(async () => { const me = await insertSakurako(); const hima = await insertHimawari(); const folder = await insertDriveFolder({ - user_id: me._id + userId: me._id }); const parentFolder = await insertDriveFolder({ - user_id: hima._id + userId: hima._id }); const res = await request('/drive/folders/update', { - folder_id: folder._id.toString(), - parent_id: parentFolder._id.toString() + folderId: folder._id.toString(), + parentId: parentFolder._id.toString() }, me); res.should.have.status(400); })); @@ -985,11 +962,11 @@ describe('API', () => { const me = await insertSakurako(); const folder = await insertDriveFolder(); const parentFolder = await insertDriveFolder({ - parent_id: folder._id + parentId: folder._id }); const res = await request('/drive/folders/update', { - folder_id: folder._id.toString(), - parent_id: parentFolder._id.toString() + folderId: folder._id.toString(), + parentId: parentFolder._id.toString() }, me); res.should.have.status(400); })); @@ -998,14 +975,14 @@ describe('API', () => { const me = await insertSakurako(); const folderA = await insertDriveFolder(); const folderB = await insertDriveFolder({ - parent_id: folderA._id + parentId: folderA._id }); const folderC = await insertDriveFolder({ - parent_id: folderB._id + parentId: folderB._id }); const res = await request('/drive/folders/update', { - folder_id: folderA._id.toString(), - parent_id: folderC._id.toString() + folderId: folderA._id.toString(), + parentId: folderC._id.toString() }, me); res.should.have.status(400); })); @@ -1014,8 +991,8 @@ describe('API', () => { const me = await insertSakurako(); const folder = await insertDriveFolder(); const res = await request('/drive/folders/update', { - folder_id: folder._id.toString(), - parent_id: '000000000000000000000000' + folderId: folder._id.toString(), + parentId: '000000000000000000000000' }, me); res.should.have.status(400); })); @@ -1024,8 +1001,8 @@ describe('API', () => { const me = await insertSakurako(); const folder = await insertDriveFolder(); const res = await request('/drive/folders/update', { - folder_id: folder._id.toString(), - parent_id: 'kyoppie' + folderId: folder._id.toString(), + parentId: 'kyoppie' }, me); res.should.have.status(400); })); @@ -1033,7 +1010,7 @@ describe('API', () => { it('存在しないフォルダを更新できない', async(async () => { const me = await insertSakurako(); const res = await request('/drive/folders/update', { - folder_id: '000000000000000000000000' + folderId: '000000000000000000000000' }, me); res.should.have.status(400); })); @@ -1041,7 +1018,7 @@ describe('API', () => { it('不正なフォルダIDで怒られる', async(async () => { const me = await insertSakurako(); const res = await request('/drive/folders/update', { - folder_id: 'kyoppie' + folderId: 'kyoppie' }, me); res.should.have.status(400); })); @@ -1052,7 +1029,7 @@ describe('API', () => { const me = await insertSakurako(); const hima = await insertHimawari(); const res = await request('/messaging/messages/create', { - user_id: hima._id.toString(), + userId: hima._id.toString(), text: 'Hey hey ひまわり' }, me); res.should.have.status(200); @@ -1063,7 +1040,7 @@ describe('API', () => { it('自分自身にはメッセージを送信できない', async(async () => { const me = await insertSakurako(); const res = await request('/messaging/messages/create', { - user_id: me._id.toString(), + userId: me._id.toString(), text: 'Yo' }, me); res.should.have.status(400); @@ -1072,7 +1049,7 @@ describe('API', () => { it('存在しないユーザーにはメッセージを送信できない', async(async () => { const me = await insertSakurako(); const res = await request('/messaging/messages/create', { - user_id: '000000000000000000000000', + userId: '000000000000000000000000', text: 'Yo' }, me); res.should.have.status(400); @@ -1081,7 +1058,7 @@ describe('API', () => { it('不正なユーザーIDで怒られる', async(async () => { const me = await insertSakurako(); const res = await request('/messaging/messages/create', { - user_id: 'kyoppie', + userId: 'kyoppie', text: 'Yo' }, me); res.should.have.status(400); @@ -1091,7 +1068,7 @@ describe('API', () => { const me = await insertSakurako(); const hima = await insertHimawari(); const res = await request('/messaging/messages/create', { - user_id: hima._id.toString() + userId: hima._id.toString() }, me); res.should.have.status(400); })); @@ -1100,7 +1077,7 @@ describe('API', () => { const me = await insertSakurako(); const hima = await insertHimawari(); const res = await request('/messaging/messages/create', { - user_id: hima._id.toString(), + userId: hima._id.toString(), text: '!'.repeat(1001) }, me); res.should.have.status(400); @@ -1111,7 +1088,7 @@ describe('API', () => { it('認証セッションを作成できる', async(async () => { const app = await insertApp(); const res = await request('/auth/session/generate', { - app_secret: app.secret + appSecret: app.secret }); res.should.have.status(200); res.body.should.be.a('object'); @@ -1119,55 +1096,67 @@ describe('API', () => { res.body.should.have.property('url'); })); - it('app_secret 無しで怒られる', async(async () => { + it('appSecret 無しで怒られる', async(async () => { const res = await request('/auth/session/generate', {}); res.should.have.status(400); })); - it('誤った app secret で怒られる', async(async () => { + it('誤った appSecret で怒られる', async(async () => { const res = await request('/auth/session/generate', { - app_secret: 'kyoppie' + appSecret: 'kyoppie' }); res.should.have.status(400); })); }); }); -async function insertSakurako(opts) { - return await db.get('users').insert(Object.assign({ - token: '!00000000000000000000000000000000', +function insertSakurako(opts?) { + return db.get('users').insert(merge({ username: 'sakurako', - username_lower: 'sakurako', - password: '$2a$08$FnHXg3tP.M/kINWgQSXNqeoBsiVrkj.ecXX8mW9rfBzMRkibYfjYy', // HimawariDaisuki06160907 - profile: {} + usernameLower: 'sakurako', + account: { + keypair: '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAtdTG9rlFWjNqhgbg2V6X5XF1WpQXZS3KNXykEWl2UAiMyfVV\nBvf3zQP0dDEdNtcqdPJgis03bpiHCzQusc/YLyHYB0m+TJXsxJatb8cqUogOFeE4\ngQ4Dc5kAT6gLh/d4yz03EIg9bizX07EiGWnZqWxb+21ypqsPxST64sAtG9f5O/G4\nXe2m3cSbfAAvEUP1Ig1LUNyJB4jhM60w1cQic/qO8++sk/+GoX9g71X+i4NArGv+\n1c11acDIIPGAAQpFeYVeGaKakNDNp8RtJJp8R8FLwJXZ4/gATBnScCiHUSrGfRly\nYyR0w/BNlQ6/NijAdB9pR5csPvyIPkx1gauZewIDAQABAoIBAQCwWf/mhuY2h6uG\n9eDZsZ7Mj2/sO7k9Dl4R5iMSKCDxmnlB3slqitExa+aJUqEs8R5icjkkJcjfYNuJ\nCEFJf3YCsGZfGyyQBtCuEh2ATcBEb2SJ3/f3YuoCEaB1oVwdsOzc4TAovpol4yQo\nUqHp1/mdElVb01jhQQN4h1c02IJnfzvfU1C8szBni+Etfd+MxqGfv006DY3KOEb3\nlCrCS3GmooJW2Fjj7q1kCcaEQbMB1/aQHLXd1qe3KJOzXh3Voxsp/jEH0hvp2TII\nfY9UK+b7mA+xlvXwKuTkHVaZm0ylg0nbembS8MF4GfFMujinSexvLrVKaQhdMFoF\nvBLxHYHRAoGBANfNVYJYeCDPFNLmak5Xg33Rfvc2II8UmrZOVdhOWs8ZK0pis9e+\nPo2MKtTzrzipXI2QXv5w7kO+LJWNDva+xRlW8Wlj9Dde9QdQ7Y8+dk7SJgf24DzM\n023elgX5DvTeLODjStk6SMPRL0FmGovUqAAA8ZeHtJzkIr1HROWnQiwnAoGBANez\nhFwKnVoQu0RpBz/i4W0RKIxOwltN2zmlN8KjJPhSy00A7nBUfKLRbcwiSHE98Yi/\nUrXwMwR5QeD2ngngRppddJnpiRfjNjnsaqeqNtpO8AxB3XjpCC5zmHUMFHKvPpDj\n1zU/F44li0YjKcMBebZy9PbfAjrIgJfxhPo/oXiNAoGAfx6gaTjOAp2ZaaZ7Jozc\nkyft/5et1DrR6+P3I4T8bxQncRj1UXfqhxzzOiAVrm3tbCKIIp/JarRCtRGzp9u2\nZPfXGzra6CcSdW3Rkli7/jBCYNynOIl7XjQI8ZnFmq6phwu80ntH07mMeZy4tHff\nQqlLpvQ0i1rDr/Wkexdsnm8CgYBgxha9ILoF/Xm3MJPjEsxmnYsen/tM8XpIu5pv\nxbhBfQvfKWrQlOcyOVnUexEbVVo3KvdVz0VkXW60GpE/BxNGEGXO49rxD6x1gl87\nh/+CJGZIaYiOxaY5CP2+jcPizEL6yG32Yq8TxD5fIkmLRu8vbxX+aIFclDY1dVNe\n3wt3xQKBgGEL0EjwRch+P2V+YHAhbETPrEqJjHRWT95pIdF9XtC8fasSOVH81cLX\nXXsX1FTvOJNwG9Nk8rQjYJXGTb2O/2unaazlYUwxKwVpwuGzz/vhH/roHZBAkIVT\njvpykpn9QMezEdpzj5BEv01QzSYBPzIh5myrpoJIoSW7py7zFG3h\n-----END RSA PRIVATE KEY-----\n', + token: '!00000000000000000000000000000000', + password: '$2a$08$FnHXg3tP.M/kINWgQSXNqeoBsiVrkj.ecXX8mW9rfBzMRkibYfjYy', // HimawariDaisuki06160907 + profile: {}, + settings: {}, + clientSettings: {} + } }, opts)); } -async function insertHimawari(opts) { - return await db.get('users').insert(Object.assign({ - token: '!00000000000000000000000000000001', +function insertHimawari(opts?) { + return db.get('users').insert(merge({ username: 'himawari', - username_lower: 'himawari', - password: '$2a$08$OPESxR2RE/ZijjGanNKk6ezSqGFitqsbZqTjWUZPLhORMKxHCbc4O', // ilovesakurako - profile: {} + usernameLower: 'himawari', + account: { + keypair: '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAtdTG9rlFWjNqhgbg2V6X5XF1WpQXZS3KNXykEWl2UAiMyfVV\nBvf3zQP0dDEdNtcqdPJgis03bpiHCzQusc/YLyHYB0m+TJXsxJatb8cqUogOFeE4\ngQ4Dc5kAT6gLh/d4yz03EIg9bizX07EiGWnZqWxb+21ypqsPxST64sAtG9f5O/G4\nXe2m3cSbfAAvEUP1Ig1LUNyJB4jhM60w1cQic/qO8++sk/+GoX9g71X+i4NArGv+\n1c11acDIIPGAAQpFeYVeGaKakNDNp8RtJJp8R8FLwJXZ4/gATBnScCiHUSrGfRly\nYyR0w/BNlQ6/NijAdB9pR5csPvyIPkx1gauZewIDAQABAoIBAQCwWf/mhuY2h6uG\n9eDZsZ7Mj2/sO7k9Dl4R5iMSKCDxmnlB3slqitExa+aJUqEs8R5icjkkJcjfYNuJ\nCEFJf3YCsGZfGyyQBtCuEh2ATcBEb2SJ3/f3YuoCEaB1oVwdsOzc4TAovpol4yQo\nUqHp1/mdElVb01jhQQN4h1c02IJnfzvfU1C8szBni+Etfd+MxqGfv006DY3KOEb3\nlCrCS3GmooJW2Fjj7q1kCcaEQbMB1/aQHLXd1qe3KJOzXh3Voxsp/jEH0hvp2TII\nfY9UK+b7mA+xlvXwKuTkHVaZm0ylg0nbembS8MF4GfFMujinSexvLrVKaQhdMFoF\nvBLxHYHRAoGBANfNVYJYeCDPFNLmak5Xg33Rfvc2II8UmrZOVdhOWs8ZK0pis9e+\nPo2MKtTzrzipXI2QXv5w7kO+LJWNDva+xRlW8Wlj9Dde9QdQ7Y8+dk7SJgf24DzM\n023elgX5DvTeLODjStk6SMPRL0FmGovUqAAA8ZeHtJzkIr1HROWnQiwnAoGBANez\nhFwKnVoQu0RpBz/i4W0RKIxOwltN2zmlN8KjJPhSy00A7nBUfKLRbcwiSHE98Yi/\nUrXwMwR5QeD2ngngRppddJnpiRfjNjnsaqeqNtpO8AxB3XjpCC5zmHUMFHKvPpDj\n1zU/F44li0YjKcMBebZy9PbfAjrIgJfxhPo/oXiNAoGAfx6gaTjOAp2ZaaZ7Jozc\nkyft/5et1DrR6+P3I4T8bxQncRj1UXfqhxzzOiAVrm3tbCKIIp/JarRCtRGzp9u2\nZPfXGzra6CcSdW3Rkli7/jBCYNynOIl7XjQI8ZnFmq6phwu80ntH07mMeZy4tHff\nQqlLpvQ0i1rDr/Wkexdsnm8CgYBgxha9ILoF/Xm3MJPjEsxmnYsen/tM8XpIu5pv\nxbhBfQvfKWrQlOcyOVnUexEbVVo3KvdVz0VkXW60GpE/BxNGEGXO49rxD6x1gl87\nh/+CJGZIaYiOxaY5CP2+jcPizEL6yG32Yq8TxD5fIkmLRu8vbxX+aIFclDY1dVNe\n3wt3xQKBgGEL0EjwRch+P2V+YHAhbETPrEqJjHRWT95pIdF9XtC8fasSOVH81cLX\nXXsX1FTvOJNwG9Nk8rQjYJXGTb2O/2unaazlYUwxKwVpwuGzz/vhH/roHZBAkIVT\njvpykpn9QMezEdpzj5BEv01QzSYBPzIh5myrpoJIoSW7py7zFG3h\n-----END RSA PRIVATE KEY-----\n', + token: '!00000000000000000000000000000001', + password: '$2a$08$OPESxR2RE/ZijjGanNKk6ezSqGFitqsbZqTjWUZPLhORMKxHCbc4O', // ilovesakurako + profile: {}, + settings: {}, + clientSettings: {} + } }, opts)); } -async function insertDriveFile(opts) { - return await db.get('drive_files').insert(Object.assign({ - name: 'strawberry-pasta.png' - }, opts)); +function insertDriveFile(opts?) { + return db.get('driveFiles.files').insert({ + length: opts.datasize, + filename: 'strawberry-pasta.png', + metadata: opts + }); } -async function insertDriveFolder(opts) { - return await db.get('drive_folders').insert(Object.assign({ +function insertDriveFolder(opts?) { + return db.get('driveFolders').insert(merge({ name: 'my folder', - parent_id: null + parentId: null }, opts)); } -async function insertApp(opts) { - return await db.get('apps').insert(Object.assign({ +function insertApp(opts?) { + return db.get('apps').insert(merge({ name: 'my app', secret: 'mysecret' }, opts)); diff --git a/test/mocha.opts b/test/mocha.opts index cf80ee74bc..907011807d 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1 +1 @@ ---timeout 5000 +--timeout 10000 diff --git a/test/text.js b/test/text.ts similarity index 77% rename from test/text.js rename to test/text.ts index 49e2f02b5d..8ce55cd1bc 100644 --- a/test/text.js +++ b/test/text.ts @@ -4,14 +4,16 @@ const assert = require('assert'); -const analyze = require('../built/api/common/text').default; -const syntaxhighlighter = require('../built/api/common/text/core/syntax-highlighter').default; +const analyze = require('../built/text/parse').default; +const syntaxhighlighter = require('../built/text/parse/core/syntax-highlighter').default; describe('Text', () => { - it('is correctly analyzed', () => { - const tokens = analyze('@himawari お腹ペコい :cat: #yryr'); + it('can be analyzed', () => { + const tokens = analyze('@himawari @hima_sub@namori.net お腹ペコい :cat: #yryr'); assert.deepEqual([ - { type: 'mention', content: '@himawari', username: 'himawari' }, + { type: 'mention', content: '@himawari', username: 'himawari', host: null }, + { type: 'text', content: ' '}, + { type: 'mention', content: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' }, { type: 'text', content: ' お腹ペコい ' }, { type: 'emoji', content: ':cat:', emoji: 'cat'}, { type: 'text', content: ' '}, @@ -19,8 +21,8 @@ describe('Text', () => { ], tokens); }); - it('逆関数で正しく復元できる', () => { - const text = '@himawari お腹ペコい :cat: #yryr'; + it('can be inverted', () => { + const text = '@himawari @hima_sub@namori.net お腹ペコい :cat: #yryr'; assert.equal(analyze(text).map(x => x.content).join(''), text); }); @@ -36,7 +38,15 @@ describe('Text', () => { it('mention', () => { const tokens = analyze('@himawari お腹ペコい'); assert.deepEqual([ - { type: 'mention', content: '@himawari', username: 'himawari' }, + { type: 'mention', content: '@himawari', username: 'himawari', host: null }, + { type: 'text', content: ' お腹ペコい' } + ], tokens); + }); + + it('remote mention', () => { + const tokens = analyze('@hima_sub@namori.net お腹ペコい'); + assert.deepEqual([ + { type: 'mention', content: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' }, { type: 'text', content: ' お腹ペコい' } ], tokens); }); diff --git a/tools/init-migration-file.sh b/tools/init-migration-file.sh new file mode 100755 index 0000000000..c6a2b862e6 --- /dev/null +++ b/tools/init-migration-file.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +usage() { + echo "$0 [-t type] [-n name]" + echo " type: [node | shell]" + echo " name: if no present, set untitled" + exit 0 +} + +while getopts :t:n:h OPT +do + case $OPT in + t) type=$OPTARG + ;; + n) name=$OPTARG + ;; + h) usage + ;; + \?) usage + ;; + :) usage + ;; + esac +done + +if [ "$type" = "" ] +then + echo "no type present!!!" + usage +fi + +if [ "$name" = "" ] +then + name="untitled" +fi + +touch "$(realpath $(dirname $BASH_SOURCE))/migration/$type.$(date +%s).$name.js" diff --git a/tools/init.js b/tools/init.js index 39c45c82d1..7a3e7d1b51 100644 --- a/tools/init.js +++ b/tools/init.js @@ -7,140 +7,119 @@ const chalk = require('chalk'); const configDirPath = `${__dirname}/../.config`; const configPath = `${configDirPath}/default.yml`; -const form = [ - { - type: 'input', - name: 'maintainer', - message: 'Maintainer name(and email address):' - }, - { - type: 'input', - name: 'url', - message: 'PRIMARY URL:' - }, - { - type: 'input', - name: 'secondary_url', - message: 'SECONDARY URL:' - }, - { - type: 'input', - name: 'port', - message: 'Listen port:' - }, - { - type: 'confirm', - name: 'https', - message: 'Use TLS?', - default: false - }, - { - type: 'input', - name: 'https_key', - message: 'Path of tls key:', - when: ctx => ctx.https - }, - { - type: 'input', - name: 'https_cert', - message: 'Path of tls cert:', - when: ctx => ctx.https - }, - { - type: 'input', - name: 'https_ca', - message: 'Path of tls ca:', - when: ctx => ctx.https - }, - { - type: 'input', - name: 'mongo_host', - message: 'MongoDB\'s host:', - default: 'localhost' - }, - { - type: 'input', - name: 'mongo_port', - message: 'MongoDB\'s port:', - default: '27017' - }, - { - type: 'input', - name: 'mongo_db', - message: 'MongoDB\'s db:', - default: 'misskey' - }, - { - type: 'input', - name: 'mongo_user', - message: 'MongoDB\'s user:' - }, - { - type: 'password', - name: 'mongo_pass', - message: 'MongoDB\'s password:' - }, - { - type: 'input', - name: 'redis_host', - message: 'Redis\'s host:', - default: 'localhost' - }, - { - type: 'input', - name: 'redis_port', - message: 'Redis\'s port:', - default: '6379' - }, - { - type: 'password', - name: 'redis_pass', - message: 'Redis\'s password:' - }, - { - type: 'confirm', - name: 'elasticsearch', - message: 'Use Elasticsearch?', - default: false - }, - { - type: 'input', - name: 'es_host', - message: 'Elasticsearch\'s host:', - default: 'localhost', - when: ctx => ctx.elasticsearch - }, - { - type: 'input', - name: 'es_port', - message: 'Elasticsearch\'s port:', - default: '9200', - when: ctx => ctx.elasticsearch - }, - { - type: 'password', - name: 'es_pass', - message: 'Elasticsearch\'s password:', - when: ctx => ctx.elasticsearch - }, - { - type: 'input', - name: 'recaptcha_site', - message: 'reCAPTCHA\'s site key:' - }, - { - type: 'input', - name: 'recaptcha_secret', - message: 'reCAPTCHA\'s secret key:' - } -]; +const form = [{ + type: 'input', + name: 'maintainerName', + message: 'Your name:' +}, { + type: 'input', + name: 'maintainerUrl', + message: 'Your home page URL or your mailto URL:' +}, { + type: 'input', + name: 'url', + message: 'URL you want to run Misskey:' +}, { + type: 'input', + name: 'port', + message: 'Listen port (e.g. 443):' +}, { + type: 'confirm', + name: 'https', + message: 'Use TLS?', + default: false +}, { + type: 'input', + name: 'https_key', + message: 'Path of tls key:', + when: ctx => ctx.https +}, { + type: 'input', + name: 'https_cert', + message: 'Path of tls cert:', + when: ctx => ctx.https +}, { + type: 'input', + name: 'https_ca', + message: 'Path of tls ca:', + when: ctx => ctx.https +}, { + type: 'input', + name: 'mongo_host', + message: 'MongoDB\'s host:', + default: 'localhost' +}, { + type: 'input', + name: 'mongo_port', + message: 'MongoDB\'s port:', + default: '27017' +}, { + type: 'input', + name: 'mongo_db', + message: 'MongoDB\'s db:', + default: 'misskey' +}, { + type: 'input', + name: 'mongo_user', + message: 'MongoDB\'s user:' +}, { + type: 'password', + name: 'mongo_pass', + message: 'MongoDB\'s password:' +}, { + type: 'input', + name: 'redis_host', + message: 'Redis\'s host:', + default: 'localhost' +}, { + type: 'input', + name: 'redis_port', + message: 'Redis\'s port:', + default: '6379' +}, { + type: 'password', + name: 'redis_pass', + message: 'Redis\'s password:' +}, { + type: 'confirm', + name: 'elasticsearch', + message: 'Use Elasticsearch?', + default: false +}, { + type: 'input', + name: 'es_host', + message: 'Elasticsearch\'s host:', + default: 'localhost', + when: ctx => ctx.elasticsearch +}, { + type: 'input', + name: 'es_port', + message: 'Elasticsearch\'s port:', + default: '9200', + when: ctx => ctx.elasticsearch +}, { + type: 'password', + name: 'es_pass', + message: 'Elasticsearch\'s password:', + when: ctx => ctx.elasticsearch +}, { + type: 'input', + name: 'recaptcha_site', + message: 'reCAPTCHA\'s site key:' +}, { + type: 'input', + name: 'recaptcha_secret', + message: 'reCAPTCHA\'s secret key:' +}]; inquirer.prompt(form).then(as => { // Mapping answers const conf = { - maintainer: as['maintainer'], + maintainer: { + name: as['maintainerName'], + url: as['maintainerUrl'] + }, url: as['url'], - secondary_url: as['secondary_url'], port: parseInt(as['port'], 10), https: { enable: as['https'], @@ -175,7 +154,6 @@ inquirer.prompt(form).then(as => { console.log(`Thanks. Writing the configuration to ${chalk.bold(path.resolve(configPath))}`); try { - fs.mkdirSync(configDirPath); fs.writeFileSync(configPath, yaml.dump(conf)); console.log(chalk.green('Well done.')); } catch (e) { diff --git a/tools/migration/README.md b/tools/migration/README.md new file mode 100644 index 0000000000..d52e84b35a --- /dev/null +++ b/tools/migration/README.md @@ -0,0 +1,11 @@ +Misskeyの破壊的変更に対応するいくつかのスニペットがあります。 +MongoDBシェルで実行する必要のあるものとnodeで直接実行する必要のあるものがあります。 +ファイル名が `shell.` から始まるものは前者、 `node.` から始まるものは後者です。 + +MongoDBシェルで実行する場合、`use`でデータベースを選択しておく必要があります。 + +nodeで実行するいくつかのスニペットは、並列処理させる数を引数で設定できるものがあります。 +処理中にエラーで落ちる場合は、メモリが足りていない可能性があるので、少ない数に設定してみてください。 +※デフォルトは`5`です。 + +ファイルを作成する際は `../init-migration-file.sh -t _type_ -n _name_` を実行すると _type_._unixtime_._name_.js が生成されます diff --git a/tools/migration/like-to-reactions.js b/tools/migration/like-to-reactions.js deleted file mode 100644 index 962a0f00ef..0000000000 --- a/tools/migration/like-to-reactions.js +++ /dev/null @@ -1,22 +0,0 @@ -db.users.update({}, { - $unset: { - likes_count: 1, - liked_count: 1 - } -}, false, true) - -db.likes.renameCollection('post_reactions') - -db.post_reactions.update({}, { - $set: { - reaction: 'like' - } -}, false, true) - -db.posts.update({}, { - $rename: { - likes_count: 'reaction_counts.like' - } -}, false, true); - -db.notifications.remove({}) diff --git a/tools/migration/nighthike/1.js b/tools/migration/nighthike/1.js new file mode 100644 index 0000000000..6ae30ad4f9 --- /dev/null +++ b/tools/migration/nighthike/1.js @@ -0,0 +1,39 @@ +// for Node.js interpret + +const { default: User } = require('../../../built/models/user'); +const { default: zip } = require('@prezzemolo/zip') + +const migrate = async (user) => { + const result = await User.update(user._id, { + $set: { + 'username': user.username.replace(/\-/g, '_'), + 'username_lower': user.username_lower.replace(/\-/g, '_') + } + }); + return result.ok === 1; +} + +async function main() { + const count = await User.count({}); + + const dop = Number.parseInt(process.argv[2]) || 5 + const idop = ((count - (count % dop)) / dop) + 1 + + return zip( + 1, + async (time) => { + console.log(`${time} / ${idop}`) + const doc = await User.find({}, { + limit: dop, skip: time * dop + }) + return Promise.all(doc.map(migrate)) + }, + idop + ).then(a => { + const rv = [] + a.forEach(e => rv.push(...e)) + return rv + }) +} + +main().then(console.dir).catch(console.error) diff --git a/tools/migration/nighthike/10.js b/tools/migration/nighthike/10.js new file mode 100644 index 0000000000..3c57b8d484 --- /dev/null +++ b/tools/migration/nighthike/10.js @@ -0,0 +1,3 @@ +db.following.remove({ + deletedAt: { $exists: true } +}); diff --git a/tools/migration/nighthike/11.js b/tools/migration/nighthike/11.js new file mode 100644 index 0000000000..2a4e8630d4 --- /dev/null +++ b/tools/migration/nighthike/11.js @@ -0,0 +1,36 @@ +db.pollVotes.update({}, { + $rename: { + postId: 'noteId', + } +}, false, true); + +db.postReactions.renameCollection('noteReactions'); +db.noteReactions.update({}, { + $rename: { + postId: 'noteId', + } +}, false, true); + +db.postWatching.renameCollection('noteWatching'); +db.noteWatching.update({}, { + $rename: { + postId: 'noteId', + } +}, false, true); + +db.posts.renameCollection('notes'); +db.notes.update({}, { + $rename: { + _repost: '_renote', + repostId: 'renoteId', + repostCount: 'renoteCount' + } +}, false, true); + +db.users.update({}, { + $rename: { + postsCount: 'notesCount', + pinnedPostId: 'pinnedNoteId', + latestPost: 'latestNote' + } +}, false, true); diff --git a/tools/migration/nighthike/12.js b/tools/migration/nighthike/12.js new file mode 100644 index 0000000000..f4b61e2ee8 --- /dev/null +++ b/tools/migration/nighthike/12.js @@ -0,0 +1,58 @@ +// for Node.js interpret + +const { default: User } = require('../../../built/models/user'); +const { generate } = require('../../../built/crypto_key'); +const { default: zip } = require('@prezzemolo/zip') + +const migrate = async (user) => { + const result = await User.update(user._id, { + $unset: { + account: '' + }, + $set: { + host: null, + hostLower: null, + email: user.account.email, + links: user.account.links, + password: user.account.password, + token: user.account.token, + twitter: user.account.twitter, + line: user.account.line, + profile: user.account.profile, + lastUsedAt: user.account.lastUsedAt, + isBot: user.account.isBot, + isPro: user.account.isPro, + twoFactorSecret: user.account.twoFactorSecret, + twoFactorEnabled: user.account.twoFactorEnabled, + clientSettings: user.account.clientSettings, + settings: user.account.settings, + keypair: user.account.keypair + } + }); + return result.ok === 1; +} + +async function main() { + const count = await User.count({}); + + const dop = Number.parseInt(process.argv[2]) || 5 + const idop = ((count - (count % dop)) / dop) + 1 + + return zip( + 1, + async (time) => { + console.log(`${time} / ${idop}`) + const doc = await User.find({}, { + limit: dop, skip: time * dop + }) + return Promise.all(doc.map(migrate)) + }, + idop + ).then(a => { + const rv = [] + a.forEach(e => rv.push(...e)) + return rv + }) +} + +main().then(console.dir).catch(console.error) diff --git a/tools/migration/nighthike/2.js b/tools/migration/nighthike/2.js new file mode 100644 index 0000000000..bb516db5b7 --- /dev/null +++ b/tools/migration/nighthike/2.js @@ -0,0 +1,39 @@ +// for Node.js interpret + +const { default: App } = require('../../../built/models/app'); +const { default: zip } = require('@prezzemolo/zip') + +const migrate = async (app) => { + const result = await App.update(app._id, { + $set: { + 'name_id': app.name_id.replace(/\-/g, '_'), + 'name_id_lower': app.name_id_lower.replace(/\-/g, '_') + } + }); + return result.ok === 1; +} + +async function main() { + const count = await App.count({}); + + const dop = Number.parseInt(process.argv[2]) || 5 + const idop = ((count - (count % dop)) / dop) + 1 + + return zip( + 1, + async (time) => { + console.log(`${time} / ${idop}`) + const doc = await App.find({}, { + limit: dop, skip: time * dop + }) + return Promise.all(doc.map(migrate)) + }, + idop + ).then(a => { + const rv = [] + a.forEach(e => rv.push(...e)) + return rv + }) +} + +main().then(console.dir).catch(console.error) diff --git a/tools/migration/nighthike/3.js b/tools/migration/nighthike/3.js new file mode 100644 index 0000000000..bde4f773d2 --- /dev/null +++ b/tools/migration/nighthike/3.js @@ -0,0 +1,73 @@ +// for Node.js interpret + +const { default: User } = require('../../../built/models/user'); +const { generate } = require('../../../built/crypto_key'); +const { default: zip } = require('@prezzemolo/zip') + +const migrate = async (user) => { + const result = await User.update(user._id, { + $unset: { + email: '', + links: '', + password: '', + token: '', + twitter: '', + line: '', + profile: '', + last_used_at: '', + is_bot: '', + is_pro: '', + two_factor_secret: '', + two_factor_enabled: '', + client_settings: '', + settings: '' + }, + $set: { + host: null, + host_lower: null, + account: { + email: user.email, + links: user.links, + password: user.password, + token: user.token, + twitter: user.twitter, + line: user.line, + profile: user.profile, + last_used_at: user.last_used_at, + is_bot: user.is_bot, + is_pro: user.is_pro, + two_factor_secret: user.two_factor_secret, + two_factor_enabled: user.two_factor_enabled, + client_settings: user.client_settings, + settings: user.settings, + keypair: generate() + } + } + }); + return result.ok === 1; +} + +async function main() { + const count = await User.count({}); + + const dop = Number.parseInt(process.argv[2]) || 5 + const idop = ((count - (count % dop)) / dop) + 1 + + return zip( + 1, + async (time) => { + console.log(`${time} / ${idop}`) + const doc = await User.find({}, { + limit: dop, skip: time * dop + }) + return Promise.all(doc.map(migrate)) + }, + idop + ).then(a => { + const rv = [] + a.forEach(e => rv.push(...e)) + return rv + }) +} + +main().then(console.dir).catch(console.error) diff --git a/tools/migration/nighthike/4.js b/tools/migration/nighthike/4.js new file mode 100644 index 0000000000..5e12e64055 --- /dev/null +++ b/tools/migration/nighthike/4.js @@ -0,0 +1,262 @@ +// このスクリプトを走らせる前か後に notifications コレクションはdropしてください + +db.access_tokens.renameCollection('accessTokens'); +db.accessTokens.update({}, { + $rename: { + created_at: 'createdAt', + app_id: 'appId', + user_id: 'userId', + } +}, false, true); + +db.apps.update({}, { + $rename: { + created_at: 'createdAt', + user_id: 'userId', + name_id: 'nameId', + name_id_lower: 'nameIdLower', + callback_url: 'callbackUrl', + } +}, false, true); + +db.auth_sessions.renameCollection('authSessions'); +db.authSessions.update({}, { + $rename: { + created_at: 'createdAt', + app_id: 'appId', + user_id: 'userId', + } +}, false, true); + +db.channel_watching.renameCollection('channelWatching'); +db.channelWatching.update({}, { + $rename: { + created_at: 'createdAt', + deleted_at: 'deletedAt', + channel_id: 'channelId', + user_id: 'userId', + } +}, false, true); + +db.channels.update({}, { + $rename: { + created_at: 'createdAt', + user_id: 'userId', + watching_count: 'watchingCount' + } +}, false, true); + +db.drive_files.files.renameCollection('driveFiles.files'); +db.drive_files.chunks.renameCollection('driveFiles.chunks'); +db.driveFiles.files.update({}, { + $rename: { + 'metadata.user_id': 'metadata.userId' + } +}, false, true); +db.driveFiles.files.update({ + 'metadata.folder_id': { $ne: null } +}, { + $rename: { + 'metadata.folder_id': 'metadata.folderId', + } +}, false, true); +db.driveFiles.files.update({ + 'metadata.properties.average_color': { $ne: null } +}, { + $rename: { + 'metadata.properties.average_color': 'metadata.properties.avgColor' + } +}, false, true); + +db.drive_folders.renameCollection('driveFolders'); +db.driveFolders.update({}, { + $rename: { + created_at: 'createdAt', + user_id: 'userId', + parent_id: 'parentId', + } +}, false, true); + +db.favorites.update({}, { + $rename: { + created_at: 'createdAt', + user_id: 'userId', + post_id: 'postId', + } +}, false, true); + +db.following.update({}, { + $rename: { + created_at: 'createdAt', + deleted_at: 'deletedAt', + followee_id: 'followeeId', + follower_id: 'followerId', + } +}, false, true); + +db.messaging_histories.renameCollection('messagingHistories'); +db.messagingHistories.update({}, { + $rename: { + updated_at: 'updatedAt', + user_id: 'userId', + partner: 'partnerId', + message: 'messageId', + } +}, false, true); + +db.messaging_messages.renameCollection('messagingMessages'); +db.messagingMessages.update({}, { + $rename: { + created_at: 'createdAt', + user_id: 'userId', + recipient_id: 'recipientId', + file_id: 'fileId', + is_read: 'isRead' + } +}, false, true); + +db.mute.update({}, { + $rename: { + created_at: 'createdAt', + deleted_at: 'deletedAt', + mutee_id: 'muteeId', + muter_id: 'muterId', + } +}, false, true); + +db.othello_games.renameCollection('othelloGames'); +db.othelloGames.update({}, { + $rename: { + created_at: 'createdAt', + started_at: 'startedAt', + is_started: 'isStarted', + is_ended: 'isEnded', + user1_id: 'user1Id', + user2_id: 'user2Id', + user1_accepted: 'user1Accepted', + user2_accepted: 'user2Accepted', + winner_id: 'winnerId', + 'settings.is_llotheo': 'settings.isLlotheo', + 'settings.can_put_everywhere': 'settings.canPutEverywhere', + 'settings.looped_board': 'settings.loopedBoard', + } +}, false, true); + +db.othello_matchings.renameCollection('othelloMatchings'); +db.othelloMatchings.update({}, { + $rename: { + created_at: 'createdAt', + parent_id: 'parentId', + child_id: 'childId' + } +}, false, true); + +db.poll_votes.renameCollection('pollVotes'); +db.pollVotes.update({}, { + $rename: { + created_at: 'createdAt', + user_id: 'userId', + post_id: 'postId' + } +}, false, true); + +db.post_reactions.renameCollection('postReactions'); +db.postReactions.update({}, { + $rename: { + created_at: 'createdAt', + user_id: 'userId', + post_id: 'postId' + } +}, false, true); + +db.post_watching.renameCollection('postWatching'); +db.postWatching.update({}, { + $rename: { + created_at: 'createdAt', + user_id: 'userId', + post_id: 'postId' + } +}, false, true); + +db.posts.update({}, { + $rename: { + created_at: 'createdAt', + channel_id: 'channelId', + user_id: 'userId', + app_id: 'appId', + media_ids: 'mediaIds', + reply_id: 'replyId', + repost_id: 'repostId', + via_mobile: 'viaMobile', + reaction_counts: 'reactionCounts', + replies_count: 'repliesCount', + repost_count: 'repostCount' + } +}, false, true); + +db.posts.update({ + _reply: { $ne: null } +}, { + $rename: { + '_reply.user_id': '_reply.userId', + } +}, false, true); + +db.posts.update({ + _repost: { $ne: null } +}, { + $rename: { + '_repost.user_id': '_repost.userId', + } +}, false, true); + +db.signin.update({}, { + $rename: { + created_at: 'createdAt', + user_id: 'userId', + } +}, false, true); + +db.sw_subscriptions.renameCollection('swSubscriptions'); +db.swSubscriptions.update({}, { + $rename: { + user_id: 'userId', + } +}, false, true); + +db.users.update({}, { + $unset: { + likes_count: '', + liked_count: '', + latest_post: '', + 'account.twitter.access_token': '', + 'account.twitter.access_token_secret': '', + 'account.twitter.user_id': '', + 'account.twitter.screen_name': '', + 'account.line.user_id': '', + 'account.client_settings.mobile_home': '' + } +}, false, true); + +db.users.update({}, { + $rename: { + created_at: 'createdAt', + deleted_at: 'deletedAt', + followers_count: 'followersCount', + following_count: 'followingCount', + posts_count: 'postsCount', + drive_capacity: 'driveCapacity', + username_lower: 'usernameLower', + avatar_id: 'avatarId', + banner_id: 'bannerId', + pinned_post_id: 'pinnedPostId', + is_suspended: 'isSuspended', + host_lower: 'hostLower', + 'account.last_used_at': 'account.lastUsedAt', + 'account.is_bot': 'account.isBot', + 'account.is_pro': 'account.isPro', + 'account.two_factor_secret': 'account.twoFactorSecret', + 'account.two_factor_enabled': 'account.twoFactorEnabled', + 'account.client_settings': 'account.clientSettings' + } +}, false, true); diff --git a/tools/migration/nighthike/5.js b/tools/migration/nighthike/5.js new file mode 100644 index 0000000000..3989ea6301 --- /dev/null +++ b/tools/migration/nighthike/5.js @@ -0,0 +1,49 @@ +// for Node.js interpret + +const mongodb = require("../../../built/db/mongodb"); +const Post = mongodb.default.get('posts'); + +const { default: zip } = require('@prezzemolo/zip') + +const migrate = async (post) => { + const result = await Post.update(post._id, { + $set: { + 'geo.type': 'Point', + 'geo.coordinates': [post.geo.longitude, post.geo.latitude] + }, + $unset: { + 'geo.longitude': '', + 'geo.latitude': '', + } + }); + return result.ok === 1; +} + +async function main() { + const count = await Post.count({ + 'geo': { $ne: null } + }); + + const dop = Number.parseInt(process.argv[2]) || 5 + const idop = ((count - (count % dop)) / dop) + 1 + + return zip( + 1, + async (time) => { + console.log(`${time} / ${idop}`) + const doc = await Post.find({ + 'geo': { $ne: null } + }, { + limit: dop, skip: time * dop + }) + return Promise.all(doc.map(migrate)) + }, + idop + ).then(a => { + const rv = [] + a.forEach(e => rv.push(...e)) + return rv + }) +} + +main().then(console.dir).catch(console.error) diff --git a/tools/migration/nighthike/6.js b/tools/migration/nighthike/6.js new file mode 100644 index 0000000000..27fff2ec19 --- /dev/null +++ b/tools/migration/nighthike/6.js @@ -0,0 +1,13 @@ +db.posts.update({ + $or: [{ + mediaIds: null + }, { + mediaIds: { + $exists: false + } + }] +}, { + $set: { + mediaIds: [] + } +}, false, true); diff --git a/tools/migration/nighthike/7.js b/tools/migration/nighthike/7.js new file mode 100644 index 0000000000..ed5f1e6b96 --- /dev/null +++ b/tools/migration/nighthike/7.js @@ -0,0 +1,41 @@ +// for Node.js interpret + +const mongodb = require("../../../built/db/mongodb"); +const Post = mongodb.default.get('posts'); +const { default: zip } = require('@prezzemolo/zip') +const html = require('../../../built/text/html').default; +const parse = require('../../../built/text/parse').default; + +const migrate = async (post) => { + const result = await Post.update(post._id, { + $set: { + textHtml: post.text ? html(parse(post.text)) : null + } + }); + return result.ok === 1; +} + +async function main() { + const count = await Post.count({}); + + const dop = Number.parseInt(process.argv[2]) || 5 + const idop = ((count - (count % dop)) / dop) + 1 + + return zip( + 1, + async (time) => { + console.log(`${time} / ${idop}`) + const doc = await Post.find({}, { + limit: dop, skip: time * dop + }) + return Promise.all(doc.map(migrate)) + }, + idop + ).then(a => { + const rv = [] + a.forEach(e => rv.push(...e)) + return rv + }) +} + +main().then(console.dir).catch(console.error) diff --git a/tools/migration/nighthike/8.js b/tools/migration/nighthike/8.js new file mode 100644 index 0000000000..e4f4482dba --- /dev/null +++ b/tools/migration/nighthike/8.js @@ -0,0 +1,40 @@ +// for Node.js interpret + +const { default: Message } = require('../../../built/models/messaging-message'); +const { default: zip } = require('@prezzemolo/zip') +const html = require('../../../built/text/html').default; +const parse = require('../../../built/text/parse').default; + +const migrate = async (message) => { + const result = await Message.update(message._id, { + $set: { + textHtml: message.text ? html(parse(message.text)) : null + } + }); + return result.ok === 1; +} + +async function main() { + const count = await Message.count({}); + + const dop = Number.parseInt(process.argv[2]) || 5 + const idop = ((count - (count % dop)) / dop) + 1 + + return zip( + 1, + async (time) => { + console.log(`${time} / ${idop}`) + const doc = await Message.find({}, { + limit: dop, skip: time * dop + }) + return Promise.all(doc.map(migrate)) + }, + idop + ).then(a => { + const rv = [] + a.forEach(e => rv.push(...e)) + return rv + }) +} + +main().then(console.dir).catch(console.error) diff --git a/tools/migration/nighthike/9.js b/tools/migration/nighthike/9.js new file mode 100644 index 0000000000..f4e1ab341e --- /dev/null +++ b/tools/migration/nighthike/9.js @@ -0,0 +1,93 @@ +// for Node.js interpret + +const { default: Following } = require('../../../built/models/following'); +const { default: FollowingLog } = require('../../../built/models/following-log'); +const { default: FollowedLog } = require('../../../built/models/followed-log'); +const { default: zip } = require('@prezzemolo/zip') + +const migrate = async (following) => { + const followingCount = await Following.count({ + followerId: following.followerId, + createdAt: { $lt: following.createdAt }, + $or: [ + { deletedAt: { $exists: false } }, + { deletedAt: { $gt: following.createdAt } } + ] + }); + await FollowingLog.insert({ + createdAt: following.createdAt, + userId: following.followerId, + count: followingCount + 1 + }); + + const followersCount = await Following.count({ + followeeId: following.followeeId, + createdAt: { $lt: following.createdAt }, + $or: [ + { deletedAt: { $exists: false } }, + { deletedAt: { $gt: following.createdAt } } + ] + }); + await FollowedLog.insert({ + createdAt: following.createdAt, + userId: following.followeeId, + count: followersCount + 1 + }); + + if (following.deletedAt) { + const followingCount2 = await Following.count({ + followerId: following.followerId, + createdAt: { $lt: following.deletedAt }, + $or: [ + { deletedAt: { $exists: false } }, + { deletedAt: { $gt: following.createdAt } } + ] + }); + await FollowingLog.insert({ + createdAt: following.deletedAt, + userId: following.followerId, + count: followingCount2 - 1 + }); + + const followersCount2 = await Following.count({ + followeeId: following.followeeId, + createdAt: { $lt: following.deletedAt }, + $or: [ + { deletedAt: { $exists: false } }, + { deletedAt: { $gt: following.createdAt } } + ] + }); + await FollowedLog.insert({ + createdAt: following.deletedAt, + userId: following.followeeId, + count: followersCount2 - 1 + }); + } + + return true; +} + +async function main() { + const count = await Following.count({}); + + const dop = Number.parseInt(process.argv[2]) || 5 + const idop = ((count - (count % dop)) / dop) + 1 + + return zip( + 1, + async (time) => { + console.log(`${time} / ${idop}`) + const doc = await Following.find({}, { + limit: dop, skip: time * dop, sort: { _id: 1 } + }) + return Promise.all(doc.map(migrate)) + }, + idop + ).then(a => { + const rv = [] + a.forEach(e => rv.push(...e)) + return rv + }) +} + +main().then(console.dir).catch(console.error) diff --git a/tools/migration/user-profile.js b/tools/migration/user-profile.js deleted file mode 100644 index e6666319e1..0000000000 --- a/tools/migration/user-profile.js +++ /dev/null @@ -1,18 +0,0 @@ -db.users.find({}).forEach(function(user) { - print(user._id); - db.users.update({ _id: user._id }, { - $rename: { - bio: 'description' - }, - $unset: { - location: '', - birthday: '' - }, - $set: { - profile: { - location: user.location || null, - birthday: user.birthday || null - } - } - }, false, false); -}); diff --git a/tsconfig.json b/tsconfig.json index 064a04e4d2..c407d554ee 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "allowJs": true, "noEmitOnError": false, "noImplicitAny": false, "noImplicitReturns": true, @@ -7,14 +8,19 @@ "noUnusedLocals": true, "noFallthroughCasesInSwitch": true, "declaration": false, - "sourceMap": false, + "sourceMap": true, "target": "es2017", "module": "commonjs", "removeComments": false, - "noLib": false + "noLib": false, + "strict": true, + "strictNullChecks": false }, "compileOnSave": false, "include": [ - "./gulpfile.ts" + "./src/**/*.ts" + ], + "exclude": [ + "./src/client/app/**/*.ts" ] } diff --git a/tslint.json b/tslint.json index dfd8309675..d3f96000b9 100644 --- a/tslint.json +++ b/tslint.json @@ -13,9 +13,11 @@ "object-literal-sort-keys": false, "curly": false, "no-console": [false], + "no-empty":false, "ordered-imports": [false], "arrow-parens": false, "object-literal-shorthand": false, + "object-literal-key-quotes": false, "triple-equals": [false], "no-shadowed-variable": false, "no-string-literal": false, @@ -23,6 +25,7 @@ "comment-format": [false], "interface-over-type-literal": false, "max-line-length": [false], + "max-classes-per-file": false, "member-ordering": [false], "ban-types": [ "Object" diff --git a/webpack.config.ts b/webpack.config.ts new file mode 100644 index 0000000000..60dbfd2ff7 --- /dev/null +++ b/webpack.config.ts @@ -0,0 +1,245 @@ +/** + * webpack configuration + */ + +import * as fs from 'fs'; +import * as webpack from 'webpack'; +import chalk from 'chalk'; +import jsonImporter from 'node-sass-json-importer'; +const minifyHtml = require('html-minifier').minify; +const WebpackOnBuildPlugin = require('on-build-webpack'); +//const HardSourceWebpackPlugin = require('hard-source-webpack-plugin'); +const ProgressBarPlugin = require('progress-bar-webpack-plugin'); + +import I18nReplacer from './src/build/i18n'; +import { pattern as faPattern, replacement as faReplacement } from './src/build/fa'; +const constants = require('./src/const.json'); +import config from './src/config'; +import { licenseHtml } from './src/build/license'; + +import locales from './locales'; +const meta = require('./package.json'); +const version = meta.version; +const codename = meta.codename; + +//#region Replacer definitions +global['faReplacement'] = faReplacement; + +global['collapseSpacesReplacement'] = html => { + return minifyHtml(html, { + collapseWhitespace: true, + collapseInlineTagWhitespace: true, + keepClosingSlash: true + }).replace(/\t/g, ''); +}; + +global['base64replacement'] = (_, key) => { + return fs.readFileSync(__dirname + '/src/client/' + key, 'base64'); +}; +//#endregion + +const langs = Object.keys(locales); + +const entries = process.env.NODE_ENV == 'production' + ? langs.map(l => [l, false]).concat(langs.map(l => [l, true])) + : [['ja', false]]; + +module.exports = entries.map(x => { + const [lang, isProduction] = x; + + // Chunk name + const name = lang; + + // Entries + const entry = { + desktop: './src/client/app/desktop/script.ts', + mobile: './src/client/app/mobile/script.ts', + //ch: './src/client/app/ch/script.ts', + //stats: './src/client/app/stats/script.ts', + //status: './src/client/app/status/script.ts', + dev: './src/client/app/dev/script.ts', + auth: './src/client/app/auth/script.ts', + sw: './src/client/app/sw.js' + }; + + const output = { + path: __dirname + '/built/client/assets', + filename: `[name].${version}.${lang}.${isProduction ? 'min' : 'raw'}.js` + }; + + const i18nReplacer = new I18nReplacer(lang as string); + global['i18nReplacement'] = i18nReplacer.replacement; + + //#region Define consts + const consts = { + _RECAPTCHA_SITEKEY_: config.recaptcha.site_key, + _SW_PUBLICKEY_: config.sw ? config.sw.public_key : null, + _THEME_COLOR_: constants.themeColor, + _COPYRIGHT_: constants.copyright, + _VERSION_: version, + _CODENAME_: codename, + _STATUS_URL_: config.status_url, + _STATS_URL_: config.stats_url, + _DOCS_URL_: config.docs_url, + _API_URL_: config.api_url, + _WS_URL_: config.ws_url, + _DEV_URL_: config.dev_url, + _LANG_: lang, + _HOST_: config.host, + _HOSTNAME_: config.hostname, + _URL_: config.url, + _LICENSE_: licenseHtml, + _GOOGLE_MAPS_API_KEY_: config.google_maps_api_key + }; + + const _consts = {}; + + Object.keys(consts).forEach(key => { + _consts[key] = JSON.stringify(consts[key]); + }); + //#endregion + + const plugins = [ + //new HardSourceWebpackPlugin(), + new ProgressBarPlugin({ + format: chalk` {cyan.bold yes we can} {bold [}:bar{bold ]} {green.bold :percent} {gray (:current/:total)} :elapseds`, + clear: false + }), + new webpack.DefinePlugin(_consts), + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify(isProduction ? 'production' : 'development') + }), + new WebpackOnBuildPlugin(stats => { + fs.writeFileSync('./version.json', JSON.stringify({ + version + }), 'utf-8'); + }) + ]; + + if (isProduction) { + plugins.push(new webpack.optimize.ModuleConcatenationPlugin()); + } + + return { + name, + entry, + module: { + rules: [{ + test: /\.vue$/, + exclude: /node_modules/, + use: [{ + loader: 'vue-loader', + options: { + cssSourceMap: false, + preserveWhitespace: false + } + }, { + loader: 'replace', + query: { + search: /%base64:(.+?)%/g.toString(), + replace: 'base64replacement' + } + }, { + loader: 'replace', + query: { + search: i18nReplacer.pattern.toString(), + replace: 'i18nReplacement' + } + }, { + loader: 'replace', + query: { + search: faPattern.toString(), + replace: 'faReplacement' + } + }, { + loader: 'replace', + query: { + search: /^<template>([\s\S]+?)\r?\n<\/template>/.toString(), + replace: 'collapseSpacesReplacement' + } + }] + }, { + test: /\.styl$/, + exclude: /node_modules/, + use: [{ + loader: 'style-loader' + }, { + loader: 'css-loader', + options: { + minimize: true + } + }, { + loader: 'stylus-loader' + }] + }, { + test: /\.scss$/, + exclude: /node_modules/, + use: [{ + loader: 'style-loader' + }, { + loader: 'css-loader', + options: { + minimize: true + } + }, { + loader: 'sass-loader', + options: { + importer: jsonImporter, + } + }] + }, { + test: /\.css$/, + use: [{ + loader: 'style-loader' + }, { + loader: 'css-loader', + options: { + minimize: true + } + }] + }, { + test: /\.(eot|woff|woff2|svg|ttf)([\?]?.*)$/, + loader: 'url-loader' + }, { + test: /\.ts$/, + exclude: /node_modules/, + use: [{ + loader: 'ts-loader', + options: { + happyPackMode: true, + configFile: __dirname + '/src/client/app/tsconfig.json', + appendTsSuffixTo: [/\.vue$/] + } + }, { + loader: 'replace', + query: { + search: i18nReplacer.pattern.toString(), + replace: 'i18nReplacement' + } + }, { + loader: 'replace', + query: { + search: faPattern.toString(), + replace: 'faReplacement' + } + }] + }] + }, + plugins, + output, + resolve: { + extensions: [ + '.js', '.ts', '.json' + ], + alias: { + 'const.styl': __dirname + '/src/client/const.styl' + } + }, + resolveLoader: { + modules: ['node_modules', './webpack/loaders'] + }, + cache: true, + devtool: false, //'source-map', + mode: isProduction ? 'production' : 'development' + }; +}); diff --git a/webpack/loaders/replace.js b/webpack/loaders/replace.js new file mode 100644 index 0000000000..03cf1fcd78 --- /dev/null +++ b/webpack/loaders/replace.js @@ -0,0 +1,18 @@ +const loaderUtils = require('loader-utils'); + +function trim(text, g) { + return text.substring(1, text.length - (g ? 2 : 0)); +} + +module.exports = function(src) { + this.cacheable(); + const options = loaderUtils.getOptions(this); + const search = options.search; + const g = search[search.length - 1] == 'g'; + const replace = global[options.replace]; + if (typeof search != 'string' || search.length == 0) console.error('invalid search'); + if (typeof replace != 'function') console.error('invalid replacer:', replace, this.request); + src = src.replace(new RegExp(trim(search, g), g ? 'g' : ''), replace); + this.callback(null, src); + return src; +}; diff --git a/webpack/module/index.ts b/webpack/module/index.ts deleted file mode 100644 index 15f36557ce..0000000000 --- a/webpack/module/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import rules from './rules'; - -export default (lang, locale) => ({ - rules: rules(lang, locale) -}); diff --git a/webpack/module/rules/i18n.ts b/webpack/module/rules/i18n.ts deleted file mode 100644 index 3023253cab..0000000000 --- a/webpack/module/rules/i18n.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Replace i18n texts - */ - -const StringReplacePlugin = require('string-replace-webpack-plugin'); - -export default (lang, locale) => ({ - enforce: 'pre', - test: /\.(tag|js)$/, - exclude: /node_modules/, - loader: StringReplacePlugin.replace({ - replacements: [ - { - pattern: /%i18n:(.+?)%/g, replacement: (_, key) => { - let text = locale; - - // Check the key existance - const error = key.split('.').some(k => { - if (text.hasOwnProperty(k)) { - text = text[k]; - return false; - } else { - return true; - } - }); - - if (error) { - console.warn(`key '${key}' not found in '${lang}'`); - return key; // Fallback - } else { - return text.replace(/'/g, '\\\'').replace(/"/g, '\\"'); - } - } - } - ] - }) -}); diff --git a/webpack/module/rules/index.ts b/webpack/module/rules/index.ts deleted file mode 100644 index 2308f4e535..0000000000 --- a/webpack/module/rules/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import i18n from './i18n'; -import themeColor from './theme-color'; -import tag from './tag'; -import stylus from './stylus'; - -export default (lang, locale) => [ - i18n(lang, locale), - themeColor(), - tag(), - stylus() -]; diff --git a/webpack/module/rules/stylus.ts b/webpack/module/rules/stylus.ts deleted file mode 100644 index dd1e4c3218..0000000000 --- a/webpack/module/rules/stylus.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Stylus support - */ - -export default () => ({ - test: /\.styl$/, - exclude: /node_modules/, - use: [ - { loader: 'style-loader' }, - { loader: 'css-loader' }, - { loader: 'stylus-loader' } - ] -}); diff --git a/webpack/module/rules/tag.ts b/webpack/module/rules/tag.ts deleted file mode 100644 index 706af35b40..0000000000 --- a/webpack/module/rules/tag.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Riot tags - */ - -export default () => ({ - test: /\.tag$/, - exclude: /node_modules/, - loader: 'riot-tag-loader', - query: { - hot: false, - style: 'stylus', - expr: false, - compact: true, - parserOptions: { - style: { - compress: true - } - } - } -}); diff --git a/webpack/module/rules/theme-color.ts b/webpack/module/rules/theme-color.ts deleted file mode 100644 index 7ee545191c..0000000000 --- a/webpack/module/rules/theme-color.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Theme color provider - */ - -const StringReplacePlugin = require('string-replace-webpack-plugin'); - -const constants = require('../../../src/const.json'); - -export default () => ({ - enforce: 'pre', - test: /\.tag$/, - exclude: /node_modules/, - loader: StringReplacePlugin.replace({ - replacements: [ - { - pattern: /\$theme\-color\-foreground/g, - replacement: () => constants.themeColorForeground - }, - { - pattern: /\$theme\-color/g, - replacement: () => constants.themeColor - }, - ] - }) -}); diff --git a/webpack/plugins/banner.ts b/webpack/plugins/banner.ts deleted file mode 100644 index 47b8cd3555..0000000000 --- a/webpack/plugins/banner.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as os from 'os'; -import * as webpack from 'webpack'; - -export default version => new webpack.BannerPlugin({ - banner: - `Misskey v${version} | MIT Licensed, (c) syuilo 2014-2017\n` + - 'https://github.com/syuilo/misskey\n' + - `built by ${os.hostname()} at ${new Date()}\n` + - 'hash:[hash], chunkhash:[chunkhash]' -}); diff --git a/webpack/plugins/const.ts b/webpack/plugins/const.ts deleted file mode 100644 index ccfcb45260..0000000000 --- a/webpack/plugins/const.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Constant Replacer - */ - -import * as webpack from 'webpack'; - -import version from '../../src/version'; -const constants = require('../../src/const.json'); - -export default () => new webpack.DefinePlugin({ - VERSION: JSON.stringify(version), - THEME_COLOR: JSON.stringify(constants.themeColor) -}); diff --git a/webpack/plugins/hoist.ts b/webpack/plugins/hoist.ts deleted file mode 100644 index f61133f8df..0000000000 --- a/webpack/plugins/hoist.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as webpack from 'webpack'; - -export default () => new webpack.optimize.ModuleConcatenationPlugin(); diff --git a/webpack/plugins/index.ts b/webpack/plugins/index.ts deleted file mode 100644 index 99b16c2b05..0000000000 --- a/webpack/plugins/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -const StringReplacePlugin = require('string-replace-webpack-plugin'); - -import constant from './const'; -import hoist from './hoist'; -//import minify from './minify'; -import banner from './banner'; - -/* -const env = process.env.NODE_ENV; -const isProduction = env === 'production'; -*/ - -export default version => { - const plugins = [ - constant(), - new StringReplacePlugin(), - hoist() - ]; -/* - if (isProduction) { - plugins.push(minify()); - } -*/ - plugins.push(banner(version)); - - return plugins; -}; diff --git a/webpack/plugins/minify.ts b/webpack/plugins/minify.ts deleted file mode 100644 index ec4c9b3405..0000000000 --- a/webpack/plugins/minify.ts +++ /dev/null @@ -1,3 +0,0 @@ -const UglifyEsPlugin = require('uglify-es-webpack-plugin'); - -export default () => new UglifyEsPlugin(); diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts deleted file mode 100644 index 5199285d55..0000000000 --- a/webpack/webpack.config.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * webpack configuration - */ - -import module_ from './module'; -import plugins from './plugins'; - -import langs from './langs'; -import version from '../src/version'; - -module.exports = langs.map(([lang, locale]) => { - // Chunk name - const name = lang; - - // Entries - const entry = { - desktop: './src/web/app/desktop/script.js', - mobile: './src/web/app/mobile/script.js', - stats: './src/web/app/stats/script.js', - status: './src/web/app/status/script.js', - dev: './src/web/app/dev/script.js', - auth: './src/web/app/auth/script.js' - }; - - const output = { - path: __dirname + '/../built/web/assets', - filename: `[name].${version}.${lang}.js` - }; - - return { - name, - entry, - module: module_(lang, locale), - plugins: plugins(version), - output - }; -});